Skip to content

feat(eval): eval coverage against a frozen need graph (#331) - #340

Merged
antonwolfy merged 9 commits into
intel:mainfrom
napetrov:feat/eval-coverage
Nov 1, 2026
Merged

antonwolfy merged 9 commits into
intel:mainfrom
napetrov:feat/eval-coverage

Conversation

@napetrov

@napetrov napetrov commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds dokimi eval coverage. Given a frozen need graph and the needs labels on an eval set, it reports how much of a subject's problem space the evals cover and how relevant each eval is. It makes no model call, so the same files always give the same report (#331, AC-J07-007 → Partial).

need_graph.json (need_graph.v1)  ─┐
evals.json rows / task.toml      ─┼─> eval coverage ─> markdown on stdout
  "needs": ["n1", "n4"]          ─┘                    eval_coverage.v1 (--out)
                                                       exit 1 under --min [METRIC=]X
  • New schemas.
    • need_graph.v1: the file novelty's save_graph writes is valid as is. Nodes carry path_class and confirmed_by (user or hypothesis).
    • eval_coverage.v1: weighted coverage, the weights it used, core+rare_critical and rare_critical shares, gaps, per-eval relevance and structured problems.
  • Relevance per eval: relevant_core, relevant_peripheral, out_of_scope, orphan (needs: []) or unlabelled (no needs key).
  • Exit 2 on:
    • an unreadable input or task;
    • an empty suite;
    • duplicate node ids;
    • a needs value that is not a list of ids;
    • a need id the graph does not have.
  • Novelty change: NeedNode.confirmed_by and NeedGraph.provenance now survive a save/load round trip, so one need-graph format has one owner.
  • CLI: the command is a FREE route. Its flags are --graph, --out and the repeatable --min [METRIC=]X, which follows the --timeout [PHASE=]SEC pattern.

Evidence

  • tests/test_eval_coverage.py: 18 tests, including an outcome-marked gaps/relevance test, determinism, digest stability and a check that both graph readers agree on the fixture. make ci-local gives rc=0.
  • Pilot on 10 intel/skills skills (in eval coverage: coverage of a skill's problem space and relevance of each eval, reproducible (need graph + optional interview) #331):
    • Replaying the response cache reproduced the results byte for byte.
    • Regenerating the graph moves coverage by up to 0.14–0.22. That is why the graph is frozen in a file instead of being rebuilt.
    • Evals borrowed from unrelated skills came out 9/9 orphan.
  • Local self-review, 2 passes (lane L4):
    • Pass 1 found 2 blocking issues, both fixed. Novelty's reader crashed on confirmed_by. The per-metric --min-* flags broke the flag-registry pattern.
    • Pass 2 found 0 blocking issues.

Merge Danger

Low. This PR adds new files and one new command. The only change to existing behaviour is that novelty's as_dict now writes confirmed_by on every node (plus provenance when it is set). Nothing in the repo reads saved need graphs yet.

Follow-ups (#331):

  • tie labels to a graph digest;
  • read needs through task_formats (this also covers the tbench task.yaml case);
  • add a graph builder command, an interview and label drafting;
  • an unwritable --out currently exits 1, the same code as "coverage below floor".

🤖 Generated with Claude Code

…nst a frozen need graph (intel#331)

need_graph.v1 and eval_coverage.v1 schemas; evals name graph nodes in 'needs'
(evals.json rows or task metadata). Pure function of files, no model call;
exit 1 under --min-coverage / --min-rare-critical, 2 on unknown ids.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…IC=]X, strict needs

Novelty's NeedNode keeps confirmed_by and NeedGraph keeps provenance, so both readers
accept the same need_graph.v1 file. Duplicate node ids, a needs value that is not a list
of ids, an unreadable task, a non-list evals and an empty suite exit 2. Problems are
structured; the report records the weights; the digest covers only identity fields.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Copilot AI balanced review requested due to automatic review settings November 1, 2026 03:37
@napetrov
napetrov requested a review from a team as a code owner November 1, 2026 03:37
@github-actions github-actions Bot added the cli-interface-change Changes dokimi's user-facing CLI surface label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

CLI interface change detected

This PR changes dokimi's user-facing CLI surface (commands and/or options):

+ added command `dokimi eval coverage`
    + argument `evals`
    + option `--graph`
    + option `--help/-h`
    + option `--min`
    + option `--out`
~ `dokimi runs list` parameters changed:
    ~ changed option `--kind`: choices: ["'answers'", "'arms'", "'cli_routes'", "'eval'", "'explore_runs'", "'guidance_reach'", "'matrix_rollup'", "'novelty'", "'plan'", "'plugin_delta'", "'questions'", "'repo_budget'", "'repo_environment'", "'repo_readiness'", "'result'", "'subject_scorecard'", "'task_inspect'", "'task_runs'", "'tool_preference'"] -> ["'answers'", "'arms'", "'cli_routes'", "'eval'", "'eval_coverage'", "'explore_runs'", "'guidance_reach'", "'matrix_rollup'", "'need_graph'", "'novelty'", "'plan'", "'plugin_delta'", "'questions'", "'repo_budget'", "'repo_environment'", "'repo_readiness'", "'result'", "'subject_scorecard'", "'task_inspect'", "'task_runs'", "'tool_preference'"]

Updated on every push; the cli-interface-change label is on while the diff is non-empty.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved defects allow output paths to overwrite consumed input files, accept null needs labels without error, and produce incorrect threshold results from rounded metrics.

3 open issues
What changed in this PR

3 new findings.

This PR adds deterministic eval coverage reporting against a frozen need graph, partially delivering AC-J07-007 without model calls.

Changes:

  • Computes weighted coverage, critical gaps and per-eval relevance, with configurable coverage floors.
  • Adds graph/report schemas and artifact integration; preserves graph confirmation and provenance metadata.
  • Adds tests, CLI documentation and journey evidence for the new command.
File Description
dokimi/​tests/​test_runs_show.py Adds display expectations for both artifact kinds.
dokimi/​tests/​test_runs_scan.py Adds artifact discovery expectations.
dokimi/​tests/​test_import_architecture.py Registers the coverage module’s architectural layer.
dokimi/​tests/​test_eval_coverage.py Tests reports, validation, determinism and CLI behavior.
dokimi/​tests/​outcomes/​journeys.yaml Records partial delivery and enforced outcome evidence.
dokimi/​tests/​fixtures/​artifacts/​need_graph.v1.json Adds a frozen graph fixture.
dokimi/​tests/​fixtures/​artifacts/​eval_coverage.v1.json Adds a coverage report fixture.
dokimi/​tests/​data/​cli_flags_allowlist.txt Allows the graph and coverage-floor flags.
dokimi/​dokimi/​schemas/​need_graph.v1.json Defines the frozen graph format.
dokimi/​dokimi/​schemas/​eval_coverage.v1.json Defines the coverage report format.
dokimi/​dokimi/​runs/​kinds.py Registers artifact subjects and summary counts.
dokimi/​dokimi/​novelty/​needs.py Preserves confirmation and provenance metadata.
dokimi/​dokimi/​eval_coverage.py Implements label reading, coverage calculations and Markdown output.
dokimi/​dokimi/​commands/​evaluate.py Registers the coverage subcommand.
dokimi/​dokimi/​commands/​eval_coverage.py Adds CLI arguments, output handling and coverage gates.
dokimi/​dokimi/​cli_routes.py Classifies coverage as a free command.
dokimi/​dokimi/​artifacts.py Registers both schema versions.
dokimi/​docs/​user-journeys/​journeys.md Documents partial coverage-gap delivery.
dokimi/​docs/​reference/​cli-interface.json Updates the CLI interface snapshot.
dokimi/​data/​skills/​dokimi-design-evals/​references/​coverage-report.md Explains how generated coverage supplements the manual report.
dokimi/​changelog.d/​20261031_180000_napetrov_eval-coverage.md Announces the command and graph metadata changes.

Review effort: Balanced

Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
Comment on lines +56 to +58
if args.out and Path(args.out).resolve() == Path(args.graph).resolve():
print("Error: --out would overwrite --graph", file=sys.stderr)
sys.exit(EXIT_USAGE)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: --out is refused when it would overwrite any file the run reads, not only the graph. That covers the evals.json, task.toml/task.md, terminal-bench task.yaml and docker-compose.yaml, and a directory's discovered files. Red tests first: test_out_never_overwrites_an_input_file and test_out_never_overwrites_a_terminal_bench_task_file.

Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
Comment on lines +33 to +35
cov = report["coverage"]
return [f"{metric} coverage {cov[metric]:.2f} < {floor:.2f}" for metric, floor in floors
if cov[metric] is not None and cov[metric] < floor]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: --min is judged on the unrounded share (exact_shares), so 6 of 7 passes 0.85714 and 2 of 3 fails 0.66669. The report keeps its rounded display. Test: test_a_floor_is_judged_on_the_unrounded_share.

Comment thread dokimi/dokimi/eval_coverage.py Outdated
Comment on lines +75 to +76
if value is None:
return LabelledEval(eid, source, None)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: an explicit needs: null is malformed in both readers and exits 2; a missing needs key stays unlabelled. Tests: test_needs_that_is_not_a_list_of_ids_is_a_problem (null case) and test_a_null_task_needs_is_malformed_and_a_missing_key_is_unlabelled.

@exai-bot

exai-bot commented Nov 1, 2026 •

Copy link
Copy Markdown

✅ OpenCodeReview: Review complete: 0 finding(s) across 21 selected item(s).

Review details
  • Commit reviewed: 0f218cd against merge base b4a0a8c
  • Coverage: 21 files in 5 groups
  • Engine: OpenCodeReview v1.13.3, us.openai.gpt-6.1-sol
  • Usage: 547k tokens (351k from cache), 28s
  • Tool calls: 44 (38 file_read, 5 code_search, 1 file_read_diff)
  • Evaluation coverage computation, schema, and tests: 5 files
  • Evaluation coverage CLI integration and interface contract: 4 files
  • Need graph generation and artifact contract: 3 files
  • Evaluation artifact publishing and run discovery: 5 files
  • Coverage documentation, changelog, and user journeys: 4 files

Comment on lines +16 to +17
unreadable input, a graph with duplicate node ids, or a `needs` label that is not a list of
ids the graph has. Novelty's need graph now keeps `confirmed_by` and `provenance`. Building the graph, the user

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation · low
Nit (accuracy): needs: null is not a list, but it does not trigger exit 2. For example, {"evals": [{"id": "e", "needs": null}]} with a valid graph and no floor exits 0 and reports unlabelled: _labelled() treats None as a missing label. Either document that explicit null is also unlabelled, or distinguish missing keys from null in the reader and add a regression test so this exit-code promise holds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: an explicit needs: null is malformed in both readers and exits 2; a missing needs key stays unlabelled. Tests: test_needs_that_is_not_a_list_of_ids_is_a_problem (null case) and test_a_null_task_needs_is_malformed_and_a_missing_key_is_unlabelled.

Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
if not evals:
print(f"Error: no evals.json rows or tasks under {root}", file=sys.stderr)
sys.exit(EXIT_USAGE)
if args.out and Path(args.out).resolve() == Path(args.graph).resolve():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
Must-fix — input clobbering: dokimi eval coverage evals.json --graph graph.json --out evals.json passes this guard and replaces the source suite with the coverage artifact. save_artifact uses os.replace without an overwrite safeguard. The same issue affects directory inputs when --out names their evals.json, evals/evals.json, or a task configuration file read by read_labels. Reject output paths resolving to any input file, not just the graph, and add regression tests asserting exit 2 and unchanged source contents.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: --out is refused when it would overwrite any file the run reads, not only the graph. That covers the evals.json, task.toml/task.md, terminal-bench task.yaml and docker-compose.yaml, and a directory's discovered files. Red tests first: test_out_never_overwrites_an_input_file and test_out_never_overwrites_a_terminal_bench_task_file.

Comment thread dokimi/dokimi/eval_coverage.py Outdated
Comment on lines +75 to +76
if value is None:
return LabelledEval(eid, source, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: explicit "needs": null is accepted as unlabelled rather than reported as malformed. For example, {"evals":[{"id":"e","needs":null}]} produces no problems and the CLI exits 0, although a present needs value must be a list of strings. Both the JSON-row and task-metadata paths use .get("needs"), conflating a missing key with an explicit null. Use a missing-key sentinel (or pass key presence separately), reject explicit null, and add a regression test alongside the malformed-label cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: an explicit needs: null is malformed in both readers and exits 2; a missing needs key stays unlabelled. Tests: test_needs_that_is_not_a_list_of_ids_is_a_problem (null case) and test_a_null_task_needs_is_malformed_and_a_missing_key_is_unlabelled.



def _share(nodes: list[dict], covered: set[str]) -> float | None:
return round(sum(n["id"] in covered for n in nodes) / len(nodes), 4) if nodes else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: rounding coverage before threshold enforcement can give the wrong exit code. With three core nodes and two covered, core_rare_critical becomes 0.6667, so --min core_rare_critical=0.66669 passes even though actual coverage (2/3) is below the floor. Rounding can also incorrectly fail an above-floor suite. The CLI's _below() compares these report values directly. Preserve full precision for threshold comparisons, handling both _share() and the weighted calculation, and add rounding-boundary tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 91d982b: --min is judged on the unrounded share (exact_shares), so 6 of 7 passes 0.85714 and 2 of 3 fails 0.66669. The report keeps its rounded display. Test: test_a_floor_is_judged_on_the_unrounded_share.

@codecov-commenter

codecov-commenter commented Nov 1, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 99.03846% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.11%. Comparing base (b4a0a8c) to head (0f218cd).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/eval_coverage.py 98.34% 0 Missing and 2 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #340      +/-   ##
==========================================
+ Coverage   94.05%   94.11%   +0.06%     
==========================================
  Files         297      299       +2     
  Lines       38018    38226     +208     
  Branches     6424     6455      +31     
==========================================
+ Hits        35756    35975     +219     
+ Misses       1452     1442      -10     
+ Partials      810      809       -1     
Files with missing lines Coverage Δ
dokimi/dokimi/artifacts.py 95.12% <ø> (ø)
dokimi/dokimi/cli_routes.py 100.00% <ø> (ø)
dokimi/dokimi/commands/eval_coverage.py 100.00% <100.00%> (ø)
dokimi/dokimi/commands/evaluate.py 90.00% <100.00%> (+0.06%) ⬆️
dokimi/dokimi/novelty/needs.py 97.59% <100.00%> (+8.84%) ⬆️
dokimi/dokimi/runs/kinds.py 100.00% <ø> (ø)
dokimi/dokimi/eval_coverage.py 98.34% <98.34%> (ø)

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… floors, needs null is malformed

--out is refused when it would overwrite the graph or any file a read consumes,
including a tbench task.yaml and docker-compose.yaml. A --min floor is judged on the
unrounded share, so 6 of 7 passes 0.85714 and 2 of 3 fails 0.66669. An explicit
needs: null is malformed (exit 2); a missing needs key stays unlabelled. Red tests first.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
sys.exit(EXIT_USAGE)
report = coverage_report(graph, evals, str(root))
if args.out:
report = save_artifact("eval_coverage", report, Path(args.out))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: output filesystem failures are unhandled here, and the CLI entry point does not catch OSError. With valid inputs, python cli.py eval coverage evals.json --graph graph.json --out . reaches os.replace in save_artifact and raises a directory-related OSError, printing a traceback rather than a controlled CLI error. Permission-denied destinations fail similarly. Catch OSError around the save, print a concise error and exit with the appropriate error code; add a regression test using an existing directory as --out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3fa238b: a failing --out write (a directory, no permission) now prints Error: cannot write <path>: <reason> and exits 2, no traceback. Regression test: test_out_on_a_directory_is_a_controlled_error.

Comment thread dokimi/dokimi/eval_coverage.py Outdated
Comment on lines +234 to +235
lines += [f"| {e['id']} | {e['source']} | {', '.join(e['needs']) or '-'} | {e['relevance']} |"
for e in report["evals"]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: escape user-provided values before inserting them into Markdown table cells. A valid row such as {"id": "save|load", "needs": ["n1"]} adds an extra column, shifting Needs and Relevance under the wrong headings; an ID containing a newline splits the row. Graph node IDs and task-directory names can also contain these characters. Escape pipes and normalize CR/LF in IDs, sources and the joined needs cell, and add a rendering regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3fa238b: table cells escape | and flatten CR/LF (_cell in eval_coverage.py), so save|load and a newline in an id stay in their columns. Regression test: test_markdown_table_cells_escape_pipes_and_newlines.

…ipes and newlines; mypy

- _below reads the share into a local, so mypy narrows it (the CI mypy failure).
- A failing --out write (a directory, no permission) is a usage error, not a traceback.
- Markdown table cells escape "|" and flatten line breaks, so an id cannot add a column or split a row.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/eval_coverage.py Outdated


# every task file a task reader may parse: Harbor / BenchFlow (task.toml, task.md), terminal-bench v1 (task.yaml, compose)
_TASK_FILES = ("task.toml", "task.md", tbench_v1.CONFIG, tbench_v1.COMPOSE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
Must-fix: the overwrite guard omits instruction.md, allowing the report to destroy a supported task's instructions. With tasks/t1/task.toml containing [metadata]\nneeds = ["n1"] and an existing tasks/t1/instruction.md, eval coverage tasks --graph graph.json --out tasks/t1/instruction.md passes this guard and save_artifact() replaces the instruction file with JSON. Although the metadata-only reader does not read instructions, load_task() uses this file when executing the suite. Include existing instruction.md files in the protected paths and add an overwrite regression test alongside the configuration-file cases.

Suggestion:

Suggested change
_TASK_FILES = ("task.toml", "task.md", tbench_v1.CONFIG, tbench_v1.COMPOSE)
_TASK_FILES = ("task.toml", "task.md", "instruction.md", tbench_v1.CONFIG, tbench_v1.COMPOSE)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in db9ef3e. The overwrite guard now lists instruction.md, which load_task runs, so --out refuses it with exit 2 (red before the fix).
Test: test_out_never_overwrites_an_input_file[instruction].

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review at 3fa238b. The labels, relevance classes, the unrounded --min gate and the overwrite guard hold up. Three things are worth fixing before merge: a stray task.md aborts the whole run, a --min floor on an empty metric passes silently, and the gate tests miss the cases that decide the exit code.

What I ran, on a Linux review host at 3fa238b:

  • make ci-local: 30 passed, 0 failed, 7 skipped; 12,007 tests; the changed-line coverage step passes.
  • Reproductions for the two code findings through python -m dokimi eval coverage, with no model involved.
  • A mutation run: 105 mutants, 37 survived the new tests. The ones that decide an exit code or a reported figure are in the inline comment on the gate tests; I re-ran three of them (< to <=, default metric, no .resolve() in the guard) and all three pass the full file.

Checked, no issues

  • Schemas: need_graph.v1 and eval_coverage.v1 are new kinds, schema_compat against the base reports OK, and a save_graph file validates as written.
  • Docs: the changelog exit codes, --min [METRIC=]X forms and relevance classes match the code; cli-interface.json matches the parser.
  • Commits: all four carry Signed-off-by.
  • Round trip: confirmed_by and provenance survive save_graph then load_graph.
Bot points I checked that no longer apply at this head (9)
  • Copilot's point on --out overwriting an eval suite does not apply at this head: the guard now resolves --out against the graph and every file the run reads, and refuses with exit 2.
  • Copilot's point on comparing rounded values with --min does not apply: the floor is judged on exact_shares, computed from unrounded counts and weights.
  • Copilot's point on needs: null does not apply: an explicit null is malformed_needs in both readers and exits 2.
  • Exai-bot's point on --out clobbering an input does not apply at this head: the guard covers evals.json, the graph and every task file the run reads.
  • Exai-bot's point on needs: null does not apply: an explicit null is malformed in both readers and exits 2, and a missing key is unlabelled.
  • Exai-bot's point on rounding before the --min check does not apply: the gate uses the unrounded shares from exact_shares.
  • Exai-bot's point on an unhandled OSError from --out does not apply: the save is wrapped, prints Error: cannot write ... and exits 2.
  • Exai-bot's point on Markdown table cells does not apply: _cell escapes pipes and flattens line breaks.
  • Exai-bot's point on instruction.md does not apply as a defect of this guard: the run never reads instruction.md, and the guard protects the files the run reads. --out naming any other existing file replaces it, as it does in every command that takes --out.

Comment thread dokimi/dokimi/eval_coverage.py Resolved
Comment thread dokimi/dokimi/commands/eval_coverage.py Resolved
Comment thread dokimi/tests/test_eval_coverage.py Resolved
…k.md notes

--out could replace a task's instruction.md, which load_task runs, because the
overwrite guard listed task.toml and task.md but not instruction.md. A task
root with a planning-notes task.md (no frontmatter) aborted the report: the
directory counted as a task and read_task_config raised. Both now follow
discover_tasks: instruction.md is a protected input, and a lone task.md
without frontmatter is not a task.

Tests: the instruction.md overwrite (red before), the notes directory (red
before), a floor equal to the share, a bare --min on the weighted figure, a
hand-computed exact_shares case, and a relative --out to an input.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
if not evals:
print(f"Error: no evals.json rows or tasks under {root}", file=sys.stderr)
sys.exit(EXIT_USAGE)
if args.out and Path(args.out).resolve() in {Path(args.graph).resolve(), *input_files(root)}:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: output-path resolution can fail before reaching the controlled write-error handler. On supported Python 3.11/3.12, create a self-referencing symlink with ln -s loop loop, then run valid inputs with --out loop: Path.resolve() raises RuntimeError, which neither this command nor the CLI entry point catches, producing a traceback instead of exit 2. Resolution may also raise OSError. Wrap the overwrite guard's path resolution/input enumeration in error handling for both exceptions and add a symlink-loop regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in daa0a1e. The output-path guard now catches OSError and RuntimeError from resolve() and exits 2 with Error: cannot resolve <path>.

Regression test test_out_that_is_a_symlink_loop_exits_2_not_a_traceback: red on Python 3.11 (RuntimeError: Symlink loop), green on 3.11 and 3.14.

A floor on a coverage figure with no node behind it (n/a) used to pass
silently. It now exits 1 with "<metric> coverage n/a < X", as the
changelog and the --min help say. The changelog also records the known
gap: a terminal-bench v1 task.yaml has its metadata.needs ignored.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Path.resolve() raises RuntimeError on a symlink loop on Python 3.11 and
3.12, and the output-path guard let it escape. The guard now treats
OSError and RuntimeError as an unresolvable --out and exits 2.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
Comment on lines +52 to +54
if not root.exists():
print(f"Error: {root} does not exist", file=sys.stderr)
sys.exit(EXIT_USAGE)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: root.exists() is outside the input-error handler. On supported Python 3.11–3.13, checking a path beneath a directory without search permission raises PermissionError; the CLI's outer handler does not catch OSError, so this prints a traceback and exits 1 rather than the promised input-error exit 2. For example, as a non-root user, create locked/evals.json, run chmod 000 locked, then invoke eval coverage locked/evals.json --graph graph.json. Move the existence check into the existing input try block and add a regression test (mocking Path.exists to raise PermissionError also works).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095d2fe: the existence check now sits inside the input error handler, so an unreadable parent exits 2 with the error, not a traceback.
Test: test_an_unreadable_evals_directory_exits_2_not_a_traceback (mocked Path.exists).

Comment thread dokimi/dokimi/commands/eval_coverage.py Outdated
sys.exit(EXIT_USAGE)
if args.out:
try:
clobbers = Path(args.out).resolve() in {Path(args.graph).resolve(), *input_files(root)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
Must-fix: the guard checks only the report destination, but save_artifact also deletes &lt;out>.rejected after a successful save. For example, with a valid graph at cov.json.rejected, eval coverage skill --graph cov.json.rejected --out cov.json passes this check and then deletes the graph. An evals input at that sibling path is equally vulnerable. Include the resolved cleanup path in the input-collision check (or disable that cleanup for this command), and add a regression asserting the input remains intact.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 095d2fe: the --out guard also checks <out>.rejected, which save_artifact removes after a valid save. An input at that path is refused.
Test: test_out_whose_rejected_sibling_is_the_graph_is_refused (the graph is left intact).

… <out>.rejected

An unreadable parent directory made the existence check raise
PermissionError outside the input error handler, so it printed a
traceback. The check now sits inside that handler.

save_artifact removes <out>.rejected after a valid save, so an input at
that path was deleted. The --out guard now treats it as an output too.
The guard moves to a helper, which keeps the command under the
complexity ratchet.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The delta closes all three threads: a task.md without frontmatter is skipped as discover_tasks does, a --min floor on an n/a metric now exits 1, and the gate tests pin the cases I listed. One nit on the symlink-loop test.

What I ran, on a Linux review host at 095d2fe:

  • make ci-local: 30 passed, 0 failed, 7 skipped; 12,017 tests; changed-line coverage 98%
  • Mutation run on the new code (14 mutants): dropping the <out>.rejected sibling, the n/a branch, instruction.md in the protected files, the frontmatter rule, or moving root.exists() out of the try each fail a test. The survivors are the RuntimeError entry in the --out except tuple and errors="replace" in _is_task.
  • Probes: a task.md without frontmatter next to a task.toml with --out on it exits 2 and leaves the file intact; relative --out to <graph>.rejected exits 2; a non-UTF-8 task.md exits 0.

Checked, no issues

  • Commits: all 8 non-merge commits carry Signed-off-by.
  • Docs: the changelog now states the n/a exit 1 and the terminal-bench task.yaml gap; --help for --min matches the code.
Bot points I checked that no longer apply at this head (12)
  • Copilot's point on --out overwriting an eval suite does not apply: the guard resolves --out against the graph and every file the run reads, and exits 2.
  • Copilot's point on comparing rounded values with --min does not apply: the floor is judged on exact_shares, from unrounded counts.
  • Copilot's point on needs: null does not apply: an explicit null is malformed in both readers and exits 2.
  • exai-bot's point on --out clobbering an input does not apply: the guard covers evals.json, the graph and every task file.
  • exai-bot's point on needs: null does not apply: null is malformed, a missing key is unlabelled.
  • exai-bot's point on rounding before the --min check does not apply: the gate uses exact_shares.
  • exai-bot's point on an unhandled OSError from --out does not apply: the save is wrapped and exits 2.
  • exai-bot's point on Markdown table cells does not apply: _cell escapes pipes and flattens line breaks.
  • exai-bot's point on instruction.md does not apply: instruction.md is now in the protected task files and --out on it exits 2.
  • exai-bot's point on a symlink-loop --out does not apply: resolve() errors are caught and exit 2.
  • exai-bot's point on root.exists() outside the handler does not apply: it is inside the try and a PermissionError exits 2.
  • exai-bot's point on the <out>.rejected cleanup does not apply: the guard checks that sibling too, and an input there is refused.

Comment thread dokimi/tests/test_eval_coverage.py Resolved
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

@antonwolfy antonwolfy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I pushed a commit per nit, up to 0f218cd, which fix the nit from my review:

  • 0f218cd: dokimi/tests/test_eval_coverage.py:396: the symlink-loop test now patches Path.resolve to raise RuntimeError("Symlink loop") for the --out path and asserts exit 2 and cannot resolve in stderr, so it reaches the except branch on every Python.

make ci-local at 0f218cd: 30 passed, 0 failed, 7 skipped.

@antonwolfy
antonwolfy merged commit ad734f5 into intel:main Nov 1, 2026
17 checks passed
antonwolfy added a commit to antonwolfy/dokimi that referenced this pull request Nov 1, 2026
Bring in main at ad734f5 (intel#339, a SCIP code index reader; intel#340, eval
coverage against a frozen need graph). Both sides register new artifact
kinds at the end of the same lists, which are in insertion order, not
sorted. Each conflict keeps both sides, main's entries first:

- dokimi/dokimi/artifacts.py: the module docstring names need_graph and
  eval_coverage and then repo_hits_judged; SCHEMA_VERSIONS lists
  need_graph.v1 and eval_coverage.v1, then repo_hits_judged.v1.
- dokimi/dokimi/runs/kinds.py: FIELDS keeps the need_graph and
  eval_coverage rows, then the repo_hits_judged row.
- dokimi/tests/test_runs_scan.py: EXPECTED keeps the need_graph.v1 and
  eval_coverage.v1 rows, then the repo_hits_judged.v1 row.

The auto-merged docs/reference/cli-interface.json is what
scripts/cli_surface.py dump writes for the merged tree, so it needs no
change. cli_routes.py, tests/data/cli_flags_allowlist.txt,
tests/test_runs_show.py and the contract keep the entries of both sides.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
antonwolfy added a commit to antonwolfy/dokimi that referenced this pull request Nov 1, 2026
Bring in main at ad734f5 (intel#339, a SCIP code index reader; intel#340, eval coverage against a frozen need graph). No conflicts. CI's schema_compat compares the branch with main, which gained eval_coverage.v1 and need_graph.v1 after this branch's base.

Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
napetrov added a commit to napetrov/dokimi that referenced this pull request Nov 1, 2026
Keep both sides of the schema-kind, allowlist and runs-test conflicts:
headroom (intel#279) and need_graph / eval_coverage (intel#339, intel#340) are additive.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
napetrov added a commit to napetrov/dokimi that referenced this pull request Nov 1, 2026
Keep both sides of the artifact docstring, stamp table, kind table and scan
expectations: need_graph and eval_coverage (intel#339, intel#340) sit beside dokimi_config.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cli-interface-change Changes dokimi's user-facing CLI surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants