Skip to content

feat(eval): add eval headroom to flag items with no headroom - #279

Open
napetrov wants to merge 57 commits into
intel:mainfrom
napetrov:feat/eval-ceiling-floor
Open

napetrov wants to merge 57 commits into
intel:mainfrom
napetrov:feat/eval-ceiling-floor

Conversation

@napetrov

@napetrov napetrov commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Part of #220.

eval headroom reads one or more arms.v1 files of the same question set, one per repetition, and lists the items that a treatment comparison cannot learn from:

  • ceiling: the no-skill arm already scores at least 0.9 (default). A skill cannot raise it.
  • floor: every arm that some run scored is at most 0 (default), and at least one non-baseline arm scored. An arm no run scored is left out of this test.
  • provisional: fewer than 3 runs (default) scored the no-skill arm for the item.

It only reports. No score, gate or comparison changes. The table always goes to stdout; --out writes a headroom.v1 JSON file. Why: a ceiling or floor item shows nothing about a skill, and one run's flag can be noise.

Evidence

  • Red then green: each new test failed before its fix and passes after. The regressions cover the review findings: a mean of 89.996 is not a ceiling; a mean of exactly 0.9 or 0.3 across runs is flagged despite float error; an unknown --baseline-arm exits 2 and lists the arms; the same file given twice counts once; a question id repeated in one file exits 2; a non-ASCII digit id is named, not a traceback; 0.89996 prints as 0.89 and is not flagged.
  • Fresh-context review (FRESH-REVIEW-A1, on 0965221) found two blockers, both fixed in df78ff5: a mistyped --baseline-arm gave zero ceilings with exit 0, and repeated inputs counted as extra runs. Its unscored-arm nit is fixed in 050de2e. An in-session pass found the float-boundary blocker, fixed in 0965221.
  • Local CI on 050de2e (make ci-local --offline): ruff, schema, registry, guidance scan, schema_compat, mypy (273 files), deptry, mkdocs and the CLI-surface job (which reports the added eval headroom) pass. Pytest: 9729 passed, 5 skipped, 1 failed: test_container_agent.py::test_probe_reports_a_writable_docker_socket (AF_UNIX path too long). The same test failed on a clean intel/main checkout in an earlier run, so it is not from this change. bandit flagged only files in a local .venv inside the package directory; with that excluded, bandit is clean.

Merge Danger

  • New command eval headroom and new persisted format headroom.v1 (schema, fixture, registry entry, widening-only compatibility). Both are new in this PR.
  • Exit codes: 0 on success, including when nothing is flagged. 2 on an unreadable input, an arms or run_config of the wrong top-level shape (arms must be a list of strings), inputs of different configurations or judge contexts (a mixed judge_context is refused on its own), an unknown --baseline-arm, a question id repeated within one file, a file where no run scored the no-skill arm, an --out that cannot be written, names a directory, or is one of the inputs, and any other usage error (for example a threshold out of range). 1 only when --out is refused by the schema (an internal failure).
  • The thresholds (0.9, 0, 3) are defaults, not calibrated values. The command is report-only; do not gate on it yet.
  • JSON goes only to --out. The table always goes to stdout.
  • Known limits:
    • With an explicit --baseline-arm, the baselines the runs declare are not compared. Without it, the runs must declare the same baseline (documented).
    • reps counts the runs in which the no-skill arm scored the item. Pass one file per repetition; repeated copies are skipped with a note.
    • Scores are not range-checked. The judge writes 0-100; an aggregate of 150 reads as 1.5 and flags a ceiling, and -5 flags as a floor.
    • There are no confidence intervals. Paired statistics stay in dokimi.eval.stats.

🤖 Generated with Claude Code

The headroom command reads arms.v1 runs of one question set and flags an item
as a ceiling when its no-skill arm's mean is at least 0.9, or as a floor
when every arm's mean is at most 0. An item with fewer than three runs
behind it is provisional. The table goes to stdout; -o writes a
headroom.v1 artifact. Flags are descriptive: no score or gate changes.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The per-arm mean was rounded to four decimals before the threshold check,
so an item at 89.996 (below 0.9) was flagged as a ceiling. The flags now
compare the exact mean; the artifact still shows four decimals.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A mistyped --no-skill-arm read as "no ceiling anywhere" and exited 0.
It is now refused (exit 2), naming the arms the runs list.

