Skip to content

ID-63 {63.1} RESEARCH — Ontology / controlled-vocabulary (CV) integration verification across the canonical pipeline

ID-63 {63.1} RESEARCH — Ontology / controlled-vocabulary (CV) integration verification across the canonical pipeline

Section titled “ID-63 {63.1} RESEARCH — Ontology / controlled-vocabulary (CV) integration verification across the canonical pipeline”
  • Task: ID-63 — Ontology / controlled-vocabulary (CV) integration verification across the canonical pipeline.
  • Subtask: {63.1} RESEARCH (this document).
  • Status: RESEARCH — records findings, options, and Open Questions. Does NOT ratify a design.
  • Author: fresh task-planner instance (Q-PLANNER-2), opus thinking:max, isolated worktree off main @ fffe0aeb.
  • Date: 29/05/2026.
  • Language: UK English.
  • Cross-refs: ID-149, ID-173, ID-9, docs/themes/canonical-pipeline/reference/canonical-pipeline-sequencing.md §2.3 + §7, ID-52 {52.5} / {52.6} journal.

Reading guide. §1 frames the brief. §2 is the code-intelligence orientation (cited verbatim). §3 is the empirical pre-ratification verification block. §4 maps the CV source-of-truth + load paths per stage (the core artefact). §5 reconciles ID-9. §6 dissects what {52.5} covered. §7 separates the residual gap from ID-149/173. §8 quantifies the re-ingest risk. §9 is the numbered Open Questions (OQ-63-N). §10 is a recommendations digest for {63.2} PRODUCT.


Sequencing-doc §7 frames CV loading as FOUNDATIONAL — gating T7 (re-ingest), form-extraction, and T10 matching — NOT merely the two test-fixes in backlog (ID-149 full-file parity, ID-173 form-type schema). The CVs (form_types, taxonomy, requirement_type) feed Path-A classification DURING ingest; re-ingesting with a parity-drifted or mis-wired CV load risks systematically wrong classification across the whole corpus → a pre-re-ingest BLOCKER tracked under the ID-64 readiness umbrella’s gate-set.

{63.1} must determine: (a) the CV source-of-truth + load path during ingest (Path-A classify); (b) whether ID-9 is/was the ontology auto-sync handler and how it relates; (c) what {52.5} actually covered vs the residual gap; (d) the residual integration-correctness gap BEYOND ID-149 + ID-173; (e) the re-ingest RISK if CV is parity-drifted at ingest time.


2. Code-intelligence orientation (cited verbatim, not paraphrased)

Section titled “2. Code-intelligence orientation (cited verbatim, not paraphrased)”

Per the .gitnexus/CLAUDE.md “Always Do” block, orientation was run before authoring. TS symbols via gitnexus; Python/markdown/SQL via grep (outside the TS index per .ast-dataflow/CLAUDE.md).

2.1 gitnexus_query({query: 'ontology'}) — verbatim definitions returned

Section titled “2.1 gitnexus_query({query: 'ontology'}) — verbatim definitions returned”
Function:scripts/ontology-sync/parse-flow.py:process_ontology_file (lines 82-94)
Function:scripts/ontology-sync/parse-flow.py:parse_cv_frontmatter (lines 97-109)
Function:scripts/ontology-sync/parse-flow.py:validate_cv_against_yaml (lines 112-128)
Function:lib/ontology/loader.ts:loadOntologyCVs (lines 42-75)
Function:lib/ontology/schemas.ts:OntologyCVSchema (lines 105-137)
File:lib/ontology/content-type-registry.ts
Function:scripts/generate-content-type-values.ts:main (lines 45-84)
Class:scripts/tests/test_cocoindex_extraction.py:TestFormTypeParity (lines 738-788)
File:docs/specs/wp6-ontology-harness/TECH.md
File:docs/reference/backlog/149.md ; File:docs/reference/backlog/173.md

processes: [] for the ontology query — i.e. no TS execution flow is indexed through the ontology loader. The loader is a module-load side-effect consumed at build time (generate-content-type-values.ts) and in the parity test, not a node on a runtime classify flow. This is the first signal that the markdown register and the ingest classifier are decoupled (confirmed in §4).

2.2 gitnexus_query({query: 'classifyContent'}) — verbatim definitions returned

Section titled “2.2 gitnexus_query({query: 'classifyContent'}) — verbatim definitions returned”
Function:lib/ai/classify.ts:classifyContent (lines 1104-1832, module Ai)
Interface:lib/ai/classify.ts:ClassificationResult (lines 705-727)
Function:lib/ai/classify.ts:coerceSubtopic (lines 698-703)
Function:lib/mcp/tools/shared.ts:getClassifyContent (lines 228-231)
Function:scripts/eval-classification.ts:classifyFixtureItem
Function:app/api/cron/classification-quality/route.ts:GET (lines 43-330)
Function:app/api/items/[id]/classify/route.ts:POST (lines 27-125)

