S528 census — Lane 3: ontology CV register rot
Lane 3 — The ontology controlled-vocabulary register and its context rot
Section titled “Lane 3 — The ontology controlled-vocabulary register and its context rot”Census only. No file was edited. Scope: the 37-CV register + README at
$KH_PRIVATE_DOCS_DIR/src/content/docs/ontology/, its derived artefacts in the canonical
repo, and the CV → DB → OKF chain. Lane 1 owns the classification code path and the DB
columns; Lane 2 owns the seeding/propagation scripts; Lane 4 owns test debt.
Measured against Platform staging rbwqewalexrzgxtvcqrh and canonical HEAD c3286753f.
Round-2 revision note — what I got wrong
Section titled “Round-2 revision note — what I got wrong”My round-1 lead recommendation (“invert the authority: generate baseline_values from the
live DB”) is withdrawn. The owner’s objection is correct and my own report contained the
refutation — §2 documented the ratified flow as register markdown → parse flow → DB →
linter, register upstream and normative, and I then recommended reversing it without saying I
was doing so. A schema-generated ontology can only describe what the DB happens to contain; it
cannot say a value is wrong, and it makes the OKF linter circular.
A recall pass (GROUNDING §0.6) also corrected a load-bearing fact. I reported the register→DB gate as “never built”. That is wrong for the half that matters:
WP6
specs/wp6-ontology-harness/TECH.md§1, Goals, verbatim: “Make the markdown ontology register underdocs/ontology/*.mdthe single source-of-truth artefact for KH controlled vocabularies, by wrapping it in a Zod-validated loader plus a parity test that fails the build when markdown and DB CHECK constraints disagree.”
Read as intent-at-S236, not as a warrant (§0.5b — see VC-1 below): what this establishes is
that the mechanism the owner is now asking for was built and did run (S236–S238). That is a
measured fact about the repo, and it is all I claim from it.
lib/ontology/loader.ts read the register with gray-matter, validated every file against
OntologyCVSchema, and markdown-parity.test.ts asserted markdown ↔ generated tuple ↔ live DB
CHECK with bidirectional set equality. Recall confirms it: “asserts: markdown ↔ generated tuple
↔ live DB CHECK — 3-way lockstep”.
It was dismantled in two steps, both recorded as amendments in the spec that specified it:
- ID-68.27 branch (b) relocated the register into the private docs-site for privacy. The public test could no longer see the markdown, so it was repointed at a frozen fixture, and the register-side replacement (the “parity-guard twin”) was booked as a follow-up. It was never built — the phrase appears in 5 documents and 0 lines of code.
- ID-133 Decision A retired
lib/ontology/loader.ts— the only component that read the register — reasoning “zero production callers” and that repointing “would add a reader without removing drift”.
So the correct diagnosis is not “the ontology never had a gate”. It is: the gate existed in the ratified direction, and a privacy relocation plus a decision taken inside the frame removed its two ends, leaving fixture↔fixture parity that cannot fail. That makes rebuilding materially cheaper than round 1 implied — the Zod schema, the codegen, the DB-side oracle and the loader’s own specification all still exist.
Per GROUNDING §2a I flag rather than close: ID-133 Decision A is a ruling taken within the inherited frame. Its stated subject was whether closed CVs should be promoted to DB tables (recall: “a DB row can’t gate LLM extraction without code regen, so a DB table buys nothing now and adds drift”) and it affirmed “docs-site register authoritative”. Its effect was to delete the mechanism that made the register authoritative over anything. That gap between what it ruled and what it did is worth the owner’s attention; I am not treating it as settled.
No verdict in this report rests on row counts or “never run” (GROUNDING §0.1 Trap 2). Where a count appears it measures prose staleness (the CV cites a figure that was never true or is no longer), never liveness.
Round-3 pass — applying §0.5b (a task is evidence of intent, never of correctness)
Section titled “Round-3 pass — applying §0.5b (a task is evidence of intent, never of correctness)”I audited my own round-2 report against the new rule. It caught my lead finding’s warrant and changed four verdicts. Listed here so the owner can see exactly what moved and why.
VC-1 — my lead recommendation was warranted by a task spec. Re-warranted.
Section titled “VC-1 — my lead recommendation was warranted by a task spec. Re-warranted.”Round 2 opened by quoting WP6 specs/wp6-ontology-harness/TECH.md §1 as establishing that
register → DB is the right design. That is a task spec, and under §0.5b it is intent at S236,
not proof of correctness. Worse, WP6 §1’s second goal names the first downstream consumer to
wire as content_items.content_type — a table dropped at ID-131 M6. The spec I was leaning on
was itself authored inside the frame this census is investigating.
Re-warranted, and the recommendation is unchanged:
- The direction is owner-ruled, this session — GROUNDING §0.4: “The ontology direction is register → DB, NOT DB → register”, with the reasoning that a schema-generated ontology loses the power to say a value is wrong. That is the authority, and it is the only authority I now claim for the direction.
- WP6 is demoted to two legitimate uses, neither of which is a correctness warrant: (a) measured history — the mechanism was once built and ran, which is a fact about the repo, not a claim in a doc; (b) a cost basis — §5.2 specifies the loader’s behaviour in five numbered steps, so P2 is a re-implementation rather than a design exercise.
- The empirical argument stands on its own and does not need WP6 at all: the
16-extraction-methodcase below shows the register carrying a ruling the DB never received. That is measured current behaviour (§0.2 tier 2), and it is the strongest evidence in this lane.
VC-2 — P5 (CHECK emission) is sequenced behind the concept rulings. Changed.
Section titled “VC-2 — P5 (CHECK emission) is sequenced behind the concept rulings. Changed.”Round 2 proposed P5 as ready-to-go for seven CVs. Withdrawn as an immediate action. P5 takes
the register’s declared value set and hardens it into a DB constraint. For 04-content-type that
set is ID-133 BI-3’s 7-value stay-set — a task decision, and content_type is a Layer-1
concept, which GROUNDING §2b rules already known to be incorrect.
Emitting a CHECK over a vocabulary whose correctness is under question is the exact “forced to fit” move this census exists to end — it would convert an unresolved concept into schema.
Revised: P5 stays in the design (it is what makes the register normative rather than advisory) but
each CV opts in only after its value set is settled. 34-entity-type / 35-relationship /
37-concept-type are ready today — they already agree with their live CHECK or gate and their
sets have been exercised. 04-content-type waits on Lane 1 and the owner. The lifecycle/governance
four (06, 08, 09, 10) are less obviously IMS-inherited but are still Layer-1 concepts;
do not batch them.
VC-3 — 03-layer-vocabulary: a task directive and a requirement point different ways. Surfaced, not resolved by deference.
Section titled “VC-3 — 03-layer-vocabulary: a task directive and a requirement point different ways. Surfaced, not resolved by deference.”This is the clean instance of the conflict the rule exists to expose:
| Source | What it says about layer | Class of evidence |
|---|---|---|
| ID-133 BI-4 (task decision, S451) | “layer is the Guides audience/depth axis, matched against guide_sections.expected_layer”; and “The CV is KEPT, not deprecated” | task directive → intent at that time (§0.5b) |
| Gap tracker B.1 (recorded “Liam decision: Option (ii)”) | “currently layers are metadata tags used for filtering, not for UI hierarchy” → layer becomes a UI nesting rule | §0.2 requirement evidence — an owner decision in a requirements doc, not a task directive |
They are not compatible: BI-4 re-homed and blessed the filter mechanism; B.1 rules that mechanism replaced. Per §0.2’s precedence the requirement wins, and my round-2 REWRITE verdict stands — but the reason is now the requirement, not “the CV is out of date”. BI-4 was a reasonable re-home at the time; it simply did not consult B.1.
VC-4 — Inv 30 downgraded from constraint to revisable task invariant. Changed.
Section titled “VC-4 — Inv 30 downgraded from constraint to revisable task invariant. Changed.”Round 2 treated “no PR-blocking CI job may use this action (Inv 30)” as a hard boundary and built the export-artefact bridge around it. Inv 30 is an ID-68 task invariant, not a DR. Under §0.5b it is intent at that time. It may well still be right — coupling blocking CI to a private-repo checkout is a real fragility — but it is a choice the owner can revisit, and I should not have designed around it as if it were fixed. The three options in §0 stay, with the relax-Inv-30 option now on equal footing rather than presented as a concession.
VC-5 — 33-reference-item’s source_document_id — same shape, now named.
Section titled “VC-5 — 33-reference-item’s source_document_id — same shape, now named.”ID-75 BI-15 (task decision) specifies reference_items.source_document_id as NOT NULL
ON DELETE RESTRICT, “provenance chain integrity”. The owner ruled at the round-1 board
(§0.5, id-364 F1/C5) that “a reference item does NOT need a source_document” — different
concepts, “source documents are walked, reference items are not”. Owner ruling outranks the
task decision; the CV documents the task decision as though settled. Flagged for Lane 1, who
owns the column.
What did NOT change, and why
Section titled “What did NOT change, and why”- §2’s eleven-CV delete list. Every row rests on
to_regclass/information_schemareturning nothing — measured non-existence, not a task saying so. Where a task claim and the measurement disagree (CVs 30/31 readAPPLIED-S246; the tables do not exist) the measurement wins, which is the rule working in the intended direction. - The
content_items/ dropped-table findings. ID-131 M6 and ID-145 W1 are cited as events verified in the schema, never as authority for a design. - B.1 and B.5. Both are owner decisions recorded in a requirements doc — §0.2 evidence, the top tier. Not downgraded.
- The DR treatment. Unchanged from round 2: each DR is cited only for what it explicitly ruled, and DR-027 is noted as explicitly excluding this register from its scope.
0. Recommended approach — re-derived in the ratified direction
Section titled “0. Recommended approach — re-derived in the ratified direction”Restore the register → DB gate that ID-68.27/ID-133 dismantled, and extend it from “detect drift” to “the register can require the DB to change”. Keep the register upstream and normative. Delete the ~21 CVs whose subject does not exist, because a normative register that contains 21 statements about nothing cannot be enforced.
Authority for the direction: the owner’s §0.4 ruling, this session. WP6 is cited below only as measured history (the mechanism ran) and as a cost basis (§5.2 specifies the loader) — never as evidence the design is correct. See VC-1.
Three layers, in dependency order:
| Layer | What it is | Status today |
|---|---|---|
| 1. The register is contracted | OntologyCVSchema validates every register file | Broken by one key. .strict() + Starlight’s title: → 37/37 files FAIL; 37/37 PASS once title is admitted. The contract cannot parse its own subject |
| 2. The register is exported | register frontmatter → a machine-readable artefact | Hand-maintained. The artefact (ontology-cv-baselines.json) exists but is updated by a prose instruction in its own _meta.update_protocol |
| 3. The DB must conform to the export | CI fails when a DB CHECK disagrees with the register | Gone. Replaced by fixture↔stale-snapshot comparison, which is a tautology for 3 of 4 CVs |
Layer 1 is a one-line fix. Layer 2 is the retired loader plus a JSON write. Layer 3 is the WP6 parity test with its DB oracle changed from a stale snapshot to live introspection.
Why this direction is the right one — a worked example that proves it
Section titled “Why this direction is the right one — a worked example that proves it”This is not a doctrinal preference; the difference is observable in the current drift.
16-extraction-method.md declares pullmd_trafilatura, pullmd_playwright,
pullmd_readability, pullmd_cloudflare, pullmd_reddit. The live
source_documents_extraction_method_check contains trafilatura, playwright, unpdf,
mcp_create and none of the pullmd_* values.
- Under my withdrawn DB→register proposal, generation would have silently overwritten the register with the DB’s names. The disagreement disappears without anyone deciding anything.
- Under register→DB, the gate fails and forces the question: which naming is correct? And
the register already holds a ruling on it — its S237 note records that Docling adoption
retires the
trafilatura/pdfplumberpaths. The register is carrying a decision the DB never received. That is the ontology doing the one job a generated register cannot do.
Same shape on 22-origin-kind: DB says derived_from_form_response, register says
derived_from_bid_response. The DB is probably right (the platform renamed bid→form throughout —
form_instances, form_responses), but the point stands: the gate makes it a decision instead of
a silent adoption.
And the settled bid form_type ruling is the same mechanism running correctly: the owner has
ruled bid out of the vocabulary everywhere, the migration reclassified bid → itt under a
guarded DELETE, and the DB is now 7 values. The register and fixture still carry 8. Under the
ratified direction the remedy is unambiguous — the register is wrong and must be corrected to
match a ruling, not to match a table.
Answering the four questions
Section titled “Answering the four questions”1. What would the register → DB sync take?
Section titled “1. What would the register → DB sync take?”Critical scoping distinction: the gate the owner wants is a test, not a pipeline. parse-flow.py
was specified (§5.5) as a write path — parse the register into a DB target table — and it was
deferred for two stated reasons: it needs (a) a DB target table for parsed CVs and (b) a
localfs.walk_dir source. Neither is needed to make the DB conform to the register. Do not
build the cocoindex flow to get the gate.
Costed, smallest first:
| Piece | What it is | Cost | Precedent it reuses |
|---|---|---|---|
P1 — admit title | title: z.string().optional() (+ kh_docubot_owned) on OntologyCVBaseSchema; add retired to STATUS_VALUES | ~10 min | verified: 37/37 pass once title is excluded |
| P2 — re-instate the reader | readdirSync → gray-matter → OntologyCVSchema.parse → emit JSON. This is the retired loader.ts, whose behaviour is still specified verbatim at WP6 §5.2 (5 numbered steps), plus a file write | ~80 lines, half a day | scripts/generate-content-type-values.ts already does exactly this for one CV |
| P3 — publish the export | docs-site CI writes ontology-cv-baselines.json into the canonical repo, replacing the hand-update protocol | ~1 workflow | the docubot lane already opens cross-repo PRs with a GitHub App token covering both repos |
| P4 — the conformance gate | Vitest case: for every CV with a live target, introspect pg_constraint and assert set-equality against the export. Fail with the missing keys in both directions | ~120 lines, half a day | this is WP6 §5.4 as originally written; blocking ci.yml already carries SUPABASE_SERVICE_ROLE_KEY, POSTGRES_PASSWORD and SUPABASE_URL |
| P5 — CHECK emission (per-CV opt-in) | For a CV the register declares closed where the DB has no CHECK, emit ALTER TABLE … ADD CONSTRAINT … CHECK (col = ANY (ARRAY[…])) for owner review. Emit for review; never auto-apply | ~60 lines | — |
Total for a working gate (P1–P4): roughly one to one-and-a-half days. That is less than hand-stripping 37 files, and unlike hand-stripping it does not re-rot.
P5 is what makes the register normative rather than advisory — and it is the one piece that must
NOT be applied in bulk (VC-2). Seven CVs declare a closed vocabulary the DB does not enforce at
all: content_type (source_documents), lifecycle_type, freshness,
governance_review_status, publication_status (source_documents half), and origin_type. The
ID-131 re-home carried the columns across but not their CHECK constraints — that gap is
measured and real.
But the value sets those CHECKs would freeze are task-era decisions (ID-133 BI-3 for
content_type; a never-landed S236 “ADD CHECK” ruling for lifecycle_type), and §2b rules the
Layer-1 concepts already known to be incorrect. Hardening an unresolved vocabulary into a schema
constraint is the “forced to fit” move this census exists to end. So P5 is per-CV opt-in after
the set is settled: 34-entity-type, 35-relationship and 37-concept-type qualify today;
04-content-type waits on Lane 1 and the owner; the lifecycle/governance four are not batched.
2. How does drift get caught?
Section titled “2. How does drift get caught?”Under the withdrawn design drift was impossible by construction, which was its hidden cost — it could never report a problem. Under the ratified direction the DB can diverge and CI must fail. Three cases, and the gate treats them differently:
| Case | CVs | Assertion | Meaning of a failure |
|---|---|---|---|
| A — live CHECK exists | 9: entity_type, relationship, requirement_type, origin_kind, extractor_kind, publication_status (q_a_pairs), extraction_method (×2 tables), ingestion_source (reference_items), plus the form_types table | set-equality register ↔ CHECK, both directions, keys listed on failure | someone changed one side. Default remedy is a migration, because the register is normative — the reverse remedy needs an explicit ruling |
| B — register says closed, DB does not enforce | 7 (listed above) | the gate reports the unenforced set; P5 emits the CHECK for review | the register is asserting something the DB cannot honour. This is the class the register has flagged three times and never closed |
| C — no DB target | the ~21 in §2 + the genuinely planned ones | nothing to assert | this is why the deletion is a prerequisite, not a separate task. A normative register cannot contain 21 unenforceable statements and still be a contract |
Where it runs. .github/actions/resolve-private-docs/ exists and would let canonical CI read
the real register, but its own header states: “Opt-in lanes only: no PR-blocking CI job may use
this action (Inv 30).” Only token-parity.yml and supabase-advisors.yml use it, both side
workflows.
Inv 30 is an ID-68 task invariant, not a DR (VC-4) — intent at that time, and revisable. Its reasoning is sound on its face (coupling blocking CI to a private-repo checkout is a real fragility), but it is the owner’s to keep or drop, and I have stopped treating it as a boundary. P3’s export artefact is the option that respects Inv 30: the artefact is derived from the register, so the arrow stays register → export → DB gate and the register never stops being upstream. Its cost is one publish hop plus the discipline that the artefact is generated and never hand-edited — which is exactly the discipline that lapsed and produced today’s state.
Two alternatives, now on equal footing rather than presented as concessions: relax Inv 30 for
this one gate and read the register directly in blocking CI; or run the gate in docs-site CI
where the register is local — conceptually cleanest, since the repo that owns the artefact is the
one that fails, but it needs read-only DB credentials in a repo whose ci.yml is explicitly built
with “NO environment / secrets”. All three are owner calls; none is a technical blocker.
3. Where does validator.py get its values?
Section titled “3. Where does validator.py get its values?”This is the OKF-linter leg and it stays broken under any design, so it is the concrete piece of the “biggest disconnect”.
Today scripts/cocoindex_pipeline/producer/validator.py:189-217 declares ALLOWED_ENTITY_TYPES
and ALLOWED_RELATIONSHIP_TYPES as hand-written Python frozensets;
bundle_writer.py:660-677 serialises them into every bundle’s ontology.json. The intended fix
is already written down — ontology-sync.md:103-112: “When id-133 promotes the register to a
first-class allowed_types / allowed_relations register with a Python-consumable export, the
two constants are meant to be swapped for a load from that export with no call-site change.”
id-133 shipped without doing it.
Cost: ~10 lines. P2’s export already contains entity_type (12 values) and relationship
(10) with per-value provenance. validator.py replaces the two frozenset literals with a
json.load of the export and a frozenset(...) over the keys. _base_ontology_snapshot is
unchanged, so the bundle’s ontology.json becomes register-derived end to end — closing
register → export → linter → bundle. The existing TestEntityTypeParity /
TestRelationshipParity guards keep passing and become meaningful rather than tautological.
One caveat, stated because it is a real design tension: extraction.py’s Pydantic Literals
cannot be loaded from JSON at runtime — a Literal is a static type. So the extraction gate stays
code-generated or hand-mirrored, and the parity test remains its enforcement. Recall records the
same conclusion behind Decision A: “a DB row can’t gate LLM extraction without code regen”. The
honest position is that validator.py and the bundle can be register-driven; extraction.py
needs codegen (the content-type-values.generated.ts pattern, in Python) or stays guarded by
parity. Either is fine — but it should be a stated decision, not the current silence.
4. generate-taxonomy-snapshot.ts — the ask, restated in the ratified direction
Section titled “4. generate-taxonomy-snapshot.ts — the ask, restated in the ratified direction”Round 1 asked Lane 2 to make the hardcodes “read from the DB”. Wrong arrow — withdrawn.
scripts/generate-taxonomy-snapshot.ts:91-127 hardcodes content_types, platforms and
requirement_type as literal arrays. Under the ratified direction the fix is that those three
read from the register export, not from the DB and not from a hardcode. The snapshot is a
consumer artefact for the Python pipeline, so it should be downstream of the register like every
other consumer.
The two keys that are legitimately DB-read — domains, subtopics — are the ones whose values
are genuinely client-extensible via admin UI, and form_types is the third. Those stay DB-read,
and the register’s role for them is to declare the shape and provenance model, not the row
list. That distinction (closed core vocabulary vs client-extensible instance table) is already
carried by provenance_model in the frontmatter and by the provenance column on the six tables
that have one.
Also relevant to Lane 2: taxonomy-sync.yml fires only on repository_dispatch from
/api/admin/taxonomy-sync after an admin mutation. That is why taxonomy_snapshot.json is dated
2026-07-07 — nobody has mutated taxonomy through the admin UI since. Any gate that uses the
snapshot as its DB oracle is reading a four-week-old cache. The gate must introspect the live
DB, not the snapshot.
Is any of this impractical? One thing, stated plainly
Section titled “Is any of this impractical? One thing, stated plainly”Inv 30 blocks the direct route. The private-docs checkout action is forbidden in PR-blocking CI, so blocking CI cannot read the register itself. Everything else is straightforward — the schema exists, the loader’s behaviour is specified, blocking CI already holds DB credentials, and the cross-repo App token already exists. The export-artefact bridge (P3) works and preserves the arrow, but it is a genuine extra moving part, and it only holds if the artefact is generated and never hand-edited. Today’s failure is exactly that discipline lapsing: the artefact exists, and its update protocol is a sentence asking a human to remember.
If the owner would rather not add a publish hop, the alternative is to relax Inv 30 for this one gate or to run it in docs-site CI with read-only DB credentials. That is a policy decision I am flagging, not making.
What depends on Lane 1
Section titled “What depends on Lane 1”Only the taxonomy rows. taxonomy_domains / taxonomy_subtopics are case-A CVs with a live
admin-editable instance table; if taxonomy retires they leave the register with their tables, and
if it survives as an ontology layer they are re-derived against the new model rather than patched.
Everything else here holds either way, marked per-row in §3. Note also that CVs 01 and 02 both
cite classification_disputes in §Relationships, which the owner has ruled retire — those
lines go regardless of the taxonomy verdict.
1. The client requirement B.1 — and what it does to my CV 03 verdict
Section titled “1. The client requirement B.1 — and what it does to my CV 03 verdict”GROUNDING §0.2 directs me to the requirement, not the proposed solution. Reading B.1 that way
changes my round-1 Group F handling of 03-layer-vocabulary.md, and it is the clearest instance
in this lane of a CV documenting a superseded mechanism for a live requirement.
The requirement (gap tracker B.1, recorded “Liam decision: Option (ii)”): “currently layers
are metadata tags used for filtering, not for UI hierarchy” → layer becomes a UI nesting
rule: within each topic section, Sales Brief at top, Bid Detail nested beneath, Reference
collapsible. Originating case: Advanced Audits §13 “Security & Compliance” exists as one
bid_detail section; the intent was Security (Sales Brief) with Security (Bid Detail) nested
under it.
What the CV says: 03-layer-vocabulary.md reframes layer as “the Guides audience/depth
axis, matched against guide_sections.expected_layer”, a query-time filter — “Free-text
column matched at query time against a record’s classified layer value”. That is exactly the
mechanism B.1 rules is being replaced. The CV’s own §Notes acknowledges the nesting ruling and
calls the spec “Deferred-S238+”, but its Definition and Relationships still define the superseded
semantics as current. Reconciled verdict: REWRITE, not KEEP.
Three concrete consequences, all verified against staging:
- The CV’s shape cannot express what the requirement needs. B.1 Phase 1 asks whether nesting
uses
guide_sections.parent_section_idor a newlayer_priority_ordercolumn. Both the ordering signals already exist in the DB —guide_sections.parent_section_id✓ andlayer_vocabulary.display_order✓ (the CV itself notes the four rows carry 10/20/30/40). ButOntologyCVSchema’sBaselineValueSchemahas no ordering field at all —key,label,provenance,definition, and three optional provenance keys. A register that is normative over an ordered vocabulary cannot state the order. That is a real gap in the contract, and it is precisely the kind of thing the register→DB direction surfaces and the DB→register direction would have papered over by inheritingdisplay_ordersilently. - B.1’s implementation target is currently broken. Phase 2 requires extending
get_guide_contentto return layer-grouped buckets. That RPC stillLEFT JOINs the droppedcontent_itemstable and does flat matching (expected_layer = ci.layer), andapp/api/guides/[slug]/route.ts:64still calls it. This is no longer a stray docs-file annotation — it blocks a recorded owner decision.guidesandguide_sectionsare populated (4 and 9 rows). - The value set does not match the requirement’s vocabulary. B.1 names Sales Brief / Bid
Detail / Reference; the CV’s keys are
sales_brief/bid_detail/company_reference/research. Whether “Reference” meanscompany_reference,research, or both is unresolved, and the nesting order forresearchis unspecified. UNDECIDABLE — owner input needed, and it is a one-line answer.
On B.5, for contrast — a requirement that landed. B.5 asked for next_review_date +
review_cadence_days + a cadence cron. Both columns exist on record_lifecycle today, re-homed
off content_items. CVs 06/08/09 describe that facet correctly. So the register is not uniformly
behind the requirements — it is behind on the ones where the mechanism changed, which is the
pattern worth acting on.
1a. scope_tag — settled valid; its stakes are higher than the register reflects
Section titled “1a. scope_tag — settled valid; its stakes are higher than the register reflects”Not re-opened. Reporting encoding only, per §0.5, and raising its stakes per the client brief.
The requirement is a safety requirement, not a tagging nicety. Client Item 3: content
correctly tagged Internal IT (Bitdefender EDR) surfaced as the answer for
production-infrastructure questions — “Production infrastructure doesn’t run Bitdefender, so we’d
have given LBBD a false claim.” Closing line: “A high-confidence match in the wrong scope is
much more dangerous than a low-confidence match in the right scope.” The client also asks for
anti-tags explicitly (“the Bitdefender entry should carry an explicit ‘does not apply to:
production infrastructure’ flag”) — which is anti_scope_tag, and it is implemented.
Register encoding: 21-scope-tag.md, status: active, provenance_model: client,
client_extensible: true, editable_via: admin_ui. Its baseline_values holds exactly one row —
key: client-defined — which is a schema artefact: .superRefine requires ≥1 baseline value
for every non-Layer-5 CV, so an open client-defined vocabulary must invent a placeholder to be
representable at all. Under a normative register that is a defect worth fixing (allow an empty
baseline_values for provenance_model: client), because the placeholder is the kind of
not-really-a-value that a conformance gate would have to special-case.
Schema encoding (verified): q_a_pairs.scope_tag and .anti_scope_tag, both text[] NOT NULL
DEFAULT ARRAY[]::text[]; mirrored on q_a_pair_history; and the separate
q_a_extractions.scope_tags (plural, different table) which the CV correctly warns against
conflating. CV 21 is substantively accurate, including its correction that
workspaces.scope_tag[] does not exist (verified) and that filtering is caller-side via the
q_a_search RPC parameter.
Raised stakes: because Item 3 is a safety requirement, the fact that the scope vocabulary is
provenance_model: client with no platform baseline and no enforcement anywhere means a
misspelled or missing tag fails open — the pair is simply not excluded. That is worth the owner’s
attention independently of this lane, and it is not something the register can fix by itself.
Lane 1 overlap, stated not assumed: Lane 1 is working the same term from the schema side. If
they find a read/write asymmetry, that is consistent with the register rather than drift against
it — CV 21 records the admin UI as “deferred to v1.1”, and 01-vision.md §3.2 places “taxonomy /
tag / scope_tag administration” in the admin surface with v1.1 extending to client-managed
vocabularies. That admin surface is the missing writer, if there is one.
2. Delete these now — zero ambiguity (unchanged; stands on its own)
Section titled “2. Delete these now — zero ambiguity (unchanged; stands on its own)”Eleven CV files whose entire declared subject is a table or column that does not exist on any
environment. Verified by to_regclass / information_schema.columns against Platform staging.
This section rests on non-existence, never on emptiness. No row count is used as evidence anywhere in it. No migration is implicated, because there is nothing to migrate. None of it depends on Lane 1’s taxonomy verdict. Under the ratified direction these deletions are a prerequisite for the gate, not an independent tidy-up: a normative register cannot contain statements the DB has no way to satisfy.
| # | File | What the CV declares its subject to be | Staging reality (verified) |
|---|---|---|---|
| 05 | 05-platform.md | content_items.platform | table dropped ID-131 M6; no platform column on any table |
| 07 | 07-dedup-status.md | content_items.dedup_status | dropped. CV’s own text: “the owner ruled (b) DROP” |
| 11 | 11-change-type.md | content_history.change_type | content_history does not exist |
| 13 | 13-ingest-source.md | content_items.ingestion_source | dropped. CV’s own text: “permanently historic” |
| 14 | 14-bid-states.md | types/bid.ts BID_STATES | file absent at HEAD; lib/bid/ absent; no bids or bid_questions table |
| 15 | 15-workspaces-type.md | workspaces.type | column gone — replaced by application_type_id |
| 17 | 17-diff-mode.md | source_document_diffs.diff_mode | source_document_diffs does not exist |
| 19 | 19-engineering-types.md | the file listing of types/*.ts | 6 of its 13 values name deleted files; 4 live files absent from it |
| 27 | 27-form-format.md | form_templates.form_format | form_templates does not exist; the column was never built |
| 30 | 30-procurement-vehicle.md | procurement_vehicles table | does not exist — while README status reads APPLIED-S246 |
| 31 | 31-procurement-vehicle-instance.md | procurement_vehicle_instances table | does not exist — likewise APPLIED-S246 |
Two corrections to carry with the deletions, both of which are live facts wrongly stated:
13-ingest-source.mdclaims two “live remnants”. One is right (reference_items_ingestion_source_check=rss_feed|url_import— verified). The other is wrong on both table and values:form_templates.ingest_sourcedoes not exist; the live column isform_instances.ingest_sourcewithapp_upload|minted, notpipeline|app_upload. Re-home the correct fact; drop the incorrect one.15-workspaces-type.md§“Relationship to tenant & application” is correct and load-bearing (workspace ≠ tenant; one Supabase project per client; notenant_idcolumn). Move it toreference/entity-glossary.mdbefore deleting the file.
Delete alongside them:
| Target | Why |
|---|---|
$KH_PRIVATE_DOCS_DIR/__tests__/ontology-content-items-annotation.test.ts | 105 lines whose function is to preserve 127 mentions of a table dropped at ID-131 M6, by requiring a marker word, with a 14-line baseline of exemptions. The previous strip attempt produced a guard that institutionalises the rot |
the platform entries in ontology-cv-baselines.json, taxonomy_snapshot.json, generate-taxonomy-snapshot.ts:127, and markdown-parity.test.ts:71-74 | CI currently asserts parity for a CV whose own first paragraph says “DEPRECATED … do NOT re-home” |
| the dropped-table narrative in the survivors | content_items 127 / 20 files, plus template_requirements 20, form_templates 12, kb_section 10, content_history 8, procurement_vehicles 7, digests 6, content_citations 4, content_item_workspaces 3, bids 2 — none of which the annotation guard’s literal content_items match catches |
scripts/ontology-sync/parse-flow.py — verdict revised. Round 1 said delete. Revised:
HOLD, and decide against the gate design. It is the stub for the write path (register → DB
target table), which P1–P4 do not need. If the owner ever wants the register materialised into a
queryable DB table, this is the specified starting point and one of its stated blockers has
cleared — WP6 §5.5 defers on “the DB target table lands alongside the application_types migration
in Q-OQR1-16”, and application_types has since landed with 6 rows. That is a status
observation, not an argument that the write path is needed (§0.5b — WP6 §5.5 is intent at S236).
If the answer is that the export artefact plus the CI gate is sufficient — which I believe it is —
then delete it, and delete the scripts/ontology-sync/ directory with it. Do not delete it
before that decision; it is the only surviving design record for that path.
3. Verdict table
Section titled “3. Verdict table”Per GROUNDING §6. Grouped by concept. L1? = conditional on Lane 1’s taxonomy verdict.
Group A — subject does not exist; delete now
Section titled “Group A — subject does not exist; delete now”See §2 for the full evidence table. Every row: RETIRE / high / verified non-existence via
to_regclass + information_schema / blast radius = README rows and cross-refs only / L1: no.
05-platform.md · 07-dedup-status.md · 11-change-type.md · 13-ingest-source.md ·
14-bid-states.md · 15-workspaces-type.md · 17-diff-mode.md · 19-engineering-types.md ·
27-form-format.md · 30-procurement-vehicle.md · 31-procurement-vehicle-instance.md
Group B — case A: live CHECK exists and the register disagrees
Section titled “Group B — case A: live CHECK exists and the register disagrees”Under the ratified direction the register is normative, so the default remedy for each is a migration — except where an owner ruling already says the register is the wrong side.
| Target | Verdict | Conf. | Evidence | Blast radius | Owner action | L1? |
|---|---|---|---|---|---|---|
16-extraction-method.md | REWRITE | high | Live source_documents_extraction_method_check = rss_content, fetch, jina_reader, firecrawl, summary_fallback, docling, trafilatura, playwright, unpdf, mcp_create. Register declares 11 of which 5 pullmd_* values exist in no CHECK; 4 live values absent from the register. The register also carries an S237 ruling (Docling adoption retires trafilatura/pdfplumber) the DB never received | feed_articles CHECK (9 values) too | the worked example for the whole direction. Settle which naming is correct, then migrate the DB to it | no |
22-origin-kind.md | REWRITE | high | Register status: planned, “column not yet in production schema” — but q_a_pairs_origin_kind_check is live with extracted_from_corpus, curated_explicit, derived_from_form_response, imported_legacy, manually_authored. Register says derived_from_bid_**response** and omits manually_authored. Also cites q_a_pairs.source_workspace_id, dropped at ID-145 W1 | 32-q-a-pair.md §2.1 transcribes the same wrong CHECK | status: active; adopt form_response (the platform-wide bid→form rename) and manually_authored; fix CV 32 | no |
23-extractor-kind.md | REWRITE | high | Register status: planned; q_a_extractions_extractor_kind_check is live with prior_bid_response, llm_extraction, yaml_frontmatter_v1, markdown_heading_v1. Register declares mempalace_miner, cocoindex_pattern_a, cocoindex_pattern_b — none exist; 2 live values absent | none | status: active; reconcile — the three register-only values look like abandoned design, not a ruling the DB missed | no |
26-form-type.md + ontology-cv-baselines.json + taxonomy_snapshot.json | REWRITE | high | Live form_types = 7 rows. Register, fixture and snapshot all carry 8, incl. bid. Owner has ruled bid out of the vocabulary everywhere and the migration reclassified bid → itt | form-type-parity.test.ts is green because it compares fixture↔snapshot and the snapshot is dated 2026-07-07 | drop bid from register + fixture; regenerate the snapshot. This is the register being wrong against a ruling — correct it, do not adopt the table | no |
12-requirement-type.md | REWRITE | med | Values correct (7, matching form_template_requirements_requirement_type_check), but the CV names the table template_requirements with a “planned rename” note; the live table is form_requirement_templates. Its cited per-value counts describe a prod state that no longer applies — a prose-staleness signal, not a liveness argument | none | correct the table name; delete the counts and the Q-OQR1-16 note | no |
Group C — case B: register declares closed, DB does not enforce
Section titled “Group C — case B: register declares closed, DB does not enforce”The ID-131 re-home carried these columns across and left their CHECK constraints behind. The missing CHECK is measured; the value set that would replace it is a task-era decision. Per VC-2 the gate reports each of these as unenforced from day one, but P5 emission is per-CV opt-in after the set is settled — never a batch.
| Target | Verdict | Conf. | Evidence | Owner action | L1? |
|---|---|---|---|---|---|
04-content-type.md → source_documents.content_type | KEEP; P5 BLOCKED | high | No DB CHECK. Enforcement is extraction.py::_validate_content_type reading the snapshot — a Python validator standing in for a constraint. But the 7-value set is ID-133 BI-3, a task decision, and content_type is a Layer-1 concept that §2b rules already known to be incorrect | gate reports it unenforced. Do not emit the CHECK until Lane 1 and the owner settle the set — freezing an unresolved vocabulary into schema is the failure mode this census exists to end | yes |
06-lifecycle-type.md → record_lifecycle.lifecycle_type | KEEP; P5 deferred | high | CV’s own Notes: “Liam ruling S236’s ‘ADD CHECK’ migration never landed against content_items and the ratification is unresolved in the new home too” — i.e. the set was never ratified in its current home | report unenforced; settle the set before emitting | no |
08-freshness.md → record_lifecycle.freshness | KEEP; P5 deferred | high | CV: the pre-M1a content_items_previous_freshness_check “did not carry over”; both columns soft in the new home | report unenforced; do not batch with 06/09 | no |
09-governance-review-status.md → record_lifecycle.governance_review_status | KEEP; P5 deferred | high | CV: “content_items_governance_review_status_check enforced this 5-value set; record_lifecycle has no equivalent CHECK” | report unenforced | no |
10-publication-status.md | KEEP (partial) | high | q_a_pairs.publication_status has a real 4-value CHECK matching the register (case A, passing) — the set is exercised, so the source_documents half is the strongest P5 candidate of this group | P5 for the source_documents half once the owner confirms the two columns should share one vocabulary | no |
29-cocoindex-source-kind.md → source_documents.origin_type | KEEP | med | Column live, populated, soft. The CV explicitly leaves the coarse-vs-fine CHECK shape as an open owner decision — correctly, in my view | leave open; the gate reports unenforced rather than failing | no |
Group D — status contradicts reality (case C or mis-stated)
Section titled “Group D — status contradicts reality (case C or mis-stated)”| Target | Verdict | Conf. | Evidence | Owner action | L1? |
|---|---|---|---|---|---|
25-application-type.md | REWRITE | high | status: needed, “table does not yet exist”. application_types exists with exactly the 6 declared values plus a provenance column | status: active; delete the Q-OQR1-16 notes. Values already correct | no |
18-entity-aliases.md | REWRITE | high | CV: “category rename to provenance is pending… not yet shipped”. Live columns: id, alias, canonical, **provenance**, is_active, created_at — landed. The CV describes the v1.1 client-vocabulary seam as not yet existing | delete the rename-pending notes and the row-count prose | no |
28-change-report-category.md | REWRITE | med | status: planned; “change_reports (renamed from digests… rename pending)”. change_reports exists; no category column | planned is right for the column; the rename framing landed | no |
24-citation-kind.md | UNDECIDABLE | med | status: planned; “planned citations table, distinct from existing content_citations”. Inverted: citations exists (with citation_type); content_citations does not | one question settles it: is citation_kind a planned column on the live citations table, or superseded by citation_type? If superseded → delete | no |
20-chunk-kind.md | KEEP | high | status: planned; content_chunks has no *kind* column — accurate. A genuinely planned vocabulary, not rot | keep; the gate asserts nothing until the column lands | no |
21-scope-tag.md | KEEP | high | Accurate (see §1a). Settled valid, and a safety requirement | keep. Fix the schema so provenance_model: client may carry an empty baseline_values | no |
Group E — the chain machinery (re-derived in the ratified direction)
Section titled “Group E — the chain machinery (re-derived in the ratified direction)”| Target | Verdict | Conf. | Evidence | Owner action | L1? |
|---|---|---|---|---|---|
lib/ontology/schemas.ts:113 .strict() | REWRITE | high | Real OntologyCVSchema executed over all 37 register files: 37 FAIL on unrecognised key title; 37 PASS once excluded. The docstring calls itself “Zod contract for every docs/ontology/*.md file’s YAML frontmatter” | P1 — admit title + kh_docubot_owned. Prerequisite for everything else | no |
lib/ontology/schemas.ts:31 STATUS_VALUES | REWRITE | high | ['active','planned','needed']. CVs 05/07/13 each carry a paragraph explaining the enum “has no literal deprecated value, so this paragraph is the deprecation marker” | P1 — add retired. A normative register must be able to state retirement | no |
BaselineValueSchema has no ordering field | REWRITE | high | layer_vocabulary.display_order exists in the DB (10/20/30/40); B.1 needs layer ordering; the register cannot express it | add an optional display_order — driven by B.1, not by tidiness | no |
lib/ontology/loader.ts — retired at ID-133 Decision A | REINSTATE (as an exporter) | high | This was the register→code leg. WP6 §5.2 still specifies its behaviour in 5 numbered steps. Decision A’s stated subject was DB-table promotion; its effect was to delete the register’s only reader | P2. Re-instate as a build-time exporter, not a runtime reader — that answers Decision A’s actual objection (“adds a reader without removing drift”) because the export removes the hand-update protocol | no |
ontology-cv-baselines.json _meta.update_protocol | REWRITE | high | The register→artefact link is a prose instruction: “When a baseline changes in the private CV register, update the matching frontmatter block here” | P3 — make the artefact generated; delete the instruction | no |
markdown-parity.test.ts:207 | REWRITE | high | Asserts “The snapshot is DB-derived, so fixture == snapshot == live DB CHECK holds transitively”. False for 3 of its 4 keys — generate-taxonomy-snapshot.ts:91-127 hardcodes content_types, platforms, requirement_type. For the fourth (form_types) the snapshot is 4 weeks stale and the DB has diverged | P4 — change the oracle from the snapshot to live pg_constraint introspection. Blocking ci.yml already carries SUPABASE_SERVICE_ROLE_KEY / POSTGRES_PASSWORD / SUPABASE_URL | partly |
validator.py:189-217 frozensets | REWRITE | high | Hand-written ALLOWED_ENTITY_TYPES / ALLOWED_RELATIONSHIP_TYPES, serialised into every bundle by bundle_writer.py:660-677. ontology-sync.md:103-112 already specifies the swap “with no call-site change” | ~10 lines — load from the P2 export. Closes register → export → linter → bundle | no |
extraction.py Pydantic Literals | KEEP (guarded) | high | A Literal is a static type and cannot be loaded from JSON | state the decision explicitly: codegen (the content-type-values.generated.ts pattern in Python) or stay parity-guarded. Currently neither is stated | no |
34-entity-type.md:127, 35-relationship.md:113 “parity-guard twin … Drift in any of the three fails CI” | REWRITE | high | grep -rn 'parity-guard twin' → 5 documents, 0 code | P2/P3 make the claim true. Until then, correct it — two CVs assert CI enforcement that does not exist | no |
lib/ontology/concept-schema.ts:130 vs 37-concept-type.md | REWRITE | high | CV claims register == TS type enum lockstep. The schema types type as a plain non-empty string, not z.enum(CONCEPT_TYPE_VALUES); the const is exported and unused. No test binds concept_type | gate the TS schema on the enum (matching validator.py:116 ALLOWED_CONCEPT_TYPES, which does gate) or correct the CV | no |
Inv 30 (.github/actions/resolve-private-docs/action.yml) | UNDECIDABLE — owner policy | high | ”Opt-in lanes only: no PR-blocking CI job may use this action.” An ID-68 task invariant, not a DR — intent at that time, revisable (VC-4). Its reasoning (don’t couple blocking CI to a private-repo checkout) is sound on its face but is the owner’s to keep or drop | three equal options: (a) export-artefact bridge — P3, my recommendation, because it respects Inv 30 and preserves the arrow; (b) relax Inv 30 for this one gate; (c) run the gate in docs-site CI with read-only DB credentials | no |
Group F — keep the substance; re-home or correct
Section titled “Group F — keep the substance; re-home or correct”| Target | Verdict | Conf. | Evidence | Owner action | L1? |
|---|---|---|---|---|---|
03-layer-vocabulary.md | REWRITE (was KEEP) | high | Task-directive vs requirement conflict (VC-3). ID-133 BI-4 (task decision) re-homed layer as the Guides filter axis and declared the CV “KEPT, not deprecated”. Gap tracker B.1 — an owner decision in a requirements doc, §0.2 top-tier evidence — rules that same filter mechanism replaced by a UI nesting rule. The requirement outranks the task directive | rewrite against B.1’s nesting semantics; add display_order to BaselineValueSchema; resolve which key “Reference” denotes. BI-4 was a reasonable re-home at the time; it simply did not consult B.1 | no |
get_guide_content / get_guide_coverage + app/api/guides/[slug]/route.ts:64 | KEEP — raise as a task NOW | high | Both RPCs still LEFT JOIN content_items (dropped ID-131 M6) and do flat expected_layer = ci.layer matching; the route still calls the first. B.1 Phase 2 requires extending exactly this RPC | this blocks a recorded owner decision, not just a docs annotation. Raise before the surrounding prose is deleted | no |
| README §“Where does new data live?” | KEEP — re-home | high | The Q1a/Q1b/Q2/Q3/Q4 decision procedure. Post-ID-131/133; routes correctly to q_a_pairs / source_documents / L-concepts; carries the Q1b→Q1a adoption edge and the “never solve placement with a new content_type value” corollary | the most valuable artefact in the directory. Move to reference/; do not lose it with the README | no |
36-three-layer-model.md | KEEP — re-home | high | 65 lines, zero content_items, zero dead paths, 4 provenance markers. Carries the anti-conflation guard | move to reference/; it is a storage model, not a CV | no |
34-entity-type.md / 35-relationship.md | KEEP | high | 12 and 10 values, exact match to entity_mentions_entity_type_check / entity_relationships_relationship_type_check and to validator.py:189/205. The two case-A CVs that are fully correct | the model for what the gate should protect | no |
37-concept-type.md | KEEP | high | 5 values == validator.py:116 ALLOWED_CONCEPT_TYPES | correct the false TS-lockstep claim | no |
32-q-a-pair.md | REWRITE | med | Most actively maintained (kh_docubot_owned, last_updated: 29/07/2026) and still documents source_workspace_id, dropped at ID-145 W1, plus a 4-value origin_kind CHECK that has 5. Its §2 duplicates database.types.ts — the part that rots | keep §1 Mission; drop the column tables. Its drift is the proof that hand-maintenance fails even under active ownership | no |
33-reference-item.md | REWRITE | med | reference_items live and shape broadly matches; 71 provenance markers, the heaviest accretion in the register. Second task-directive vs owner-ruling conflict (VC-5): the CV documents source_document_id as NOT NULL ON DELETE RESTRICT per ID-75 BI-15 (“provenance chain integrity”) — a task decision. The owner ruled at the round-1 board (id-364 F1/C5) that a reference item does NOT need a source_document — “source documents are walked, reference items are not” | keep §1 Mission; drop §2. The owner ruling outranks BI-15; the CV presents BI-15 as settled. Column is Lane 1’s — flagged, not resolved here | no |
01-taxonomy-domains.md, 02-taxonomy-subtopics.md | UNDECIDABLE | high | Case A with a live admin-editable instance table (provenance column present). Values correct (7 domains exact; 34 subtopics vs 33 declared). Existence is Lane 1’s call | hold for Lane 1. Either way: the classification_disputes Relationships lines go (owner ruled retire), and “15 active domains / 57 active subtopics” is wrong today | yes |
Group G — register-wide prose rot (unchanged)
Section titled “Group G — register-wide prose rot (unchanged)”| Target | Verdict | Conf. | Evidence | Owner action |
|---|---|---|---|---|
| Dead code-path citations | REWRITE | high | Scripted check of every backtick-quoted repo-rooted path across the 39 files: 74 of 190 (38%) do not exist at HEAD. README §File index’s “Where to look first” column — the register’s navigation surface — is 9-for-26 dead | after the §2 deletions, extend the docs-site link-validator.test.ts to code paths |
| Stale counts | REWRITE | high | README “33 controlled vocabularies”, “Output: 29 .md files… Current wave (S236 WP6 A1)”; index.md “37”; schemas.ts:51 “the existing 33 CVs”. Live: 37 | the export makes the count derivable; stop stating it |
| Provenance-archaeology accretion | REWRITE | high | 561 markers (S###, {N.M}, ID-N, Q-OQR1-N, BI-N, INV-N, OQ-*, DR-NNN) over 3,474 lines — ~1 per 6. Worst: 33 71, 13 60, 29 49, 04 46, 37 44 | confine provenance to one ## Provenance footer per file. A normative register states the rule, not how it was reached |
26-form-type.md inline revision history | REWRITE | high | Four ~~struck-through~~ passages (:483, :486, :503, :508) recording what the CV used to say, plus a §Relationships bullet duplicated verbatim in §Notes | delete |
| README §Multi-agent update workflow | RETIRE | med | A Drafter/Verifier/Editor/Ratifier wave described as “Current wave (S236 WP6 A1)” — ~290 sessions stale — instructing “do NOT collapse these stages” | delete; the export + gate supersede it |
| Client names in prose | REWRITE | med | A named client appears in CVs 01, 18, 21, 32 as example data. Private repo, so not a leak — but it makes 01/18 read as tenant records rather than platform baselines | strip; the provenance column already separates core from client rows |
4. What contradicted the framing
Section titled “4. What contradicted the framing”-
The register→DB gate was built, not merely designed.
markdown-parity.test.tsonce asserted markdown ↔ tuple ↔ live DB CHECK, via a loader that read the register. My round-1 “never built” was wrong for the gate; it is right only forparse-flow.py’s write path. This makes the fix cheaper — but it is a precedent for feasibility, not for correctness (VC-1). WP6 §1 states the same goal the owner now states, which is corroboration, not authority; and WP6’s own second goal namescontent_items.content_typeas the consumer to wire, so the spec was authored inside the frame under investigation. -
The gate is not dead — it is alive, green, and blind.
bunx vitest run __tests__/lib/ontology/→ 39 tests pass. It compares a frozen fixture to a four-week-old snapshot whose three most-cited keys are hardcoded in the generator. The one assertion capable of catching a real drift (form_types) is currently wrong and passing. -
The layer conflation is genuinely fixed —
36-three-layer-model.md:62and03-layer-vocabulary.md:90carry reciprocal anti-conflation guards. But03’s filter semantics is superseded by B.1’s nesting ruling, which is a different and live problem. Fixing the conflation did not make the CV current. -
The register’s frontmatter is sound; its prose is not. All 37 files pass the real
OntologyCVSchemaoncetitleis admitted. Only five CVs have materially wrong values. This is why the exporter is cheap — the register is already machine-readable, which is also why generating from it (P2) is available and generating into it was never necessary. -
content_itemswas addressed, but wrongly and too narrowly. The definitional rot was swept; the remedy was annotate-and-baseline and it matched only the literal string, socontent_history,content_citations,content_item_workspaces,form_templates,template_requirements,procurement_vehicles,digestsandkb_sectionsurvived untouched.11-change-type.mdis the clearest case: 96 lines aboutcontent_history,status: active, flagged by nothing. -
01-vision.md§1.1 corroborates the rot rather than settling anything. Both mechanisms it names — “markdown-as-source-of-truth scaffold” and “the parity-tested registry” — were dismantled or never completed after §1.1 was written. Per §2a it is an input with stated provenance, and Lane 1’s diff of the 28/07/2026 commit (15 insertions / 15 deletions, all path re-points and DR-038 noun swaps, §1.1 untouched) confirms the stamp never covered the claim.
5. Coverage — and what I could not cover
Section titled “5. Coverage — and what I could not cover”Recall pass (GROUNDING §0.6) run before deriving, using the corrected FTS recipe (each term double-quoted). Queries: ontology/register/sync; parse-flow; register source-of-truth; markdown source-of-truth + CHECK; loader + Decision A + retire; wp6 + ontology + harness; 3-way lockstep; layer + nesting + Sales Brief. Recall and code agreed, and recall supplied the fact code alone would not have: the WP6 architecture diagram showing the 3-way lockstep, and Decision A’s stated rationale. Recorded above rather than silently preferred.
Covered in full: all 37 CV files + README + index; every backtick-quoted repo-rooted code path
checked for existence (190 paths, scripted, 74 missing); every CV’s declared DB target checked
against staging (to_regclass, information_schema.columns, pg_constraint, provenance-column
sweep); the real OntologyCVSchema executed against all 37 files; the fixture diffed against
register frontmatter for all 6 guarded CVs; taxonomy_snapshot.json diffed against the live DB;
the ontology parity suite executed (39 pass); git history of the register (19 commits; 12 of 37
files unmodified since the 2026-06-07 relocation); wp6-ontology-harness/TECH.md §1–§5.5;
validator.py, concept-schema.ts, generate-taxonomy-snapshot.ts, markdown-parity.test.ts,
form-type-parity.test.ts, ontology-content-items-annotation.test.ts, content.config.ts,
parse-flow.py; .github/actions/resolve-private-docs/action.yml, ci.yml, schema-parity.yml,
taxonomy-sync.yml; DR-016/019/027/079/082; ontology-sync.md, semantic-linter-gate.md;
01-vision.md §1.1/§2/§3.2; kh-client-feedback.md in full; gap tracker B.1 and B.5;
reference/entity-glossary.md.
Not covered, and why:
- Gap tracker B.2, B.4, B.6 — named by the owner but read only via recall summaries, not
opened. B.2 (Product Guide section naming) and B.4 (Advanced Audits dual-tier) both sit on
guide_sections, so they likely bear on03-layer-vocabularythe same way B.1 does. This is the largest gap in my coverage and I would open them next. phase-b-prerequisite-1-onthology-pipeline.md(65 KB) +-feedback-investigation.md(79 KB) — where “Layer 1 = Controlled Vocabulary” and the Talisman 1–6 framing were decided, and the source of the frontmatter shape. Not audited. Given §2b’s ruling that the Layer-1 concepts are already known to be incorrect, these two are the natural follow-on: no DR I read re-derived that framing, and the register’s wholelayer:axis rests on it.- Whether the values are right as vocabulary (is
evergreenthe correct lifecycle set?). This census establishes whether each value binds something and whether register and binding agree — not whether the vocabulary is well designed. 06-mcp-tooling.md(cited by CV 31) — could not resolve in either repo.
Round-3 self-audit (§0.5b). I re-read my round-2 report for the “task directive read as
correctness” shape. Five instances found, all recorded as VC-1..VC-5 above. The one I would most
want a second pair of eyes on is VC-2: I have blocked P5 for content_type on the grounds
that ID-133 BI-3’s 7-value set is a task decision over a Layer-1 concept §2b rules incorrect. If
Lane 1 concludes content_type survives with that exact set, P5 unblocks immediately and the
delay costs nothing; if it does not, emitting the CHECK first would have written the wrong
vocabulary into the schema. The asymmetry is why I blocked rather than deferred to the task.
What I need from the owner (per §0.6 — cheaper than a wrong verdict):
- B.1’s “Reference” layer — does it mean
company_reference,research, or both? And where doesresearchsit in the nesting order? One line settles03-layer-vocabulary. 24-citation-kind— iscitation_kindplanned on the livecitationstable, or superseded bycitation_type? One line settles delete-vs-rewrite.- The Inv 30 choice — export-artefact bridge, relax Inv 30 for this gate, or DB credentials in docs-site CI.
16-extraction-method—pullmd_*or the DB’s bare names? This is the first real exercise of the register’s authority and it should be the owner’s call, not an agent’s.- P5 sequencing (VC-2) — is ID-133 BI-3’s 7-value
content_typeset still the intended vocabulary, or does it go back to Lane 1 with the rest of the Layer-1 concepts? Until that is answered I will not propose emitting a CHECK over it. Same question, lower stakes, forlifecycle_type/freshness/governance_review_status.