Stop the QC dashboard asserting a coverage target nothing can reach (#325) - #492
Conversation
…325) #325 reports curation_history on 2 of 311 records. Still 2, now of 312. The issue's preferred fix - option 3, point the QC config's `timestamp_paths` at the `history/` provenance tree - **cannot be done from this repo.** `_corpus_timestamp(records, paths)` in the shared generator reads from `records`, which are the YAMLs under `yaml_dir`. `timestamp_paths` selects a dotted path *within each record*; it cannot name a different directory. So option 3 needs a claw-side change, not a config change here. The `history/` tree does exist and is being written to (2 entries), so the layer is real - the dashboard just cannot read it. What is wrong and fixable here is smaller and separate: the config asked for **90%** curation_history coverage against **0.6%** actual. That slot could only ever clear via a bulk backfill of 310 records - which is the open question in #325, not something curation closes - so it had been warning permanently. A gate in that state teaches its readers to skip past it, and the next slot to go genuinely wrong inherits the habit. Set to 0.0, matching the `discussions` precedent already in the file: honest that there is almost no data, never blocking. Raise it when coverage actually grows, which it now can - since #395 every `gtdb_ground.py` write appends an event, so a grounding sweep records itself. tests/test_qc_thresholds_are_reachable.py draws the distinction that matters. A threshold may sit above current coverage - that is what a target is - but not so far above that no plausible curation closes it: associated_datasets asks 0.30, has 0.282 six records short, a target curation_history asked 0.90, had 0.006 a factor of 150, not a target Mutation-checked: restoring 0.90 fails two tests. Every other slot passes today. Worth recording, because it makes #325 less urgent than filed: the dashboard already labels the thin base on its face - "Latest curation 2026-07-02 (from 2 of 312 records)". The generator's docstring says a date resting on 2 of 311 records deserves to be labelled as such, and it does. The gap is visible, not silent. What stays open in #325: whether to backfill the other 310. Git could supply a date per file, but that dates the file rather than the curation, and writing 310 provenance entries whose timestamps mean something other than what the field claims is a curation-workflow decision - which is where the issue leaves it too. 2350 passed, 16 skipped.
…325) The review agent hit a session limit before starting, so I ran its two crux checks myself. One passed and one found a real weakness. **Passed:** the new test computes coverage identically to the generator it gates. Compared `_coverage()` against `kg_microbe_qc._score()` slot by slot - 0 mismatches across all 13, on the same 312 records. A test asserting thresholds are reachable would be worthless if it measured reachability differently from the thing enforcing them. **Found:** REACHABLE_FACTOR alone is a rationalisation, not a rule. A purely multiplicative bound collapses on a near-empty slot: at 0.6% coverage it permits at most 1.9%, so the only legal non-zero target is a rounding error. A curator who wants "grow curation_history to 10%" is forbidden from saying so, and the rule ends up permitting exactly the 0.0 this PR chose and nothing else. Added REACHABLE_MARGIN = 0.10, applied as max(actual * 3, actual + 0.10). A near-empty slot can now carry a real growth target while 0.90-against-0.006 is still rejected: curation_history coverage 0.64% ceiling 10.64% 0.90 legal? no associated_datasets coverage 28.21% ceiling 84.63% 0.90 legal? no Mutation-checked both directions: restoring 0.90 still fails, and a legitimate 0.10 growth target now passes the sweep - it did not before. Also checked the objection that lowering to 0.0 hides the gap. It does not: the dashboard row reads `curation_history 2 / 312 0.6% 0% PASS`, so the count, the percentage and the floor-level threshold are all on the face of it. What changes is that the status stops being a WARN nobody can clear. Worth stating plainly though - the WARN was a standing reminder, and 0.0 trades that for not crying wolf. The numbers remain either way. 2350 passed, 16 skipped.
Self-review (the review agent hit a session limit before starting)I ran its two crux checks myself rather than assuming. Check 1 — does the test measure what the generator enforces? Yes.A test asserting "thresholds are reachable" is worthless if it computes coverage differently from the thing that enforces them. Compared Check 2 — does
|
| slot | coverage | ceiling | is 0.90 legal? |
|---|---|---|---|
curation_history |
0.64% | 10.64% | no |
associated_datasets |
28.21% | 84.63% | no |
Mutation-checked both directions — restoring 0.90 still fails; a legitimate 0.10 growth target now passes, where before it did not.
2350 passed, 16 skipped. ruff, black clean.
Not checked, and worth someone's eye before merge: whether dashboard/ regenerates deterministically. It is a tracked generated artifact with no freshness gate, so it can drift exactly as docs/ did before #477 — that would be its own issue rather than a blocker here.
Addresses the part of #325 that is actionable from this repo. The preferred fix is blocked upstream — see below.
The issue's preferred option cannot be done here
_corpus_timestamp(records, paths)in the shared generator reads fromrecords— the YAMLs underyaml_dir.timestamp_pathsselects a dotted path within each record, not an alternate directory. So it cannot namehistory/, and no config change here reaches it.The
history/tree does exist and is being written to (2 entries, each carryingsession.timestampin the shape the dashboard wants). The layer is real; the dashboard simply can't read it. Filed as #491.What is fixable here
conf/qc_config.yamlasked for 90%curation_historycoverage against 0.6% actual — 2 records of 312. That slot could only ever clear via a bulk backfill of 310 records, which is the open question in #325 rather than something curation closes. So it had been warning permanently, and a gate in that state teaches its readers to skip past it. The next slot to go genuinely wrong inherits the habit.Set to
0.0, matching thediscussionsprecedent already in the file: honest that there is almost no data, never blocking. Raise it when coverage actually grows — which it now can, since #395 made everygtdb_ground.pywrite append an event, so a grounding sweep records itself.The distinction the new test draws
A threshold may sit above current coverage — that is what a target is — but not so far above that no plausible curation closes it:
associated_datasetscuration_historytests/test_qc_thresholds_are_reachable.pysweeps every slot with a deliberately loose 3× allowance, so aspirational targets stay legal. Every other slot passes today.Mutation-checked: restoring
0.90fails two tests.One thing that makes #325 less urgent than filed
The dashboard already labels the thin base on its face:
_corpus_timestamp's docstring says "a date resting on 2 of 311 records deserves to be labelled as such" — and it does. The gap is visible, not silent, which is the opposite of what the issue feared.What stays open
Whether to backfill the other 310. Git could supply a date per file, but that dates the file rather than the curation, and writing 310 provenance entries whose timestamps mean something other than what the field claims is a curation-workflow decision — which is where #325 leaves it too. Not mine to make.
2350 passed, 16 skipped.ruff,blackclean. Dashboard regenerated (it is tracked).🤖 Generated with Claude Code