Skip to content

Stop the QC dashboard asserting a coverage target nothing can reach (#325) - #492

Merged
realmarcin merged 2 commits into
mainfrom
curation-history-coverage-325
Aug 8, 2026
Merged

Stop the QC dashboard asserting a coverage target nothing can reach (#325)#492
realmarcin merged 2 commits into
mainfrom
curation-history-coverage-325

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

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

Adopt the history/ provenance layer … and have the QC config point timestamp_paths at it instead.

_corpus_timestamp(records, paths) in the shared generator reads from records — the YAMLs under yaml_dir. timestamp_paths selects a dotted path within each record, not an alternate directory. So it cannot name history/, and no config change here reaches it.

The history/ tree does exist and is being written to (2 entries, each carrying session.timestamp in 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.yaml asked for 90% curation_history coverage 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 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 made every gtdb_ground.py write 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:

slot asks has verdict
associated_datasets 0.30 0.282 six records short — a target
curation_history 0.90 0.006 a factor of 150 — not a target

tests/test_qc_thresholds_are_reachable.py sweeps every slot with a deliberately loose 3× allowance, so aspirational targets stay legal. Every other slot passes today.

Mutation-checked: restoring 0.90 fails two tests.

One thing that makes #325 less urgent than filed

The dashboard already labels the thin base on its face:

Latest curation 2026-07-02T00:00:00+00:00 (from 2 of 312 records)

_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, black clean. Dashboard regenerated (it is tracked).

🤖 Generated with Claude Code

…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.
@realmarcin

Copy link
Copy Markdown
Contributor Author

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 _coverage() against kg_microbe_qc._score() slot by slot, same 312 records:

id 1.0000/1.0000  environment_term 0.9872/0.9872  taxonomy 0.9936/0.9936
curation_history 0.0064/0.0064   associated_datasets 0.2821/0.2821  ...
mismatches: 0

Check 2 — does 0.0 hide the gap? No, but it does trade a signal.

The dashboard row reads:

curation_history    2 / 312    0.6%    0%    PASS

The count, the percentage and the floor-level threshold are all on the face of it — a reader sees the slot is nearly empty and that nobody is asking otherwise. What changes is that the status stops being a WARN.

Stating the counter-argument plainly, since it has force: the WARN was a standing reminder that provenance coverage is 0.6%, and 0.0 removes the only status-level signal. My judgement is that a warning which cannot be cleared by any amount of curation is noise rather than signal, and the numbers remain visible either way — but that is a judgement, not a fact, and it is reversible in one line.

What I found while checking: my own rule was a rationalisation

REACHABLE_FACTOR = 3.0 alone 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 — and a curator wanting "grow curation_history to 10%" is forbidden from saying so. The rule therefore permitted exactly the 0.0 this PR chose and essentially nothing else. That is not a rule.

Added REACHABLE_MARGIN = 0.10, applied as max(actual * 3, actual + 0.10):

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.

@realmarcin
realmarcin merged commit a44ffb5 into main Aug 8, 2026
3 checks passed
@realmarcin
realmarcin deleted the curation-history-coverage-325 branch August 8, 2026 04:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant