Last-pass report: which entries are due for a re-pass
just last-pass-report answers a question git cannot: when was this entry last
given a real curation pass, by whom, and with which model? It reads history/,
excludes bulk sweeps, and prints the result stalest-first so the output is a
worklist rather than a statistic.
just last-pass-report # summary + stalest 25
just last-pass-report --status PASSED # oldest genuine passes
just last-pass-report --status NO_HISTORY # entries with no history record at all
just last-pass-report --model sonnet-4 # everything last passed by an old model
just last-pass-report --list-bulk # audit the sweep classification
just last-pass-report --format tsv --limit 0 > /tmp/passes.tsv
Why not git, and why not updated_date
git log -- kb/disorders/Foo.yaml reports every touch. A whole-KB slot migration, a
dataset-accession backfill, and somebody sitting down to re-read the pathograph all
look identical in it. That is the objection in
#5334.
Re-introducing updated_date does not fix it. The field was deprecated
(#2892,
#3151) because a
mutable line inside every entry conflicts between any two concurrent curation PRs,
and because a bare timestamp carries no actor, model, or scope — so it could not
distinguish a Haiku touch-up from a Fable pass either. Bumping it during a mass
migration is exactly as easy as not bumping it, so it would inherit the same blind
spot on day one.
history/ already records what is needed — actor, model, agent tool, event type,
outcome, sections, and free-text rationale — and does so append-only, one file per
session, which is why it does not reintroduce the conflicts. See
history.md.
The part history/ does not solve on its own
A bulk sweep writes a history record too. On the corpus at the time of writing, one
identical Backfill therapeutic_modality record sits on 700 entries and
Add public dataset records from GEO on 320. Reading only the newest record reports
those entries as freshly curated.
So the report separates two things:
| Meaning | |
|---|---|
| last touch | the newest history record of any kind |
| last substantive pass | the newest record that is not part of a detected bulk sweep and that actually changed the entry (outcome: changed) |
A bulk sweep is detected structurally rather than from a hand-maintained list: an
event whose summary recurs verbatim across at least --bulk-threshold distinct
targets (default 15) was a campaign over many entries, not a pass over this one.
The recurrence gradient is continuous — there is no crisp natural cut between 16
entries and 13 — so the threshold is a dial, and the classification is printed rather
than hidden. Use --list-bulk to see exactly what was excluded, and
--bulk-threshold to move the line.
How much the default misses. About 11% of PASSED entries (165 of 1543 when this
was measured) have a last "pass" whose summary recurs across 5-14 entries — under the
default threshold, so counted as a pass. At --bulk-threshold 8 the near misses are
unambiguous sweeps (backfill reference_title on evidence items, normalize dataset
accession prefixes). Worse for the residual: a campaign split into tranches gets a
distinct summary per slice (tranche 2, hgnc tranche 4/5), so verbatim matching
cannot see it as one campaign at any threshold. Fuzzy matching would catch those and is
deliberately not done — it would make the classification unexplainable, which is the one
property this heuristic has going for it. Read PASSED as "no sweep was detected", not
as "definitely a real pass".
One documented exemption: GENERAL events whose summary starts with
Legacy curation summary are the roll-ups that imported pre-history/ activity.
Several coincidentally share a summary (same event count, same date range) without
being a campaign, so they are never classified as bulk.
The known false positive
Review: 50 least-recently edited Mendelian entries is classified as a sweep, and it
was not one — it was a real per-entry review campaign, so those 50 entries are
reported staler than they are. (It is also direct evidence that staleness-driven
re-passing is already being done by hand, which is the workflow this report exists to
support.)
A refinement was tried and rejected: "a sweep touches the same narrow section on
every entry; a real pass varies". It inverts on this corpus. The Mendelian review
recorded one identical eight-section list across all 50 records, while the genuinely
mechanical Reference-title backfill produced 42 distinct section sets. Section
variance would have promoted the backfill and demoted the review — the opposite of
the intent — so summary recurrence stands as the only signal, with the threshold
exposed as a dial rather than a hidden constant.
Statuses
| Status | Meaning |
|---|---|
PASSED |
a substantive pass is recorded; the row shows its date, event type and model |
BULK_ONLY |
history records exist, but all of them are sweeps or no_change events |
NO_HISTORY |
no history record at all |
BULK_ONLY and NO_HISTORY entries sort to the top of the worklist, ordered by how
long it has been since anything touched them. A * on a PASSED row means that
entry's newest record is a sweep — i.e. it looks recently updated in git and in
the raw history tree, but has not actually been re-curated since the date shown.
The limitation to keep in mind
The report is only as complete as the history ledger. An entry genuinely re-curated in a PR that forgot its history record reads as stale here.
Down_syndrome is the worked example. Its last recorded substantive pass is
2025-12-15, but PR #5877 (2026-07-09) really did augment it with a tPBM trial and
left no history record, so the report overstates its staleness by seven months. Every
other commit touching that file since December was a sweep, which is the finding
the report exists to surface — but the July pass is a false negative.
The fix is ledger discipline, not a different mechanism: CI already posts an advisory
warning when a KB entry changes without a matching history record, and just
new-history scaffolds one. Treat a surprising row as a prompt to check
git log -- <path> before concluding an entry was neglected.
Output
--format summary (default) prints corpus counts plus the stalest --limit rows.
--format tsv and --format json emit one row per entry with status, last_pass,
age_days, event_type, model, agent_tool, actor_type, last_touch,
touch_is_bulk, records and bulk_events — enough to drive a scheduled re-pass
campaign or a dashboard.
Ages are measured against now (UTC) unless --as-of <ISO date> is given, which makes
output reproducible in tests and reports.
Implementation: src/dismech/last_pass.py, tests in tests/test_last_pass.py.