Deep-Research Template Versioning
A deep-research report records the prompt it came from as a bare path:
template_file: templates/disease_pathophysiology_research.md
That is a path, not a version. The file behind it changes; the reference does not. So a report cannot tell you what it was actually asked.
This is not a hypothetical. The disease template has four committed revisions,
and commit 0729e8e5b6 ("Remove MAXO ontology; remap all treatment/diagnosis
terms to NCIT") changed what it asks for without touching a single report. Every
report written before that commit was asked for MAXO terms; the file now asks
for NCIT; nothing in those reports records the difference. Issue #10183.
Two things follow that matter to curation:
- Provider and time comparisons silently assume a fixed prompt. Two reports for the same disease six months apart may have been asked materially different questions.
- A curator reviewing a report cannot tell whether a provider ignored an instruction or was never given it.
What this adds
Going forward, reports are stamped. Every just research-* recipe now
records the template's git blob hash before the report is finalised:
template_file: templates/disease_pathophysiology_research.md
template_sha: "1e7ea4ee817acfe1dda5f77fafe6f2e8b5927666"
Looking back, older reports are resolved rather than rewritten. A report's
revision is recoverable from its start_time against the template's commit
history, so no backfill of committed reports is needed — see
Why there is no backfill.
Using it
just template-version-audit # census across research/
just template-version-audit --stale-only --format list
just template-version-audit --template templates/disease_pathophysiology_research.md
just template-version-audit --format tsv --out /tmp/versions.tsv
--stale-only and --unresolved-only are mutually exclusive and argparse
rejects the pair: an unresolved report has no revision to compare, so it can
never also be stale.
In a shallow clone there is no history to infer from, so every unstamped
report resolves to unknown. The summary says so on stderr rather than letting
missing history read as an unresolvable corpus — worth knowing because
actions/checkout defaults to fetch-depth: 1.
The summary reports how each answer was reached:
Reports scanned: 2645
How the template revision was determined:
stamped 0 0.0%
inferred 2154 81.4%
unknown 491 18.6%
The scan recurses: reports also live in research/modules/, groupings/,
surrogacy/ and datasets/. It skips citation sidecars and the contents of
*_artifacts/ directories, which hold the tables and figures a provider
returned beside a report — 993 of the 1,023 nested markdown files, and not
reports.
stamped and inferred are not the same claim. A stamp is a fact the
generator recorded. An inference is reconstructed from timestamps and assumes
the working tree matched a committed revision when the report ran — which an
uncommitted local template edit would break. Do not present an inferred answer
as a recorded one.
undetermined is not stale. They are separate rows for that reason. A
report whose revision could not be established has not been shown to be out of
date; it has not been shown to be anything.
Finding what a hash means
The stamp is a git blob hash, so git resolves it directly:
git log --find-object=1e7ea4ee817a -- templates/
git cat-file blob 1e7ea4ee817a | head -40 # the prompt exactly as it ran
That is the reason for a hash over a hand-maintained template_version: string:
a version string goes stale the first time someone edits the template and forgets
to bump it, while a content hash cannot disagree with the content. The cost is
that a hash says nothing about what changed — git log --find-object and the
commit message carry that.
Why there is no backfill
The obvious move is to write template_sha into all ~2,650 committed reports.
It was considered and rejected:
- The information is already derivable. A report's
start_timeplus the template's commit history gives the revision that was in effect, which is what the audit does on demand. - The diff would be enormous, and would conflict with anything else touching
research/, to record something computable. - It would still leave every report whose file cannot be rewritten unanswered, so the resolver has to exist regardless.
Stamping earns its place for new reports because it is authoritative — it records what actually happened rather than inferring it, and it survives a template being edited without being committed.
What the audit found
At the time of writing, on 2,645 reports:
| Generated from a superseded revision | 1,974 |
| Undetermined | 491 |
| Current | 180 |
Those three partition the corpus — is_current() returns exactly True,
False or None — so they sum to the 2,645 scanned. Undetermined can never
fall below the unknown provenance count either, since every UNKNOWN
resolution has no blob.
Every report generated from the disease template predates the current
revision — 1,000 from 630b0d5e4cd7, 601 from 839c47271432, 373 from
c566b1bad3b8, none from the current 1e7ea4ee817a. This is expected: that
template changed in #10182, after all of them were written.
That is also why staleness is reported, never gated. A check that failed on a superseded revision would be red on 1,974 reports the moment it was added, and would go red again for the whole corpus after every prompt edit. Staleness here is a fact about the corpus, not a defect in any one report.
The 491 undetermined split into honest categories rather than failures:
- 452 record no
template_fileat all — mostly hand-written syntheses. - A dozen record a free-text label rather than a path (
manual_curation,codex_supplement_local,provider_failure_fallback). No path is invented for these; a label is not a template. - Two predate their template's first commit. The comorbidity Jinja template's only commit is hours after the two reports that used it, so they genuinely ran against an uncommitted version. Reporting them as undetermined is correct; attributing them to a revision that did not yet exist would not be.
One quirk is repaired rather than reported: 20 reports record the path with
Windows separators (templates\disease_pathophysiology_research.md). That names
the same template every other report names, so the separator is folded before
lookup.
Scope
The mechanism is template-agnostic — it keys on whatever template_file a report
records, so all eight templates under templates/ are covered by the same code
with nothing per-template to maintain.
Follow-ups not done here
- Surfacing staleness in
just qc-deep-research. Deferred deliberately: see above for why it cannot be a gate today. Worth adding as an advisory line once there is a view on what a curator should do about a superseded report. - A per-template changelog. A hash says a prompt changed, not what changed.
Commit messages carry that today; a
templates/CHANGELOG.mdwould make it browsable, at the cost of discipline to keep it current. - Re-running on prompt change. Almost certainly not automatic at this volume, but the audit now makes the affected set enumerable if that is ever wanted.