Repository navigation
fix(novelty): analyze keeps what a --max-usd stop paid for in a partial file - #633
Conversation
…al file (intel#624) A `skill novelty analyze` run stopped by the spend cap (or by a call with no known price) exited with "...; nothing was written": the extracted atoms, the need graph and every probe answer already paid for were lost, and `atoms` and `probe` had kept theirs since the partial-result work. `analyze` now tracks the stage it is in and what each stage produced. A spend stop in extraction, the faithfulness check, the need graph, probe wording, the measurement, classification or attribution returns a partial record instead of raising: `status: "partial"` and a `partial` block (`reason`, `stage`, `message`, `cause`, and `spans_read`/`calls_done` where known), the extracted atoms, the spans, the probe set once built and the probe answers and judgments so far, and no summary over the uneven arms. `skill novelty analyze` writes it to `<out stem>.partial.json` beside `novelty.json` with the sidecar rules of `atoms` and `probe` (never over a finished file), and exits 1 with a message that names the file and says whether a rerun with `DOKIMI_RESPONSE_CACHE` replays the paid calls at $0. The file opens with `status`, so `runs` does not list it as a finished run, and `load_artifact("novelty", ...)` refuses it, which covers `novelty view` and `--novelty-artifact`. `novelty.v1` is widened in place: `status`, `partial`, `extracted_atoms`, `source_spans` and `runs` are new optional properties, and `paired`, `k0` and `spans` leave the schema's `required` list; `validate_artifact` still requires them of every novelty record without `status`, so a finished record is checked as before. `skill shorten` shares no code with this path and still writes nothing at the cap. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
… file (intel#624) The `except ValueError` branch of `replay_hint` had no test. A capped `analyze` run with `DOKIMI_RESPONSE_CACHE` set to an existing file now has a test that the exit message says the variable was not set on this run and a rerun pays again, and does not claim the paid calls replay. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 18 selected item(s). CI at This PR has reached its review limit, so only a re-request starts a review. Review details
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #633 +/- ##
==========================================
+ Coverage 95.67% 95.71% +0.03%
==========================================
Files 394 397 +3
Lines 53026 53247 +221
Branches 9137 9161 +24
==========================================
+ Hits 50733 50963 +230
+ Misses 1417 1410 -7
+ Partials 876 874 -2
🚀 New features to boost your workflow:
|
…l#624) Settles three review points on the partial analyze file. A record with `status: "partial"` and no `partial` block is refused by `load_artifact`, with or without validation; the schema lets either marker stand alone. The partial record's `probes` block gains `items`, each probe's atom id, kind, question and answer key, so a stop right after the wordings were generated (before any answer) still keeps what was paid for. The finished record is unchanged. The recovery hint after a `no_price` stop says to price the model or drop `--max-usd`, since a higher cap does not clear it; the higher-cap advice stays for `spend_cap`. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…el#624) Settles the same-class check of 633/4550285228: `load_probe_cells` refused a partial probe file only when the `partial` key was present, so a file with `status: "partial"` and no `partial` block was read by `skill novelty items` as a finished measurement. It now refuses either marker, as `load_artifact` does for `novelty.json`. A test writes a status-only probe file and expects the refusal naming the file; the changelog fragment names the reader. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…versal call (intel#633) Settles the review point that the partial record lost the probe wordings when the --max-usd stop fell inside wording generation. build_probe_set takes an optional `into` set that it fills in place, and analyze hands it the Progress probe set before the first wording call, so the direct probes and every accepted paraphrase are in the partial record when the reversal call raises SpendCapReached. A test stops the run between the paraphrase and the reversal call and checks the partial record keeps the direct and paraphrase probes and their fidelity row. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
|
The red |
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
vchamarthi
left a comment
There was a problem hiding this comment.
Review at 0fc4dc2. 3 new findings: 2 must-fix, 1 nit. The change makes a --max-usd stop in skill novelty analyze keep its paid work in novelty.partial.json instead of writing nothing, and makes the readers of a finished novelty.json refuse that file. The changed tests pass on 0fc4dc2, and a loader that checks only the partial key is caught. One must-fix: a spend stop in extraction leaves cause out of its partial block, which the schema and docs say a spend stop carries.
Verified
- Finished run unchanged: A run that the cap does not stop returns no
partialblock and nostatuskey, as on main. - Cap in extraction: A cap that stops extraction after 50 of 200 spans keeps the 50 atoms, marks the 150 other spans unread, and the record validates against
novelty.v1. - Cap between the two wording calls: A cap that falls after the paraphrase call and before the reversal call keeps the direct and paraphrase entries in
probes.items, and has noreversalkind. - Call with no known price: A call with no price stops the run with
reason: no_priceand nocause, and the rerun advice does not mention a higher--max-usd, even withDOKIMI_RESPONSE_CACHEset. - Cache variable names a file: When
DOKIMI_RESPONSE_CACHEnames a file rather than a directory, the stop message says the cache was not set on the run, and does not promise a replay. - Readers refuse a partial novelty file:
load_artifact("novelty", ...)raises for a partial file with or without validation, andnovelty view, the subjects loader anddatasets_exportall go through it. - Status-only partial
skill novelty probefile: Askill novelty probefile that carriesstatus: "partial"and nopartialblock is refused byload_probe_cells. - Loader refusal of status-only partial: A loader that checks only the
partialkey lets a status-only partial record through, and the readers test fails on it. - Merge with main: The PR merges onto current main with no conflict.
Not verified
- The replay promise in the changelog fragment, the docs and the stop message (a rerun with a higher
--max-usdand the sameDOKIMI_RESPONSE_CACHEreplays the paid calls at $0) is not tested end to end. No test runs a cappedanalyzewith the cache set, then a second run on the same cache, and checks the second run's spend and atoms: Add that two-run test: capanalyzewithDOKIMI_RESPONSE_CACHEset, rerun with a higher cap on the same cache, and assert zero spend on the replayed calls and the same atoms.
Pre-existing, not blocking
dokimi/dokimi/novelty/atoms.py:787:load_atoms_recordrefuses an atoms file only when it has apartialkey. An atoms file with"status": "partial"and nopartialblock loads as a finished atom set: on0fc4dc2and on main,load_atoms_recordreturns the atoms and raises nothing for such a file. The PR fixes the same check for novelty files (dokimi/dokimi/artifacts.py:321) and not this one; the thread names it as tracked in #642.
Findings outside the diff
dokimi/dokimi/schemas/novelty.v1.json:13
nit: The published novelty.v1 schema no longer requires paired, k0 or spans, so a schema-only check of a finished record passes without them.
On 0fc4dc2 the required list at lines 7-13 names five keys; on main it names eight, with paired, k0 and spans. dokimi/runs/show.py:147 checks a record with schema_errors(schema, data) alone, so a finished novelty.json without the three summaries gets no error there; on main the same check reports the missing key. validate_artifact (dokimi/artifacts.py:215) still enforces them on save and load, so save_artifact and load_artifact are unchanged. The PR's risk note names this for tools that validate with the schema file, and runs show is one of them.
Also at: dokimi/dokimi/runs/show.py:147.
Suggestion: Have runs show also run the summary check of validate_artifact for a record without status, or move that check into schema_errors for novelty.v1 so both readers share it.
dokimi/dokimi/novelty/probes.py:471
must-fix: A no-price stop in the wording stage discards the paraphrase wordings its paid reply carried, so probes.items keeps only the direct entries.
On 0fc4dc2, analyze run with SpendCap(limit_usd=1.0) through the _run helper of dokimi/tests/test_novelty_analyze_cap_partial.py with cost_of=lambda prompt: None if 'Reword each question' in prompt else 0.0 returns a partial record with reason: no_price at stage: probes. Its probes.items holds only direct entries, although the scripted transport returned valid paraphrases. The reply is on SpendCapReached.reply (dokimi/dokimi/novelty/client.py:183), but _generate catches only Exception at line 471, and SpendCapReached is a BaseException, so the exception leaves build_probe_set before any paraphrase is appended. On main the same input raises SpendCapReached out of analyze and writes no record, so main has no partial record to compare. The changelog fragment says the partial record keeps the generated wordings in probes.items, and it does not limit that to the spend cap. No test covers this: test_a_call_with_no_price_stops_analyze_and_is_named_as_such asserts reason, the absence of cause and the message, and test_a_cap_between_the_two_wording_calls_keeps_the_paraphrases_already_paid_for uses a spend cap, not a no-price stop.
Suggestion: Read the wordings from SpendCapReached.reply in _generate when it is set, as dokimi/dokimi/novelty/atoms.py:309-313 does for extraction, and raise the stop only after build_probe_set has appended them to into; the reversal call needs the same handling. Assert the paraphrase kind in the no-price test.
… probe set A wording call with no known price is paid for and its reply comes back on SpendCapReached.reply. build_probe_set now keeps those paraphrase or reversal wordings before the stop is raised, as extraction already does for its atoms. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…says A --max-usd stop in extraction wrote a partial block with no cause, while novelty.v1 and docs/skill-novelty.md give every spend_cap block one (what was spent). The CLI message for that stop now carries the spend too. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…store the calls store_response fails quietly on a path it cannot write (under a regular file, a read-only directory), yet the stop message still promised that a rerun replays the paid calls at $0. The hint now asks for a writable directory. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
…tial record The partial analyze record is a stored artifact, but its cost, run_config, sampling, faithfulness and the need_graph stage label could be dropped or emptied with every test still green. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
The role clients moved to dokimi.novelty.cost on main, so the command tests patch cost.clients. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
…ailed Settles the review point that the stop message promised a free replay when a cache write failed on a full disk or a quota (the directory passes the writable check, `store_response` logs the OSError and keeps nothing). `store_response` now remembers the cache directories a write failed in during the process, `response_store_failed` reports it, and `replay_hint` treats such a directory as no cache. The hint then says the cache could not be written and a rerun pays again. Also adds the end-to-end test the review asked for: a capped `analyze` through the real `LLM` path with `DOKIMI_RESPONSE_CACHE` set, then a rerun with a higher cap on the same cache, which asks the model none of the prompts the first run paid for, spends only on the fresh ones and keeps the same atoms. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…or spans Settles the review nit that `runs show` checks a record with `schema_errors` alone, and `novelty.v1` no longer lists `paired`, `k0` and `spans` as required in the schema file, so a finished `novelty.json` without them showed as valid. The check that `validate_artifact` made inline is now `summary_errors` in `dokimi/artifacts.py`, and `runs show` adds it to the schema's errors. A record with `status` (a partial one) is still valid without the summaries. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@vchamarthi (#633 (review)): No-price wordings (must-fix): already kept, since 065f019 ( |
@xaleryb (#633 (comment)): A status comment on commits already in the head. The |
Settles codecov on probes.py 490-491: a wording call with no price whose reply carries no JSON array is paid for but unreadable. The test checks the stop is still raised and each atom asked for is tallied as a rejected empty wording, for both the paraphrase and the reversal call. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
Review at aefc7e2. The change is right: a --max-usd or no-price stop at any stage of analyze keeps what was paid for in <out stem>.partial.json, and every reader of a finished novelty.json refuses it. What is left is small: a few fields of the partial record mean something else than in a finished one, and a library path that can still save it as finished. Details are in the line comments.
What I ran, at aefc7e2:
make ci-local --offline: 0 failed, 21 passed, 19 skipped (network-only jobs); the suite 18,968 passed, 72 skipped.- A capped
analyzewithmax_atoms=1(measurement stop vs. finished run), asave_artifactof the partial record followed byruns.scan.head_version, and an extraction stop withseed=7, max_atoms=30to see itsrun_config,samplingandwarnings.
Checked, no issues
- Earlier threads: the status-only partial refusal (
load_artifact,load_probe_cells), the reason-aware replay hint, the probe wordings kept inside and between the wording calls and from a no-price reply,causeon an extraction stop, the full-disk cache case and theruns showsummary check are all in the code and each has a test. - Stage tracking:
SpendCapReachedis aBaseException, so theexcept Exceptionblocks in the wording and need-graph code do not swallow it; each stage setsprogress.stagebefore its first paid call. - Schema and docs:
novelty.v1is only widened (fewer required keys, new optionalstatus,partial,probes.items), whichschema_compataccepts; the docs and the changelog fragment match the behaviour; the new files have the SPDX header.
…ne and cannot be saved as one Settles the review threads on the partial record of intel#633. - `atoms.n` is the probed sample once the cap's sample is chosen, as a finished record counts it, and every extracted atom for a stop in extraction (`sampling` is `none` there, now documented in the schema). - A stop in extraction keeps `run_config`: it is set before the extraction stop is raised. - `save_artifact` refuses a `novelty` record with `status` or `partial` with an `InputError`, the check `load_artifact` makes, and the `analyze` docstring says a caller checks `result.partial`. - The schema describes `calls_done` as the answers paid for, ungraded ones included. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…ig, not its warnings The fragment claimed a stop in extraction keeps the extraction warnings, but the code does not set them: a cap stop's warnings are empty by design and the partial block already names the unread spans (`extraction.unread_span_ids`). The fragment now says only what the code does; the docs and the PR text already did. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
@vlad-perevezentsev (#633 (review)): The review's points are its four threads above ( |
vlad-perevezentsev
left a comment
There was a problem hiding this comment.
LGTM
I checked b74a588. 3038f94 settles all four points from my last round: atoms.n counts the probed sample as a finished record does, an extraction stop keeps run_config and sampling: none is documented, save_artifact refuses a partial novelty record, and the calls_done wording matches the code. Leaving the warnings of an extraction stop empty is fine with me: unread_span_ids and spans_read/spans_total already say what was not read.
Merge: safe now: it merges cleanly into current main, 5 commits behind, and its tests pass on the merge. Once it is in, #702 (docs/skill-novelty.md), #595 (commands/novelty.py) and #472 (novelty/probes.py) conflict with it and need a rebase.
What I ran, at b74a588:
make ci-local --offline: 0 failed, 21 passed, 19 skipped (network-only jobs); the suite 18,976 passed, 72 skipped.- On this branch merged with current
main: the analyze cap, probe cap,runs showand analyze tests, 201 passed.
Checked, no issues
- New tests: the capped measurement and need-graph stops assert
atoms.n == 1with two atoms extracted, the extraction stop asserts the seed andmax_atomsinrun_config, and thesave_artifacttest covers either marker alone; each asserts a value the code before 3038f94 got wrong. - Docs and changelog: the fragment and
skill-novelty.mddescribe the refusal insave_artifact, theatoms.nrule and the library caller'sresult.partialcheck.
@vlad-perevezentsev (#633 (review)): Approval with no change requested. I checked the refusal you describe at |
Summary
Issue #624 reports two problems. The first is that
skill novelty analyzeloses everything it paid for when--max-usdstops it. Onmain, a capped run (answers cost 0.6 USD each under--max-usd 1) exits withthe measurement stopped: 11 probe answers were not run or graded; spend cap reached: $1.20 of $1.00 after 9 calls (next call: judge-m). Raise --max-usd, or measure less (a cheaper --tier, a lower --max-atoms); nothing was written, and the output directory holds nothing: no atoms, no probe answers.atomsandprobealready keep a partial result at the cap;analyzedid not. A cap in extraction, the faithfulness check, the need graph, probe wording, classification or attribution lost its work the same way.mainanalyzestopped by--max-usdnothing was writtennovelty.partial.jsonbesidenovelty.jsonwithstatus: "partial", the stage reached, the extracted atoms, the spans and the probe answers and judgments so far; exit 1 naming the file--max-usdand the sameDOKIMI_RESPONSE_CACHEreplays them at $0, or that the cache was not set on this run--estimate-onlybelow the real billTaken: a partial sidecar written with the helpers and sidecar rules
atomsandprobeuse (<out stem>.partial.json, never over a finished file, FIFO/read-only/long-name handling shared), because the pieces already exist and the readers' refusal model is the same. The exit code stays 1.skill shortenshares no code with this path and still writes nothing at the cap; this is tracked in #630. Problem 2 (the estimate) needs measured runs per tier and role and touchesestimate_usd, which an open PR moves tonovelty/cost.py; it is tracked in #631. Exit code 3 for a budget stop that kept its partial output would changeatoms,probe,routingandanalyzetogether against a documented table, and is tracked in #632. What this PR leaves of the issue is tracked in the three follow-ups, so closing the issue loses none of it.Closes #624
Changes
dokimi/novelty/analyze.py:analyzewraps the pipeline body (_analyze) and records each stage and what it produced; a spend stop anywhere returns anAnalyzeResultwithpartialset instead of raising. A cap that stopped extraction (already recorded on the extraction report, and ignored byanalyzebefore) now ends the run there. The probe stage handsbuild_probe_setthe empty set onProgress(into=), which the builder fills in place, so a cap that falls between the paraphrase call and the reversal call keeps the direct probes and the accepted paraphrases.dokimi/novelty/analyze_partial.py(new): the partial record.status: "partial"and apartialblock (reason,stage,message,cause,spans_read/spans_totalorcalls_done/calls_planned),extracted_atoms,source_spans, the probe set once built (its counts and fidelity figures, plus every probe's wording and answer key inprobes.items, so a stop before the first answer keeps what the wording calls paid for) andruns. It holds nopaired,arms,k0or view summaries, since the cut leaves the arms uneven.dokimi/commands/novelty_budget.py,novelty_run.py,novelty.py:analyzesaves the record throughwrite_runand exits with the message, the file and theDOKIMI_RESPONSE_CACHEhint (true for these calls: every novelty call goes through the response cache, and a cut-off or unusable reply is asked again). The hint promises the replay only when the cache directory is writable and no cache write failed during the run (caching.response_store_failed, set whenstore_responsehits anOSErrorsuch as a full disk); otherwise it says a rerun pays again. After ano_pricestop the hint says to rerun with the model priced, or without--max-usd, since a higher cap does not clear it; aspend_capstop still says a higher--max-usd.dokimi/artifacts.py:load_artifact("novelty", ...)refuses a partial file by name, whether it carriesstatus: "partial", thepartialblock or both, with or without validation, which coversnovelty viewand--novelty-artifact;save_artifact("novelty", ...)refuses such a record with anInputErrorand writes nothing, so the library path cannot save it as a finished run (the command writes the partial file throughwrite_run).dokimi/novelty/items.py: a probe file read byskill novelty itemsis refused on either marker too. The record opens withstatus, sorunsskips it, as it doesatoms.partial.json.dokimi/schemas/novelty.v1.json: widened in place withstatus,partial,extracted_atoms,source_spansandruns;paired,k0andspansleaverequiredandvalidate_artifactrequires them of every record withoutstatus(a schemaallOfwas reported as breaking byschema_compat). That requirement isartifacts.summary_errors, whichvalidate_artifactandruns showboth apply, soruns showreports a finished record without them as invalid.atoms.nis the probed sample once the cap's sample is chosen, as a finished record counts it (max_atoms=1on a two-atom skill:n: 1, sampling: cap-spread:1in both), and every extracted atom for a stop in extraction, wheresamplingisnone(documented in the schema). A stop in extraction also keepsrun_config(seed,max_atoms, arms, budgets,prompts_sha256). The schema sayscalls_donecounts the answers paid for, the ungraded ones included.analyze()'s docstring says a caller checksresult.partial.docs/skill-novelty.md: the two sentences that saidanalyzeloses its work now describe the partial file.scripts/growth_baseline.json:analyze.py::analyzeis_analyze(complexity 21, unchanged).Risk
A capped
analyzestill exits 1 and writes nonovelty.json; it now also leavesnovelty.partial.json. A finished run writes what it wrote before. A script that checks the output directory for emptiness after a cap sees one more file.novelty.v1accepts the partial record and no longer listspaired,k0andspansas required in the schema file itself;validate_artifactandruns showenforce them for any record withoutstatus, but a tool that validates with the schema file alone would accept a finished record without them.Testing
On a Linux review host, at
b74a588(onmainfc9fbf5):make ci-local: 32 passed, 0 failed, 13 skipped (docker/network smoke, Claude Code install, the other Python matrix legs, conda-forge, fuzz, semgrep, test-latest and the other jobs that do not run on a PR). 19029 tests passed, 19 skipped; changed-line coverage is reported per file.tests/test_novelty_analyze_cap_partial.py(a cap in extraction, the faithfulness check, the measurement and the classify stage, a call with no price, the command, the finished file untouched, readers refusing the file,runsskipping it) all fail onmain(the run exits withnothing was written) and pass here.statusfrom the record (5 fail), makingload_artifactaccept a partial file (1 fails), writing overnovelty.jsoninstead of the sidecar (3 fail), and treating aDOKIMI_RESPONSE_CACHEthat names a file as an active cache (the test for that branch fails).partialonly, notstatus(the readers test fails), droppingprobes.itemsfrom the record (the stop-after-the-wordings test fails), advising a higher cap for every stop reason (the no-price hint test fails).skill novelty itemsreads a probe file throughload_probe_cells, which refused a partial file only by itspartialkey: a stub probe file withstatus: "partial"and nopartialblock was accepted as a finished measurement. Now refused (test_novelty_items_refuses_a_probe_file_marked_partial_by_status_alone); the test fails with the key-only check.test_a_cap_between_the_two_wording_calls_keeps_the_paraphrases_already_paid_for): the partial record keeps the direct and paraphrase probes and their fidelity row. Mutant: not sharing theProgressprobe set withbuild_probe_set(assigning it after the build returns) leaves the record withoutprobes, and the test fails withKeyError: 'probes'.OSErrorENOSPC from the write, in a directory that passes the writable check) used to leave the message promising a $0 replay; now it says a rerun pays again (test_no_replay_is_promised_when_a_cache_write_failed_on_a_full_disk). Mutant: not recording the failed directory instore_responsefails it; onmainit fails too.analyzewithDOKIMI_RESPONSE_CACHEset, then a rerun with a higher cap on the same cache through the realLLMpath (test_a_rerun_with_a_higher_cap_and_the_same_cache_replays_the_paid_calls_at_zero): the rerun sends none of the prompts the first run paid for, spends only on the fresh ones, and keeps the same two atoms. Mutant:store_responsestoring nothing fails it (the rerun asks the model for the paid prompts again).runs showon a finishednovelty.jsonwithoutpaired,k0andspanslists three errors, and shows a record withstatus: "partial"as valid (test_a_finished_novelty_record_without_its_summaries_is_invalid_but_a_partial_one_is_not). Mutant:runs showusing the schema errors alone fails it.no_priceand each atom asked for is tallied as a rejected empty wording (test_a_no_price_wording_reply_that_is_not_json_keeps_no_wording_and_still_stops, paraphrase and reversal). Mutant: skipping that tally when the reply cannot be parsed fails both cases.len(progress.extracted)foratoms.n(test_a_capped_run_stopped_in_measurement_counts_the_probed_sample_as_a_finished_one_doesandtest_a_cap_in_the_need_graph_counts_the_sample_not_the_extractionfail), not settingrun_configbefore the extraction stop (test_a_cap_in_extraction_keeps_the_atoms_read_and_marks_the_rest_unreadfails), and removing the partial check fromsave_artifact(test_save_artifact_refuses_a_partial_novelty_record_and_writes_nothingfails forstatus,partialand both).tests/test_novelty_cap_partial.pythat expectedanalyzeto raise now check the partial block.test_llm_delegates,test_judge_sampling,test_model_defaults,test_openai_compatible_route,test_llm_retry_credentials) on pydantic'sReadOnlyTypedDictUserWarningraised by a newer LiteLLM. They do not touch this PR;mainignores that warning for any class since 3b61dd0 (test: ignore pydantic's ReadOnly TypedDict notice for any class #610), which this branch now contains. Those five files pass here at the merge ofmain(117 passed).Revert
A plain revert restores the old behaviour (a capped
analyzewrites nothing). Nothing else is stored; anovelty.partial.jsonalready written stays on disk, is no longer refused by name, and would fail the old schema, which requirespaired,k0andspans.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)