reps counted score entries, so one file passed three times, or a
question id repeated within one file, faked three runs and lifted the
provisional guard. The CLI now drops repeated input files by content
digest with a note, and a question id repeated within one run is an
error. The library counts each Run as one run.

Also: the JSON keeps the unrounded mean (no 4-decimal rounding that
turned 0.89996 into 0.9), the table rounds down with the flag's
tolerance, the table lists the control arm once, a non-ASCII digit
question id is named in the error instead of crashing, and the docs
say the explicit --no-skill-arm skips the baseline agreement check.

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

reps was the smallest run count over the arms, so one arm the skill
missed in a run lowered the item's reps. It is now the number of runs
in which the no-skill arm scored the item.

An arm no run scored is listed in the item's unscored_arms (JSON and a
table note) and is left out of the floor test. The floor also needs at
least one non-baseline arm that scored, so the baseline alone at zero
is not a floor.

-o that cannot be written prints "Error: ..." and exits 2. The
duplicate-input note says "byte-identical", as the docs do. The docs
state that JSON goes to -o only and the table always goes to stdout.

headroom.v1 is not on intel/main yet, so the schema gains unscored_arms
in place without a version bump.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
@github-actions github-actions Bot added the cli-interface-change Changes dokimi's user-facing CLI surface label Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 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 headroom`
    + argument `arms`
    + option `--baseline-arm`
    + option `--ceiling`
    + option `--floor`
    + option `--help/-h`
    + option `--min-reps`
    + 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'", "'explore_runs'", "'guidance_reach'", "'headroom'", "'matrix_rollup'", "'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.

@codecov-commenter

codecov-commenter commented Oct 29, 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 98.03150% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.07%. Comparing base (b4a0a8c) to head (c574718).

Files with missing lines Patch % Lines
dokimi/dokimi/commands/headroom.py 97.16% 2 Missing and 1 partial ⚠️
dokimi/dokimi/eval/headroom.py 98.63% 1 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #279      +/-   ##
==========================================
+ Coverage   94.05%   94.07%   +0.02%     
==========================================
  Files         297      299       +2     
  Lines       38018    38272     +254     
  Branches     6424     6468      +44     
==========================================
+ Hits        35756    36003     +247     
- Misses       1452     1457       +5     
- Partials      810      812       +2     
Files with missing lines Coverage Δ
dokimi/dokimi/artifacts.py 95.12% <ø> (ø)
dokimi/dokimi/cli_routes.py 100.00% <ø> (ø)
dokimi/dokimi/commands/evaluate.py 90.00% <100.00%> (+0.06%) ⬆️
dokimi/dokimi/runs/kinds.py 100.00% <ø> (ø)
dokimi/dokimi/eval/headroom.py 98.63% <98.63%> (ø)
dokimi/dokimi/commands/headroom.py 97.16% <97.16%> (ø)

... and 1 file with indirect coverage changes

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

@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 050de2e. The flag logic is sound and the edge cases I probed hold. Two test gaps let a real regression change the verdict; I'd close both before merge. One doc nit.

What I ran, on a Linux review host at 050de2e:

  • make ci-local: 27 passed, 1 failed, 7 skipped. 9731 tests passed, 4 skipped; the changed-line coverage step passes. The failing pip-audit step lists advisories against filelock 3.32.7 in the lock files, which this PR does not touch.
  • Reproductions of the CLI on hand-built arms files (empty, non-UTF-8, directory, non-arms, mixed question sets, unknown --no-skill-arm, odd thresholds). No traceback, and exit codes match the docs.
  • A mutation run of 79 broken copies of the new code: 30 passed the 36 headroom tests. The two that decide a verdict are inline; I re-ran both by hand.

Checked, no issues

  • Schema: the CLI's -o output and the fixture validate against headroom.v1.json; headroom is registered in SCHEMA_VERSIONS and the schema is new, so schema_compat has nothing to widen.
  • Commits: all five carry Signed-off-by.
  • Dedupe and reps: a repeated or byte-identical file counts once; a repeated question id in one file exits 2; an item missing from some runs gets fewer reps and is provisional.

Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/dokimi/commands/headroom.py Resolved
Comment thread dokimi/docs/evaluating-treatments.md 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.

