Roadmap — 0.8 and later minors¶
What this file is. Items that are legal in a minor and were not taken into 0.7, each with the
reason it waits. It is the direct successor of ROADMAP_0_7.md, which was split out of
ROADMAP.md on 2026-08-13 so that the active roadmap describes the line being built and a
deferral is filed against the release that will decide it, rather than accumulating in one document
nobody can read as a plan.
Why it was renamed rather than kept. 0.7 was built on 2026-08-28 and the file's name had stopped being true: it read as the plan for a release that is finished, so an item in it looked shipped-or-late rather than waiting. On 2026-08-31 the round it recorded closed — history/ROADMAP_0_7.md keeps that record, entries and all, including the five taken back into 0.6 and the two that built in 0.7 — and everything still waiting moved here. Nothing about any item below changed in the move; the file name did. Expect the same succession at the next minor: the deferral file is named for the release that will decide its contents, so a cut closes one and opens the next.
What 0.8 is, decided 2026-09-11 with the maintainer: a stabilization and competitor-parity release. Stabilization is the first half — the items below are mostly a specified thing nobody implemented (RM122), a prose behaviour two readers split on (RM149), a digest that says something changed and not what (RM181) — and parity is the second, with RM188 as its spine: running Calwbio's and genomi's pipelines on real input and re-folding what their reports do into module mechanics is the mechanism that files the parity items, so most of them do not exist as numbers yet.
The theme does not admit an item and does not reorder one. Membership is still the rule in the paragraph below — legal in a minor, waiting on a design question, a corpus or a caller — and legality is still decided by Principles 3/4/8 first. A theme says what a release is about when it is cut and what a reviewer weighs when two legal items compete for the same round; it is not a second gate, and nothing below was moved, re-severitied or re-scoped to fit it.
Everything here is additive under Principles 3/4/8 — a new optional column or table — so none of it is waiting on a version. Each waits on a design question, a corpus, or a consumer. An item waiting on a version belongs in ROADMAP_1_0.md instead, and RM69 moved there on 2026-08-27 for exactly that reason: filing by what a fix costs rather than by what decides it is how an item becomes unreachable from either plan. RM68 stays, and the two look alike enough to be worth separating: its governing exit is a real author with a non-GRCh38 module saying which outcome they wanted, and a demand exit keeps an item here where a version exit does not.
Two of these are not waiting on us at all. RM84's own half shipped in the 0.6 PT2 batch and only the consumer's discovery half is open; RM67 is not work — a documented divergence, numbered so it stays findable and does not get re-probed. Both are here so that a reader meets the reasoning instead of re-deriving it.
Indexed in RM_TOC.md, which is the complete list and the place to look an item up. The 0.6 decisions that touched these items are in PROPOSAL_0_6.md and PROPOSAL_0_6_PT2.md; the 0.7 round that emptied the rest is PROPOSAL_0_7.md.
RM317 — FMR1 repeat methylation is in every PacBio TRGT VCF as FORMAT/AM, and no module can bin it on a two-allele record¶
Severity medium · Status open — a minor, taken into 0.8 on 2026-09-30, design first · Owner format (a measure_kind member, the element-rule vocabulary, the host table) + compiler · Motivating case the maintainer, 2026-09-30: a beneficiary will soon hold PacBio samples, so methylation is wanted, "just don't build on thin air"; evidence in METHYLATION_PACBIO §§ 1.1, 3, 4.1 · related RM317–RM319, RM66, RM65, RM164
What is real. TRGT, PacBio's tandem-repeat genotyper and part of the HiFi WGS pipeline since at
least v2.1.0, writes ##FORMAT=<ID=AM,Number=.,Type=Float,Description="Mean methylation level per
allele">: the mean 5mC level over the CpGs inside the repeat tract, one value per called allele.
Open samples exist: NA09237 (male full mutation) is GT=1, MC=898, AM=0.85, reproduced with TRGT
5.1.0, and HG002 (normal male) is AM=0.1. source_field: FORMAT/AM already passes the pointer
grammar. fmr1_cgg_repeat asserts in prose that a full mutation "is methylated and silenced", and this
is the field that can check it.
What a module cannot do today.
- Select the right allele. HM06968 (female) is
MC=33,112,AM=0.90,0.07: the expanded allele is the unmethylated one. NA06905 goes the other way. The band means "the methylation of the expanded allele", i.e. theAMat the index whereMCis largest. Every element rule picks by the field's own values, so none can say it. - Name the quantity. No
measure_kindis a methylation fraction.allele_fractionwould put two quantities under one name (P5).repeat_alleles.csvpinsmeasure_kind=repeat_count. - Name the absence.
AM=.means either no CpG in the span or no MM/ML tags in the BAM, and the file cannot tell them apart.
Scale trap. TRGT 0.5.0 wrote AM as an Integer (values up to 185; probably the 0–255 byte,
unverified). From 0.7.0 it is a Float on [0, 1], and the changelog is silent about the change.
To design (the 0.8 interview). The kind's name, audited under P5, and whether the modification
(5mC) is its own axis. Whether repeat_alleles.csv hosts a second kind or a sibling table does. How a
cross-field selection is stated without hard-coding TRGT's MC into a vocabulary: the probe prices an
optional pointer column naming the selecting field at full cost. And the band values: none is
cited yet. The probe quotes sample observations, not boundaries, and a row needs a paper behind it
(@rm47-bin-cites).
Legality and price. Everything is additive, so it is minor-legal (P3, P6, P8). A kind member is cheap but a one-way door (P5). The selector column is full cost.
RM318 — imprinting-region methylation is real in PacBio outputs, but lives in BED and TSV files that no pointer can name¶
Severity medium · Status open — a minor, taken into 0.8 on 2026-09-30, design first · Owner format (a new optional region table kind, a non-VCF pointer) + compiler · Motivating case the maintainer, 2026-09-30: a beneficiary will soon hold PacBio samples, so methylation is wanted, "just don't build on thin air"; evidence in METHYLATION_PACBIO §§ 1.2–1.4, 3, 4.2 · related RM317–RM319, RM66, RM65, RM164
What is real. HG002 shows allele-specific methylation at two imprinted regions in public PacBio
pipeline outputs, and MethBat 1.1.0 run on them reports PASS:
SNURF:TSS-DMR (15q11–13) hap1 80.1 / hap2 14.6, and KCNQ1OT1:TSS-DMR (11p15 IC2) hap1 15.3 / hap2 86.4,
with pooled values near 49. The consumer holds per-CpG 5mC.bed.gz (MethBat 1.x) or
cpg_pileup.*.bed.gz (pb-CpG-tools 3.x), plus a profile.tsv. None of it is in a VCF, and no PacBio
tool writes VCF 4.5 M5mC.
What a row would need. Every item comes from the probe's measurements:
- a region (chrom, start, end, build), with no
variant_key; - a pointer to a BED or TSV column, which
source_field(a VCF pointer) cannot be widened to without overloading it (P5); - the aggregation from sites to region;
- a row selector (
Total, or a statement about both haplotypes); - the unit and the modification;
- a coverage floor, because a missing row is never "unmethylated";
- bands: pooled around 20% and 80%, plus a haplotype-delta band.
Traps the data showed.
- Haplotype labels are not parental. The same sample has hap1 methylated at SNURF and hap2 at KCNQ1OT1, so no band can say "the maternal allele".
- The scale moved at MethBat 1.0.0 (fractions to percent) under unchanged column names.
- pb-CpG-tools
modelandcountmodes disagree (hap2 4.1% vs 16.1% at one CpG). The mode is a header line, not a column. - The pipeline runs
methbat profileover CpG islands, nevermethbat report, so a user holds island numbers, not the imprinted regions. - H19 had no haplotype rows at all.
Not decided, and not to be built on thin air. The 20%/80% cut-offs are MethBat's defaults, not a guideline. No clinical source was read. MethBat ships 15 imprinted regions from Mackay et al. 2022 (Table 2), which is the corpus a design would start from.
Legality and price. A new optional table kind is minor-legal (P3). Its price is full, and it is the
largest addition in the methylation work. The probe prices a separate pointer column over widening
source_field.
RM319 — PacBio's TRGT carries the repeat count as FORMAT/MC, a per-motif String, so repeat_alleles.csv's count half does not reach a PacBio user¶
Severity medium · Status open — a minor, taken into 0.8 on 2026-09-30, design first · Owner format (the consumer contract, possibly the element-rule vocabulary) + the corpus · Motivating case the maintainer, 2026-09-30: a beneficiary will soon hold PacBio samples, so methylation is wanted, "just don't build on thin air"; evidence in METHYLATION_PACBIO § 4.1, last paragraph · related RM317–RM319, RM66, RM65, RM164
What was found. No PacBio file opened in the probe carries REPCN, the ExpansionHunter key that
fmr1_cgg_repeat (and the corpus's other repeat examples) point at. TRGT writes the count as MC,
Type=String, per allele, with _-joined per-motif counts inside each allele (18_8,25_8 at HTT
under TRGT 5.x). A PacBio consumer therefore cannot follow the module's source_field at all. An
author who writes FORMAT/REPCN|FORMAT/MC hands the consumer a motif-segmented string that no element
rule defines a reading of.
Why it matters now. A beneficiary of this work is about to hold PacBio samples, and TRGT is the
repeat caller in PacBio's pipeline. Every repeat_alleles.csv module is invisible to them today.
To decide.
- Whether this is a consumer-contract rule ("sum the motifs", or "the motif the row's
repeat_unitnames") or a vocabulary addition. The second is a minor. - How it meets RM66, one repeat locus with several motifs: TRGT's per-motif segments are exactly RM66's shape.
- Whether the reference examples gain the TRGT alternation.
Measure against the PureTarget VCFs first. The probe lists them, with FMR1, HTT and more.
Legality. A contract sentence alone is a patch. A new element rule or pointer shape is a minor. It is filed against 0.8 because the RM66 overlap wants one decision.
RM303 — the snapshot layout is a contract with six parties and no model, so it belongs in just-dna-format¶
Severity medium · Status open — filed 2026-09-28 at the maintainer's request, taken into
0.8 · Owner format (the model) + enricher (the writers and readers) · Motivating case the
builders' release.json keys have drifted, and one drift already reached cache status
The problem. A reference snapshot is data/*.parquet, optional sidecars, LICENSE.txt and
release.json, and locations.py's own header says that at least four parties (builder, publisher,
provisioner, reader) have to agree on those names. The names are constants in the enricher, so any
other repo that wants the layout has to import the network tier to get them. release.json has no
model at all. read_release returns a dict, and the 14 builders write it by hand at 15 sites. The
keys have drifted:
| Key | Writers |
|---|---|
built_at |
all 15 sites |
builder_version |
13 |
source_url, source_sha256, dataset |
11 each |
rows |
alphagenome_avi, mane, mitomap |
row_count |
clinvar, clinpgx, acmg |
license / licence |
strchive / civic |
builder, release, parents |
acmg; civic; mitomap_miss |
ClinVar writes no dataset, so cache status printed a blank release label for it. The repair was a
per-lane exception (clinvar_dataset_label, see caches.py near _dataset_label). That is the drift
surfacing as an incident, and it was repaired at the reader rather than at the contract.
The split. The two files mix two kinds of thing, and they are separated by what each is tied to:
- Contract →
just_dna_format.snapshot, pydantic + stdlib only, no environment, no network: the layout names (data/,release.json,LICENSE.txt, the sidecar and root-filename tuples), aSnapshotReleasemodel,read_release(dir) -> SnapshotRelease | None, and a payload predicate ("this directory holds a snapshot") that reads no environment. - Deployment → stays in the enricher:
CACHE_BASE_VARand the per-lane variables,.envloading, platformdirs andAPPNAME, the per-lane resolvers,CACHE_LANES, prepare, rebuild and publish. It provisions and fetches, and it is tied to specific lanes. The enricher'slocationsnames become imports from format.
Charter checks.
- Goals: an amendment is owed. Goal 1 scopes format to "annotation modules", and a snapshot is not a module. No principle forbids the move (no network, no new dependency), but it widens what the package is for, so it lands as a deliberate one-bullet Goals amendment, with the reasoning in CONSTITUTION_AMENDMENTS_HISTORY.md.
- P3 from then on. Once the model is published,
release.jsonkeys are additive-only within the major. That makes the drift above a decision to take before the first publish: which spelling of each drifted pair becomes the field, and whether the other is read as an alias. - P9. A derived file that no human authors: half price at most, and no authored schema moves.
- The tri-state rule.
read_releasekeepsNonefor both absent and unreadable, as it does today. A key the model makes optional staysNonewhen it is unstated, never a default, because the ClinVar label bug above is exactly a missing key being read as a blank answer.
Open questions.
- Core plus extension.
parents(the derived lane) and lane-specific keys either get typed optional fields or ride inextra="allow". Typed fields are safer; extra keys keep the model small. Decide by listing what each reader actually reads. - The drifted pairs.
rows/row_countandlicense/licencepick one spelling each; the old spelling is read on input and never written again.builderagainstbuilder_versionneeds a look at what acmg means by it. - A guard. Every builder writes through the model rather than through
json.dumps. An AST walk over*_build.pyrefusing a hand-builtrelease.jsondict is the usual shape (@registry-completeness). - Published snapshots. Snapshots already on HuggingFace keep their old keys. The model reads them, and a republish writes the canonical spelling, never an in-place edit.
Considered and refused on the same day.
- Renaming
just-dna-format(e.g. tojust-dna-schema, the workspace directory's name). With the snapshot contract added, "format" becomes more accurate: the package defines two on-disk formats. A rename would touch about 130 importing files across five repos and keep two distribution names alive for a major. Refused by the maintainer. - A
just-dna-datasets/just-dna-builderspackage. First offered on 2026-09-25 (the RM261 round) as the recommended way to take the builders out of the wheel, and declined then without being recorded. Weighed again now, it forms a cycle: builders need the layout from the enricher, andcache rebuildneeds the builders. Breaking the cycle means moving the layout below both, which is this item, and after that the separate package buys nothing. Its dependencies are the enricher's own, and the columns it writes are read by enricher passes, so every snapshot change would become a release of two packages. It would also need a Goal 2 / Principle 2 amendment, since builders fetch and the enricher is named as the only tier that may. Builders and readers stay beside the pass that reads their columns. just-prs's parquet lanes stay in just-prs (PRS_CACHE_DIR), and the v1 port is a consumer (pipelines → enricher), not a dataset. - A nano-library holding only the convention. Too small to be its own package. Format already
depends on pydantic and already owns the module's provenance record (
manifest.json), andrelease.jsonis that record's snapshot twin.
RM261 — strip comments from the enricher wheel (measured, deferred)¶
Severity low · Status open — deferred by the maintainer on 2026-09-25 as risky ("dangerous, defer") · Owner enricher (packaging) · Motivating case the 0.7.2 enricher wheel is 893 KB zipped, all of it Python source
Measured on just_dna_enricher-0.7.2. Of 2.58 MB of .py, docstrings are 31%, comments 19%, and
code plus whitespace 51%. Removing the comments alone takes the zipped .py from 847 KB to 664 KB
(−22%). Docstrings stay in any version of this item: they are the help() text and the API pages.
Why it is not simply done. hatch_build.py would have to leave the original .py out of the wheel
and force-include stripped copies. artifacts / force_include collide on the same path (see that
file's own note). The copies must keep line numbers, with a comment-only line becoming an empty line,
or a traceback from an installed wheel points at the wrong source line. Editable installs must not be
touched. It reaches the enricher only: format and compiler are on uv_build, which has no hooks,
and covering them means changing their build backend.
Considered and refused in the same round: taking the 15 *_build.py snapshot builders out of the
wheel (112 KB zipped). scripts/rebuild-caches.sh documents pip install 'just-dna-enricher[dev]'
then cache rebuild as a supported deployment, and caches.py imports every builder at module
level. The maintainer kept them in.
RM253 — a repeat-count star allele (UGT1A1 *28 = TA(8)) has no home a diplotype can name, and the CPIC drafter translates none of CPIC's notation¶
Severity medium · Status open — a minor, taken into 0.8 on 2026-09-21 · Owner format (schema) +
enricher (pgx_draft) · Motivating case
S106
— the most common UGT1A1 allele, the one atazanavir and the irinotecan labels key on, ships undefined
Reproduced on the snapshot, and the reporter's reading holds with one correction. CPIC defines
UGT1A1*1 as TA(7), *28 as TA(8), *36 as TA(6) and *37 as TA(9), all at rs3064744
(the report said rs8175347, the promoter's other name in the literature), plus *80+*28 and *80+*37
as two-variant haplotypes. draft_gene skips all six repeat rows, so haplotypes.csv defines *28 by
nothing while allele_function.csv and diplotypes.csv name it, and validate warns "Star allele(s)
used but not defined" — correctly, and unfixably by the author. Across the whole snapshot the notation
the drafter skips is 25 distinct spellings: DEL<bases> (the bulk, e.g. DELTCT), a bare DEL (4
rows, lengthless), INSCGGG, and UNIT(n) repeats (TA(8), AAAGGGGCG(2), GGA(1), TCAG(2)).
NUDT15 *2 has no defining row in the snapshot at all, so that half of the report is scoped to the
live API, which was not probed.
Three seams, and they are not one item's worth of the same thing.
- A repeat count as a defining allele. The format holds
<CNV:TR:n>since RM5 — butnis a length in bases, soTA(8)would be<CNV:TR:16>: legal, distinguishable from*36/*37/*1by length, and lossy about the unit. Whether that is the right spelling, or whether the sequence should be spelled out (sixteen bases, the standard's own preference when the sequence is known — but VCF-anchored, which needs the preceding base the drafter does not have and the Ensembl row RM251 now records does), or whetherrepeat_alleles.csvshould gain a join to a haplotype name, is the design question. The reporter's framing is exact: today it is a bin or a name, never both, and the diplotype table needs the name.@hosting-tri-statesays a symbolic allele compares as undecided against a spelled call, so the first option moves the join to the consumer's caller (requires_callable) — which may be the honest answer for a VNTR, and is still a decision. DEL<bases>/INS<bases>→<DEL:n>/<INS:n>. A drafter translation, not a schema gap: the length is the notation's own, and the symbolic spelling is exactly what RM5 built for a variant "whose sequence is deliberately unspelled". The bareDELstays skipped (no length). The open question is whether a spelled deletion should be spelled — CPIC gives the deleted bases and the position, and the anchor base is one reference read away.- HGVS-named alleles (DPYD).
HAPLOTYPE_NAME_PATTERNrefuses whitespace, and CPIC's names arec.1003G>T (*11)andc.1129-5923C>G, c.1236G>A (HapB3). The report's claim thathaplotype_nameaccepts them is wrong as spelled — probed, all three PGx models refuse on the space — so this is a naming policy for the drafter (the legacy star in the parenthesis? the HGVS string with the space removed? both, one assuballele?) before it is anything else. 167 defining rows and 3,570 diplotypes wait on it.
What shipped with the filing (message only, in the uncut 0.7 line): the RM5 notation warning now names the three symbolic spellings the format holds and says the drafter does not translate into them, pointing here; and the unparsable-diplotype sentence is bucketed by reason, so DPYD's HGVS names are no longer reported as CYP2D6's copy-number notation.
Parking-condition gate audit: none of the three needs a consumer to act first — the corpus is the
snapshot, already provisioned, and the probe module is a draft_gene("UGT1A1") away. This is a design
decision, not a wait.
RM248 — a declarative report schema: research, not a design¶
Severity — (unsized, deliberately) · Status open — a minor, taken into 0.8 on 2026-09-21 — a research item, asked by the maintainer 2026-09-20. Nothing here is a proposal yet; the exit is a measurement and a charter ruling, not a draft schema. The class is the most additive outcome this could have, which is a new optional table or spec block; two of its three exits ship nothing at all · Owner unassigned — the placement question is half the research · Pairs with RM28, RM188, and RM7 below (which is not the same item — see What this is not)
The ask, verbatim in intent: some kind of report schema — Jinja templates or something — where the contents are static and declarative and all the fancy CSS lives downstream.
Why it is filed as research rather than as a design¶
Because the charter has something to say first, and it does not obviously say yes. The Non-goals are explicit: "No UI and no gene–disease inference. The format catalogs curated annotations that consumers join against variant data; interpretation and presentation belong to those consumers." A report schema that describes a rendered page is that non-goal by another name.
The maintainer's own framing is also the resolution worth testing: a report is a declaration of CONTENTS, not of appearance. What a report says, in what order, keyed to which annotation rows is arguably still annotation; how it looks is unambiguously downstream. If that line holds, this is legal and the non-goal is untouched. If it does not — if a contents schema cannot be written without smuggling in ordering, emphasis or layout — then the honest answer is that this belongs to a consumer and the entry closes. That ruling is the first deliverable.
Principle 1 decides the other half before any syntax is chosen. OakVar's reporter modules are Turing-complete, and that is exactly the shape P1 rejects: code in the compile path, no byte-reproducibility, a runtime every consumer must embed. Jinja is not obviously safer — it has loops, filters and arbitrary attribute access, so "a template language" is a spectrum and the sanctioned escapes are named: a non-Turing-complete boolean predicate and declarative pattern grammars. Any candidate has to be placed on that spectrum explicitly rather than adopted because it is familiar.
What this is not¶
Not RM7. That entry is a per-sample evaluation output — a measurement, and therefore a consumer contract by the data-agnostic north star. This one is about the structure of a report over annotation content, with no sample in it. The two are adjacent enough to be confused and are listed together on purpose.
Not a fourth library, on today's evidence. The maintainer's read is that it looks like overkill, and
nothing here contradicts that yet: a declarative contents schema is a schema, which is
just-dna-format's job, and a renderer is a consumer's. A fourth tier would only be justified if the
research finds a real body of shared rendering logic that is neither schema nor consumer — and finding
that out is part of the item.
The competitor evidence, which is the reason this is worth research at all¶
This pairs with RM188 because the surveys already ran and the reports are where every competitor's declarative layer visibly ends:
- ClawBio —
pharmgx-reporteris a 2,327-line Python file holding its variant tables inline (PGX_SNPS,GENE_DEFS,GUIDELINES: 32 variants, 13 genes, 59 drugs), andclinical-variant-reporterandcnv-acmg-classifierareplanned. Their reports are good and their data is a module in a shape nothing validates. Seeprobes/CLAWBIO_SURVEY.md. - genomi — a runtime rather than a format, with a curated thirteen-record catalogue. See
probes/GENOMI_SURVEY.md. - OakVar — reporter modules, Turing-complete. The counter-example, and the one this repo's P1 already has an answer to.
- SelfDecode and the consumer-genomics vendors — the reports are the product. Not surveyed here, and a survey of what their reports contain (as opposed to how they look) is cheap and is probably the highest-value first measurement this item can take.
just-dna-litealready has a built-in reporting system. It is the reference consumer, so what it does today is the baseline any schema has to beat — and if it needs nothing from us, that is an answer rather than a gap.
Why it pairs with RM28¶
RM28's surviving half is the predicate — a claim keyed on more than one subject, which no brick
holds. Its second corpus entry is exactly a report finding: ClawBio's pharmgx-reporter renders one
AVOID across 59 drugs, warfarin, and reaches it through "special": "warfarin" — a hardcoded branch
calling get_warfarin_rec(profiles). The report is where the missing predicate surfaces: a section
that has to say "CYP2C9 and VKORC1 together" is RM28's gap wearing a consumer's clothes, and a report
schema that cannot express it would be shipping the same hardcoded branch one layer up. So the two
items constrain each other: RM28's answer bounds what a report section can be keyed on, and the
report is the place where an unexpressible pairing is most visible.
There is already a presentation surface, and it is small and deliberately bounded¶
Worth knowing before anything is designed, because it sets the precedent this item either follows or
breaks. module_spec.yaml carries report_title; normalize.py splits identity keys from
presentation keys with a stated reason per key (PRESENTATION_AUTHORITY_KEYS, today just
short_description); and RECOMMENDED_* vocabularies exist for colours and icons. So the format
already admits some presentation, as named keys with per-key justifications and a storing authority.
The question this item should answer is whether a report schema is the same thing at a larger scale, or
a different kind of thing that happens to rhyme.
What would close this¶
One of three, and the research is choosing which:
- Dissolved —
just-dna-lite's existing reporting needs nothing from the format, and the contents/appearance line cannot be drawn cleanly. The entry closes with the reasoning recorded. - Closed additively — a bounded, declarative contents schema (a table kind, or a spec block) that passes P1 and the human-authorable ⇔ machine-precise gate, with a motivating report from a real consumer. Then it is an ordinary minor-legal addition.
- Parked with a sharper reason — the shape is real but blocked on RM28's predicate, which is the most likely outcome if the warfarin case turns out to be representative rather than singular.
The cheapest first measurement, and the one to take before any syntax is discussed: read what
just-dna-lite actually renders today, and list every field it needs that a module does not carry.
That is a list, it is finite, and it decides between (1) and (2) without a design round.
RM181 — a byte digest that moves beside intact signatures says something changed and not what, and provenance has no shift tracker¶
Severity low · Status open — filed 2026-09-03 for the 0.8 review of what the hash family covers, and a candidate for the 1.0 one if it turns out to want a manifest field per domain · Owner format · Motivating case the maintainer's decision on S87 (RM180), in CONSUMER_SUGGESTIONS_HISTORY.md
The observation. With RM180, an author rewording an overlay reason produces: content_signature
unchanged, every fact signature unchanged, resolution_signature unchanged, artifact.digest moved.
Read from outside, that says something changed and nothing more — the byte digest is a canary, not a
locator. Before RM180 the same edit moved content_signature too, which was wrong for the opposite
reason: a provenance edit read as a content one. Either way there is no hash whose movement means the
provenance moved, and the maintainer's words for the gap were that metadata has no dedicated shift
tracker.
The shape named: digest by domain. One identity per concern — content (have), facts per sidecar (have), bytes (have), and a provenance or metadata one (do not have) — so a consumer holding two manifests can say which domain moved by diffing the hash family rather than diffing parquets. That is a separation of concerns, not a new axis on an existing hash, and it is why this is not a repair to RM180.
Why it waits. Three questions before it is an item. What the provenance domain contains — the
overlay's three cells only, or also sources.csv's fetched_at, verification.json's checked_at,
the README bytes and module_spec.yaml's display half, each of which is outside some hash today for
its own reason. Whether it is a manifest field (additive, minor-legal) or a member of the
*_signature family, whose roster rule in SCHEMAS is one per derived sidecar — and this is not a
sidecar. And whether manifest.inputs already answers it: the raw-bytes entry for overrides.csv
moves on a reason edit, so per file the answer exists, and what may be missing is the reading rather
than a hash. RM126's release record answers the neighbouring question — what a release changed about
compiled output — not this one, what an edit changed about a module.
What would close it. A consumer asking what moved and getting the wrong answer from the family as it stands; or the 0.8 review deciding the family is complete and this becomes a FAQ entry.
RM188 — the competitor survey — run Calwbio's and genomi's pipelines, read their reports, and re-fold the logic into module mechanics¶
Severity medium · Status open — round 1's two surveys filed 2026-09-13 and their findings routed; round 2's roster is below, searched for rather than named · Owner format (the survey), then whichever tier the findings land in · Motivating case the maintainer's direction, not a consumer report
Progress. Both named competitors are now probed, one document each, and neither turned out to be
a competing format: probes/GENOMI_SURVEY.md (genomi is a runtime; its
thirteen-record curated catalogue is a variants.csv in Python, and §8 ranks seven annotation gaps,
all priced as derived sidecars) and
probes/CLAWBIO_SURVEY.md (ClawBio is 97 agent skills — 68 of them
planned — whose genotype-interpreting half carries four hand-curated variant tables inlined as
Python dicts and JSON). The two surveys overlap in almost nothing, which is itself the result: one
reaches for pathway, target–disease and regulatory content we do not carry, the other for the
per-variant clinical axes an ACMG engine reads.
The ClawBio half's findings are filed, 2026-09-13 — a probe records and the tracker allocates, so
the numbers came from .claude/rm-next.py rather than the probe: RM236 (consequence/impact,
and RM23's grain question with them), RM237 (region-keyed ClinGen dosage), RM238 (per-tissue
eQTL, open but parked on a consumer), RM239 (fine-mapping posterior), RM240 (land the
translated nutrition panel in the corpus) and RM241 (weighting does not survive reverse). The
sixth finding was not filed as a new item because it is not one: warfarin's multi-gene call is
RM28's surviving pairing across subjects clause, and
it went in there as that entry's second corpus entry. The genomi half's seven ranked candidates
are recorded in its own § 8 and are not filed here.
What it is. A survey of the consumer-genomics competitors, with two named first — Calwbio and genomi — done the way PUBMIND_ASSESSMENT was done and not the way a feature comparison is: run their pipelines end to end on real input, obtain the reports they produce, and read the logic back out of the reports — which annotations they join, at what grain, which rules turn a genotype into a sentence, and where a number comes from. Then re-fold what survives into the module mechanics here: a table kind, a bounded rule, a vocabulary member, or a use case in USE_CASES.md that names the gap.
What it is not. Not a marketing comparison and not a licence to copy a rule: a competitor's inference is evidence about what a report needs, and the charter's non-goals still hold — no gene–disease inference in the format, annotation tables only. Where a competitor's logic is an inference, the outcome here is the table that would let a consumer make it, never the inference.
Method, so it can be repeated. One document per competitor under docs/probes/, the shape of
PUBMIND_ASSESSMENT: what was run, on what input, what came back, what it competes with, what it
complements, and the adoption design if any. Real data, the tool turned on its own output
(@adversarial-role, @probe-uniform-corpus), and every claim about a competitor pinned to an
artifact obtained rather than a page read — the ClinPGx rounds showed a documented surface and the
real one disagreeing.
Exit. Each competitor's probe filed, and its findings either dissolved (already enabled), closed additively (an RM with a motivating report in hand), or parked with the reason — the USE_CASES → PROPOSAL loop, entered at the top.
Round 2 — the candidate roster, built 2026-09-13. Round 1's two competitors were named by the
maintainer; round 2's were searched for, and the search is the first half of the result. Method:
GitHub repository search over eight query shapes (personal genome interpretation, 23andMe raw data,
annotation modules, MCP + bioinformatics, nutrigenomics, pharmacogenomics, ACMG, PGS), sorted both by
stars and by recency, plus awesome-genetics (last pushed 2024-03) for the pre-agent generation.
Twenty-two repositories were then skimmed — not surveyed — against one question: does it carry
hand-curated per-variant or per-gene rows a human wrote and committed, or does it fetch public
sources at query time? That is the discriminator round 1 established: both genomi and ClawBio turned
out to be the second kind, and the curated fraction of each was tiny.
Re-derive the search with:
gh api -X GET search/repositories -f q='topic:personal-genomics' -f sort=updated -f per_page=15 \
--jq '.items[] | "\(.full_name)\t\(.stargazers_count)\t\(.pushed_at[:7])\t\(.description)"'
varying q over the shapes above. Nothing found in the 2026 crop has more than ten stars, which
is itself a finding: the local-first personal-genomics field is a long tail of weekend projects, and
the established tools are older and narrower. Do not read the roster below as a competitive
landscape — it is a list of places to go looking for annotation shapes.
The four that get a full survey¶
| # | Target | Pinned | Class | What its survey asks |
|---|---|---|---|---|
| 1 | OakVar | d4e8090df8, 48★, licence NOASSERTION |
a competing module format with a store | A fresh peer read, not a migration post-mortem — the maintainer's framing: an OakVar module is in essence a plugin, arbitrary code, so it is a module plus half an annotator. Its manifest (<name>.yml) declares type (annotator / postaggregator / reporter), level, requires (other modules), input_columns, and typed output_columns — a column contract plus a dependency graph, where ours is a row schema with neither. Ask what the column contract buys, what requires expresses that no module here can, and what is left of a module once the arbitrary code is removed. |
| 2 | SNPedia, via snappy | 2d5255f, 52★, BSD-2-Clause; SNPedia content CC BY-NC-SA 3.0 US |
the corpus round 1 never had | The largest curated variant-trait corpus in existence: 106,603 SNP entries frozen into data/snps.json from a MediaWiki XML dump, plus genotypes.json (per-genotype magnitude / good-bad / summary) and genosets.json. Survey the corpus, not the SPA — snappy is the extraction route that proves a static dump works, where OSGenome (146★) only crawls live. Its terms are CC BY-NC-SA 3.0 US, established off three primary pages 2026-09-13, which makes it adoptable rather than not — as an unsellable, share-alike, attributed module of its own. See USE_CASES § 2e, now the maintainer's named use case for this format. |
| 3 | BioMCP | bb3a1d4ad7, 630★, MIT |
the agent-era tier done at scale | Thirteen times genomi's stars and the same architectural class. Ask the one question genomi could not answer at its size: when an agent tool surface is the product, what does it end up needing to say about a variant that a table does not? If the answer is "nothing", that closes the whole class and round 3 can skip it. |
| 4 | Exomiser | 98f4e0b6f2, 265★, AGPL-3.0 |
phenotype-driven prioritization | The only established tool in the roster that ships its annotation as a versioned data bundle rather than fetching it — the closest existing thing to a compiled artifact. Ask what its bundle contains, how it is versioned, and how HPO term sets sit in it, given that HPO ships no route here for licence reasons. |
The three small ones worth a read after those¶
| Target | Pinned | Why |
|---|---|---|
| dosedna | 530cfe7, 2★, MIT |
Hand-curated PGx over six genes, and the closest small analogue of our haplotypes/diplotypes/pharm_variants trio — plus a committed, provenance-stamped CPIC snapshot (allele_definition 39, diplotype_phenotype 666, recommendations 1,180). |
| dna-engine | 5791bb3, 0★, Apache-2.0 |
403 curated markers with the richest per-marker schema found anywhere: tier, effect, transferability, chips, per-genotype label/impact/summary/detail/actions, and citations as (pmid, note) pairs. Also a deliberate non-interpretive posture — it refuses to translate genotype into phenotype, and says why. |
| allelix | 4b56bbe, 30★, AGPL-3.0 |
Carries no corpus, and is here anyway: 38 ADRs documenting source-precedence and suppression rules (PharmGKB non-finding suppression, somatic-on-germline suppression, a GWAS odds-ratio magnitude modifier). Competing-source arbitration is authority_precedence's problem, and this is the only project found that wrote its reasoning down. |
| Custom-Personal-Genome-Interpretation | 15fe28a, 0★, MIT |
Two authored tables: 98 rsIDs carrying GRCh37 and GRCh38 coordinates side by side on one row plus population-split MAF, and 540 PRS weights transcribed out of a paper's supplementary table — see RM16. |
What the skim already found — five axes, each sighted independently more than once¶
This is the part that did not need a survey. Convergence across unrelated projects is the
@probe-uniform-corpus heuristic firing: when three people who have never met each add the same
field, the field is answering a real question.
- Which genotyping arrays can call this variant — three sightings: dosedna's
array_callable+ free-textcoverage_noteper gene, dna-engine'schipsper marker, andMrOrtiz/dna-annotator's measured-vs-imputed tiering per source array (it refuses to let an imputed call outvote a measured one). We have nothing.requires_callable/callable_from/min_qualityask whether the consumer's own VCF saw the position; this asks which platforms in the world interrogate it, which is a fact about the variant and therefore annotation. Note the per-gene / per-variant scope split, which is the shape that already cost 39 variants once.
A fourth sighting, and the first with a source and a measurement: S111 (just-module-creator,
2026-09-24). just-prs already holds the Illumina GSA v3 backbone that 23andMe v5, AncestryDNA v2,
MyHeritage and FTDNA v2 share, as typed positions on both builds (648,379), plus a 1000G LD-proxy
table. Measured over 16 real modules' resolution.csv: 336 of 965 positions typed (35%). The GWAS
modules came out near 22% and the PGx panels at 51%, and lifting to GRCh37 lost one position. The
report asks for a module-level expected match rate. Recorded here rather than as an RMn because
it is this axis with a candidate source, and three things a design would have to settle came up in
the reply (CONSUMER_SUGGESTIONS_HISTORY § S111):
(a) the fact is per variant (which platforms interrogate this position), and the module figure
is a manifest summary derived from it, the way the literature block summarizes
literature.csv. The per-variant form is also what the reporter's other use, picking the typed one
of two lead SNPs in LD, actually needs. (b) Counts, never a rate, with typed, LD-proxyable
and not position-matchable kept apart. The proxy count describes a substitution the consumer's
engine may or may not make, so it stays beside typed and is never added to it. Position-only goes
in the field's name or description, since a position match says nothing about allele or strand.
(c) The source is a snapshot lane, never an import of just-prs: the per-chip position sets
published on the HF org with a release.json, walked through CACHE_SURFACE.md's
checklist, and the GSA manifest's licence probed before it is republished
(@probe-the-real-file, @no-named-licence).
2. A per-variant claim's ancestry transferability — three sightings: dna-engine's transferability, Bluefinee/kaiseki's Japanese-cohort reconciliation, drhudsonandrade/OmniGenis recording discovery-cohort ancestry per GWAS association. We carry PgsRow.training_ancestry and GwasEffectRow.ancestry, both scoped to their own table; there is no way to say this variants.csv row was established in one population.
3. Effect modified by a non-genetic factor — two sightings, and genomi is the third: sinhaankur/open-genome-atlas splits each marker's evidence by kind (diet / lifestyle / geo), each axis independently cited; drdaviddelorenzo/nutrigenomics scopes its weight to a nutrient_domain; genomi's own caveat "folate fortification status of the population modifies effect size" is the same fact in prose. CopyNumberRow's modifier_gene / modifier_cn is the precedent shape for a genetic modifier and there is no environmental one. Note that the second sighting also lands on @weight-has-no-unit: their weight at least names the domain it is a weight in.
4. Two sources disagreeing, as a recorded verdict — three sightings: kaiseki's DISCORDANCE_RATIO, which refuses to publish a consensus frequency when cohorts disagree; Gunshipz/genomine's cross-tool confidence/disagreement layer; allelix's ADRs. clin_sig_concordance.csv does exactly this for clinical significance and only for clinical significance — kaiseki does it for allele frequency, where frequencies.csv has per-population rows and no verdict.
5. A hand-assigned salience separate from clinical severity — two sightings: alexlaverty/dna-health-report's magnitude 0–6 per genotype, and SNPedia's own m field carried through snappy. VariantRow.priority is the candidate analogue; whether "priority level override" means the same thing is a question for the survey, not an assumption.
A sixth shape was seen and is already decided against, recorded so it is not re-raised.
dianguan0105/Custom-Personal-Genome-Interpretation stores GRCh37 and GRCh38 coordinates on the same
row, so no liftover is needed at read time. That is the opposite of the rule here — genome_build
lives in the manifest and in no parquet column (@build-in-manifest-only), the build is injected at
load and never authored on a row (@build-injected), and a module is single-build by design with
reference_examples/grch37_build as the worked case. Their design buys convenience and pays for it
with two coordinates that can disagree and nothing able to notice. Not a gap. Worth one paragraph
in whichever survey reads them, and no more.
And one that is not an axis but a corpus: RM28 has a third entry. snappy's
genosets.json carries SNPedia's boolean-combinator DSL — and(rs4988235(C;C), rs182549(C;C)), with
genoset-of-genoset nesting for haplogroup trees. That is a third independent grammar for the same
thing after CIViC's molecular profiles (RM174) and ClawBio's guideline logic, and unlike those two it
is a general-purpose one written for consumer genetics rather than falling out of one domain.
Counting its operators and its nesting depth is worth doing whether or not the rest of snappy is read.
Checked and skipped — recorded so round 3 does not re-search them¶
Same class as genomi (fetch public sources at query time, no curated content): lagodinm/dna-health-report,
MrOrtiz/dna-annotator, Gunshipz/genomine, itsrudaynah/Modrik, techninja/asili,
drhudsonandrade/OmniGenis (58k LOC and its own scripts say "Nothing here is authored"),
mentatpsi/OSGenome (146★, crawls SNPedia live and freezes nothing).
Pipeline or converter, no interpretation: GeiserX/Personal-Genome-Pipeline (10★),
captainzonks/GeneGnome.
Curated but too thin or too stale to teach anything: Michael-Sebero/Genetic-Trait-Detector
(245 rows, one 432-line file, last pushed 2025-03), dev-kvt/GenomeUpload (27 uncited rows inline in
JS, with an LLM writing the actual report).
Curated only as a panel — a list of rsIDs with no fields on them: brandonsaldan/codex (37★, 225
JSON files of bare rsID membership, all interpretation scraped live from SNPedia, dead since 2023).
Documentation, not a tool: matbanik/agentic-genomics (a setup guide wiring up three MCP servers
that live elsewhere).
Not competitors, one line each: the single-source MCP servers (berntpopp/clinvar-link,
cyanheads/gnomad-genetics-mcp-server, and our own dna-seq/ensembl-mcp) wrap one API and carry no
annotation; the general AI-science workbenches (ScienceClaw, aipoch/open-science, wisp-science, all
600–4,000★) are not genome tools; WGLab/InterVar (213★) is ACMG classification, which ClawBio's
half already covered, and its last commit is 2021.
Commercial, no source: Promethease (now MyHeritage), Genomelink, SelfDecode, Codegen, Nebula.
RM188's method does not reach them — a report can be bought and read, but no pipeline can be run
and nothing can be pinned, so a survey of one would be a marketing comparison, which § What it is
not forbids. Promethease is reachable through SNPedia instead, which is why row 2 is the corpus
and not the product.
The agent-skill class is closed, and it cost one read¶
BioTender-max/awesome-bio-agent-skills
(178★) is a directory of AI agent skills for biomedical work. Enumerating the entries that interpret a
human personal genome — as opposed to answering "what does the literature say about gene X" — gives
21, and they cluster under four collections: clawbio/ (six: pharmgx-reporter,
nutrigx-advisor, gwas-prs, genome-compare, claw-ancestry-pca, clinical-variant-reporter),
openclaw/, omicsclaw/ and bioskills/. The rest are ACMG classification, PRS, and PGx under
different names.
Round 1 already surveyed the representative of this class. ClawBio's six skills are in this
directory, and CLAWBIO_SURVEY.md read them end to end. Nothing in the
remaining fifteen is a different shape — they are the same fetch-and-render procedure over the same
public sources, and the directory itself is the evidence for that rather than an argument. So the
agent-skill class is closed for round 2, at the cost of one directory read rather than fifteen
surveys, and round 3 should not reopen it without a reason that is not "there are more of them now".
Two names in it are domains nothing here touches and neither survey raised: chip-clonal-hematopoiesis-agent
(somatic clonal haematopoiesis) and prs-net-deep-learning-agent. Neither is an annotation table, and
both are recorded only so the next reader knows they were seen.
Security note, since the search surfaced it. Barrelsravennagrass984/Personal-Genome-Pipeline is a
near-verbatim clone of GeiserX/Personal-Genome-Pipeline whose README is replaced with download bait
for a .zip committed inside the module tree. Do not fetch or run it. Recorded here because the
next person to run this search will find it in the same result set.
RM236 — consequence and impact are planned axes with no slot, and an ACMG engine reads both as primary inputs¶
Severity medium · Status open — filed 2026-09-13 from RM188's ClawBio half ·
Owner format (schema + compiler), then enricher · Motivating case
probes/CLAWBIO_SURVEY.md § the two columns their engine reads that we
cannot store
ROADMAP § Reserved namespace lists consequence (the VEP/Sequence
Ontology term) and impact (HIGH|MODERATE|LOW|MODIFIER) as planned future annotation axes, with
the rule that they "get a slot and a specific diagnosis only when a release actually commits to
building them". Nobody had committed, because nobody had a case. The case is now in hand:
ClawBio's skills/clinical-variant-reporter/acmg_engine.py is a running 653-line ACMG/AMP engine
whose per-criterion evidence table prints consequence=frameshift_variant for PVS1, impact=HIGH
for PM1, and consequence=frameshift_variant, SpliceAI=N/A for BP7 — three of its twelve implemented
criteria read a column this format cannot store, and a fourth reads the transcript the term was
called against.
Get the mechanism right before touching anything. Neither name is in RESERVED_NAMES_0_4, so
reject_reserved never claimed them and there is nothing to move out of a "built half": an author
writing consequence today gets the generic extra="forbid" message. Committing means one of two
things, and choosing is the first deliverable — build the column, or add the reserved slot plus
a vocab.RESERVED_NAME_REASONS entry so an author hears what the name is held for instead of the
stray-column message. The second is the honest outcome if the grain question below defers the build.
The blocker is grain, and it is RM23's blocker
restated. A variant has one consequence per transcript; VariantRow is one row per
(variant_key, genotype). Three options:
- MANE Select only — one value per variant, with
GeneMetricsRow.mane_selectnaming the transcript it was called against. Cheap, lossy, and defensible only if the column's description says so in the field itself (@field-description-is-a-claim). - Most severe across transcripts — a ranking policy. That is an interpretation, so it is out by the same clause that keeps threshold-picking out of RM23.
- A derived
consequences.csvsidecar keyed(variant_key, transcript)— half cost under Principle 9, no authoring burden, and it is the same shape RM23 needs.
Recommend (3), and settle RM23's grain in the same pass. They are one question asked twice, and answering it once is the whole saving; splitting them is how two sidecars end up with two different answers to "which transcript". Do not build (1) as a stepping stone — a column shipped under a major cannot be retyped into a table.
Related RM23 (same grain blocker, and the pass that should settle both), RM188.
RM237 — ClinGen dosage is gene-keyed, so a recurrent-CNV region has nowhere to be written¶
Severity medium · Status open — filed 2026-09-13 from RM188's ClawBio half ·
Owner enricher (the lane) + format (the model) · Motivating case
probes/CLAWBIO_SURVEY.md § dosage keyed on a region, not a gene
GeneMetricsRow carries haploinsufficiency and triplosensitivity, keyed on a gene symbol, and
enricher/clingen.py fills them. ClinGen publishes four dosage lists — gene curation, region
curation, recurrent CNVs and ISCA regions — and enricher/acmg.py's own probe note from 2026-08-03
records seeing all four on the FTP tree while reaching for one. The three unread ones are the half a
CNV classifier actually needs: ClawBio's skills/cnv-acmg-classifier/data/curated_dosage_map.csv
carries an element_type column precisely because two of its four demo rows are regions
(22q11.2, chr22:18,900,000–21,500,000, hi=3 ts=3), and its ClinGen/ACMG 2019 Section 1 scores off
them.
A region is genuinely the subject, which is why this is not a widening of the existing table. A
22q11.2 deletion is not a claim about any one gene in the interval; filing it under a gene symbol
would be a false attribution of the kind @gene-map-is-another-sources-attribution forbids. So the
shape is a sibling sidecar — region_metrics.csv, one row per (chrom, start, end, name) with
haploinsufficiency, triplosensitivity and ClinGen's curation id — rather than an element_type
discriminator on gene_metrics.csv. "One CSV = one concern" and @sidecar-name-and-place both push
apart here: a region row carries coordinates and a gene row carries a symbol, and a table holding both
would have half its key null on every row.
Two things to settle before writing the pass, and the second is the one a review will catch:
- Terms. ClinGen's dosage lists are believed unrestricted and that is recalled, not probed
(
@no-named-licence). Read the file and its SPDX id first. - Which
(source, layer)row this claims.@write-the-sourcerow's second-surface clause says a second surface of an already-declared source may not claim the lane's existing row. Whether region curation is a second surface ofclingenor the same lane is undecided, and it decides theSourceRowkey.
Related RM188, @write-the-sourcerow, @gene-map-is-another-sources-attribution.
RM238 — a per-tissue eQTL has no row, and expression_effects.csv is the wrong table to widen¶
Severity low · Status open — filed 2026-09-13 from RM188's
ClawBio half; PARKED on a consumer, deliberately · Owner enricher · Motivating case
probes/CLAWBIO_SURVEY.md § nine databases, and the two axes that come
back with no home
clawbio.py run gwas --demo on rs3798220 returns five cis-eQTLs by named tissue from GTEx and the
EBI eQTL Catalogue — LPA in Liver at β −0.82, in Adipose at −0.45, SLC22A3 in Liver at +0.31.
Nothing here holds that. expression_effects.csv exists and is the wrong place: it is
AlphaGenome-shaped, one row per (variant, gene) aggregating 371 tissue tracks into
tracks_agreeing/tracks_total, and expression.py
argues at length that one row per track "is lossless and unreadable". A GTEx row is not a track — it
is a measured cis-eQTL in one named tissue with its own β and p, ~50 tissues rather than 371, and
the tissue is the fact rather than something to consensus over. Different grain, different source,
different terms. So: eqtl_effects.csv, one row per (variant, gene, tissue, dataset), tissue as an
ontology term where the source gives one.
This is filed open and parked, and the parking reason is a rule rather than a shortage of time.
USE_CASES § 7.2
closes with "reopen this with a consumer, never with an argument", and a competitor rendering the
table is an argument. Reopen it with somebody who needs the answer. When that happens, the first
step is the acquisition measurement that correctly parked the frequency snapshot — what the GTEx and
eQTL Catalogue bulk artifacts weigh, and what their terms say (@probe-the-real-file).
Related RM188, RM194/RM200 (the AlphaGenome table this must not be folded into), USE_CASES § 7.2.
RM239 — a fine-mapping posterior has no column, and gwas_effects.csv already has its key¶
Severity low · Status open — filed 2026-09-13 from RM188's ClawBio half ·
Owner enricher · Motivating case probes/CLAWBIO_SURVEY.md §
nine databases, and the two axes that come back with no home
The same gwas-lookup report returns three fine-mapping credible sets for rs3798220 with a posterior
probability and 95%/99% set membership per (trait, study) — PP 0.92 for Lipoprotein(a) in
GCST005140, 0.45 for aortic valve stenosis in GCST90038614 with 95% No and 99% Yes.
A posterior inclusion probability is the same class of object as an allele frequency or a LOEUF: a
number a named dataset publishes, no measurement by us, no inference by us. Its key is
(variant, trait, study), which is GwasEffectRow's key plus nothing — the row already carries
trait_efo_id, study_accession and p_value_num. So the cheap shape is two optional columns on
the existing table, posterior_probability and credible_set, minor-legal under P3/P8, and the
GWAS Catalog now publishes credible sets for its harmonised studies.
Smallest item on the survey's list, and it should not be done on its own. Do it the next time
enricher/gwas.py is open for another reason. Two things a design owes: whether credible_set is a
membership flag or the set's size/level (95/99 are two answers to one question, and putting both
in one column is the @field-description-is-a-claim failure), and a withhold for a study whose
harmonised release carries no credible set, which is a nobody-asked third state and not a zero.
Related RM188, RM90 (the table this lands on).
RM240 — the ClawBio nutrition panel compiles, and the corpus should carry it¶
Severity low · Status open — filed 2026-09-13 from RM188's
ClawBio half; the translation is run, the landing is not · Owner format (the corpus) ·
Motivating case probes/CLAWBIO_SURVEY.md § The translation, run
The survey's headline claim — a competitor's inlined panel is a module written in the wrong language —
was run rather than asserted. skills/nutrigx/data/snp_panel.json at 1fcecb7e (28 SNPs) translated
field-for-field into a spec, validate green, compile producing 84 weight rows over 28 variants, 24
genes and 12 categories, and compile → reverse → compile reproducing content_signature exactly.
Land it as reference_examples/nutrigenomics_panel/, which is @probe-becomes-example and is the
only form in which the claim stays true as the code moves.
What it broke on the way, and the README has to say all of it:
stateis required anddirectionis not. 84 rows refused for omitting the superseded column while carrying the modern one. Already in the 1.0-cleanup tracker with the right reason (P8 forbids demoting a required field inside a major, so the demotion and the deprecation land together at 1.0). What the run adds is that this is the first error a first-time author meets, before anything about their data.- Their 28
ref_allelecells carry no coordinate, and the schema refuses a bareref— correctly, since an unanchored reference allele cannot be checked against a reference genome. The honest translation dropsrefand keepseffect_allele.
What the landing still needs, none of it optional because the corpus walkers assert it: a
resolution.csv so validate --strict and compile --strict pass
(test_reference_example_compiles_under_strict), a sources.csv whose terms are read rather than
recalled — their data_license is blank and the repo is MIT, which is a licence for the code
(@a-hosts-terms-are-not-its-contents-terms) — a verification.json closure
(test_every_reference_example_is_closed_and_its_closure_still_describes_it), a README, and a section
in REFERENCE_EXAMPLES.md. Two enricher passes are worth running first because
they are what makes it the worked answer rather than a copy: check-identifiers (their panel names
BCMO1, a retired HGNC symbol) and literature over the 28 PMIDs.
One observation for the README, not a repair. Their panel weights rs429358 and rs7412 as two
independent additive rows in two different nutrient domains. Those two variants are the APOE ε pair,
which reference_examples/apoe_epsilon/ models as a haplotype because the isoform is the joint state.
That is a claim about their curation, and it belongs in the README as a sentence.
Related RM188, RM92 (see RM241 — the weighting block this example is forced to write).
RM241 — weighting does not survive reverse, so a reversed module has no scale to read¶
Severity low · Status open — filed 2026-09-13 from RM188's
ClawBio half · Owner format · Motivating case
probes/CLAWBIO_SURVEY.md § The translation, run, last paragraph
weight is the one magnitude in this format with no unit beside it (@weight-has-no-unit), and RM92
built module_spec.weighting — scale, method, note — to be the place a module says what its
weights mean. It is advisory, copied into the manifest, and not reconstructed by reverse_module,
in the same class as panel / authorship / license. All of that is documented and, taken one field
at a time, defensible: it is not content, so it is correctly outside content_signature.
The survey hit the consequence. Translating ClawBio's nutrition panel forced an honest weighting
block to be written — their weights are hand-assigned importances within a nutrient domain, and
their scorer normalises by the weight of the SNPs it happened to find, so a domain score is comparable
only inside one domain of one run. That block is the single most useful sentence in the translated
module. reverse then drops it. So a consumer holding two reversed modules has two weight
columns and no way to learn that they are on different scales — which is the exact failure RM92 was
built to prevent, surviving in the one path RM92 does not cover.
Three candidate dispositions, and this entry does not pick one. (a) Nothing — write it in the
FAQ as a known lossy field and let a consumer read the original spec, which is what
reverse's own contract already says. (b) Carry weighting through reverse from the manifest,
which is where it already lives — cheap, and it changes what reverse claims to be. (c) Decide the
lossy set is right and that the real defect is that nothing warns, the way the dropped verification
attestation warns. (c) is the most likely right answer and is the cheapest to test: reverse
already emits one warning for a dropped attestation, so a second for a dropped weighting costs one
line and tells the author the thing they need to know at the moment it stops being true.
Do not fix this by putting weighting inside content_signature. It is prose about a column, not
the column; hashing it would make a reworded note move the digest, which is the defect
FAQ already answers twice.
RM164 — heteroplasmy.csv is a shipped table kind with no source behind it¶
Severity medium · Status open — PARKED to 0.8, decided 2026-09-01, moved into this file 2026-09-11 · Owner enricher · Motivating case the 2026-09-01 source-adoption round
Decided 2026-09-01 with the maintainer: parks, on the measured negative below. The candidate field
anyone has named is MITOMAP and the population callsets it re-hosts, and none of them publishes the
axis the kind binds; that is a fact about what exists, not about how hard anyone looked, which is what
makes the deferral honest rather than indefinite. It stays open and visible rather than closed,
because a kind with a one-module corpus is exactly what @probe-uniform-corpus says to keep in view —
and if a source that bands heteroplasmy by tissue appears, this entry is where it is checked against.
Reopen it with a source, never with an argument. The spin-off it noticed is now
RM171.
Probed and drafted in PROPOSAL_0_7_PT2 on 2026-09-01 — proposed PARKS to 0.8; the maintainer pass took it as proposed. Answered by reading the source, after the maintainer supplied the 2026-08-24 pg_dump (61 MB, 95 tables). MITOMAP is reachable — plain curl gets the dump at mitomap.org/downloads/, HTTP 206 with ranges; the Cloudflare challenge is on the web surface only, and two earlier readings of this entry (a "refusal", then "unreachable by the machinery") were both a 403 from a path that was not the data path. The axis answer is a measured no. The schema has exactly one tissue column, on mitomap.unpublished — per-patient submissions beside sample_id and ethnicity, i.e. sample data this format does not carry. mitomap.mmutation is 602 rows whose homo/hetero are presence flags (+ 286/270, - 216/238, nr 90/89, plus ./na/NULL), with no threshold, no band and no tissue — the only levels in the table are two rows where a percentage was typed into a flag column. The only heteroplasmy numbers anywhere are re-hosted blood-cohort data (mitomap.gnomad 18,164 rows, mitomap.helix 14,104), where max_observed_heteroplasmy is a cohort observation, not a clinical threshold. So HeteroplasmyRow's binding columns have no source-side value in MITOMAP. Terms are unread, not unestablishable — the dump carries no licence text in 6.7 M lines and the page a browser reaches was not opened. Parks, not closed. Separately noticed and not part of this entry: mmutation is a plausible mtDNA variants.csv source, blocked on status being 29 free-text strings rather than a vocabulary — its own item when taken.
The measurement, taken over _TABLE_KINDS and the enricher's providers. Every table kind is in
DRAFTABLE by construction, so structurally all nine are draftable. A provider exists for four:
haplotypes/allele_function/diplotypes (pgx_draft ← CPIC), pharm_variants (clinpgx_draft ←
ClinPGx), and variants (clinvar_draft, civic_draft, pubmind_draft). heteroplasmy.csv,
repeat_alleles.csv, copynumbers.csv, pgs.csv and activity_phenotype.csv have none, and no
enrichment pass reads them for a cross-check either. enrich() does resolve heteroplasmy rows — it is
the third table that can ask, and the one that keys with alts — but resolution is not a source.
The corpus behind the kind is one module: reference_examples/mt_heteroplasmy, two MT-TL1 variants of
one gene, hand-authored from the literature. That is the @probe-uniform-corpus shape exactly — the
schema generalized from a single case, and nothing since has taken a second one.
MITOMAP is the canonical mtDNA variant table and the obvious candidate. Two things must be established before that is a plan, and neither is:
- The terms. MITOMAP is not CC0, and this entry states nothing further about its licence.
Whether it is expressible as a
SourceTermsat all, and whether it lands as a draft source or only as a check, is decided by reading its published terms. RM153 is the standing reminder that a source's terms page can answer HTTP 200 with something that is not terms, and that the honest record of an unestablished axis isNone(@no-named-licence). - Whether it carries the axis at all.
HeteroplasmyRowbinds a level band per(gene, reference_sequence, tissue, variant_key). A per-variant pathogenicity table with no tissue and no threshold fills the identity columns and none of the binding ones — it would draft rows that say nothing the kind exists to say. Probe the real file and name the table probed (@probe-the-real-file,@probe-names-the-table); a negative here is as useful as a positive and closes the item cleanly rather than leaving it open forever.
Related RM165 (the same shape on the other uncovered binning kind), RM171 (the spin-off),
@probe-uniform-corpus.
RM122 — the measure lookup is specified and nothing anywhere implements it¶
Severity medium · Status parked on demand, moved here 2026-08-21 — additive and minor-legal whenever it is wanted; what it waits on is a caller, not a decision · Owner format · Motivating case S58 (just-module-creator, in CONSUMER_SUGGESTIONS_HISTORY.md)
The specification half shipped; this is the part that was not asked for and might still be right. S58 reported that the four binning kinds annotate nothing downstream and asked for either a normative paragraph or an admission that the family is specified ahead of its consumers. Both are now in SCHEMAS.md § The measure lookup a conforming consumer implements, and that closes the item they filed. What is filed here is the next question, which they did not ask: whether the rule should also exist as a public function so that the first two consumers to implement it cannot disagree.
The argument for. It is the shape S51/RM115 settled one layer down — a rule kept as prose is a rule
every reader re-derives, and the two derivations differ on exactly the cases that matter. Here those
cases are enumerable and sharp: the continuous shared endpoint, the float32 comparison, unresolved
versus no-match, and pleiotropy returning several rows rather than one. A consumer will get at least
one of the four wrong, and the failure is silent — a wrong bin renders as a confident phenotype.
alleles.split_genotype is the precedent: the reader half of RM81 shipped as one public leaf every
tier calls, while the retype waits for a major. It costs the format tier nothing — pure arithmetic over
loaded rows, pydantic-only, no dependency moves.
The argument against, and it is why this is open rather than done. There is no consumer to check the
shape against, which is the same reason measure_step is not a column: a signature fixed against a
hypothesis fixes the wrong thing, and this one has real shape questions. Does it take rows or a table?
Does it return one row, or one per trait_efo_id (the honest answer, and the inconvenient one)? Does it
answer None for no-match, or a three-state result distinguishing no match from unresolved selected
— which is what the house algebra would demand and what a None return would collapse. Getting that
wrong ships a leaf whose first real user has to work around it, and P3 keeps it working forever.
What would settle it: one consumer implementing the lookup against the paragraph. Their questions
are the signature. Until then the paragraph is the contract and this stays filed — the same
wait-for-the-demand rule that governs measure_step, applied to a function instead of a column.
Decided 2026-08-21: wait for demand, and the wait is the answer rather than a way of postponing
one. The four shape questions are the reason — does the lookup take rows or a table, does it return
one row or one per trait_efo_id (the honest answer, and the inconvenient one), does it answer None
for no-match or a three-state result distinguishing no match from unresolved selected. There is
nobody to check any of those against, and P3 keeps a wrong leaf working forever. This is the same
wait-for-the-demand rule that keeps measure_step out of the schema, applied to a function.
It moved out of the active roadmap because "undecided release" was the wrong bucket for it. Nothing about this is undecided; it is parked, which is what this file is for, and it sat under a heading that made it read like an unmade call. The settling event is specific: one consumer implementing the lookup against the paragraph in SCHEMAS.md. Their questions are the signature — file it back in the active roadmap when they arrive, not before.
RM23 — Computational predictor scores as a table¶
Severity medium · Status deferred — considered for 0.6 on 2026-08-13 and held, on the two blockers unchanged · Owner format (schema + compiler) + enricher · Motivating case pathogenicity triage; splice-impact panels
predictions.csv — the groundwork every predictor source needs, built once: one row per
(variant, predictor, score_kind) with score, dataset, source, and an optional transcript.
Long-form, not wide, is the load-bearing choice — SpliceAI is four deltas plus positions, CADD is
one number, AlphaMissense is one plus a class, so wide columns would make every new predictor a schema
bump while long form makes it data. A predictor score is the same class of object as an allele
frequency or a LOEUF (a per-variant number from a named dataset, no measurement), so the 0.5 sidecar
precedent covers it.
Why it is still deferred, after the 0.6 review. Neither blocker is code, and neither has moved:
- Grain. SpliceAI scores are per-transcript, and there is no settled way to name the four splice deltas without inventing a predictor-specific column set — which is the exact thing long form exists to avoid. Whether a per-transcript score is one row each or a picked representative is undecided.
- Acquisition. Precomputed splice scores need the masked vs raw file sizes measured and the Broad lookup API's terms read. This is the same measure-first question that correctly parked the frequency snapshot, and skipping it is how a shape gets fixed against a guess.
Unlike the two derived tables 0.6 does build (RM24, RM25), this one is a full-cost authored table under the 2026-08-13 charter amendment, so the bar is higher rather than lower.
Licensing is solved, not blocking — do not re-raise it as the reason. SpliceAI/Pangolin, dbNSFP,
AlphaMissense, REVEL, CADD and PrimateAI are all non-commercial or academic-only, and licensing.csv
plus the compile gate already confine that to the modules that use them, while phyloP/phastCons/GERP
(UCSC, free, queryable per-range rather than a bulk download) keep a module sellable.
What would unpark it: the acquisition measurement done, and a decision on per-transcript grain. That is a research task, not a schema task, and it commits nothing.
RM16 — Authored PRS weights (a scoring file, not a manifest)¶
Severity medium-large (on demand) · Status deferred — considered for 0.6 on 2026-08-13 and held; parked on shape rather than on demand since 2026-09-13, and its unpark condition needs restating · Owner format (schema + compiler) · Motivating case authored-weight PRS modules
0.4 shipped pgs.csv as a manifest of PGS Catalog IDs with the ancestry-validity fields — not
authored per-variant weights. just-prs resolves a PGSxxxxxx id to a harmonized scoring file and
scores each id itself, so inlined weights would be dead data; and a PRS is a
Z/percentile-in-reference, a shape the format does not bin.
What is deferred is a distinct, digest-bearing effect_allele + effect_weight scoring table, for the
case where a module must ship weights the PGS Catalog does not host (a score published only in a
paper's supplementary table).
Why it is still deferred. It is not derivable — nobody can fetch weights that exist only in an appendix — so it is a full-cost authored table. And the one thing that would validate its shape, a real consumer combining authored weights into a score, does not exist. A score's shape (how weights combine, what the reference distribution is, whether a percentile travels with it) is exactly what a first real case would dictate, so fixing it now spends a one-way door on a guess.
What would unpark it: a real consumer. See PROPOSAL_0_5.md D1.
This entry and RM28 are one axis at two operators (maintainer, 2026-09-13), and they should be decided together rather than merged. Both ask the same question — how does a conclusion get derived from more than one variant row, and where does the combining rule live? — and the format already answers it once:
| Combiner | Where the rule lives | Status |
|---|---|---|
| Enumerative — write out each combination | in the table: a diplotypes.csv row is the combination |
shipped, reference_examples/apoe_epsilon |
| Boolean — a predicate over genotypes | nowhere: the table would hold terms, something else the operator | RM28, parked on a corpus |
| Weighted sum — terms plus coefficients | nowhere: the table would hold weights, the consumer the arithmetic | this entry, parked on shape |
Three facts follow from the table and none of them from either entry alone. The enumerative answer is why both are parked and neither is urgent — it covers the small-arity cases, which is most of them, so any argument for a new combiner has to beat it rather than merely want one. Both non-shipped rows split the same way against the data-agnostic goal: the module carries terms, the consumer does the combining, so neither is a licence to put an evaluator in the compiler. And one corpus supplies evidence for both — SNPedia has genosets and per-genotype magnitudes, which is why one survey touched both entries on one day.
They stay separate items. The outputs differ in kind — RM28's is a categorical conclusion, this one's is a number that means nothing without a reference distribution — and RM28 was already halved once on 2026-08-13 for being too broad, so merging would make the smaller hostage to the larger. What they share is a precondition, not a design: whoever opens either should read both, and the where does the combining rule live decision should be taken once rather than twice.
The hypothesised case now has an instance in the wild (2026-09-13), and it does not unpark this.
From RM188's
round-2 skim: dianguan0105/Custom-Personal-Genome-Interpretation
(15fe28a) ships metaprs_weights.tsv — 540 variants transcribed by hand out of a European Heart
Journal 2022 supplementary Table S3, with rsid, effect_allele, other_allele, weight, maf
and traits. That is this entry's motivating sentence — "a score published only in a paper's
supplementary table" — with a real file behind it instead of a hypothesis, and the column set they
arrived at independently is close to the one deferred here.
The first reading of that was wrong, and the maintainer corrected it the same day. It was written here as the wrong half of the evidence — the unpark condition is a real consumer, a competitor transcribing a table is a producer, so nothing moves. That dichotomy does not hold, and the entry's own gate is what has to give.
An author with a corpus in hand is demand, and for a format it is the demand that matters. People reach for familiar shapes: somebody who already has a scoring table, or a set of genosets, adopts a format when the format can hold what they already have, and declines it when it cannot. That makes "can I bring this in" a consumer question in the sense that decides whether a format gets used at all — a narrower reading, where a consumer is a downstream tool that reads authored weights and scores them, gates this entry on the one party who cannot appear until after the table exists. Written as it stands, the unpark condition cannot be met by anything except a tool built against a table this entry is refusing to build. That is a gate on its own output, and it should be restated when this entry is next opened for a decision — not silently, and not here, since restating a parking condition is a change to the item rather than a note on it.
What survives the correction, and it is the half that was always load-bearing. The demand question is answered — somebody did the typing, and SNPedia's magnitude-and-genoset corpus is the same argument at a hundred thousand rows. The shape question is not: how the weights combine, what reference distribution the score is stated against, and whether a percentile travels with it are still unanswered, and a transcribed table shows the columns without showing the semantics. The one-way door is the shape, never the demand, so what this instance does is move the entry from parked-on-demand to parked-on-shape — a smaller and much more answerable place to be parked.
RM28 — Meta-conclusions (the predicate half)¶
Severity medium (after the corpus) · Status parked on a corpus — and halved on 2026-08-13: the injected-cofactor half closed, the predicate half stays here · Owner format (schema + compiler) · Motivating case combination annotations; disclosure policy
Read this entry knowing how much of the original item has already dissolved. Probing in 0.5 removed most of it, and the 0.6 review removed the rest of the cofactor axis. What is left is genuinely small and genuinely unsolved.
The corpus this was parked waiting for has its first real entry (2026-09-02, RM174).
Not an argument — an adopted source whose grammar was counted. CIViC publishes molecular profiles as
boolean expressions over variants: of 1,964 profiles, 209 are multi-variant — AND 141, OR 72,
NOT 1 — and they nest (BRAF Amplification AND ( BRAF V600E OR BRAF V600K )). Both halves of this
entry's surviving case appear there, with instances:
- Economy, and it is this entry's own phrase. Evidence item 8721 is a claim about
VHL S183L AND VHL D126Nwhose description reads "heterozygous compound mutation" — two variants in trans, observed, cited.HaplotypeRowcannot hold it: a haplotype is same-strand co-location, so drafting it there asserts cis where the source says trans, silently, because no column carries phase for anything to contradict. - Open-world negation.
MET Amplification AND NOT KRAS Mutationquantifies over a set no module can close, on a class term rather than an enumerable allele.
The 72 disjunctions are the half that is already expressible — rows are a disjunction — which is where this entry drew the line and it holds. This is evidence for the corpus, not a reason to unpark: one source, one observed trans instance, one negation. RM174 carries the measurement; the decision stays here.
Second corpus entry, 2026-09-13 — and it is a routine clinical guideline rather than an oncology
molecular profile. From RM188's
ClawBio half (probes/CLAWBIO_SURVEY.md § pharmgx-reporter). Their
pharmacogenomic report renders exactly one AVOID across 59 drugs, and it is warfarin, keyed
"genes": ["CYP2C9", "VKORC1"]. Our own reference_examples/cyp2c9_warfarin_grch37/ carries the
CYP2C9 diplotype phenotypes in diplotypes.csv and the VKORC1 per-genotype claims in
pharm_variants.csv, side by side, with nothing keying the pair — which is this entry's surviving
"pairing across subjects" clause with a shipped module standing on it.
Three things make it sharper than the CIViC entry rather than a repeat of it:
- It is the drafter's recorded gap, not an inference.
pgx_draft's own comment says it: "the guideline exists, it is a dosing algorithm over several genes rather than a per-phenotype recommendation, so nothing lands here and the author was told CPIC has nothing." What shipped for it was a better warning, deliberately, not a table. - The competitor has no declarative form either. ClawBio's answer is
"special": "warfarin"— a hardcoded branch callingget_warfarin_rec(profiles), for one drug out of 59. That is evidence the shape is genuinely hard, not evidence that everyone else solved it. - It is CPIC, so the population is countable. The measurement this entry still wants, and the one cheap enough to do before any design: how many CPIC guidelines are keyed on a gene pair rather than a single gene phenotype, counted off the CPIC snapshot already provisioned. If the answer is three, this parks again with a sharper reason; if it is thirty, the economy argument changes.
Third corpus entry, 2026-09-13 — and this one is a general-purpose grammar rather than a domain's
by-product. From the same item's round-2 roster:
SNPedia publishes genosets, boolean expressions over genotypes, and
zhaofengli/snappy (2d5255f) has them extracted from a
MediaWiki dump into data/genosets.json beside 106,603 SNP entries. The grammar is
and(rs4988235(C;C), rs182549(C;C)), it nests, and a genoset may reference another genoset — the
haplogroup trees are built that way.
What makes it the sharpest of the three is that CIViC's profiles and ClawBio's warfarin branch each fell out of one domain solving one problem, where this grammar was written for consumer genetics in general and has been in use for over a decade. It is also the first entry where the condition side and the conclusion side are both published, at scale, by one source.
This entry and RM16 are one axis at two
operators — boolean here, weighted-sum there, with the enumerative combiner already shipped in
diplotypes.csv and covering the small-arity cases both would serve. RM16 carries the table setting
that out; read it before opening this one, and take the where does the combining rule live decision
once rather than twice. They stay separate items, because a categorical conclusion and a number
needing a reference distribution are different outputs.
One argument from it does reach beyond the count, and it is RM16's rather than this entry's (maintainer, 2026-09-13): genosets are a familiar shape, in use for over a decade, and an author's ability to bring one across is an adoption signal in its own right. It bears on this entry only as a reason the count below is worth running rather than as a reason to unpark — this item is parked on a corpus, and a corpus is measured, not argued.
It changes no decision yet, and the reason is the same one that has held twice. The measurement
this entry wants from it is cheap and has not been done: how many genosets are there, what is the
operator distribution, and how deep does the nesting go — counted off genosets.json, which is a
committed file needing no network. A corpus of forty flat conjunctions argues differently from four
thousand nested ones. The licence is not the obstacle a first reading here made it: CC BY-NC-SA
3.0 US is established and adoptable as an unsellable share-alike module
(USE_CASES § 2e).
What still has to be beaten is reference_examples/apoe_epsilon, which answers the two-variant case
with no predicate at all.
Fourth corpus entry, 2026-09-13 — and it is the first where enumeration succeeds and the schema
around it complains anyway. Raised by the maintainer from their own genotype: Kulminski's
subdivision of APOE ε4 by linked TOMM40/APOC1 markers, where a carrier is not simply ε4 but
1-0-0, 1-1-0, 1-0-1, 1-1-1 — a binary vector over the ε4 call plus the linked sites, each
pattern carrying a different reported risk.
This one is expressible today, and that is the finding rather than a reason to close. Three
markers is eight patterns and at most thirty-six pairs, which is haplotypes.csv (one row per pattern
× defining marker) plus diplotypes.csv (pair → phenotype) — the shape
reference_examples/apoe_epsilon already ships for ε2/ε3/ε4 across rs429358 and rs7412, with one more
marker. The enumerative combiner is not beaten here, so this entry argues for no predicate at all.
What it does show is the shape being outgrown at a different seam — the subject, not the combiner (the maintainer's reading, and it is the right one). Expressing it fires two shipped warnings, and neither is a bug:
composite_gene_cell. The haplotype spans TOMM40, APOE and APOC1;DiplotypeRow.geneis required and single-valued (HaplotypeRow.geneis optional, so only the diplotype half forces the choice). Labelling the locusTOMM40-APOE-APOC1publishes a gene name no registry index will match, beside its parts — which is exactly what the warning says. So the author picks between one findable symbol and an honest locus label, and there is no third option.apoe_epsilonnever met this because ε alleles sit in one gene.diplotype_phase_ambiguous, conditionally.HaplotypeRowis same-strand co-location, so this translation is only honest if Kulminski's patterns are phased haplotypes. If they are unphased multi-locus genotype vectors, writing them as haplotypes asserts cis the source never claimed — RM174's trap exactly — and the pairs that collapse to one unphased genotype with different conclusions are what this warning already catches, telling a consumer with unphased calls to withhold.
So the honest verdict is: yes, with caveats, and the caveats are the entry. The format holds the
claim; what it cannot do is hold it cleanly. A use case that compiles only by choosing between
findability and honesty, and only under a phase assumption, has grown past the gene-keyed subject the
PGx tables were cut for — and that pressure lands on DiplotypeRow's key rather than on any predicate
language. Read against the other three entries, this is the one that argues RM28 may be the wrong
item for its own class of case: CIViC's contribution was that enumeration cannot reach trans and
open-world negation, ClawBio's was that nobody has a declarative form for a multi-gene dosing
algorithm, SNPedia's was a general grammar at scale — and this one enumerates fine and still does not
fit. A locus-keyed subject would settle it and a predicate would not.
The count this entry asked for, 2026-09-20 — and it is between the two numbers the question
named. Measured off the provisioned CPIC snapshot (cpic_snapshot_b0ffd4c6f010, built 2026-09-02)
while answering S102, whose reporter had counted the same table from the other side: of 103 drugs
with a recommendation row, 18 are keyed only on a gene pair and none is keyed both ways — every
drug is single-gene or pair-keyed, never a mix — and gene_count never exceeds 2. By rows it is
2,656 of 3,411 pair-keyed, which is what made the drafter's "the snapshot has no row for it"
false for a drug with 35 of them. The 18 drugs fall into six pairs:
| pair | drugs |
|---|---|
| CACNA1S + RYR1 | desflurane, enflurane, halothane, isoflurane, methoxyflurane, sevoflurane, succinylcholine |
| CYP2C19 + CYP2D6 | amitriptyline, clomipramine, doxepin, imipramine, trimipramine |
| NUDT15 + TPMT | azathioprine, mercaptopurine, thioguanine |
| ABCG2 + SLCO1B1 | rosuvastatin |
| CYP2B6 + CYP2C19 | sertraline |
| CYP2C9 + SLCO1B1 | fluvastatin |
Warfarin is not among them: it has no recommendation row at any arity, because its guideline is a dosing algorithm rather than a recommendation table, so the ClawBio entry's case is the one CPIC drug this count cannot reach. Read against the question — three parks again, thirty changes the economy argument — eighteen drugs across six pairs is the middle: more than a warfarin-shaped exception, less than a pattern the enumerative combiner is failing at scale, and every one of them is arity 2. The drafter now names the partner gene instead of claiming absence (RM249), which is a message and not a table; whether six pairs earn a pair-keyed subject stays the maintainer's decision, and this entry stays parked on it. The reporter's reading — ClawBio keys each of these on a single gene and never mentions the partner, a lossy answer, not a model to copy — is the right one and is recorded beside the survey's plan item 1, which this measurement answers.
Unmeasured, and flagged as such. This is a reported case: the paper has not been read here, so
the marker count, the exact patterns, and above all whether Kulminski phases them are unverified.
That last one decides which of the two warnings above applies, and it is the first thing to establish
if anyone builds it. Separately, just-module-creator's assets/longevity_2026 was checked and does
not attempt this — it takes the opposite reading, collapsing the cluster to rs429358 as the shared
causal signal on colocalization evidence, with TOMM40/APOC1 named only in prose and no
haplotypes.csv at all.
Still parked, on the same rule: the corpus grows, the decision does not move until the count is in.
2026-09-20 — the count is in, and it does not support the parking reason. Measured directly off
the provisioned CPIC snapshot (data/interim/cpic/, dataset: cpic_snapshot_3d2123598711, built
2026-08-07, recommendations.parquet 3,411 rows), prompted by a peer session drafting ClawBio's PGx
corpus that hit it from the other side:
- 2,656 of 3,411 recommendation rows (78%) are
gene_count = 2. Multi-gene is not the exception in CPIC's recommendation table; single-gene is. - 36 of 121
(gene, drug)pairs have no single-gene row at all. For those the drafter writes nothing, becauseCpicSnapshot.recommendationsfiltersgene_count = 1(deliberately — a row about CYP2C19 and CYP2D6 is not a statement about CYP2C19 alone) and the live client applies the samelen(phenotypes) != 1filter. - They are recognisable clinical pairs, not edge cases: TPMT+NUDT15 over the three thiopurines (azathioprine, mercaptopurine, thioguanine — 35 rows each); CYP2D6+CYP2C19 over five tricyclics (amitriptyline, clomipramine, doxepin, imipramine, trimipramine — 206 rows each); SLCO1B1 with CYP2C9 and ABCG2 over fluvastatin and rosuvastatin; CYP2B6+CYP2C19 over sertraline.
- RYR1+CACNA1S is the one that breaks the fallback argument. Seven drugs — the six volatile anaesthetics plus succinylcholine — and neither gene appears in a single-gene recommendation anywhere in the table. Elsewhere a module can at least carry the single-gene rule and lose the refinement; for malignant-hyperthermia susceptibility there is no single-gene rule to fall back on, so a module carries nothing for those seven drugs.
Independently reproduced on a second snapshot, and the two units reconcile exactly. The peer
session measured cpic_snapshot_b0ffd4c6f010 and counted drugs where the above counts
(gene, drug) pairs: 103 drugs carry a recommendation, 85 single-gene, 18 pair-keyed across
six pairs — NUDT15+TPMT (3 drugs), CYP2C19+CYP2D6 (5), CACNA1S+RYR1 (7), ABCG2+SLCO1B1
(rosuvastatin), CYP2B6+CYP2C19 (sertraline), CYP2C9+SLCO1B1 (fluvastatin). Those 18 drugs times two
genes each is 36 (gene, drug) pairs, which is the number measured here from a different
snapshot built six weeks apart. Two denominators, stated apart deliberately
(@two-surfaces-two-denominators): 18 of 103 drugs and 36 of 121 pairs are the same fact at two
grains, and the drug grain is the more legible one to quote. Warfarin is in neither: CPIC's
warfarin guideline is a dosing algorithm and carries no recommendation row at all, so the shape this
entry was reopened on is not even the marginal case — it is off the end of the table.
What this changes and what it does not. It does not by itself unpark the entry: the economy and open-world-negation objections in What remains are untouched, and a table keyed on two subjects is still a design nobody has drawn. What it removes is the premise the parking rested on — that pairing across subjects is a rare shape worth deferring until a corpus appears. Three sources now carry it (CIViC 209 profiles, CPIC 36 pairs / 78% of rows, ClawBio's one hardcoded branch), and in CPIC's case the pairs are the routine half of a guideline set this project already ingests and drafts from.
The smaller finding beside it is filed separately as S102 (peer report, 2026-09-20): the drafter
does not just skip these, it misreports them — the snapshot path's knows_drug returns None
unconditionally, so the known is None arm always fires and tells the author the snapshot has no
row, when it has 35 or 206 of them and filtered every one. That is @answered-is-not-absent
(answered-and-rejected is a fourth state; the row stays and the reason moves), and the arm's
remedy — consult the live API — reaches the identical filter. The fourth arm, the one whose text
names warfarin, is unreachable whenever a snapshot is present.
A blood-group instance, S119 (2026-09-27): Lewis is keyed on two genes' phenotypes. The
Lewis phenotype (Le(a−b+), Le(a+b−), Le(a−b−)) is a function of the FUT3 phenotype and the FUT2
secretor phenotype, not of either gene's diplotype. just-dna-lite shipped FUT2 secretor status as
its own module and left Lewis out for exactly this reason. It is the same subject-pairing shape as
CPIC's six gene pairs, outside pharmacogenomics, and like them it enumerates (a small table over two
per-gene phenotype vocabularies), so it argues for a two-subject key and not for a predicate.
Unbuilt, so it is corpus evidence and not a measured case.
What dissolved, so it is not re-proposed¶
- No operator is missing. Rows are a disjunction and columns are a conjunction, so the existing
tables already span any finite boolean function over an enumerable set of genotypes:
ORis two rows,XORand boundedNOTare enumeration, andhaplotypes.csvis same-strandAND. - APOE — whose ε4 condition (
rs429358==C AND rs7412==C) is the Constitution's own example of a predicate — was built with 0.4 bricks and no predicate at all (reference_examples/apoe_epsilon/).HaplotypeRowis a junction table, so same-strand co-location is what it already expresses. - The cis/trans motivation closed as a check, not a table.
reference_examples/hfe_compound_het/showed that a diplotype is already a statement about two homologs, so cis and trans are two rows — the relational notion the grammar was going to add is what a diplotype pair is. What building it surfaced instead was that the two rows are indistinguishable without phase, which shipped as_cross_validate_phase_ambiguity(a warning, never a block). Arequires_phasecolumn was rejected: it would make an author restate what the data determines and go stale the moment a haplotype is edited. Confirmed from the caller side, S116 (2026-09-26): a shippedjust-dna-litecaller ran against this example and the enumeration held — because the module defines the cis allele (C282Y-H63D) as its own haplotype with its own diplotype row, the caller could name the ambiguity (ambiguous, both candidates) rather than guessing or reportingno_match. So this is a case where enumeration succeeds and no predicate is wanted, which is where the entry drew its line. The one thing the caller had to re-derive — which diplotype pairs are the phase-confusable set — is not a predicate the author states but a value the compiler already computes and discards, filed as RM266, not here.
What remains¶
- Pairing across subjects — no table keys on more than one.
- Economy and intent — "any two pathogenic variants in trans" over 300 of them is ~45,000 pairs: expressible, unwritable, unreadable.
- Open-world negation — "no pathogenic variant in this gene" quantifies over a set the module does
not close, and absence is only assertable where the region was callable (
requires_callable). No operator fixes this, and a negation feature ignoring it would manufacture reassurance, the worst failure mode this format has.
It also blocks the "shy module" signal.
Why it stays parked¶
It waits on a corpus to generalize from — roughly 70% built; nutrigenomics and supplements do not exist yet — because fixing a shape against four table kinds and then meeting the fifth is how a one-way door gets spent badly (P3/P5).
The design thread, the starter shape and what is deliberately left open are in
PROPOSAL_0_5.md § G3: a new optional table, a predicate that never blocks, a
grammar kept to the smallest thing covering the motivating case, and a three-valued algebra
(true/false/unknown, Kleene operators) — with unknown withheld, never reported and never negated.
Kleene matters concretely: a conclusion gated on "ε4 present AND QUAL ≥ 60" is decidably false at
ref/ref whatever the quality was, so a blanket withhold-on-any-unknown would be strictly worse than the
tables it replaces.
The cofactor half — CLOSED in the 0.6 review, do not re-open as a general mechanism¶
The original item proposed injected cofactors: values the consumer supplies at query time that a
module must never hold, in three classes. Two of the three were resolved in 0.5 by making them plain
columns — clinical context became DiplotypeRow.clinical_context, call quality became
quality_from / min_quality (RM29) — and neither needed a general mechanism.
Decided 2026-08-13: the general "injected cofactor" mechanism is dropped as never-earned. Each remaining class gets the same treatment — a plain column, on demand, one at a time. Two classes wait:
- Ancestry — a panel-scale inference, not derivable from a curated module's own gnomAD frequencies, since real models do not rely on single SNPs.
- Family structure — this is RM10, folded in here on 2026-08-13. A declarative trio / de-novo / Mendelian-consistency expectation is only meaningful once the consumer supplies family structure at query time, which makes it a cofactor class rather than its own item. Designing it separately would fix a shape for one class before the axis exists. It was on-demand-only and shapeless for its whole life; it stays that way, here.
Neither is built until a real module needs it.
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.
Register cross-reference (2026-09-27). RM302
gives conclusion optional lay and concise wordings, two columns by default. If this item lands a
separate meta-conclusion table, the maintainer's steer is that the registers may live there instead,
so the two are decided together.
RM56 (policy half) — the rule for a measurement that spans bins¶
Severity high on the flagship example · Status 0.6 ships withhold plus an explicitly not-implemented warning; the policy lands here, gated on real caller output · Owner format (schema) + compiler
A real repeat call is RUC=38, CIRUC=-5,5 — [33,43], crossing all three Huntington thresholds, so
htt_repeat_expansion says benign, uncertain and fully penetrant at once. 0.6 makes withholding
the stated behaviour and says loudly that no policy exists yet.
The policy is a closed vocabulary — withhold / take the worst bin / take the point estimate — stated by the curator and applied by the consumer. That is annotation rather than measurement, so it stays legal. Its grain is deliberately undefined: per table matches how the decision is actually made (a stance on a whole disorder), per row is more expressive and nobody has demonstrated the need.
The prerequisite is a real caller VCF, so the vocabulary is fixed against what callers emit rather than against a guess. Same gate as RM65; RM66's evidence arrived separately (see its entry).
Never widen the measurement into an interval on the row (measure_min_observed and friends): that
puts a measurement in the module, which the data-agnostic north star forbids outright.
RM65 (implementation half) — repeat and copy-number tables are positional¶
Severity medium · Status 0.6 corrects the false claim in the code; the coordinates wait here · Owner format (schema) + compiler
§5.6 says POS and SVLEN specify the interval a copy number is defined over; §5.7 says a <CNV:TR>
record's POS and END "should match the STR/VNTR reference catalog sizes for catalog-based callers". So
a tandem repeat and a copy-number segment are loci with coordinates, emitted at fixed published
positions, and the compiler's claim that these tables are unjoinable "which is a property of what they
describe rather than a gap" is false for both. 0.6 fixes the comment.
Adding the coordinates waits on a real repeat-caller or CNV VCF sample, or a consumer field report. Without one it is scaffolding in thin air — and it is not free: it would put two more tables into RM43's coordinate-filling path, taking that lane from three tables to five.
And it carries an RM87 obligation, noticed while that lane was being built. The reverse writer's
positional-table pass hard-codes locus_index = 0 (_write_resolution_csv, the second loop), which is
honest only while those tables never expand — true today, since RM43's fill is one locus per row.
Putting coordinates on the repeat and copy-number tables is exactly what could make one of them expand,
and the 0 would then be a wrong number rather than a trivially correct one. Not a blocker for RM65;
a line whoever implements it must clear.
RM66 — one repeat locus, several motifs¶
Severity medium · Status deferred here; filed beside RM65, but its evidence has since arrived on its own (below) · Owner format (schema)
§5.7: a <CNV:TR> allele "can encode multiple different repeat motifs in a single allele" (RN=3,
RUS=CAG,TG,CAGG). RepeatAlleleRow is keyed (gene, repeat_unit) and binds one count to one motif.
For HTT the interruption structure (CAG)n(CAA)(CAG) is exactly what a modern caller reports as several
RUS entries, and the pure-CAG tract length differs from the total — a difference with published effect
on age of onset. The key cannot say which count the thresholds are about, and two motifs for one gene
read as two unrelated groups rather than components of one allele.
A keying change on a shipped table, which is the expensive kind. Filed beside RM65 so both would
arrive with the same evidence — and they no longer will (2026-09-01,
RM165).
STRchive publishes locus_structure on 23 of 82 loci as typed data with its own three-member
vocabulary, HTT's being exactly the (CAG)n(CAA)(CAG) structure above; that is enough to decide this
item and not enough to make the answer universal. RM65's prerequisite — a real repeat-caller or CNV
VCF — is still missing, so the two items now wait on different evidence and this one is decidable
first.
RM67 — polyploid and partially-phased genotypes¶
Status not work — a documented divergence, numbered so it is findable and not re-probed
VCF 4.4 §7.2 added polyploid partial phasing (GT |0|0/1/2), first phasing indicator optional. Our
grammar caps at two alleles and refuses a leading separator (probed: A/A/G and A|G/T both rejected).
This is a defensible generalization — the format annotates human diploid loci, and
_check_contig_ploidy already handles the hemizygous and haploid directions. No change proposed. The
spec's own polyploid example is a tandem duplication with SNVs on it, which a CNV-aware consumer will
meet, so revisit if one actually does.
The message changed on 2026-08-14; the decision did not. Dogfooding a duplicated CYP2D6 — the spec's own polyploid example — refused the call with a bare restatement of the grammar, so a deliberate limit read as a syntax error, while every other deliberate refusal in this schema names its own limit in-line. The arity refusals now carry that sentence: two alleles is a decision, VCF 4.4 §7.2 permits more, and nothing is queued against it. Recorded as D3-2 in DOGFOOD_0_6_FINDINGS.md.
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.
RM68 — a drafting provider on a non-GRCh38 module: refuse, or strip to the rsID¶
Severity medium (high before the warning shipped) · Status the warning shipped in 0.6
(enrich.source_build_mismatch); what the providers should do instead is deferred here · Owner
enricher (the three drafting providers) · Found by dogfooding on 2026-08-13,
reference_examples/cyp2c9_warfarin_grch37/
What was observed¶
draft, draft-panel and draft-clinpgx all take a spec_dir, and until the 0.6 dogfooding round
none of them read genome_build. enrich.spec_genome_build — written one release earlier for the bug
where the guard existed and the value never arrived — had exactly one caller. Every source these
providers read serves GRCh38: CPIC's allele_definitions, the ClinVar snapshot, the ClinPGx
annotations. So drafting CYP2C9 into a genome_build: GRCh37 module writes 10,94942290 for
rs1799853, whose GRCh37 position is 96702047 — a different base 1.76 Mb away — and nothing anywhere
said a word.
Nothing downstream catches it. A coordinate is legal on either assembly; it is simply a different place.
What the online diagnosis reaches is now measured rather than asserted: of the module's two GRCh37 rows
declared as GRCh38, grch37.diagnose_wrong_build caught one and the other minted
ga4gh:VA.pgprki8YgzfOSV9Dpe1ccPX4uNdlyAvB and recorded resolved, because GRCh38 happens to carry the
authored ref at that position too. That is the documented ~3-in-4 sensitivity, so "the compiler
catches wrong-build coordinates" is not a reading anyone should take. Two of the three providers write
coordinates and can do this; draft-clinpgx writes none.
It hid because test_pgx_draft.py's fixture declares GRCh38, and it is the only drafting test that
mentions a build at all.
What shipped in the same session. enrich.source_build_mismatch: each drafting command asks before
it writes and warns naming both builds, what a drafted coordinate will actually mean, and the two
remedies. The provider still writes the row, which is the enricher's standing shape for a disagreement
— report, never repair.
The question¶
Report-and-still-write is the right default. What is undecided is whether a provider meeting a
non-GRCh38 module should go further: refuse, or strip to the rsID — writing the identity the
source does state without stating an assembly, which derive_variant_key prefers anyway.
Refuse — wrong three ways. It makes a GRCh37 module undraftable, so the author hand-authors instead,
and hand-authoring against a printed contract is where this format's most expensive documented failure
came from (the 0-vs-1-based start description shifted four whole modules by one base and passed every
offline gate, --strict included). It refuses a provider that cannot do the harm: draft-clinpgx writes
no coordinate, so a refusal keyed on the module's build stops a command whose entire output is
build-free. And it is the wrong granularity by the repo's own rule — scaffolding refuses per file,
drafting refuses per row — while a build mismatch is a property of the module, so a build-keyed refusal
is per run, which is the granularity that self-defuses into "this module cannot be drafted at all".
Strip to the rsID — wrong, and worst on the rows that need it most. It is tempting because the row's
identity does not move (derive_variant_key returns the rsID first) and an rsID names a variant without
naming an assembly. But CPIC's sequence_location publishes defining variants with a position and no
rsID — 18 in CYP2C9, 14 in TPMT, 4 in NUDT15 — and HaplotypeRow requires an rsID or chrom+start.
Stripping the coordinate there is not "write less", it is "drop the row", and it drops exactly the rows
the 0.5.1 gene.chr repair recovered after a year of being skipped. It also produces a row
indistinguishable from one the source had no coordinate for, so the author cannot tell a stripped row
from a bare one. Any partial strip is barred outright: a drafting provider fills identity whole or not
at all, and a lone alts on a position-only row makes derive_variant_key mint a ga4gh:VA.… instead
of chrom:start:ref, silently changing which variant the row is.
Lift the coordinate over — refused, and RM48 already argues it. RM48 is deliberately one-way and
reporting-only: a GRCh37 coordinate recovers an rs-number, and the rs-number is reported, never
filled, because filling it would make resolution verify a value against the service that produced it.
A liftover inside a drafting provider is that move with an extra assembly on it, and chrom/start
are both in hints.REDUNDANCY_BEARING for the same reason.
A --build flag on the drafting commands — wrong twice. A flag saying "write GRCh37" asks the
provider to convert, which is the liftover above. A flag saying "yes, I know" is a warning suppressor,
and the tier's standing rule is that --offline is the switch and a pass adds no second CLI flag.
What would unblock it. Either a real author with a non-GRCh38 module saying which of the two outcomes they wanted, or RM15, which dissolves the premise: once identity is build-agnostic a provider can write the coordinate under the build it came from, and there is nothing left to refuse or strip. A behaviour fixed before RM15 lands is one RM15 would have to undo, which is the strongest single argument for leaving this at a warning.
Vocabulary residue from the 0.7 consumer round¶
RM149 — expected behaviour lives in prose, and the prose is where two readers split¶
Severity medium · Status open — a minor, taken into 0.8 on 2026-09-21; asked by the maintainer 2026-08-31 · Owner format + compiler (the test corpus) · Found by running the consumer loop
The ask, verbatim in intent: express our described scenarios as Gherkin, because the freeform prose in expected-behaviour descriptions is producing ambiguities faster than it resolves them.
The evidence for it is this repo's own recent record, which is what makes this an item rather than a preference. Three consumer reports in one week were two readers splitting on one sentence, none of them a code defect:
- S80 —
state's six members printed as peers; an agent chose a retired one honestly. - S83 — two runs of a byte-identical prompt wrote
riskandunknownfor one variant on one body of evidence, both green, both defensible against the field description. - S79 — a warning's text read as your declaration is unsupported when it meant not universal.
Each was answered by writing a better sentence. That is three fixes to prose in a week, and the pattern says the next one is already in flight somewhere.
What is actually being asked¶
Not a testing framework — the suite is not the problem, and a pytest-to-behave migration would be
motion rather than progress. The gap is that a scenario — given a module with a partial
resolution.csv, when validate --strict runs, then it refuses with the compile's own error — exists
today as a docstring, a test name, and a paragraph in COMPILER.md, and those three can drift from
each other and from the code. A structured form is one statement, and the natural home is a
.feature-shaped corpus each side is derived from or checked against.
Open questions this needs decided before it can be built¶
- What is the source of truth. Gherkin generated from the tests is documentation that cannot drift; tests generated from Gherkin makes the feature files the contract and every existing test a migration. These are opposite projects with the same output, and the ask does not say which.
- What is in scope. Every check the compiler runs is ~140 warning codes plus a mode ladder. The release-gate scenarios, the tri-state outcomes and the parity rules are the parts where ambiguity has actually cost something; the round-trip fixed points are already pinned by assertion and would gain nothing from prose.
- Where it lives. A
features/tree at the root, per-package, or insidedocs/. That decides whether it ships to consumers — and if it does, it becomes a published surface under P3, which is a much larger commitment than an internal one. - What it costs the next contributor. A second dialect to learn beside the docstring convention
this repo already leans on heavily, and every new check owing a
.featureclause. That is the P9 question one layer up: this is a maintenance surface, not an authored one, and it is not free.
Why it is filed rather than started¶
The three reports above were each fixed by naming what the rule is, and the fix was one string. A scenario corpus is worth building when the cost of ambiguity exceeds the cost of the corpus, and the measurement that would show that has not been taken — this entry is where it goes when it is. What is not in doubt is the direction: the recurring failure is real and repeatedly measured, and it is filed here so the next instance lands against a number rather than as a fourth anecdote.
Addendum, 2026-09-13 — a first corpus is drafted, and this is the measurement¶
Status is still open. What exists is a draft, unreviewed, at features/ in the repository root:
12 .feature files, 213 scenarios, guarded by schema/tests/test_feature_corpus.py. Nothing
about it is a contract yet, and the entry stays here rather than moving to ROADMAP_HISTORY until it has
been read.
Two of the four blocking questions are answered by the ask itself, and are recorded in
features/README.md as assumptions rather than decisions:
- Source of truth: code → Gherkin. The scenarios describe what the code does. No step definitions,
no
behave, nopytest-bdd, no new dependency in any tier; thepytestsuite stays the executable statement of behaviour and this is the readable one. - Where it lives: the repository root, not
docs/. That keeps the P3 publishing question open rather than answering it by side effect, which a drafting round has no standing to do.
Scope is decided by registry rather than by taste, which is the third question. Three walked sets
are covered in full: VALID_WARNING_CODES, VALID_VERIFICATION_CHECKS, VALID_VERIFICATION_SKIPS.
Round-trip fixed points are out, on this entry's own argument. And the scope estimate in the body
above is wrong by a factor of two: it says ~140 warning codes, and VALID_WARNING_CODES has 73.
A number in prose beside a registry, in the entry that exists because prose drifts — left in place
above rather than silently corrected, because it is the fourth instance of the shape this round
measured and the most on-the-nose.
The fourth question — what it costs the next contributor — has a partial answer and it is the cheaper half of the estimate. A new warning code without a scenario fails the guard, so the cost is one scenario per code, not a second dialect to learn: there are no step definitions to write and no runner to learn. What is not yet measured is whether a reviewer finds the scenarios easier to check than the docstrings they stand beside, and that needs a reader rather than a round.
The measurement this entry was waiting for¶
The case for the corpus was to be made on the cost of ambiguity exceeding the cost of the corpus. Drafting it produced four findings in the first pass, two of them code-or-doc defects that were filed and fixed rather than noted:
| # | finding | disposition |
|---|---|---|
| 1 | Two of our own documents disagree about whether strict builds. COMPILER.md's validate-by-redundancy table gives the rsid↔coordinate check the severity warning with no qualifier; its own mishap matrix gives the same check ⚠️ warning / ❌ refuses. The matrix is the half the code agrees with. |
marked # DRIFT: in features/compiler/mode_ladder.feature, unrepaired — the round's findings are its output |
| 2 | The mode ladder is two mechanisms and reads as one. Four codes flip channel on one sentence; three resolution codes pair a warning with a different, longer refusal through ResolutionOutcome.strict_errors. Calling those ladder members quotes a text that does not exist; calling them warn-only describes a compile that succeeds. |
written down in the same feature; no defect, a distinction nothing stated |
| 3 | alphagenome check with no API key raised instead of recording a skip — skipped(CHECK, "unchecked"), and "unchecked" is not a vocabulary member. |
RM242, fixed |
| 4 | Four per-command attestation counts summing to 17 of 24, in ENRICHER.md's paragraph that refuses to state a total. | RM243, fixed |
What produced each of them is worth separating from the fact that they were produced. None came from reading prose more carefully. Finding 1 came from writing the severity down as a table and noticing two tables; finding 2 from having to name a mechanism in order to tag a scenario; findings 3 and 4 from AST walks the corpus needed anyway — the emission-site check and the skip-reason enumeration. So the argument this addendum supports is narrower and stronger than Gherkin is good: the act of deriving a scenario from an emission site is what found things, and the guard is what will keep finding them. A corpus of prose scenarios with no grounding check would have found none of the four.
The three assertions that make it worth more than the prose — and what each really covers¶
Each was proven by reintroducing the defect and watching it go red, which is this workspace's standing requirement. Two of the three were narrower than they read on the first pass, and both narrowings were found by measuring rather than by re-reading, which is worth recording because a guard that half-covers its subject while the reference claims it covers all of it is worse than no guard:
- a
@code:Xscenario's# source:sits within three lines of X's ownCodedWarningcall. Held from the start. - the same alignment for
@check:and@skip:. Missing at first, and it cost precisely what it exists to prevent: RM242 and RM243 — the two fixes this round produced — inserted six and eight comment lines above referenced sites, eleven# source:lines silently began pointing six to eight lines early, and the suite stayed green, because those two tags had only the line is inside the file. The guard now catches all fourteen when the insert is reproduced. Two of the eight skip reasons (tautology,not_permitted) are never passed toskipped()as a literal at all — they travel as a variable — so the alignment is against any literal naming the reason, which is the better pointer anyway: where a reason is decided is what a reader wants. - every phrase a step quotes as a warning's text is a real substring of a real string literal in the
module named. Keyed on a verb before the quote at first (
contains/says/states), which extracted 46 of the 162 quoted phrases — aThenalso sayssaying the flag "…",ends at "…",continues "…",offers "…". Keyed on the step keyword instead, it now checks 160 of 160 on outcome steps, and the widening found three real defects in the corpus.Given/Whenstay excluded: those name an input value, which has no reason to appear in the module's own strings.
The residue, measured rather than estimated. Of 213 scenarios, 108 carry a registry tag and are aligned; 105 are structural — pointing at a docstring, a branch or a constant, with no call site to align against — so they carry only the line is inside the file check and will rot on an edit above them. Measured 2026-09-13 by walking the corpus, because the first draft of this paragraph said "roughly 130" on an estimate, in the document whose whole subject is unmeasured counts beside registries. That is a real contributor cost and it belongs in the fourth question's answer above rather than in a footnote.
One mechanism had to be added rather than worked around: a message built by one module and coded by
another (layout.deprecation_notice writes the sentence, _locate_sidecar names the code) needs a
# text: line, so the code is checked against the emission site and the phrase against the module that
holds the words. That is the boundary where a code goes missing
(@finding-loses-its-code-at-a-boundary), and it is better as a checked fact than a convention.
What a review should push on¶
- Is the root the right home, or should this publish? Publishing makes every clause a P3 commitment. The draft assumes not, and the assumption is the thing to overturn first if at all.
- Are the scenarios readable by someone who did not write them? That is the whole premise and it cannot be self-assessed. If the answer is no, the corpus is a second dialect with none of the benefit and this entry should close rather than grow.
- Should the DRIFT finding be repaired here or filed? Finding 1 is one sentence in COMPILER.md. Leaving it marked was deliberate — a round that silently fixes what it finds cannot be audited — but it should not stay marked for long.
- The corpus has no scenarios for the compiler's
errorsthat carry no warning code, beyond the handful written as@refusal. Whether a refusal without a code deserves the same coverage as a coded warning is an open scoping question the registry cannot answer, because refusals have no registry.
Second addendum, 2026-09-13 — the second pass, and where coverage was not what the equalities said¶
Still open, still a draft. The corpus is now 13 files and 228 scenarios — 118 registry-tagged and aligned, 110 structural — with 182 quoted phrases on outcome steps. The question this pass put was the one the first pass could not put to itself: are all the features covered? The three registry equalities said yes and they were answering a narrower question than they read.
Four findings, and what produced each. None came from reading the scenarios again; three came from walking a set the first pass did not walk, and the fourth from reading the residue the first pass measured and left.
| # | finding | disposition |
|---|---|---|
| 5 | A code is not a text. Nine resolution codes are emitted by the compiler's resolution.py and by the enricher's resolver.py, and seven of the nine pairs are a different sentence. rsid_unresolved is "not found in resolution table, position remains unset" in one tier and "not in the injected Ensembl snapshot" in the other. The per-code equality accounted for all nine while the corpus held none of the enricher's words for any of them, and a warning's text is an API. |
fixed — the equality now keys on (code, tier), and features/enricher/resolution_warnings.feature is the nine in the enricher's own words |
| 6 | The house algebra was covered as a mechanism and not as a value. tri_state.feature had Kleene, the withhold, classify and restate, and not one of the columns that carries a three-valued answer into an artifact: five of the nine members of VALID_FREQUENCY_STATUS, VALID_RESOLUTION_STATUS and VALID_AUTHORITY_CALL_STATUS appeared nowhere in the corpus, not_covered among them — the member the gnomAD Y-PAR probe was run to justify. |
fixed — six scenarios; the walk that found them (three-member VALID_* sets) is recorded as the heuristic it is |
| 7 | @ladder was a judgement written four times over a walkable set. The mode ladder's first mechanism is (errors if strict else warnings_out), and the codes reachable from a function carrying that shape are exactly the four tagged. |
fixed — a fourth registry equality, proven red by untagging p_value_encodings_disagree |
| 8 | Ten # source: lines pointed at a blank line, a bare """, a bare return [ or the middle of a comment — and two pointed at the wrong subject. a total function cannot decide a three-valued answer named a comment about a length constant in normalize.py when the rule is mitomap.vcep_clin_sig; an absent input is the unknown arm named the concordance block in vocab.py when the rule is needs_recompile. Both scenarios stated their rule correctly and cited the right tag; only the pointer was wrong. |
fixed — all ten re-anchored; not rot, since no source file here has changed since the corpus was written, so they were authored that way and nothing could say so |
Two questions the first addendum left open, now answered with a measurement¶
- Refusals without a code have no registry, and the reason is that they have no shape.
compiler.pyalone reaches the error channel from four distinct mechanisms — an inlineerrors.appendof a literal, anextendof a helper's returned list, the(errors if strict else …)selector, andResolutionOutcome.strict_errors— across roughly forty sites, against 22 scenarios tagged@refusal/@strict_only. So the answer to should a refusal without a code get the same coverage as a coded warning is not no; it is that there is nothing to walk, and inventing a registry so an equality can be written would be the@registry-completenessdefect committed on purpose. It stays a scoping question for a reviewer, now with a number beside it. - The phrase check's file scope is correct, and that was measured rather than assumed. Eight of the
182 quoted phrases match no message at the site their scenario names but do match another literal in
the same module. All eight were read: every one is a message built into a variable a few lines away
(
ambiguous_warnings,_verify,coordinate_disagreement,layout.deprecation_notice). Tightening the check from the module to the call site would need dataflow and would report eight correct scenarios as defects. Recorded so the next pass does not re-derive it.
What this pass did not touch¶
The # DRIFT: in mode_ladder.feature still stands — finding 1 of the first pass, one sentence in
COMPILER.md, and a repair decision rather than a drafting one. And # anchor: <token>, a symbol the
guard could grep for instead of a line number, is the obvious repair for the 110 structural scenarios
and is deliberately unbuilt: it is a convention change across the whole corpus and belongs to whoever
reviews it.
Not to be confused with just-module-creator's authoring guidance, which is a different
document for a different reader and stays prose. This is about our stated behaviour, not an author's.
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.
RM84 — a module has no version identity on the discovery path, and the publisher is the half we own¶
Severity medium-high · Status our half SHIPPED in the 0.6 PT2 batch (lane D, 2026-08-17) —
upload_module writes data/<name>/ and data/<name>/v<version>/. The segment spelling is settled
and both asks are answered (S35, 2026-08-17): v<version> verbatim
stays. Only the consumer's discovery half is open, and it is theirs, which is why this entry stays here.
Previously: our half taken into 0.6 PT2 on
2026-08-16 (PROPOSAL_0_6_PT2.md § RM84); before that, open — joint with the
reference consumer, and their half is already agreed in writing · Owner enricher
(upload.upload_module) + just-dna-lite discovery ·
Motivating case a republished module on the HuggingFace path
Why a partial mitigation does not close it (S34 §3): the fields a consumer can read today record where a module came from, never which build of it. Two uploads under one path are then indistinguishable on the discovery side, which is the half this item exists for.
What was observed¶
MODULE_LIFECYCLE § 6.8 traces two acquisition paths with two entirely different notions of "updated", and neither delivers a notification. The registry path at least has a per-version audit. The discovery path has no version identity at all: no version in the path, no manifest fetch, no digest check. A republished module keeps the same URL, so a cached copy shadows it, and the only invalidation is a purge keyed on the consumer application's own package version. Stated plainly: on that path the identity used to detect "the module changed" is a property of the reader, not of the module. A module republished with new science while the app stays pinned is invisible; an app patch release with no module change purges everything.
Half of that is ours. just_dna_enricher.upload.upload_module writes the flat data/<name>/ layout
— so this tier publishes the shape that cannot express a version, and no amount of consumer-side work
invents one.
Why it is joint rather than ours alone¶
A pinning surface is a change to our publisher and to their discovery in the same breath: a version
segment nobody reads is dead bytes, and a reader looking for a segment nobody writes finds nothing.
S34 § 4 is the consumer's half, already stated: "If the publisher grows a
version segment we will follow it in discovery; the vN fallback in our generic fsspec scan is already
the shape." That is as close to a pre-agreement as a cross-repo item gets, and it makes this cheaper
than "wants agreeing" implied when it was first written down.
What is undecided¶
Was: the layout itself — a vN segment, a digest segment, or a pointer file — and what happens to
every module already published flat, which is all of them. Whatever is chosen has to leave an
unversioned path working, because that is what is deployed.
Settled on our side. The layout is the dual write, decided in PROPOSAL_0_6_PT2 § RM84 and built in lane D. Nothing already published moves, because the flat path keeps being written and keeps meaning latest. The full behaviour, including the null-version fallback and the two-commit caveat, is ENRICHER § the publisher surface.
The ask was put, and answered the next day — both questions closed¶
Asked in ENRICHER.md (RM27's shape: a finding about a downstream reader is an explicit ask, never an
implication), delivered there rather than into their tree because just-dna-lite carries no consumer
inbox. Answered as S35 on 2026-08-17, read off their code file
and line rather than recalled.
(1) Their scan matches only v-plus-integer — ^v(\d+)$, compared with int(), so v1.0.0 does
not match and v10 would sort under v9. The half that actually decides it is their correction, not
the regex: that fallback lives only in _discover_fsspec_source, the generic github/http/s3 branch.
HuggingFace has its own branch with no version fallback at all — so on the path this item is about,
no spelling is read today and the segment cannot be chosen to suit one. Their words: S34 § 4's "the vN
fallback in our generic fsspec scan is already the shape" was accurate about the shape and quoted about
a branch that does not serve HF, and they call that their error rather than a misreading here.
So v<version> verbatim stays — a bare major segment would still collide two patch releases at one
path, and would buy nothing since the code that would read it is not on this path.
(2) No, and by construction rather than by luck. Both discovery branches call fs.ls at exactly
one level and never fs.find, a ** glob or a recursive listing, and their probe asks fs.exists on
named files rather than listing the directory it is probing — so a nested data/<name>/v<version>/ is
never enumerated and never probed. Verified in their tree by search: no fs.find, recursive=True,
maxdepth or snapshot_download against a module path anywhere. The one part of this change that could
have regressed a consumer who never adopts it, and it does not.
One consequence recorded rather than fixed, raised by them as a consequence and not an objection:
the dual write doubles the collection's bytes and nothing prunes data/<name>/v<version>/, so the repo
grows one full artifact set per release forever. It does not affect discovery. Retention is the
collection owner's decision, not the publisher's; noted in
ENRICHER § the publisher surface.
What is left here is theirs and unscheduled: teach _discover_hf_source a versioned fallback, and
replace the regex and int() with just_dna_format.identity.Version, which already gives them parsing
and ordering. Nothing is broken meanwhile — the flat path resolves and keeps meaning latest — so their
read_module_provenance states version: None for every HF-discovered module, which their report
renders as Not stated.
Confirmed by exhaustive search to have had no Sn and no RMn before this entry, which is why it
is filed rather than cross-referenced.
Two items building this surfaced, filed 2026-08-17 rather than folded in¶
Both were found by writing the code, not by planning it, and neither is a defect in what shipped — recorded here because this entry is where a reader meets the publisher.
- RM88 — the versioned path cannot notice that the version has not moved, so a republish
without a
version:bump overwrites it with different bytes. Refusing needs a remote read and an undecided policy (warn / refuse /--force), which is why it is an item and not a fix. - RM89 —
_REQUIREDstill demanded all three SNP-core parquets, so a table-only module could not be published at all: seven of the sixteen reference examples, measured. Its open question — what the discovery path actually needs open — went to the same team as the two asks above rather than as a third message, and came back with them in S35, so it shipped on 2026-08-17. Answering it found the larger half:_ALLOW_PATTERNScarried no 0.4 family and no derived-fact table either, so eight more examples published a manifest attesting parquets that were never uploaded.