Repository navigation
refactor(novelty): one flag-to-setting resolver (RunSettings); thin the novelty CLI - #105
Conversation
…he novelty CLI analyze, --estimate-only, atoms and probe each mapped the tier-controlled flags to settings by hand: five mapping sites and 22 direct flag reads in commands/novelty.py (`0 if args.no_paraphrase else 1` three times, the estimate rebuilding the settings dict once more). tiers.resolve_settings(tier, flags) -> RunSettings is now the only mapping. RunSettings owns the forfeits, the warnings, the run_config.tier record and the --estimate-only bill. AnalyzeConfig carries tier/tier_overrides (filled by RunSettings.config_fields) and takes its setting defaults from DEFAULT_SETTINGS; analyze() builds RunSettings.of(config). analyze()'s `tier=` argument still works and records the same thing. The CLI parses, calls and prints. Arm parsing moved to measure.parse_arms, probe-file loading and the item record to items.load_probe_cells / item_analysis, the routing field and query-file checks and the catalog to routing.check_field / load_queries / load_candidate / catalog_competitors, the default-judge choice to client.default_judge. _atom_cap and tiers.resolve are gone. Every message, exit and ordering is kept: the new domain errors are ValueError subclasses the CLI catches by name, so an unreadable or non-JSON file still raises as before. No output changes: tests/test_novelty_settings_parity.py compares 40 CLI cases with a golden captured on main at f76a760. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #105 +/- ##
==========================================
+ Coverage 90.26% 90.40% +0.14%
==========================================
Files 206 206
Lines 24289 24488 +199
Branches 3906 3944 +38
==========================================
+ Hits 21924 22138 +214
+ Misses 1700 1690 -10
+ Partials 665 660 -5
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟢 Approval recommended
No blocking issues were identified, and the parity evidence supports preservation of existing CLI behavior.
No open issues 🎉
What changed in this PR
Continues #4 and #44 by centralizing novelty run settings and moving domain logic out of the CLI.
Changes:
- Adds
RunSettingsfor flag resolution, estimates, warnings, and tier records. - Moves arm parsing, input loading, item analysis, and judge selection into domain modules.
- Adds golden parity tests and updates interface tests.
| File | Description |
|---|---|
| dokimi/tests/test_novelty_tiers.py | Tests settings resolution and round trips. |
| dokimi/tests/test_novelty_settings_parity.py | Adds 40 CLI parity cases. |
| dokimi/tests/test_novelty_routing.py | Tests extracted routing helpers. |
| dokimi/tests/test_novelty_measure.py | Uses domain arm parsing. |
| dokimi/tests/test_novelty_items.py | Tests probe loading and analysis. |
| dokimi/tests/test_novelty_cli.py | Checks CLI validation and warnings. |
| dokimi/tests/test_novelty_analyze.py | Tests config-carried tier metadata. |
| dokimi/tests/test_default_branches.py | Uses relocated judge selection. |
| dokimi/tests/fixtures/novelty_settings_main.json | Stores baseline parity expectations. |
| dokimi/dokimi/novelty/tiers.py | Centralizes settings and derived records. |
| dokimi/dokimi/novelty/routing.py | Adds routing input helpers. |
| dokimi/dokimi/novelty/measure.py | Adds paired-arm parsing. |
| dokimi/dokimi/novelty/items.py | Adds probe loading and analysis assembly. |
| dokimi/dokimi/novelty/client.py | Houses default judge selection. |
| dokimi/dokimi/novelty/analyze.py | Integrates shared defaults and settings. |
| dokimi/dokimi/commands/novelty.py | Delegates domain logic to shared helpers. |
Review effort: Balanced
antonwolfy
left a comment
There was a problem hiding this comment.
The refactor holds up: I found no behaviour change against the base. Two test gaps are worth closing before merge, because the lines they cover are new code that nothing pins.
What I ran, on a Linux review host at e8e52f3:
make ci-local: 18 passed, 1 failed, 4 skipped. The failure ischangelog, which locally cannot see theskip-changeloglabel; the PR has it and the check passes on GitHub, so it is not a finding. 5236 tests passed, changed-line coverage 98% (misses:commands/novelty.py:282,293, the no-description warnings).- 20 CLI scenarios on the base and on the head (estimate-only with and without a tier,
--all-atomsvs--max-atoms, bad--arms, routing and items error paths): stdout, stderr and exit status are byte-identical. The LLM transport was mocked. - A mutation run on the new code: 148 mutants, 108 killed. The survivors that matter are in the two inline comments below.
- The golden fixture: the parity test and
novelty_settings_main.jsonpass on the base (f76a760) as well, and regenerating the fixture on the head gives the same bytes.
Checked, no issues
- Precedence:
--all-atomsstill beats--max-atoms, with and without a tier. A flag equal to the tier's value is not recorded as an override, and overrides stay{}without a tier. - Defaults:
DEFAULT_SETTINGSholds scalars only,resolve_settingscopies it, and mutating a resolvedvaluesdoes not touch it. - Removed helpers: no reference to
tiers.resolve,_arms,_atom_cap,_default_judgeor_catalog_competitorsis left in code, tests or docs. - Error handling:
ProbeFileErrorandRoutingInputErrorare caught only around their own calls, and error ordering matches the base. - Repo rules: SPDX header on the new test module, the JSON fixture is exempt, no schema change, tests run offline.
…sion Add test cases that verify arm parameter is correctly passed through item_analysis to discrimination and kind_agreement functions. Catches mutations that drop arm= or hardcode "full". Add test for catalog_competitors exclusion set to catch mutations where --competitors names are dropped from the exclusion, causing duplicates. Clarify in analyze.py docstring that tier= replaces both tier and overrides entirely, not merged with config.tier_overrides. Fixes intel#105 review threads. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
Review effort: Lite
antonwolfy
left a comment
There was a problem hiding this comment.
Review at aa4a428. The new tests close the kind_agreement half of my arm thread. The other two threads are still open: two of the three mutants I reported survive the new tests. The docstring thread is resolved.
What I ran, on a Linux review host at aa4a428:
make ci-local: 18 passed, 1 failed, 4 skipped. The failure ischangelog, which locally cannot see theskip-changeloglabel, so the label covers it. 5239 tests passed, changed-line coverage 98%.- A mutation check of the three mutants from my earlier review against the new tests, with
test_novelty_items.py,test_novelty_routing.py,test_novelty_cli.pyandtest_novelty_settings_parity.pyrun together.
Still open from my earlier review:
dokimi/tests/test_novelty_items.py:301: nothing pinsarm=armin thediscriminationcall. Hardcodingarm="full"inkind_agreementis now caught.dokimi/tests/test_novelty_routing.py:439: the exclusion set{gold.name, *named competitors}incommands/novelty.py:290is still unpinned by any test.
Checked, no issues
- Docstring: the
tier=text atanalyze.py:158matches the code. A truthytierreplaces both the config's tier and its overrides, with no merge. - Source change: only that docstring changed since e8e52f3. The commits since are one docstring edit and the new tests.
…rsing 1. Validate that `runs` is a list before iterating, yielding clearer error messages for non-list or non-dict entries. 2. Use ast.walk to detect direct `args` flag reads instead of regex scan, avoiding false positives in comments and docstrings. 3. Replace O(n²) names.count in set comprehension with Counter for competitor name deduplication. Addresses review concerns about error clarity, test robustness, and efficiency. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… exclusion The discrimination test asserted on kind_agreement and used two models, below the three-respondent minimum, so dropping arm= from discrimination went unnoticed. The catalog exclusion had only a direct call, not the CLI's set. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
12d1675 replaced the broad except with two isinstance guards; nothing tested them, so removing either turns a malformed probe file into a TypeError traceback with CI green. Add {runs: null} and {runs: [1]}. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. Both pins from my earlier review now hold, and the new type guards in load_probe_cells are tested. Two nits inline.
What I ran, on a Linux review host at a4419d1:
make ci-local: 18 passed, 1 failed, 4 skipped. The failure ischangelog, which locally cannot see theskip-changeloglabel, so the label covers it. 5242 tests passed, changed-line coverage 99%.- A mutation run on the delta since aa4a428: 10 mutants, all killed. They were: each
isinstanceguard removed,arm=dropped fromdiscrimination,kind_agreementhardcoded to"full", the catalog exclusion reduced to{gold.name}, twocheck_fieldduplicate-check mutants, and three directargs.<flag>reads added tocommands/novelty.py(plain,no_verify, inside a lambda).
I agree with @xaleryb that "byte-identical" no longer holds for novelty items --probes. New evidence: {"runs": null} now reports has `runs` as NoneType, not a list instead of 'NoneType' object is not iterable. The new text is better; reword the description.
Checked, no issues
- Arm pass-through: the rewritten test uses three models, so
discriminationhas variance, and it fails whenarm=is dropped. - Catalog exclusion:
test_a_competitor_that_is_also_in_the_catalog_is_counted_oncefails when onlygold.nameis excluded. - Guards:
except KeyErroris enough after theisinstancechecks.ProbeRuntakes any field value, so noTypeErroris reachable. check_field: theCounterversion returns the same duplicate set asnames.count.
…rop a local import A probe file holding a JSON list reached data.get and escaped as a raw AttributeError instead of the ProbeFileError the other guards raise. routing.py re-imported Counter inside the function although it is already imported at module level. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. Both nits from my earlier review are fixed in 05f18d8, and the delta since a4419d1 is three small changes. One nit on the description.
What I ran, on a Linux review host at 05f18d8:
make ci-local: 18 passed, 1 failed, 4 skipped. The failure ischangelog, which locally cannot see theskip-changeloglabel, so the label covers it. 5243 tests passed, changed-line coverage 99% (misses:commands/novelty.py:282,293, the no-description warnings).- A mutation check on the delta: removing the
isinstance(data, dict)guard inload_probe_cellsfails the new[1]case withAttributeError: 'list' object has no attribute 'get'.novelty items --probeson files holding"x",5,null,1.5,trueand[]exits 1 with theProbeFileErrortext and no traceback.
Nit on the description. It lists _arms among the removed CLI helpers, but _arms is still in commands/novelty.py:798, now a thin wrapper that turns the ValueError from parse_arms into a SystemExit. It has three callers (lines 66, 84, 190). The behaviour is right; reword the "removed" line.
Checked, no issues
- Delta: the
isinstanceguard raisesProbeFileErrorbeforedata.get, the function-localCounterimport is gone, andcheck_fielduses the module-level import (routing.py:37). - Tests:
test_novelty_items.py,test_novelty_routing.pyandtest_novelty_cli.pypass together.


