Roadmap — the 0.7 deferral round (closed)¶
Closed on 2026-08-31, when 0.7 was built. This was docs/ROADMAP_0_7.md: items legal in a minor
and not taken into 0.6, each with the reason it waited. Split out of ROADMAP.md on
2026-08-13 so the active roadmap described the line being built and a deferral was filed against the
release that would decide it. It is kept here for the reasoning, which is the half worth not
re-deriving — several of these entries are the record of what a probe observed, and two of them say
where an entry was later overturned.
Where its contents went. Everything still waiting moved to ROADMAP_0_8.md — RM122, RM23, RM16, RM28, RM56, RM65, RM66, RM67, RM68, RM84 — unchanged but for the file they sit in. The two that built in 0.7, RM126 and RM71, have their shipped entries in ROADMAP_HISTORY.md. What stays below is the round's closed record: RM55, RM72, RM82, RM87 and RM124, each shipped, plus RM83 (closed, not shipped) and RM86 (closed). RM_TOC.md indexes every one of them and is where to look an item up.
Nothing here is edited as routine work, the same rule as the rest of this folder. A fact that is still true belongs in a maintained document; if you find yourself wanting to correct an entry below, the entry is evidence of what was believed then, and the correction goes where the rule lives.
What the round recorded about itself, kept verbatim. RM69 left for ROADMAP_1_0.md on 2026-08-27 because its only stated unblocker was RM15, a major: an item whose deciding release is 1.0 is waiting on a version, not on a design question, and this file said nothing in it was. Five entries — RM55's fix, RM72, RM82, RM84 and RM87 — were taken back into 0.6 on 2026-08-16 through PROPOSAL_0_6_PT2.md, which is authoritative for all five; their entries below are the record of what was observed. Nothing moved on a legality argument — everything here was already legal in a minor — so what moved them was severity: a shipped thing that is silent, wrong or lying outranks a capability nobody has asked for.
The VCF 4.4 items deferred out of 0.6¶
Numbered and triaged on 2026-08-13 from VCF_4_4_AUDIT.md; the rest of that cluster
(RM53, RM54, RM56–RM64, and RM65's comment fix) went into 0.6 — see
PROPOSAL_0_6.md. The audit remains the evidence document: spec quotations,
file:line references and probe transcripts live there and are not duplicated.
RM55 — copy number and repeat count are not whole numbers (the usable fix)¶
Severity high · Status ✅ shipped in 0.6 — built to
PROPOSAL_0_6_PT2.md § RM55, which is authoritative and against which this entry
is stale in its suggested fix; only the removal of modifier_cn is still 1.0
(ROADMAP_1_0.md). Indexed in RM_TOC.md; the built shape is described in
SCHEMAS.md · Owner format (schema) + compiler
One deviation from the proposal, taken while building and recorded here. The proposal says a group
carrying a fractional value is read under the continuous rules "whatever its kind's default says".
As built, the inference fires only where the kind would default to quantised — that is the only
reading a fraction contradicts. activity_score defaults to neither precisely because the score is
summed onto a grid whose step the schema does not know, and it is fractional by nature: reading it as
continuous produced three "coverage gap" warnings on reference_examples/cyp2d6_structural, whose real
bins sit at 0.25/0.5/1.25/2.25, for intervals no activity score can land in. The proposal's own
sentence two paragraphs earlier — that activity_score "keeps its third behaviour (no gap warning, a
shared endpoint is an error)" — is the half that was kept.
Stale below. The proposal's probe found the bin bounds are already
float | None(binning.py:231,:238),_INTEGER_KINDShas exactly one code reader (binning.py:688), and the unstated half of the defect is that the shared-endpoint rule refuses the continuous tiling that would fix it (binning.py:666-677). So "a parallel float column beside the integer one" names a column that mostly does not exist. What was decided instead: an optionalmeasure_tilingcolumn ({quantised, continuous}) for the tiling half, read as an effective tiling — declared, else inferred from a fractional value (one-directional and announced, since fractional-ness is incompatible with quantised semantics while integer-ness implies nothing), else the kind's current default — plus the parallel float column applied tomodifier_cn— the one genuineint— asmodifier_copy_number, read through an effective-value alias that falls back tofloat(modifier_cn), with_KEY_FIELDSkeying on the effective value so a key never holds two spellings.modifier_cnis deprecated in 0.6 and removed at 1.0, so the three-release route collapses to two.
VCF 4.4 §7.2: "Redefined INFO and FORMAT CN to support non-integer copy numbers", with fractional
worked examples throughout, and §3 standardises the repeat count RUC as a Float. Both kinds sit in
_INTEGER_KINDS here, so a copy number of 2.4 matches no bin at all, silently, --strict included —
and CopyNumberRow.modifier_cn is typed int, so even a modifier dosage cannot be written down.
Suggested fix, and the reason it is this one. A parallel float column beside the integer one, with the integer column deprecated and removed at 1.0. It is strictly additive and therefore charter-clean, where the direct correction is a retype plus a change to what published bin tilings mean — reserved for a major. Three-release route, decided 2026-08-13: 0.6 makes the defect loud, 0.7 makes it fixable, 1.0 removes the wrong column. An author is never more than one minor away from being able to write a fractional dosage, and no published table is retyped under anyone.
Still open when this is designed, and the parallel column does not foreclose either: whether quantised-versus-continuous is a per-table declaration or a sixth measure kind. The two kind-sets answer "can a hole be arbitrarily small?" and "can two bins touch?" separately, and a rounded catalog count and a continuous segment mean genuinely differ on both — so this is not a formality.
Do not simply move the kinds into the continuous set: [2,2] beside [3,3] is a legal integer
tiling today, and continuous semantics change both the shared-endpoint rule and the gap warning for
tables already published.
The 0.6 dogfooding items deferred out of the fix round¶
The findings from DOGFOOD_0_6_FINDINGS.md whose obvious repair is itself a design decision — the round filed five, and two have since left: RM69 for ROADMAP_1_0.md (see the header), and RM70 shipped in 0.7, its entry now in ROADMAP_HISTORY.md. The three below are what remains. No fixed count is stated here on purpose: one goes wrong silently the next time an item leaves, and this section has already lost two. The ledger classes each surface rather than fix, which is this repo's standing split: a false claim, a misdiagnosis, an unaggregated wall or an unreached guard gets fixed in the round that finds it; anything whose repair has to be chosen gets filed with the candidates and the reason each one fails. The refutations are the point of these entries — an item that only names a gap is one somebody re-derives from scratch a release later.
Everything below is legal in a minor. Where a repair would be additive it says so; where the only candidate repairs are illegal it says which principle bars them. Legality sizes the release; severity only orders the queue.
RM72 — six verification members still emitted by nothing, and the "Writes nothing" contract¶
Severity medium · Status SHIPPED in 0.6 PT2 (lane C), 2026-08-17. The four members are
wired unconditionally (check-identifiers three, check-acmg one), the reword landed at the three
sites that carry this promise, every member of VALID_VERIFICATION_CHECKS now says on its own line
whether it is wired or reserved, and merge_records no longer lets a skipped record displace a
ran one. The merge rule gained a condition the proposal did not anticipate and the implementation
found: the protection holds while the authored bytes stand still, because an answer over bytes the
author has since edited is not an answer this document may keep asserting — without it the fix
re-opened a defect an earlier literature round had closed. The create-module skill needed no edit,
as predicted: its rows for both commands carry no "Writes nothing" annotation. See
PROPOSAL_0_6_PT2.md § RM72; the two reserved members did not move. Previously:
deferred — four members blocked on a printed contract, two deliberately
reserved, one general question open · Owner enricher · Found by dogfooding on 2026-08-13,
reference_examples/hboc_palb2/
The quoted docstrings below are the pre-RM72 text, kept because they are what the item was about. The citations in this entry had already drifted to lines that no longer held them once, so they now name the command or the symbol rather than a line number.
Scope. This is the surfaced remainder of the D4-1 finding. That finding counted twelve of
seventeen VALID_VERIFICATION_CHECKS members as emitted by nothing; six of them were wired in the same
round that filed this (citation_existence, citation_identifier, provenance_quote from literature;
allele_function and vrs_allele_id from pgx and vrs mint; rsid_coordinate_agreement inside
enrich). The honest remainder is narrower than the headline, and the narrowing is the point.
The four blocked on a printed contract¶
gene_symbol_currency, trait_currency and gene_locus_agreement belong to check-identifiers;
acmg_secondary_findings belongs to check-acmg. Both commands do put their question, report the
answer to stdout, and let the record die with the process — the sentence RM45's own docstring opens with
as the thing it exists to fix. Wiring them is not a small matter, because both commands promise not to
write:
check-identifiers: "Writes nothing: unlike the rsID check (whose verdict lands on resolution.csv), these are module-level identifiers with no sidecar column to record, so the report is the whole output." (enricher/src/just_dna_enricher/cli.py,check_identifiers_)
check-acmg: "Writes nothing, for the same reasoncheck-identifierswrites nothing:acmg_sfis an authored cell this asks a registry about, not a fact this pass contributes. Filling it here would break the check — seehints.REDUNDANCY_BEARING." (same file,check_acmg_)
The same promise is made by hint in both CLIs (hint_app's help in each cli.py), by lookup's
module docstring, and — the part that decides how expensive a
reword is — by docs/ENRICHER.md, docs/COMPILER.md's coverage table and the create-module skill's
command tables, which are printed for an author who has pip installed the package and has no checkout.
It is a published contract in both CLIs, both package references and the skill, not an implementation
note.
What "writes nothing" protects, and it is not one thing. For hint/lookup it protects the
inject-only escape hatch and the Class-2 checks: lookup's docstring says "not a file, not a cell",
and the reason is REDUNDANCY_BEARING — a surface that can write is a surface someone asks to write,
and --apply on a lookup would ship the parked co-authoring item without deciding it. For
check-identifiers/check-acmg the stated reason is narrower and different: the cell in question is
authored, and filling it breaks the check. Both reasons are about not writing into the module's
authored data. Neither is about not writing a record of having asked.
Is an attestation a write in the sense the promise means? On the reason, no: verification.json
holds two counts and a closed skip key per check and no authored cell at all, so it cannot make a
Class-2 comparison vacuous, which is the harm both docstrings name. On the words, yes — and the words
are what a reader outside this repo has. The asymmetry gives the item away: enrich writes
verification.json and nobody calls that a violation, because enrich never promised otherwise. So the
blocker is not the design, it is that a narrowly-justified rule was written down as a blanket. Restating
one is not a docstring edit here: a printed contract is the surface this repo has measured the cost of
getting wrong, at 3,038 rows for the 0-vs-1-based start description.
Would --attest be a second switch? Yes, and it is the wrong shape twice over. The tier's standing
rule is that --offline is the switch and a pass adds no second CLI flag. Worse, a flag makes the
record optional, and an optional record is ambiguous between "the check was not run" and "it ran
without the flag" — which reintroduces the two-readings-of-one-absence defect the skip vocabulary
(not_requested versus offline) was built to end. A record has to be unconditional or absent by
design, never conditional on a switch.
Candidate repairs, then:
- Wire them and leave the docstrings. Ships commands whose printed contract is false.
describe/requirements/referenceand the skill's command tables are the authoring contract, not commentary, and this batch already found four other items landing just short of that surface. - Wire them and reword every site that carries the promise. Legal, probably right, and a decision —
the reword has to separate writes no authored cell from writes no file without inviting the
--applyrequest the blanket wording deters, and one of those sites is written for a reader who cannot follow a pointer to the reason. - Fold both checks into
enrichso the record comes from the pass that already writes. They are separate commands because they are separately expensive and separately optional (OLS4, HGNC, the ACMG page), so this makes every enrichment pay for them. It also contradictsmerge_records, whose whole purpose is that two commands write into one document. - Drop the unreachable members. The one move Principle 3 bars: the vocabulary is closed and permanent within a major, so a removed member cannot return until 1.0 — and the vocabulary's own comment argues the opposite direction, that adding a name late means the release that needs it has none to write.
The two deliberately reserved — not gaps, and not to be "wired too"¶
gene_disease_validity. Argued in the code beside vocab.VALID_VERIFICATION_CHECKS: it
"has no emitter yet and is kept on the withdrawn precedent … 0.6's enrich_gene_validity
records ClinGen/GenCC verdicts into a derived table and compares nothing authored, so it does not
emit this. The member is for a future pass that checks an authored gene/phenotype pair against those
verdicts." Wiring it to gene-validity would report a check where no question was put, which is the
confusion RM45 exists to end.
dosage_sensitivity. The same shape, with less written down. dosage is a gene_metrics.csv
writer and does not appear in docs/ENRICHER.md's check table at all; the member's own comment reads
"an authored dosage claim vs ClinGen's curation", and there is no authored dosage claim anywhere in
the schema — haploinsufficiency and triplosensitivity are columns on the derived
GeneMetricsRow. So it is reserved for a pass that does not exist, exactly like gene_disease_validity,
and unlike it nothing states so. That asymmetry is the small concrete deliverable this item carries: a
member should say on its own line whether it is wired or reserved, which is what would have made the
D4-1 headline count of twelve read correctly the first time. Both now do, and both stay reserved.
The exclusion half is already tested in one direction.
schema/tests/test_verification.py's test_a_pass_that_only_records_a_source_gets_no_member pins
that clinvar_assertion_tier, clinical_assertions, allele_frequency, gene_constraint and
article_license are not members, on the rule the assertions command states about itself: "It
records; it does not adjudicate." Nothing tests the other direction, which is where the two reserved
members live.
A finding about the merge itself, not about any one pass¶
Wiring the first of the six turned up something that belongs here rather than in the pass that found
it, because it is a property of the mechanism every future wiring will use: merge_records replaces
per check, so a later run can downgrade a true verdict to a skip. Measured — an offline literature
re-run, which is a documented no-op that keeps the existing sidecar as the pin, rewrote a real
subjects=2 findings=1 record to subjects=0 findings=0 skipped=offline. The pass that found it is
repairing its own instance.
What is worth recording is that the general rule may be the one at fault. merge_records is newest
wins, per check, and its own docstring already argues the opposite case one step out:
A check absent from
freshkeeps its earlier record: a run that did not put a question has said nothing about it, and deleting the older answer would turn "not asked this time" into "never asked", which is the exact collapse RM45 exists to undo.
A skipped(offline) record is a run that did not put the question — the same fact, spelled as a
record instead of as an absence — so the argument that protects an absent check protects this one too,
and newest-wins does the deletion the docstring refuses. The counter-argument is real and is why this
is not decided here: a skip is a fact about the latest run, a reader may legitimately want to know
that today's enrichment could not reach the source, and a rule that lets an old ran outlive every
later skip can present a stale verdict as current. Both readings are defensible, which is what makes
the merge rule a design question rather than a bug: whether "newest wins" should hold unconditionally,
hold only between two ran records, or be replaced by something that keeps both facts. It reaches
every check wired from here on, so it wants deciding once.
Decided and shipped in 0.6 PT2. Newest-wins holds between two records of the same disposition; a
skip does not displace an answer. The counter-argument above is answered rather than dismissed — a
skip is a fact about the run and verification.json is a per-check document, so a run-level fact
needs a run-level place, which is a separate question and was not opened (RM284, filed 2026-09-27). The stale-verdict half of
the counter-argument turned out to be the real constraint and is handled by a condition the
implementation added: the protection applies only while the earlier record still describes the
module's authored bytes (existing_still_binds). Once they have moved the older record is about rows
that no longer exist, and this run's honest "could not ask" wins — which is what literature relies
on when a module's citations change, and without it this fix would have re-opened a defect an earlier
round had closed.
The general question¶
Is a check that reports to stdout and writes no record a defect, or a legitimate read-only
surface? Both readings are live in this codebase and neither is stated. hint and lookup are
read-only on purpose and should stay that way. enrich and enrich_clinpgx attest. check-identifiers
and check-acmg sit between: they put a real authored-versus-source question, which is precisely the
membership rule VALID_VERIFICATION_CHECKS states — does this compare something the module asserts
against what a source says? — and then answer it only to a terminal.
What would unblock it: a decision on that line, which then settles the reword. The four members follow mechanically once it is made; the two reserved ones do not move either way.
Decided for these two commands, and only for them. The line is not does it write but does it
compare something the module asserts against what a source says — a surface that puts that question
owes a record of having put it; a surface that answers a question about a value owes nothing. So
hint and lookup stay read-only on purpose, and their "writes nothing" is untouched.
The lifecycle items — what writing down the second pass surfaced¶
Filed on 2026-08-16 out of MODULE_LIFECYCLE.md, which mapped a module from origin to publish to a consumer's join and found that the second pass had never been written down at all. Four items, none of them a defect in a rule: each is a place where two individually-correct rules compose into something nobody chose, or where an absence only bites the second time somebody opens a module. The document keeps the measurements (§5.1 the canary, §6.2 the six-edit consequence matrix, §6.3 what deleting a sidecar costs); these entries keep the decisions and the refused repairs, and do not restate the numbers.
They were the closing section of that document — an "open questions" list — which is exactly the shape this repo has twice found to be a backlog nobody reads. A question filed against a release is findable; a question at the bottom of a prose document is not.
Everything here is legal in a minor.
RM82 — the attestation binds raw bytes, so an editor's line endings un-close a module¶
Severity low-medium · Status shipped in 0.6 (2026-08-17) — \r\n → \n before hashing,
nothing else; integrity.newline_normalized_file_entry, used by the binding and by nothing else ·
Owner format (schema: verification.module_binding) · Found by the §6.2 measurement, and
sighted once before from the other side
One fact this entry does not carry, and it is the whole implementation.
module_bindingisartifact_digest(verification.py:79-90), andsize=stat().st_sizeis inside the hashed listing (integrity.py:80-83,:106-112). So normalizing the digest input while reporting the on-disk size still moves the binding on exactly the CRLF files the fix exists for — the naive implementation is a no-op that looks like a fix. The normalized length has to travel with the normalized digest, through a builder used by the binding and by nothing else. See PROPOSAL_0_6_PT2.md § RM82.
What was observed¶
Measured in MODULE_LIFECYCLE § 6.2:
rewriting variants.csv with a different line ending changed no value, no digest and no signature —
and still dropped the attestation and the closure. verification.json's module_hash binds the raw
bytes of the authored files, so a formatting change an author never intended costs exactly what a
re-authored conclusion costs. An author whose editor normalizes newlines un-closes their module without
touching a cell.
The decision¶
Normalize \r\n → \n on the bytes before module_binding hashes them. What separates this from
the content-aware binding RM45 rightly refused is that it needs no loader, no schema knowledge and no
parse — it is a byte transform, and the binding stays a byte binding. Precedent is on its side:
normalize-before-hashing is already the house move for content_signature (defaults folded in,
exclude_none, the default build omitted).
Three things settled with it, because each is a way the fix could go wrong:
- The stopping point is newlines, and it is chosen rather than inherited from the first symptom
reported. A BOM, trailing whitespace and a missing final newline are the obvious next steps, and
each makes the binding more content-ish without making it content. Newlines are the one difference a
tool introduces on a file the author did not edit — an editor's default, or Git itself through
core.autocrlf; the others are things a human typed. If a real case arrives for one of them it is additive (P3) and gets argued then, on its own evidence. - The cost is a one-time invalidation of every
module_hashin existence, all sixteen closed reference examples included. That is a soft break: a stale attestation warns and is dropped, it never fails a build. But it is every module in the wild re-attesting once, and the corpus re-closing in the same commit as the change. manifest.inputs[]must not follow it. Those raw-byte hashes answer are these the exact bytes, which is a different question — so normalizing one and not the other is coherent, not an inconsistency to tidy up later.
As built, and where the prediction was wrong. The invalidation is not universal: a binding moves
only for a module that actually carries \r\n in one of the twelve authored files, so 7 of the 16
reference examples re-bound and 9 were byte-identical — for those nine close_module took its
held branch and kept records, producer and nonce verbatim. Which way round the corpus was CRLF is the
part worth keeping: csv.writer's default line terminator is \r\n, so the machine-written half of
the corpus ships CRLF and the edit an author really makes is normalizing to LF. Two modules lost four
attested check records each (cyp2c9_warfarin_grch37, hboc_palb2) — dropped rather than re-bound,
which is the rule working. Nothing else moved: artifact.digest, content_signature,
source_signature and resolution_signature are byte-identical across all sixteen, measured before and
after.
It was sighted once before, from the other side¶
DOGFOOD_0_6.md's lane plan asked whether a round-tripped spec's attestation may read
as stale to its own compiler with nothing edited, since reverse normalizes cell formatting and
column order, and closed with "Both are legitimate outcomes; neither is documented." Same mechanism,
different trigger. The editor case is the one an author actually meets, which is what promoted the
question from an observation to a decision.
What this is not¶
A change to which files the binding covers. That question — should the authorship: block be inside
it, given that appending a reviewer drops the attestation while moving no identity — was asked beside
this one and answered the other way on 2026-08-16: a review stamp is an attestation that zero
changes were needed, so un-closing is exactly correct and the reviewer re-closes. Recorded in
MODULE_LIFECYCLE § 6.6. Do not re-open it as a
by-product of building this.
RM83 — a derived sidecar can only be refreshed by deleting it, which discards the overrides it exists to hold¶
Moved to ROADMAP_HISTORY.md on 2026-08-28. Closed rather than shipped, dissolved by RM124 once derived tables became pure build products, and moved out of this file in the commit that landed the overlay — which is the condition PROPOSAL_0_7 attached to the closure, because a closure recorded against an unlanded dependency is the kind of bookkeeping that makes a ledger untrustworthy. The heading is kept verbatim for its anchor — six documents link to it — and the entry and its reasoning now live in the history file.
RM86 — a review pass is legal at the gate, refused by the pre-flight, and invisible once published¶
Severity medium-high · Status ✅ CLOSED 2026-08-20 — all three findings answered by
just-dna-registry, two of them by code, and the question that was ours is decided and written into
MODULE_LIFECYCLE § 6.6. Filed with them on 2026-08-16
as their S10, S11 and S12, one item per finding; all three are answered and archived in their history ·
Owner just-dna-registry, with a documentation half here — that half is now done · Found by
reading the registry tree on 2026-08-16, while settling whether a review pass costing the attestation is
a defect (it is not) · Closed by re-reading that tree at registry 0.18.3 on 2026-08-20, prompted
by S46 from just-module-creator, who measured the recognition half
from outside and reported §6.6 as stale
How it closed, per finding. The dispositions are not uniform, which is the reason this entry keeps all three rather than collapsing to one line:
- The pre-flight — fixed in registry 0.16.0.
published_elsewhereis a new field carrying the subset of content hits under a different(namespace, name), andwould_publish_module_levelquantifies over it, so the verdict now matches the gate;published_asstill lists the same-module hit, because a review pass wants to see it. The namespace is threaded through both pre-flight routes. Verified first-hand atservices/enrich.py— the carve-out and the comment naming this exact defect. - The closure — fixed across 0.16.0 and 0.17.
verification.jsonis inRECOGNIZED_SPEC_FILES(0.16.0), so a rebuild carries it forward; it is in that registry'sDERIVED_FILESandmanifest.derived(0.17), sodownload(include_inputs=True, layout="split")returns it atderived/verification.json; andmanifest.verificationis projected onto the module-detail response, withclosedre-bound by that server's own compile. Still out ofSIGNATURE_INPUTS, which is the property that made recognising an unread file safe and is asserted by their tests. The version-lag clause is also dead: they now requirejust-dna-format>=0.6.1/just-dna-compiler>=0.6.1, not 0.5.4. authorshipunsurfaced — answered as policy, not repaired, and correctly. It stays payload: a server that compiles what it publishes must not render the author's statement about their own reviewer beside claims of its own. Read it from the manifest, where it is plainly the manifest's word.
The question that was ours is answered too, by their reply to S12, and it is now §6.6's advice
verbatim: a reviews row by default; an authorship entry when the record has to travel inside the
module or be signed; both when both matter. The version-bump path is legal and costs a version number
and nothing else.
Where the report lives: ../just-dna-registry/docs/CONSUMER_SUGGESTIONS_HISTORY.md (the tree is also
reachable as just-dna-marketplace, a symlink to it, which is the name this entry used to give), under
Field notes from just-dna-format — the second pass, as their S10–S12 with a reply on each. The original
filing carried the standing caveat that 0.6 was not published and not finished, so the half of S11
needing a reader of ours was explicitly filed as do not build against this yet; 0.6 has since been cut
and that caveat has expired. This entry is the tracking record; their file is the authoritative report,
and the citations below were each verified in their tree first-hand rather than taken second-hand.
Everything below this line is the 2026-08-16 finding as first written, kept because the reasoning is what a publisher should still hold and because two of the three sections describe behaviour that a registry deployment older than 0.16.0 still has. Read it against the dispositions above.
Why this was looked at¶
The decision that a review stamp is an attestation of zero changes settles the format side and
immediately raises the downstream one: a review pass publishes a version whose content_signature
and artifact.digest are byte-identical to its predecessor, differing only in authorship and
verification.json, neither of which is in either identity. Whether a catalog can even represent that
is not something this repository can assert from its own rules, so the registry was read. RM27's shape
applies — a finding about a downstream enforcer is filed as an explicit ask, not as an implication.
What the publish actually does¶
It succeeds, and the gate is right. The duplicate-content check is keyed on content_signature
alone, enforced in the service layer before enrich/compile, and the same-module carve-out is real code
rather than prose — it compares (namespace, name) and is pinned by a test. Nothing else can bite:
there is no unique index on the digest or the content hash, and storage keys are
namespace/name/version, deliberately not content-addressed, with a comment giving this exact
reason. latest advances normally. find-by-hash returns both versions in a deterministic list, which
is the one place the shared digest is visible and is by design.
So the §6.6 sentence this document has carried — that the gate permits identical content within one module — is now confirmed against code rather than believed.
The three things that are wrong¶
- The pre-flight disagrees with the gate on precisely this case.
validation_reportcomputespublished_asfrom the raw content lookup with no same-module carve-out, and the namespace is never threaded into the worker — sovalidateandcheckreport the predecessor and answerwould_publish: falsefor a publish that then succeeds. The registry's own test file states the standard this breaks ("a pre-check that disagreed with the gate would be worse than none, because it would give a publisher confidence before taking it away"), and its coverage tests only the different-name direction. This is the one with a blast radius: an automated publisher branching on the field the API documentation says to branch on refuses a legal publish, and a review pass is the commonest way to reach it. - The closure reaches nothing.
verification.jsonis uploaded by the client and copied into storage, and then read by no code path: it is not inRECOGNIZED_SPEC_FILES, so every server-side spec rebuild (revalidate,upgrade) drops it, and it is outside the served allow-list so no endpoint hands it back.provenance.json, one file over, is recognised and is served — so this is an omission rather than a policy. Compounded but not caused by a version lag: the registry pins 0.5.4, wheremanifest.verificationdoes not exist andclose_moduledoes not either, and the server regeneratesmanifest.jsonfrom its own compile — so today the closure cannot appear even in principle. The recognition gap outlives that upgrade; the pin does not. - The review is served but unsurfaced.
authorshipdoes reach the published manifest and is readable two ways, but no projected field, column, filter or card element carries it, so seeing who reviewed a module means parsing the manifest — and only for the latest version without a second request. The registry's own schema states the policy this follows ("a column is for something you filter or sort by; the rest is payload"), namingauthorshipas payload.
The question that is ours, not theirs¶
The registry already has a reviews table with a verdict tier and a highlighted flag, projected
onto module cards, costing no version number at all. This document has been saying that a pure review
is "a real version bump without pretending the data changed" — which is true of the format and may be
the wrong instrument on that catalog. The two mechanisms record different things (an authorship entry
travels inside the module, survives a download and a hand-off on disk, and is signed by the same
key; a registry review lives in the catalog and does not travel), so this is not a case of one being
redundant. What is undecided is what an author should be told to do, and whether both should be
recorded — and it is a documentation decision here, not a schema one: nothing in the format changes
either way.
What this is not¶
A reason to re-open the binding question. Un-closing on a review is correct (MODULE_LIFECYCLE § 6.2); every finding here is about what a catalog does with the result, and none of them would be fixed by keeping a closure the reviewer did not make.
What would unblock it (as written on 2026-08-16; all three happened): the pre-flight carve-out and
the verification.json recognition are theirs and are small; the review-versus-version guidance is ours
and needs deciding once, wherever an author is told how to run a review pass.
RM87 — an expanded row is indistinguishable from an authored one in the artifact¶
Severity medium-high (a consumer produced 3,762 false findings on it; caught before rendering) ·
Status SHIPPED in 0.6 PT2 (lane B) — VariantRow.locus_index + locus_count, both
stamped_identity_field (exclude=True, so no content_signature moved anywhere), stamped at the
expansion loop in resolution.py and at its twin in the deprecated resolver.resolve_variants
(digest parity between the two paths is a guarantee, and two round-trip tests caught the omission);
locus_count defaults to 1; _build_weights materializes both as UInt32; reverse prefers the
stored column over its encounter-order recompute and keeps the recompute for a pre-0.6 artifact.
_freeze_identity overwrites an authored cell of either name, the way it already does for
variant_key. Measured over the sixteen reference examples: artifact.digest moved on the nine
carrying a variants.csv — hence a weights.parquet — and on nothing else; content_signature,
sources.signature, verification.signature and compilation.resolution_signature all held on all
sixteen. (The proposal predicted twelve and four; the real split is nine and seven, which is a
miscount in the prediction and not a behaviour difference.)
PROPOSAL_0_6_PT2.md § RM87 is the design record · Owner format (schema) +
compiler (materializer, reverse) · Motivating case S33 in
CONSUMER_SUGGESTIONS_HISTORY.md, from just-dna-lite
A one-to-many rsID is paired with every locus it resolves to, so K authored genotypes at that key
become K×N rows in weights.parquet and only the member whose alleles can carry a given genotype can
match it. The others are ordinary well-formed rows — real coordinate, real reference allele, the
module's own conclusion — and nothing on the row says so. locus_index lives in resolution.csv,
which SCHEMAS.md states is a lookup rather than a consumer contract and which gets no parquet by
design; the artifact carries variant_key and authored_ident and neither separates the members.
The reporting consumer that found this measured 2,579 rows into one genome's pathogenic section and
1,183 into cancer, each stating that a subject carried a pathogenic variant they do not have, all
from TA/TA-beside-ref=TA reference homozygotes at ClinVar duplication/deletion pairs. Their
mitigation — withhold any locus the artifact spells with more than one ref — took those to zero and
is partial by their own account, because same-ref expansions are invisible to it. They are real
here: --keep-par-twin records a pseudoautosomal locus on X and Y with identical alleles, which is
what reference_examples/shox_par1/ was built from (nine of ten SHOX variants, 20 rows for 10
findings — RM32), and a paralogous rsID can name two positions carrying the same reference base.
0.6 shipped the artifact-level half — manifest.compilation.expanded_keys/expanded_rows, plus
the read-side contract in SCHEMAS § the consumer join contract and one expansion warning per rsID
carrying the true row total. Those answer does this artifact contain expansion rows. They do not
answer is this row one, which is the question a row-by-row reader actually has.
The premise this item exists to correct¶
S33 declines to ask for the parquet column: "the 0.5 digest window is closed and a new column moves
every module's digest, so that is a 1.0 conversation if it is one at all." That is wrong under our
own charter, and the mistake is ours for not having said so plainly enough. Principle 3: "A new
optional column, or a new optional table, is additive and lands in a minor: the authored identity —
content_signature and the per-input hashes — is unchanged, and only a recompile's artifact.digest
moves." Principle 4 scopes byte-reproducibility to a fixed compiler_version in the first place, so a
digest that moves between compiler versions is the documented behaviour rather than a cost. And the
0.6 cost amendment prices this exact object at the bottom of its scale: "Parquet columns —
approximately free. Materialized and derived; no human ever types one, and an author cannot see one. A
stamped, compiler-managed column is the cheapest thing this format can add."
So the thing the reporter says they actually want is both legal in a minor and the cheapest class of change the charter recognises. It is filed rather than shipped in the same pass for one reason only, and it is not legality.
The open design question — locus_index alone does not answer it¶
locus_index is what S33 names, and taken by itself it is under-determined: it is 0 on every
non-expanded row and on the first member of every expansion, so a reader holding one row cannot tell
the two apart. Making it answerable needs one of:
locus_index+locus_count— index within the key's expansion, and how many members that key has.locus_count > 1is then the row-level predicate, self-sufficient and mirroring whatresolution.csvalready records. Two columns for one fact, which is the cost.locus_countalone — the predicate without the ordinal. Cheaper, and it loses the ability to line a weights row up with itsresolution.csvrow, which is the other thing an index buys.- A single boolean
expanded— cheapest to read, and it forecloses both of the above under Principle 5's one-way-door rule: aboolcannot later carry an ordinal without a retype, which is major-only.
None of these is obviously right, the name is permanent within the major (P5), and picking one during
a triage pass is exactly the kind of decision this repository files instead of guessing. Whichever
lands, it is compiler-managed and stamped, so it needs the three touch points — model, compile-side
row dict plus polars schema, and the reverse fieldnames/_scalar_cell pair — plus a round-trip test:
reverse currently recomputes locus_index by encounter order over the weights rows (which works only
because those rows are sorted on it), and a stored column would have to be shown either to agree with
that or to supersede it.
What it does not change. The expansion stays. Filtering the non-matching member is refused for the
two reasons COMPILER.md § Resolution already gives — a source's allele list is incomplete at least as
often as a module is wrong, and dropping rows changes what reverse_module reads back, which
Principle 7 forbids. The reporter argued the same case against their own first candidate, and they are
right.
RM124 — an author's correction to a derived table has nowhere to live except inside it¶
SHIPPED 2026-08-28 — the entry below is kept as the record of what was observed, and
ROADMAP_HISTORY
carries what it did when it landed. Two things the entry and the proposal both got wrong or left
open, corrected there rather than here: the covered set is seven tables and not six (the proposal
never enumerated them and the six was a miscount — gwas_effects.csv is the one nobody counted), and
the wildcard member is refused for insert as well as suppress, because the row an insert
would create under an empty member carries no member value and so could never be matched again.
Decided in PROPOSAL_0_7 on 2026-08-28 — BUILDS in 0.7, and it is the round's keystone. overrides.csv, keyed (table, subject, member, field); operations update/insert/suppress, with the wildcard refused for the destructive one; applied at load and merge-not-clobber's cost dropped for the covered tables; P7 held by idempotency, so no previous_value column. Question 2 answered as a dated succession — see RM135.
Severity medium-high · Status ✅ SHIPPED in 0.7 (2026-08-28) — the four questions were settled in PROPOSAL_0_7 and the entry below is kept as the record of what was observed · Owner format + compiler · Motivating case S60 (just-module-creator, in CONSUMER_SUGGESTIONS_HISTORY.md) · Answers the blocking sub-question of RM83
What RM83 was blocked on, and what this is¶
RM83 named a missing operation — a --refresh that re-asks and reports — and named the thing that made
half of it unbuildable: on most sidecars nothing records that a row was overridden, so "re-derive the
machine rows and keep the overrides" cannot be implemented, because the tier cannot tell a curator's
edit from what the source said last time. It offered two exits: compare and report every difference
without classifying it, or something has to start recording the edit, which it called a schema
question with the usual cost.
This is that second exit, proposed by a consumer who built the first one and hit its ceiling. They wrote a non-destructive wrapper around the delete-and-re-derive sequence — capture, verify the capture, delete, re-derive, classify, reapply what is provably the author's — and it stops exactly where RM83 predicted: when a subject is present in both copies with a differing fact, the fresh row is either a cell the author edited or a revision the source published, and with two data points there is no third to separate them. Their tool reports and refuses to resolve, which is the honest outcome and is a symptom.
The proposed shape¶
A recognized authored overlay table that lies on top of a derived one and is never merged into it:
one row per (table, subject, field) carrying the authored value, the reason in prose, who decided, and
when. The derived files become pure build products — derived = f(source, overlay) — and four things
follow, three of which are theirs and all of which check out:
- nothing is hand-edited, so re-derivation is non-destructive by construction rather than by a wrapper being careful;
- a difference between a fresh row and a previous one means the source revised, full stop; the three-explanations ambiguity stops existing rather than being reported;
- the reason for a correction travels with the module instead of living in someone's memory;
- the terminal state becomes detectable and it is free — an overlay row that no longer changes anything means the source caught up, which is evidence that an authored judgement was later vindicated and is available nowhere else in this format. That is the observation to keep whatever else changes; it is the same shape as S52's, recorded there for the same reason.
The charter, first-hand¶
Legal, and specifically invited. A new optional authored table is additive and minor-legal (P3), it
demotes nothing (P8), it is data and not code (P1), and it is authored input rather than a fetch (P2) —
a compiler reading it is doing what it already does with every other authored table. Under the 2026-08-12
cost amendment it is the full-cost layer, a human writes it; but that same amendment is what names
this class in the first place — a derived table that is both machine-written and human-overridable can
be edited into a state that is not merely stale but is a false claim, and that wants a mechanism rather
than a convention. RM45 discharged that for exactly one table by making verification.json unwritable
by hand. Nothing discharges it for the seven where overriding is the intended feature —
enumerated in PROPOSAL_0_7 § RM124,
which corrected this count on 2026-08-28. It said six here and there, and listed them in neither.
Four questions, and none of them is the tier¶
Their tier argument is accepted and is not an open question. An overlay is authored input, not a repair, so report-never-repair is not at stake; and if each downstream tool applies its own overlay, two consumers compiling one spec directory disagree about what the module says and the artifact stops being a function of the spec. That settles where, and leaves:
(table, subject, field)is not enough, and the table it fails on is their flagship case. RM115 shipped the merge keys they were blocked on, and it published something their derivation could not reach:resolution.csv's key isrule="subject", not a uniqueness constraint. Onevariant_keylegitimately resolves onto several loci,locus_indexorders them, and a pass replaces the group whole — so a subject names a group of rows, and an overlay row keyed on it cannot say which locus it corrects. Theirsource="manual"rows are in exactly that table. Either the subject gains a within-group discriminator for the one table that needs one, or overlays onresolution.csvare group-scoped and say so.gene_validity.csvneeds the same care in a different direction — its key has two levels (assertion_idelse the gene's grain), so an overlay written against one level is silent about rows keyed by the other.- P5: this and
provenance.jsonmust not become two records of one concept. S52 shippedProvenanceItem.outranks: dict[str, str]—{column: why}, an authored cell outranking a source, with prose. The overlay is a corrected cell in a derived table, with prose. Their split is clean as stated (authored vs derived) and it is exactly the kind of line that erodes: the first author who wants to explain why theirclin_sigbeats ClinVar and why theirchrombeats Ensembl has to learn which of two files each belongs in. Decide whether one record with a table column serves both, or whether the two are genuinely different axes, before either grows a second field. - What P7 makes of a build product. If the compiler applies the overlay,
reverse_modulehas to reproduce a spec directory that recompiles byte-identically — which means deciding whether it re-emits the pre-overlay derived table plus the overlay, or the post-overlay table with the overlay beside it (in which case the overlay applies twice and the fixed point has to be checked, not assumed).resolution_signatureand the fact signatures are over the derived tables as they stand today, so which of the two they cover is the same question wearing an identity. - Whether merge-not-clobber survives. The real prize is that
derived = f(source, overlay)lets the rule be dropped for the tables the overlay covers, which removes the operational fact RM83 opens with. The real cost is that every pass writes through it, and dropping it changes what a re-run does to every module already published. That is the part that makes this 0.7 rather than a minor.
The dependency that is real but routine¶
The registry rebuilds a spec directory from its own RECOGNIZED_SPEC_FILES, and a name missing there is
a file dropped on the next re-publish — which is how licensing.csv was lost before their 0.16.2. That
tuple is built from SPEC_DATA_FILES, a hand-kept mirror of our table constants, so an overlay needs
one entry added there. It is the same one-line change every new table kind already needs, and their own
comment records the scar; it is a coordination step, not a design blocker.