2.3 gitnexus_query({query: 'form_types'}) — verbatim definitions returned

Section titled “2.3 gitnexus_query({query: 'form_types'}) — verbatim definitions returned”
Function:scripts/cocoindex_pipeline/extraction.py:_load_canonical_form_types (lines 93-123)
Method:scripts/cocoindex_pipeline/extraction.py:FormMetadata._validate_form_type (lines 180-189)
Function:scripts/cocoindex_pipeline/extraction.py:extract_qa_form (lines 568-590)
Method:scripts/tests/test_cocoindex_extraction.py:TestFormTypeParity.test_snapshot_form_types_non_empty_and_bounded
Method:scripts/tests/test_cocoindex_prompts.py:TestPromptsEnumeratesEnums.test_q_a_form_enumerates_form_types

2.4 gitnexus_query({query: 'controlled vocabulary'})

Section titled “2.4 gitnexus_query({query: 'controlled vocabulary'})”

No ontology/CV-specific execution flow surfaced (top hits were layer-schema / draft-stream / classify-quality flows — lib/validation/layer-schemas.ts, app/api/layers/*). Confirms there is no single indexed “CV-loading” execution flow in the TS graph — CV loading is distributed across build-time scripts (TS), a Pydantic module-load read (Python), and live DB queries (TS classify).

2.5 gitnexus_context({name: 'classifyContent'}) — verdict + caller picture

Section titled “2.5 gitnexus_context({name: 'classifyContent'}) — verdict + caller picture”

Ambiguous — 4 symbols share the name (relevance score in parens):

Function:lib/ai/classify.ts:classifyContent (0.56) ← the implementation
Const:lib/mcp/tools/ai.ts:classifyContent (0.50) ← MCP tool wrapper
Const:lib/mcp/tools/content.ts:classifyContent (0.50) ← MCP tool wrapper
Const:lib/mcp/tools/governance.ts:classifyContent (0.50)← MCP tool wrapper

Direct upstream API callers of the TS implementation (grep over app/ + scripts/*.ts): app/api/ingest/url/route.ts, app/api/items/batch/route.ts, app/api/items/[id]/classify/route.ts, app/api/items/route.ts, app/api/items/[id]/route.ts, app/api/upload/route.ts, app/api/cron/classification-quality/route.ts, scripts/backfill-classify-content-items.ts, scripts/batch-reclassify-keywords.ts. Verdict: HIGH (broad caller fan-out across the app ingest + cron + MCP surfaces). This is the app-side classify path — NOT the canonical-pipeline T7 re-ingest path (see §4.2).

2.6 gitnexus_context({name: 'loadOntologyCVs'}) — verdict + caller picture

Section titled “2.6 gitnexus_context({name: 'loadOntologyCVs'}) — verdict + caller picture”
{ "status": "found", "uid": "Function:lib/ontology/loader.ts:loadOntologyCVs",
"incoming": {}, "outgoing": {}, "processes": [] }

Zero indexed callers, zero processes. Empirically: callers are __tests__/lib/ontology/markdown-parity.test.ts:40 and (transitively, via the generated tuple) scripts/generate-content-type-values.ts. Verdict: LOW as a runtime symbol — it does NOT participate in any ingest classify flow. This is the load-bearing orientation finding: the markdown CV register (loadOntologyCVs) does not feed the runtime classifier on any path (§4).

2.7 Python / SQL grep sweeps (outside the TS index)

Section titled “2.7 Python / SQL grep sweeps (outside the TS index)”
  • scripts/cocoindex_pipeline/extraction.py — reads content_types + form_types from scripts/tests/fixtures/taxonomy_snapshot.json; does NOT read requirement_type.
  • scripts/kb_pipeline/classify.py — a third classifier (classify() line 721, _validate_classification() line 917 “Warns but doesn’t reject”); reads prompt from docs/reference/classification-prompt.md.
  • scripts/ontology-sync/parse-flow.py — a CV-frontmatter parser/validator (parse_cv_frontmatter, validate_cv_against_yaml). Relationship to the build-time loader is an open question (see OQ-63-7).
  • requirement_type grep across lib/ scripts/ app/ supabase/migrations/: consumers are lib/templates/template-coverage.ts, lib/mcp/formatters/templates.ts, scripts/calibrate-coverage-thresholds.ts, scripts/catalogue-standard-sq.ts, scripts/catalogue-charnwood-itt.ts, app/api/coverage/gaps/route.ts, plus migrations + ontology markdown. No Path-A ingest consumer.

3. Empirical pre-ratification verification (OQ-3 / Q-EX2 forcing function)

Section titled “3. Empirical pre-ratification verification (OQ-3 / Q-EX2 forcing function)”

External + internal symbols cited in this RESEARCH were checked against the installed pins. The Q-EX2 precedent (cocoindex 1.0.0 removed ExtractByLlm, drift propagated unchecked) is exactly the failure mode this block exists to prevent.

DatePinSymbol / artefact checkedResult
29/05/2026cocoindex[postgres]==1.0.3scripts/cocoindex_pipeline/extraction.py imports + @coco.fn extractorsPRESENT (pipeline is built + tested; ExtractByLlm already known-ABSENT and avoided per {52.1})
29/05/2026anthropic==0.79.0ANTHROPIC_MODEL = "claude-opus-4-6" (extraction.py:65)PRESENT — single source mirrors lib/anthropic.ts
29/05/2026pydantic==2.12.5FormMetadata, ClassificationExtraction, field_validator strict + extra='forbid'PRESENT — _PYDANTIC_ERROR_TO_ERROR_CLASS empirically verified vs 2.12.5 strict error strings (extraction.py:94-96)
29/05/2026tenacity==9.1.4_anthropic_retry (AsyncRetrying, stop_after_attempt, wait_exponential)PRESENT
29/05/2026gray-matter ^4.0.3lib/ontology/loader.ts frontmatter parsePRESENT
29/05/2026zod ^4.4.3lib/ontology/schemas.ts:OntologyCVSchema (.strict(), .superRefine)PRESENT

Ground-truth test runs (this worktree):

  • bun run test __tests__/lib/ontology/2 files, 12 tests, ALL PASS (526ms). markdown-parity.test.ts + form-type-parity.test.ts both green; loadOntologyCVs() returns ≥29 records without throwing.
  • Snapshot inventory (scripts/tests/fixtures/taxonomy_snapshot.json): content_types = 15 values; form_types = 8 keys (bid, checklist, itt, pqq, questionnaire, rfp, sales_proposal_template, tender); no requirement_types key present.
  • Markdown CV docs/ontology/26-form-type.md baseline_values = 8 keys (matches snapshot — {52.6} lockstep holds).
  • Markdown CV docs/ontology/12-requirement-type.md baseline_values = 7 keys (policy, statement, evidence, data, narrative, declaration, reference).
  • Live DB form_template_requirements.requirement_type carries a CHECK constraint (form_template_requirements_requirement_type_check, migration 20260416102457:4167, renamed 20260520120828:242) enforcing exactly those 7 values — markdown ↔ DB CHECK are currently in parity, but no parity TEST guards them (unlike content_type).

SIGNATURE_DRIFT / BEHAVIOUR_DRIFT finding (escalated as OQ-63-1, blocking):

  • scripts/cocoindex_pipeline/prompts.py:104 Q_A_FORM_PROMPT enumerates 11 legacy form_type values: bid, rfp, pqq, itt, tender, framework, dps, gcloud, checklist, questionnaire, sales_proposal_template.
  • The canonical set (snapshot + live form_types table + 26-form-type.md) is 8 — the 3 values framework, dps, gcloud are absent from canonical.
  • {52.6} replaced FormMetadata.form_type: Literal[...] with form_type: str + a field_validator that rejects any value not in the 8-key snapshot (extraction.py:179-190).
  • Net effect: the prompt instructs the LLM to emit framework/dps/gcloud for procurement forms; the validator then rejects those rows as invalid_enum. This is an active ingest-time form-cataloguing defect — Mode-1 q_a_form extractions for framework/DPS/G-Cloud procurement forms will fail validation. (Previously surfaced by the {52.6} Executor as OQ-52-PROMPT-DRIFT; recorded here as the canonical owner.)
  • Compounding: scripts/tests/test_cocoindex_prompts.py:112-130 (test_q_a_form_enumerates_form_types) asserts the legacy 11 set is present with the docstring “Q_A_FORM_PROMPT should list the 11 canonical form_type values”. The suite is GREEN, so the drift is locked in by a passing test that encodes a stale, partly-invalid contract — a docs/reference/test-philosophy.md violation (test asserts a legacy implementation list, not canonical behaviour). This is the archetypal “test-green but integration-wrong” gap ID-63 was opened to find.

4. CV source-of-truth + load path during ingest (brief item (a)) — the core artefact

Section titled “4. CV source-of-truth + load path during ingest (brief item (a)) — the core artefact”

4.1 There are THREE CVs, owned by THREE different pipeline stages

Section titled “4.1 There are THREE CVs, owned by THREE different pipeline stages”
CVOwning stage (§2.3)Built?Runtime CV source-of-truthWrite target
taxonomy (content_type + domain + subtopic)Path-A classifyBUILTcontent_type: snapshot (Python) / hardcoded tool enum (TS). domain+subtopic: see §4.3content_items
form_typesPath-A q_a_form cataloguing + form-template write (Path B)BUILTsnapshot form_types[] (Python _VALID_FORM_TYPES)q_a_extractions / form_templates.form_type
requirement_typePath-C AI-cataloguing skill + T10 matchingUNBUILT (Path-C = {52.14} pending)DB CHECK (7 values) + markdown 12-requirement-type.md; no ingest consumerform_template_requirements.requirement_type

Brief framing correction (load-bearing). The brief states all three CVs “feed Path-A classification DURING ingest”. Sequencing-doc §2.3 and the code disagree for requirement_type: it is authored by Path-C (the unbuilt AI-cataloguing skill, ID-52.14) and consumed by T10 matching — it is never read by the Path-A ingest classifier. So the re-ingest risk for requirement_type is nil at ingest time (it is not in the ingest write path at all); its risk surfaces only when Path-C/T10 are built. The CVs that genuinely gate T7 re-ingest are taxonomy and form_types.

4.2 There are THREE classifier implementations, with THREE different CV wirings

Section titled “4.2 There are THREE classifier implementations, with THREE different CV wirings”

This is the central integration-correctness finding. “Path-A classify” is not one code path:

  1. lib/ai/classify.ts:classifyContent (TS, app-side; verdict HIGH).

    • content_type: hardcoded enum in the Anthropic tool JSON schema (classify.ts ~line 1247+).
    • domain / subtopic: built from live DB taxonomy_domains + taxonomy_subtopics (sb() reads at lines 1150-1166), injected into the prompt as {TAXONOMY}.
    • Does NOT call loadOntologyCVs() and does NOT read the snapshot.
    • Callers: app/api/items/*, app/api/upload, app/api/ingest/url, cron classification-quality. This is the app/UI ingest path.
  2. scripts/kb_pipeline/classify.py:classify (older Python CLI pipeline).

    • Reads prompt from docs/reference/classification-prompt.md.
    • _validate_classification() validates domain/subtopic against live DB taxonomy but “Warns but doesn’t reject” (line 918) — soft validation.
    • Relationship to the cocoindex pipeline (superseded? still used for any corpus?) is an open question (OQ-63-5).
  3. scripts/cocoindex_pipeline/extraction.py (newer cocoindex Path-A; the flow.py writer of content_items).

    • content_type: ClassificationExtraction._validate_content_type rejects anything not in snapshot content_types (hard reject → invalid_enum).
    • domain / subtopic: primary_domain is a plain str with NO CV validation (CLASSIFICATION_PROMPT says “short snake_case domain name e.g. security, compliance…”); secondary_classifications likewise free-text. There is no taxonomy_domains / taxonomy_subtopics enforcement on this path at all.
    • This is the path flow.py uses (mount_table_target for content_items) — i.e. the T7 re-ingest classifier.

Asymmetry (integration-correctness gap). The canonical re-ingest classifier (cocoindex, #3) applies the weakest taxonomy constraint of the three: it hard-checks content_type against a snapshot, but leaves primary_domain/primary_subtopic entirely unconstrained free-text — whereas the app-side TS classifier (#1) and the legacy Python CLI (#2) both build/validate domains+subtopics against the live DB taxonomy. Re-ingesting the corpus through cocoindex will therefore produce primary_domain/primary_subtopic values with no guarantee of taxonomy-slug validity — exactly the “systematically wrong classification” risk the brief names, but located in the domain/subtopic dimension, not content_type.

4.3 CV source-of-truth chains (who regenerates what)

Section titled “4.3 CV source-of-truth chains (who regenerates what)”
LIVE DB (canonical authority)
├─ content_items.content_type CHECK (15) ─┐
├─ form_types table (8 rows) ─────────────┤ bun run sync:taxonomy
├─ taxonomy_domains / taxonomy_subtopics ──┘ (generate-taxonomy-snapshot.ts:
│ SELECT content_types, form_types, …)
│ │
│ ▼
│ scripts/tests/fixtures/taxonomy_snapshot.json
│ ├─ content_types[] → Python _VALID_CONTENT_TYPES
│ └─ form_types[] → Python _VALID_FORM_TYPES
└─ form_template_requirements.requirement_type CHECK (7) ── (no snapshot, no Python reader)
MARKDOWN REGISTER docs/ontology/*.md (29 files; "narrative" source-of-truth)
├─ 04-content-type.md ─ generate-content-type-values.ts ─→ CONTENT_TYPE_VALUES.generated (build-time, TS)
├─ 26-form-type.md (8) ── guarded by form-type-parity.test.ts vs snapshot
├─ 12-requirement-type.md (7) ── guarded by NOTHING (no parity test vs DB CHECK)
└─ loadOntologyCVs() ── consumed ONLY by markdown-parity.test.ts + the generated-tuple build script
PROMPTS (separate, hand-maintained — drift-prone)
├─ scripts/cocoindex_pipeline/prompts.py Q_A_FORM_PROMPT (11 — DRIFTED, OQ-63-1)
├─ scripts/cocoindex_pipeline/prompts.py CLASSIFICATION_PROMPT (content_type list — verify, OQ-63-2)
└─ docs/reference/classification-prompt.md (kb_pipeline path)

sync:taxonomy = generate-classification-prompt-taxonomy.ts && generate-taxonomy-snapshot.ts && sync-plugin-taxonomy.ts && build:plugin (package.json:31). It regenerates the snapshot and (separately) the classification-prompt taxonomy, but it does not regenerate Q_A_FORM_PROMPT (hence the drift) and does not touch requirement_type.


5. ID-9 (Astro Docs) reconciliation (brief item (b))

Section titled “5. ID-9 (Astro Docs) reconciliation (brief item (b))”

Finding: ID-9 is NOT the CV runtime loader, and was never going to be. ID-9 is the Astro+Starlight docs-site + Warp docubot auto-sync port. Its “ontology auto-sync” is a documentation-publishing concern, not a classifier-CV-loading concern:

  • ID-9’s OQ-4 was ratified (S58) to split the docs-site IA into five spaces, carving ontology/ out as its own top-level docs space (separating canonical vocabularies from product-functionality docs). The docubot/sync mechanism publishes docs/ontology/*.md into the rendered docs-site ontology/ space.
  • ID-9 {9.6} sync-content.ts is a build-time SUPPLEMENTARY path that mirrors docs/ markdown into docs-site/src/content/docs/ with deny-list + cross-space link rewriting. It reads markdown; it does not feed classifyContent or the cocoindex pipeline.
  • ID-9’s docubot writes/regenerates docs, not DB CHECK constraints, not the snapshot, not the prompts.

Reconciliation: the sequencing-doc §7 line “Astro was slated as the ontology auto-sync handler” refers to publishing the ontology markdown to the docs site, i.e. keeping the human-readable register fresh — it is orthogonal to the runtime CV-loading correctness ID-63 is about. The two meet at exactly one artefact: docs/ontology/*.md is both (i) the input to ID-9’s docs-site sync and (ii) the input to loadOntologyCVs() / generate-content-type-values.ts. There is no shared auto-sync that keeps the markdown register ↔ live DB ↔ snapshot ↔ prompts in lockstep; that lockstep is enforced only by the two Vitest parity tests (content_type + form_type) and is absent for requirement_type and for the prompts. See OQ-63-6 (should ID-63 add a docubot-driven or CI-driven parity guard, or stay test-only?).


6. What {52.5} (+ {52.6}) actually covered vs the residual (brief item (c))

Section titled “6. What {52.5} (+ {52.6}) actually covered vs the residual (brief item (c))”

{52.5} CV-loader re-baseline (status done, commits 504f288f + 68e1c0eb; integrated S275 45d26dc9/b6deb533). Scope was narrow and is fully accounted for:

  1. Re-baselined 4 ontology CV files (26-form-type.md, 30-procurement-vehicle.md, 31-procurement-vehicle-instance.md, 32-q-a-pair.md) from status: APPLIED-S{246,249}status: active, so loadOntologyCVs() stops throwing on the status-enum. core_seed_path preserved.
  2. Struck 3 stale claims in 26-form-type.md (table-not-in-schema; template_requirements.template_type CHECK; a Notes-section duplicate) that contradicted the now-live schema.
  3. Paired with {52.5a} (Layer-5 KG-entity schema relaxation, OQ-52-WAVE-1-A Option 3) which let 32-q-a-pair.md validate (no baseline_values; 3 declarative keys).

Result: markdown-parity.test.ts went 6 → 9 passing; loadOntologyCVs() returns 29 records.

{52.6} form_type triple-source lockstep (status done, commit d9fd1d9d/20fb7788). Added form_types[] to the snapshot (regenerated from live form_types table by sync:taxonomy), replaced the Python Literal[...] with str + snapshot-backed field_validator, and added form-type-parity.test.ts (markdown ↔ snapshot). 12/12 ontology tests now green.

What {52.5}/{52.6} did NOT cover (the residual — see §7):

  • The Q_A_FORM_PROMPT drift ({52.6} Executor explicitly flagged it as out-of-scope OQ-52-PROMPT-DRIFT; not fixed).
  • The cocoindex CLASSIFICATION_PROMPT content_type-list freshness vs the 15-value snapshot (unverified — OQ-63-2).
  • The primary_domain / primary_subtopic unconstrained-free-text gap on the cocoindex re-ingest path (§4.2 #3).
  • requirement_type having no parity test vs its DB CHECK (latent drift, OQ-63-4).
  • The three-classifier divergence (§4.2) — {52.x} touched only the cocoindex Python path’s form_type/content_type validators.

So {52.5} cleared the loader-crash (the symptom the sequencing-doc §7 line pointed at) and {52.6} locked form_type parity — but neither addressed end-to-end integration-correctness across the classify/cataloguing/matching trio, which is precisely ID-63’s remit.


7. Residual integration-correctness gap BEYOND ID-149 + ID-173 (brief item (d))

Section titled “7. Residual integration-correctness gap BEYOND ID-149 + ID-173 (brief item (d))”

7.1 ID-149 and ID-173 are now effectively RESOLVED by {52.5}/{52.6} (reconciliation)

Section titled “7.1 ID-149 and ID-173 are now effectively RESOLVED by {52.5}/{52.6} (reconciliation)”
  • ID-149 (“markdown-parity.test.ts full-file failure … likely ontology source-of-truth drift”) — the full-file failure was the loadOntologyCVs() status-enum throw + schema-shape violations. {52.5} + {52.5a} cleared exactly this; the test is GREEN (12/12). ID-149 is a stale backlog entry describing a now-passing test. Recommend the Orchestrator route ID-149 to the Curator for close-as-resolved verification (NOT a Planner action).
  • ID-173 (“26-form-type.md … schema validation failure”) — {52.5} re-baselined 26-form-type.md status + struck stale claims; {52.6} aligned its baseline_values to the live 8-key form_types. The file validates. ID-173 is likewise effectively resolved. Same routing recommendation.

Caveat: these are test-green resolutions. ID-63’s whole premise is that test-green ≠ integration-correct. So “ID-149/173 resolved” means “the named tests pass”, not “CV integration is correct end-to-end”. §7.2 is the real residual.

7.2 The residual gap (the substance of ID-63)

Section titled “7.2 The residual gap (the substance of ID-63)”
#Residual gapStage affectedSeverityEvidence
R1Q_A_FORM_PROMPT enumerates 11 form_types (3 invalid); validator rejects framework/dps/gcloud → ingest-time q_a_form cataloguing failures for those procurement formsPath-A cataloguing / Path-B form-templateHIGH / blocker§3 finding; prompts.py:104 vs extraction.py:185
R2test_cocoindex_prompts.py:113 asserts the legacy 11 set is present (GREEN test encoding a drifted contract)test integrityHIGH§3; test-philosophy violation
R3cocoindex re-ingest classifier leaves primary_domain/primary_subtopic unconstrained free-text (no taxonomy_domains/subtopics enforcement) → drift-prone domain/subtopic on full re-ingestPath-A classify (T7)HIGH§4.2 #3; CLASSIFICATION_PROMPT + ClassificationExtraction
R4requirement_type markdown CV (7) has no parity test vs its DB CHECK (7) — they happen to match now, but nothing guards future driftPath-C / T10MEDIUM (latent)§3; markdown-parity.test.ts SNAPSHOT_KEY_BY_CV_NAME omits it
R5THREE classifier implementations (TS app / kb_pipeline CLI / cocoindex) with three different CV wirings — no single source-of-truth contract; which is canonical for re-ingest, and are the others retired?architectureMEDIUM§4.2
R6markdown-parity.test.ts DB-CHECK-parity case does not cover form_type: 26-form-type.md carries editable_via: admin_ui, so it is excluded by the editable_via === 'database_migration' filter (markdown-parity.test.ts:84) before the SNAPSHOT_KEY_BY_CV_NAME lookup is even reached. form_type parity is instead covered by the separate form-type-parity.test.ts (markdown ↔ snapshot) — fine in aggregate, but the “every database_migration CV matches the live DB CHECK both ways” claim is narrower than it reads, and form_type’s editable_via: admin_ui (vs the live form_types table which is database-migration-managed) is itself a possible mis-classification worth a glance.test integrityLOW§2 markdown-parity.test.ts:84,89-96; 26-form-type.md:6
R7scripts/ontology-sync/parse-flow.py (a second CV-frontmatter parser/validator) duplicates the loader’s job in Python — is it live, and does it agree with OntologyCVSchema? Divergent validators = silent drift surface.architectureLOW§2.7

R1–R3 are the blocker-grade residual that gates T7 re-ingest. R1+R2 are a tightly-coupled fix (prompt + test). R3 is the larger design question (does the cocoindex path need domain/subtopic CV enforcement parity with the TS path, or is free-text domain acceptable for v1 re-ingest?). These are the items {63.2} PRODUCT must turn into testable invariants.


8. Re-ingest RISK if CV is parity-drifted at ingest time (brief item (e))

Section titled “8. Re-ingest RISK if CV is parity-drifted at ingest time (brief item (e))”

The canonical re-ingest path is cocoindex flow.pyextraction.py (it owns the content_items / q_a_extractions / source_documents writes via mount_table_target). Risk decomposition for a full-corpus T7 re-ingest at the current drift state:

  1. content_type (15) — LOW risk. Hard-validated against the snapshot; snapshot ↔ markdown ↔ DB are in parity (parity test green). A drifted snapshot would be high-risk, but it is regenerated from the live table by sync:taxonomy and currently matches.
  2. form_type (8) on Mode-1 q_a_form — HIGH risk (R1). Forms whose true type is framework/DPS/G-Cloud will be prompted toward framework/dps/gcloud, then rejected by the validator (invalid_enum) → those Q&A-form extractions are lost / error-classed across the whole corpus of such forms. Systematic, type-correlated data loss.
  3. primary_domain / primary_subtopic — HIGH risk (R3). Unconstrained free-text on the cocoindex path means a full re-ingest can populate content_items.primary_domain/primary_subtopic with values that do not match any live taxonomy_domains/taxonomy_subtopics slug. Downstream taxonomy-faceted browse / filters / coverage rely on those slugs; a corpus-wide re-ingest could systematically mis-slug the domain dimension. This is the closest match to the brief’s “systematically wrong classification across the whole corpus”.
  4. requirement_type — NIL risk at ingest. Not in the ingest write path (Path-C/T10, unbuilt). Risk deferred to when Path-C ships.

Net: re-ingesting now would (a) lose framework/DPS/G-Cloud q_a_form extractions and (b) produce taxonomy-unvalidated domain/subtopic slugs corpus-wide. Both are pre-re-ingest blockers consistent with the ID-64 readiness gate-set framing. content_type itself is safe.


These are decisions the Orchestrator/Liam must make before {63.2} PRODUCT can encode invariants. AskUserQuestion is not used (headless). Each carries a recommended default; none is ratified here.

  • OQ-63-1 — Q_A_FORM_PROMPT drift fix (R1+R2), blocker. The prompt enumerates 11 form_types (3 — framework, dps, gcloud — rejected by the {52.6} validator); test_cocoindex_prompts.py:113 locks the legacy 11 with a passing test. Fix in ID-63, or carve to a fast remediation Subtask? Recommended default: fix inside ID-63 as a first implementation slice — regenerate Q_A_FORM_PROMPT form_type list from the snapshot (single source) AND rewrite test_q_a_form_enumerates_form_types to load the expected set from the snapshot (not a hardcoded 11). This is the literal “test-green-but-wrong” defect ID-63 exists to fix. Sub-question OQ-63-1a: are framework/dps/gcloud genuinely dropped from the form_type vocabulary (the S275 3-tier split intent), or should they be re-added to form_types + 26-form-type.md + snapshot? The S275 {52.6} journal implies dropped (canonical = 8); confirm so the fix direction (shrink prompt vs grow CV) is unambiguous.

  • OQ-63-2 — cocoindex CLASSIFICATION_PROMPT content_type freshness. Q_A_FORM_PROMPT drifted; has CLASSIFICATION_PROMPT’s content_type enumeration (prompts.py:55) drifted from the 15-value snapshot too? {63.1} confirmed the prompt lists canonical values and the validator hard-checks the snapshot, but did not byte-diff the prompt’s listed set vs the snapshot’s 15. Recommended default: add a Vitest/pytest guard asserting the prompt’s enumerated content_type set == snapshot content_types (same shape as the proposed OQ-63-1 form_type guard), and fix any delta found. Treat as in-scope for ID-63.

  • OQ-63-3 — primary_domain / primary_subtopic enforcement on the cocoindex re-ingest path (R3), blocker-grade. The cocoindex classifier leaves domain/subtopic as unvalidated free-text, unlike the TS + kb_pipeline paths. Should the cocoindex path enforce (hard-reject or soft-warn) domain/subtopic against live taxonomy_domains/taxonomy_subtopics before T7 re-ingest? Recommended default: yes — add validation parity with the existing paths. Soft-warn vs hard-reject is a PRODUCT-MAY-PROPOSE decision; kb_pipeline precedent is warn-not-reject (classify.py:918), which argues for soft-warn + a coverage metric rather than dropping rows. Liam’s call at the PRODUCT gate.

  • OQ-63-4 — requirement_type parity guard (R4). The markdown CV (7) and DB CHECK (7) match but nothing tests it. Add a parity test (markdown 12-requirement-type.md ↔ DB CHECK) now, even though Path-C is unbuilt? Recommended default: yes — cheap, closes a latent drift surface before Path-C/T10 land. Wire requirement_type into a snapshot key + extend the parity test (mirrors the content_type/form_type pattern).

  • OQ-63-5 — three-classifier reconciliation (R5). lib/ai/classify.ts (app), scripts/kb_pipeline/classify.py (legacy CLI), scripts/cocoindex_pipeline/extraction.py (canonical). Which is canonical for the corpus, is kb_pipeline/classify.py retired, and should ID-63 assert a single CV-contract across whichever survive — or is divergence (app live-DB vs pipeline snapshot) intentional and acceptable? Recommended default: declare cocoindex canonical for pipeline/re-ingest, app-TS canonical for interactive/UI ingest, and treat kb_pipeline/classify.py as legacy (confirm retirement status separately). ID-63 scopes the CV-correctness contract to the cocoindex path (the re-ingest blocker); the app-TS path is out of scope unless Liam folds it in. Flag whether a kb_pipeline/classify.py retirement belongs in ID-63 or its own backlog item.

  • OQ-63-6 — lockstep enforcement mechanism + ID-9 relationship. Currently lockstep is Vitest-test-only and absent for prompts + requirement_type. Should ID-63 add a CI-driven or docubot-driven (ID-9) auto-sync/guard that keeps markdown register ↔ live DB ↔ snapshot ↔ prompts aligned, or stay with expanded parity tests? Recommended default: expand the test/CI guard set (cheap, in-repo, blocks PRs) rather than couple to ID-9’s docubot (which publishes docs, not DB/prompts). Keep ID-9 orthogonal. Revisit a docubot-driven markdown-from-DB regeneration as a later enhancement, not an ID-63 blocker.

  • OQ-63-7 — scripts/ontology-sync/parse-flow.py status (R7). A second Python CV-frontmatter parser/validator exists alongside the TS loadOntologyCVs(). Is it live (and on which flow), and does its validation agree with OntologyCVSchema? Divergent validators are a silent-drift surface. Recommended default: {63.2}/{63.3} investigate during PRODUCT/TECH; if dead, flag for Knip/Curator removal; if live, assert validator agreement. Not a blocker but record the disposition.

  • OQ-63-8 — ID-63 deliverable shape. Is ID-63 a verification-only Task (audit + parity-guard tests + the R1/R2 prompt fix, gating T7), or does it also own the larger R3 domain/subtopic enforcement design (which borders T7/ID-45 spec territory)? Recommended default: ID-63 owns the blocker-clearing slice (R1, R2, R4, OQ-63-2 guard) + an explicit R3/R5 finding hand-off to the T7/ID-45 re-ingest spec, rather than absorbing the full re-ingest classifier redesign. Keeps ID-63 bounded as a pre-re-ingest gate. Liam/Orchestrator to confirm scope at the PRODUCT gate.


10. Recommendations digest for {63.2} PRODUCT (no ratification — direction only)

Section titled “10. Recommendations digest for {63.2} PRODUCT (no ratification — direction only)”
  1. Frame the three CVs by stage, not as a single “Path-A feed” (§4.1) — correct the brief’s framing: requirement_type is Path-C/T10, not ingest.
  2. Make R1 + R2 the first, blocker-grade invariantQ_A_FORM_PROMPT form_type list == snapshot (8), and the prompts test loads its expectation from the snapshot. Pending OQ-63-1a direction (shrink vs grow).
  3. Encode the cocoindex re-ingest classifier as the gated surface (§4.2 #3, §8) — content_type safe; form_type (R1) and domain/subtopic (R3) are the corpus-wide risks. R3’s enforce-vs-warn is PRODUCT-MAY-PROPOSE.
  4. Add parity guards for requirement_type (OQ-63-4) and CLASSIFICATION_PROMPT content_type (OQ-63-2) as testable invariants.
  5. Keep ID-9 orthogonal (§5) — no PRODUCT invariant should couple CV-loading correctness to the docs-site/docubot path.
  6. Route ID-149 + ID-173 to the Curator as resolved-by-{52.5}/{52.6} (§7.1) — Orchestrator action, not a PRODUCT invariant; but note test-green ≠ integration-correct.
  7. Bound the Task scope (OQ-63-8) — verification + R1/R2 fix + parity guards in ID-63; hand R3/R5 redesign to the T7/ID-45 spec.

KH quality bars that PRODUCT/TECH must carry forward: UK English; sb()/tryQuery() from @/lib/supabase/safe for any new DB read; no barrel re-exports; tests verify behaviour not implementation (docs/reference/test-philosophy.md — directly relevant to R2); DDL via CLI only if any CHECK/table change is proposed; SET search_path = public, extensions on any new PL/pgSQL.