I pushed a commit per nit, up to 38d219c, which fix the nit from my review:

  • 38d219c: dokimi/docs/evaluating-treatments.md:757: added the sentence that an out-of-range threshold (--ceiling in (0, 1], --floor in [0, ceiling), --min-reps at least 1) exits 2.

make ci-local at 38d219c: 27 passed, 1 failed (pip-audit, unchanged from the reviewed head), 7 skipped.

Not approving yet. Still open: the two risks from my review.

@vchamarthi

vchamarthi commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 1 issue(s) in this PR.

By severity: 1 low

Severity Category Location Finding
low bug headroom.py:33 Nit: an arms file that is deeply nested JSON (e.g.
Review details
  • Commit reviewed: 95f84dd against merge base f0f95a2
  • Coverage: 8 files in 2 groups
  • Engine: OpenCodeReview v1.13.3, us.anthropic.claude-sonnet-5-5
  • Usage: 266k tokens (148k from cache), 1m0s
  • Tool calls: 10 (5 code_search, 4 file_read, 1 code_comment)
  • Headroom evaluation feature: core logic, command, schema, and tests: 6 files
  • Documentation and changelog for headroom feature: 2 files

Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/tests/test_headroom.py Resolved
Comment thread dokimi/tests/test_headroom.py Resolved
…-ceiling-floor

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
An item with baseline 40 and skill 0 must not be a floor. Red when the floor
test drops the baseline (all(... if a != control)), green on head.

Thread: PRRT_kwDOVBg7g86xhYFw
Thread: PRR_kwDOVBg7g88AAAABU4x1TA
Thread: PRR_kwDOVBg7g88AAAABU5Bc8g
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…mand

Red when the command calls headroom() with the default thresholds; the test
asserts the flags and the thresholds of the -o artifact for ceiling 0.5,
floor 0.2, min-reps 1.

Thread: PRRT_kwDOVBg7g86xhYFz
Thread: PRR_kwDOVBg7g88AAAABU4x1TA
Thread: PRR_kwDOVBg7g88AAAABU5Bc8g
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Red when an option is renamed, its dest changes or a default drifts from the
DEFAULT_* constants; one test also runs the command from parsed arguments.

Thread: PRRT_kwDOVBg7g86xlrgs
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Five items cover ceiling+provisional, floor, unflagged+provisional and
unflagged+steady. Red when flagged_provisional counts every provisional item,
no_flag is hard-coded, or the floor count is read as the ceiling count.

Thread: PRRT_kwDOVBg7g86xlrf-
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Numeric ids come first in numeric order, then the other ids. Red when the sort
is lexicographic, insertion order, or ignores the numeric key.

Thread: PRRT_kwDOVBg7g86xlrgN
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Each rejected threshold now names its own message (ceiling 0, negative and
above 1; floor equal to the ceiling and negative; min_reps 0 and negative),
and ceiling=1.0, floor=0 and min_reps=1 are accepted. Red when a bound moves.

Thread: PRRT_kwDOVBg7g86xlrgX
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
headroom([]) is refused as "no runs to read"; a run with no questions and a run
nothing was judged in give no flag, at library and command level. Red when the
empty-input refusal is removed or the command stops printing the table.

Thread: PRRT_kwDOVBg7g86xlrgc
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
With no_skill_arm="skill" on runs that declare "baseline", no_skill, reps and
both flags follow "skill"; the mixed-baseline case now asserts its item too.
Red when a given arm is ignored and the declared baseline is returned.

Thread: PRRT_kwDOVBg7g86xlrgh
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The patch-coverage report named lines of dokimi/eval/headroom.py (the empty-input
refusal) and the output-less branch of dokimi/commands/headroom.py. The tests of
544394e and 4bbc04a cover both: test_headroom.py alone reaches 100% of lines
and branches of the two files.

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

Copy link
Copy Markdown
Contributor Author

Your two test gaps are closed: the floor baseline term in d5113e0 and the non-default thresholds through the command in 15695ac. The doc nit was already fixed on the branch by you in 38d219c. The pip-audit failure is in lock files this PR does not touch.

@napetrov

Copy link
Copy Markdown
Contributor Author

The two open risks are fixed: d5113e0 (baseline term of the floor test) and 15695ac (non-default thresholds through the command). Thanks for the doc commit 38d219c.

@napetrov

Copy link
Copy Markdown
Contributor Author

This review is the empty run marker of the OpenCodeReview bot; it carries no claim. Its six findings were the inline threads, each answered there.

Path() drops a trailing slash, so -o newdir/ with newdir absent passed
the directory check and wrote a plain file named newdir while printing
"Wrote headroom artifact to newdir/". The check now also refuses a
trailing separator, and "", "." and ".." by name. Red first: the
absent-with-slash case wrote the file before this change.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Two gaps in the directory refusal added for -o:
- "newdir/." (absent) ended in ".", not a separator, so its last component
  was "newdir" and the save wrote a plain file of that name. The check now
  reads the last component of the spelling: "", "." or ".." refuses.
- is_dir() raises PermissionError under a parent without search permission,
  which escaped the guard as a traceback. An unstat-able path is now not a
  directory, and the save reports it as before.

The message says "names a directory"; the exit-2 list in
evaluating-treatments.md names the directory and .rejected refusals.

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

A trailing separator on an existing regular file (-o existing.json/) was
refused with "names a directory", which is false for a file. The message
now says the spelling is not a file name, which holds for both. The
directory test pins the wording and that an existing file is left alone,
and the stat-error test asserts the save writes the file when the directory
check cannot stat its path.

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

"Every item is provisional" held for one file only: reps add up across the
files of one run, so three files that score an item give reps=3 and it is
not provisional. The exit-2 list also names the path-with-separator refusal.

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

Copy link
Copy Markdown
Contributor Author

The CI failures named in the 9a191ba review are fixed on the branch: the flag registry check in test_cli_flags.py by 7b69f5a (the four eval headroom flags allowlisted with reasons), and the mypy "CLI snapshot is current" check by 9872a50 (docs/reference/cli-interface.json regenerated). At 6a1e7f2, tests/test_cli_flags.py and tests/test_headroom.py pass (83 passed, 1 skipped), and a fresh dump of the CLI matches the committed snapshot. The line-level items are answered in their threads: 37cd2c8, 6a1e7f2, 1e164b8.

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

# Conflicts:
#	dokimi/dokimi/artifacts.py
… row

--output/-o becomes --out (the primary artifact is always a file; -o and
--output are not canonical), and --no-skill-arm becomes --baseline-arm (the
baseline concept's spelling). The route table lists eval headroom as free:
it reads arms files and makes no model call. Snapshot regenerated.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The destination checks tested the value's truth, so "" skipped the
directory and clobber checks and the save, and the command exited 0
without the artifact. They now test for None; "" is refused with exit 2.

Address review: refuse an empty output through the real parser

Thread: PRRT_kwDOVBg7g86yKCDW
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Exit 2 is for input and usage errors. A write that the OS refuses is a
runtime failure, so eval headroom exits 1 for it, as the other commands do.

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

Copy link
Copy Markdown
Contributor Author

Pushed 156c82d. What changed in this round:

  • Merged intel/main (72d7add) into the branch.
  • eval headroom has its route row (free: it reads arms files and makes no model call) and the flag registry's spellings: --output/-o is --out, and --no-skill-arm is --baseline-arm.
  • A failed write of --out exits 1 (runtime failure). Input and usage errors stay 2.
  • The empty destination --out "" is refused with exit 2 (58891e3, the review thread).

The PR body and docs/evaluating-treatments.md match the new spellings and exit code.

napetrov and others added 5 commits October 30, 2026 22:03
eval headroom is in the route table as a leaf, so the snapshot lists it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The runs scan and show tests check every schema version has a row. headroom.v1
was added by this PR without one, so the Tests and Windows smoke jobs failed.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The exit-code table in docs/quickstart.md gives exit 2 to an output
that cannot be written, and exit_on_unwritable implements it. Use it;
a schema refusal of the payload stays exit 1.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
Inputs count as repetitions, but runs of different models, judges,
run configs or libraries were averaged per question id and lifted the
provisional flag. Compare report.model_compare.replicate_key and the
library, as model compare does, and exit 2 on a mismatch.

Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
@xaleryb

xaleryb commented Oct 31, 2026

Copy link
Copy Markdown
Contributor
  • Ran on Python 3.13: the full suite, ruff, mypy, bandit and the benchmark gate, on the head plus the commits below, alone and merged with current main. All green.
  • The runs kinds table row for headroom.v1 is fixed in b971c61. The earlier runs list / runs show KeyError no longer reproduces.
  • Pushed:
    • 97f0f7a: when --out cannot be written (--out f/out.json with f a file), the command exited 1. The exit-code table in docs/quickstart.md gives 2 for this case. It now uses exit_on_unwritable. A schema refusal still exits 1.
    • 976f06a: arms files from different models, judges, run configs or libraries were averaged per question id as repetitions, which lifted the provisional flag. They now exit 2, using report.model_compare.replicate_key plus the library. Runs that differ only in seed still pool.
  • Left for the author, in the PR body (it becomes the squash message):
    • Known limits says an unjudged file exits 0. It exits 2.
    • It says -5 never flags. It is flagged floor.
    • The test count is stale. Suggest dropping the exit-0 bullet, adding "no run scored the no-skill arm" to the exit-2 list, and dropping the count.- With refactor(cli): bad input exits 2 through one InputError #316: tests/test_headroom.py::test_command_exits_2_on_an_unreadable_input asserts file not found, and refactor(cli): bad input exits 2 through one InputError #316 changes that message to cannot read <file>: No such file or directory (exit code still 2). Whichever of the two lands second changes that one assertion.

Comment thread dokimi/dokimi/commands/headroom.py Outdated
or library: averaging such runs per question id would mix different measurements."""
from dokimi.report.model_compare import replicate_key

keys = [(replicate_key(dict(run.meta)), run.meta.get("library_name")) for run in runs]

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: records.load preserves top-level metadata without validating run_config, but replicate_key calls .items() on it. For example, running eval headroom bad.json with {"baseline_arm":"baseline","run_config":"bad","evaluations":[{"question_id":"1","scores":{"baseline":{"aggregate":95}}}]} raises an uncaught AttributeError, even with only one input. This call is outside the command's error handlers, so malformed input produces a traceback rather than a diagnostic and exit 2. Validate that each non-null run_config is an object before computing the key, report the offending input path, and add a command 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.

Confirmed on 976f06a: eval headroom with an input whose run_config is a string ("bad") crashed in replicate_key with an uncaught AttributeError and a traceback, exit 1.

Fixed in ea85151. Right after each input loads, the command checks that a non-null run_config is a JSON object and otherwise exits 2 with Error: <path>: run_config must be a JSON object, not str (the path names the offending input). Regression test test_command_exits_2_when_run_config_is_not_an_object covers a string, a number and a list; it failed before the change and passes after.

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 aa49e53 (a non-object run_config exits 2) and eabdcda (any top-level arms or run_config of the wrong shape exits 2, naming the file and field). Tests: test_command_exits_2_when_top_level_metadata_has_the_wrong_shape, test_command_exits_2_when_run_config_is_not_an_object. Head c574718. The earlier reply citing ea85151 is not on this branch.

@napetrov
napetrov force-pushed the feat/eval-ceiling-floor branch from ea85151 to aa49e53 Compare October 31, 2026 19:31
@napetrov

Copy link
Copy Markdown
Contributor Author

Correction to my reply on the run_config thread: the fix is now aa49e53b, not ea851518. I re-signed the commit for the DCO check (Signed-off-by) and moved the new check into a helper, so cmd_eval_headroom stays under the complexity cap of the growth ratchet (the Tests job failed on that). The behaviour is the same as described in the reply.

records.load keeps the top-level metadata as is, and replicate_key calls
.items() on run_config, so a scalar or list run_config crashed eval
headroom with a traceback. The command now exits 2 and names the input.
The check is a helper, which keeps cmd_eval_headroom under the
complexity cap of the growth ratchet.

Thread: PRRT_kwDOVBg7g86yOcdc
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/commands/headroom.py Outdated
or library: averaging such runs per question id would mix different measurements."""
from dokimi.report.model_compare import replicate_key

keys = [(replicate_key(dict(run.meta)), run.meta.get("library_name")) for run in runs]

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: validate top-level arms before calling replicate_key. For example, an input containing {"baseline_arm":"baseline","arms":5,"evaluations":[{"question_id":"1","scores":{"baseline":{"aggregate":95}}}]} passes records.load, which preserves the malformed metadata, but replicate_key executes tuple(5) and raises an uncaught TypeError. Even a single input therefore produces a traceback instead of the expected exit-2 diagnostic. Extend the metadata validation to reject a non-null arms value that is not a list of arm names, and add a command 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 eabdcda and c574718: a non-null top-level arms that is not a list of strings exits 2, naming the file and field. Regression tests in test_headroom.py (the shape test, parametrized over arms). Head c574718.

first_seen[digest] = str(path)
runs.append(run)
inputs.append(str(path))
_exit_unless_replicates(runs, inputs)

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: replicate_key does not include judge_context, so this check accepts runs scored under different judge protocols. For example, two otherwise identical scored arms files, one with judge_context: "treatment_material.v1" and one without that field, pass this guard and have their scores averaged. The latter judge saw only report snippets, not the stored treatment material, so these are different measurements; report/model_compare.py::_check_judge_context explicitly refuses this pairing. Check the protocol stamps here as well, including refusing to pool treatment_material.v1+snippet_fallback runs, and add a CLI regression expecting exit 2 for incompatible protocols.

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 c574718: judge_context joins the replicate key, so differently stamped runs are refused as not repetitions, and a mixed-protocol input exits 2 (the rule of report.model_compare._check_judge_context). Tests: test_command_exits_2_when_runs_were_judged_under_different_contexts, test_command_exits_2_on_a_mixed_judge_context.

…hape

replicate_key reads run_config as a mapping and arms as a list, so an
arms file with "arms": 5 (or a scalar run_config) crashed eval headroom
with a traceback. The command now checks both fields before grouping
and exits 2, naming the file and the field.

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

replicate_key calls .items() on run_config and tuple() on arms, so a wrong
type there crashes it, or a string or object arms is silently split.
The comment names both, instead of calling every wrong shape a traceback.

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

replicate_key does not read judge_context, so runs judged on the stored
treatment material and runs judged on report snippets were averaged as
repetitions. judge_context now joins the replicate key, and a run with the
mixed protocol exits 2, as report.model_compare already refuses it. A list
of arms holding a non-string now exits 2 too.

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

Copy link
Copy Markdown
Contributor Author

@xaleryb mapping your items (head c574718):

first_seen[digest] = str(path)
runs.append(run)
inputs.append(str(path))
_exit_unless_replicates(runs, inputs)

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: matching configuration metadata does not establish that shared question IDs refer to the same questions. For example, give two otherwise identical arms files question_id "1", but question_text "What is X?" with baseline aggregate 100 in one and "What is Y?" with baseline aggregate 0 in the other. This guard accepts both, and headroom pools them into one item with mean 0.5 and two reps, hiding the first question's ceiling. Before pooling, reject conflicting non-empty question text (and available ground truth) for shared IDs, naming the input and ID; add a command regression test. Partial runs can still be allowed without requiring identical ID sets.

@vlad-perevezentsev vlad-perevezentsev 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 c574718. The flag registry point is fixed: 6399536 takes the registry's --baseline-arm and --out, and the branch merges cleanly into current main. One point from my first round is still partly open, and I replied in its thread: an item whose no-skill arm is unscored still counts as unflagged.

Agreed with @exai-bot: files that share a question id but not its text are pooled into one item.

What I ran, at c574718:

  • make ci-local --offline: 0 failed, 19 passed, 13 skipped (network and Docker jobs).
  • Two arms files with the same config, seeds 1 and 2, and question 1 worded differently (baseline 100 in one, 0 in the other) give one item at 0.5 with 2 reps and no flag.
  • A file where item 2's baseline is unscored: unflagged 1 for that item. --out "" exits 2.

Checked, no issues

  • Repetition guard: 976f06a and c574718 refuse files whose model, judge, judge context, run config other than the seed, harness, arms or library differ. Runs that differ only in seed still pool. eabdcda refuses a run_config or arms of the wrong shape before replicate_key reads them.
  • Exit codes: an unwritable --out exits 2 through exit_on_unwritable (97f0f7a); a schema refusal of our own payload exits 1. The docs say the same.
  • Earlier rounds: the --out guard (inputs reached through .., a symlink, the .rejected sibling, a directory, an empty name), the 0.01 threshold floor, with_docs-named arms and long digit ids all hold, each with a test.

This branch has not been deployed

No deployments
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.

7 participants