Part of #4.
Follow-up to #44 (its "then
RunSettingsand a thin CLI" line).Summary
analyze,--estimate-only,atomsandprobeeach mapped the tier-controlled flags (--max-atoms,--all-atoms,--no-*) to settings by hand. Now one resolver does it:The CLI now parses, calls and prints. The domain logic moved next to the code it belongs to:
The probe record already moved to
report.build_probe_recordin #44.resolve_settings)commands/novelty.pycommands/novelty.pylines_clientstub seam)_clients), 9 testsEvidence
This is a refactor, so the evidence is that nothing changed.
tests/test_novelty_settings_parity.pyruns 40 CLI cases with scripted clients, so nothing goes over the network. The cases cover--estimate-onlyfor every tier, overrides (including a flag that matches the tier),--all-atomsbeating--max-atoms, every--no-*, budgets, arm counts, rejected arm sets and--max-usd. They also cover fullanalyzeruns,atomsandprobe. For each case the test stores the exit status, stdout, stderr, model calls per role,run_config(includingtier), warnings, the atom cap and the probe kinds. The golden filetests/fixtures/novelty_settings_main.jsonwas captured onmainat f76a760, and the module only uses interfaces that exist there.Before: 41 passed on
main. After: 41 passed on this branch.noveltysubcommand (97 cases, 146 invocations), includingitems,routing,shorten,viewand all their error paths. It recorded stdout, stderr, the exit or exception, calls per role and every file written (novelty.json,.md, annotated views,atoms.json,probes.json,routing.json,items.json). The capture from this branch is byte-identical to the one frommain. Error ordering is kept on purpose:--armsis still checked where it was checked before;ValueErrorsubclasses (ProbeFileError,RoutingInputError) and the CLI catches them by name.RunSettings: config round trip, estimate, warnings, andtier=matchingconfig.tier;parse_arms,load_probe_cells/item_analysis,check_field/load_queries/catalog_competitors,default_judge;test_every_tier_controlled_flag_is_read_through_the_resolver, which fails ifcommands/novelty.pyreads one of those flags directly again.pytest-randomlyseeds: 5235 passed;mypy(strict config) clean, no exemption added;ruffand pre-commit on the changed files pass;make ci-local --offline: 13 passed, 7 skipped (network/docker), 1 failed:changelog, waiting for the label below;maintoo.Merge Danger
Door: two-way. Everything is code, with no persisted-format or schema change. A revert restores the old call sites.
Blast Radius: none
CLI: output, artifacts, numbers, warnings and exit codes are unchanged (shown above). No new flags.
Library, additive:
AnalyzeConfiggainstierandtier_overrides, both optional (None/{}).run_configis written exactly as before.analyze(..., tier={...})still works. It now rebuilds the record from the dict'snameandoverrides, so other keys in a hand-built dict are no longer copied intorun_config.tier. The CLI never passed any.Library, removed:
dokimi.novelty.tiers.resolveis replaced byresolve_settings. It is not exported fromdokimi.novelty, and its only callers were the CLI and tests._arms,_atom_cap,_default_judgeand_catalog_competitorsare removed.Changelog:
skip-changelog. There is no user-visible effect: CLI output is byte-identical, and the public API change is additive.Overlap:
analyze.pyandtest_novelty_analyze.py.git merge-treemerges it cleanly, and the 624 novelty tests pass on the merged tree.Not in this PR. Each of these would change behaviour:
probe --tier:probewrites no tier record, and themax_atoms/verify/classify/shorten forfeits don't describe a probe run.--max-usdstays outsideRunSettings: it caps the clients, not what is measured, and it is not inrun_config.probechecks--armsonly after generating wordings (2 calls are spent before it refuses).analyzewith an unknown arm fails with a traceback after extraction.probeaccepts--max-atoms/--all-atomsand ignores them.All of these are candidate follow-ups on Architecture cleanup: fix live bugs, then deepen the LLM, eval-record, novelty, arms and process modules #4.
Checklist
pytestpassesruff,mypy, and schema checks pass.github/.licenserc.yaml): the new test module has it; the JSON golden is exempt🤖 Generated with Claude Code