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.
1. Brief (verbatim framing)
Section titled “1. Brief (verbatim framing)”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.tsFunction: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.mdFile:docs/reference/backlog/149.md ; File:docs/reference/backlog/173.mdprocesses: [] 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:classifyFixtureItemFunction: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_boundedMethod:scripts/tests/test_cocoindex_prompts.py:TestPromptsEnumeratesEnums.test_q_a_form_enumerates_form_types2.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 implementationConst:lib/mcp/tools/ai.ts:classifyContent (0.50) ← MCP tool wrapperConst:lib/mcp/tools/content.ts:classifyContent (0.50) ← MCP tool wrapperConst:lib/mcp/tools/governance.ts:classifyContent (0.50)← MCP tool wrapperDirect 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— readscontent_types+form_typesfromscripts/tests/fixtures/taxonomy_snapshot.json; does NOT readrequirement_type.scripts/kb_pipeline/classify.py— a third classifier (classify()line 721,_validate_classification()line 917 “Warns but doesn’t reject”); reads prompt fromdocs/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_typegrep acrosslib/ scripts/ app/ supabase/migrations/: consumers arelib/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.
| Date | Pin | Symbol / artefact checked | Result |
|---|---|---|---|
| 29/05/2026 | cocoindex[postgres]==1.0.3 | scripts/cocoindex_pipeline/extraction.py imports + @coco.fn extractors | PRESENT (pipeline is built + tested; ExtractByLlm already known-ABSENT and avoided per {52.1}) |
| 29/05/2026 | anthropic==0.79.0 | ANTHROPIC_MODEL = "claude-opus-4-6" (extraction.py:65) | PRESENT — single source mirrors lib/anthropic.ts |
| 29/05/2026 | pydantic==2.12.5 | FormMetadata, 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/2026 | tenacity==9.1.4 | _anthropic_retry (AsyncRetrying, stop_after_attempt, wait_exponential) | PRESENT |
| 29/05/2026 | gray-matter ^4.0.3 | lib/ontology/loader.ts frontmatter parse | PRESENT |
| 29/05/2026 | zod ^4.4.3 | lib/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.tsboth 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); norequirement_typeskey present. - Markdown CV
docs/ontology/26-form-type.mdbaseline_values= 8 keys (matches snapshot —{52.6}lockstep holds). - Markdown CV
docs/ontology/12-requirement-type.mdbaseline_values= 7 keys (policy, statement, evidence, data, narrative, declaration, reference). - Live DB
form_template_requirements.requirement_typecarries a CHECK constraint (form_template_requirements_requirement_type_check, migration20260416102457:4167, renamed20260520120828:242) enforcing exactly those 7 values — markdown ↔ DB CHECK are currently in parity, but no parity TEST guards them (unlikecontent_type).
SIGNATURE_DRIFT / BEHAVIOUR_DRIFT finding (escalated as OQ-63-1, blocking):
scripts/cocoindex_pipeline/prompts.py:104Q_A_FORM_PROMPTenumerates 11 legacy form_type values:bid, rfp, pqq, itt, tender, framework, dps, gcloud, checklist, questionnaire, sales_proposal_template.- The canonical set (snapshot + live
form_typestable +26-form-type.md) is 8 — the 3 valuesframework, dps, gcloudare absent from canonical. {52.6}replacedFormMetadata.form_type: Literal[...]withform_type: str+ afield_validatorthat rejects any value not in the 8-key snapshot (extraction.py:179-190).- Net effect: the prompt instructs the LLM to emit
framework/dps/gcloudfor procurement forms; the validator then rejects those rows asinvalid_enum. This is an active ingest-time form-cataloguing defect — Mode-1q_a_formextractions for framework/DPS/G-Cloud procurement forms will fail validation. (Previously surfaced by the{52.6}Executor asOQ-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 — adocs/reference/test-philosophy.mdviolation (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”| CV | Owning stage (§2.3) | Built? | Runtime CV source-of-truth | Write target |
|---|---|---|---|---|
taxonomy (content_type + domain + subtopic) | Path-A classify | BUILT | content_type: snapshot (Python) / hardcoded tool enum (TS). domain+subtopic: see §4.3 | content_items |
form_types | Path-A q_a_form cataloguing + form-template write (Path B) | BUILT | snapshot form_types[] (Python _VALID_FORM_TYPES) | q_a_extractions / form_templates.form_type |
requirement_type | Path-C AI-cataloguing skill + T10 matching | UNBUILT (Path-C = {52.14} pending) | DB CHECK (7 values) + markdown 12-requirement-type.md; no ingest consumer | form_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:
-
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.
- content_type: hardcoded enum in the Anthropic tool JSON schema (
-
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).
- Reads prompt from
-
scripts/cocoindex_pipeline/extraction.py(newer cocoindex Path-A; theflow.pywriter ofcontent_items).- content_type:
ClassificationExtraction._validate_content_typerejects anything not in snapshotcontent_types(hard reject →invalid_enum). - domain / subtopic:
primary_domainis a plainstrwith NO CV validation (CLASSIFICATION_PROMPTsays “short snake_case domain name e.g. security, compliance…”);secondary_classificationslikewise free-text. There is notaxonomy_domains/taxonomy_subtopicsenforcement on this path at all. - This is the path
flow.pyuses (mount_table_targetforcontent_items) — i.e. the T7 re-ingest classifier.
- content_type:
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 publishesdocs/ontology/*.mdinto the rendered docs-siteontology/space. - ID-9
{9.6}sync-content.ts is a build-time SUPPLEMENTARY path that mirrorsdocs/markdown intodocs-site/src/content/docs/with deny-list + cross-space link rewriting. It reads markdown; it does not feedclassifyContentor 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:
- Re-baselined 4 ontology CV files (
26-form-type.md,30-procurement-vehicle.md,31-procurement-vehicle-instance.md,32-q-a-pair.md) fromstatus: APPLIED-S{246,249}→status: active, soloadOntologyCVs()stops throwing on the status-enum.core_seed_pathpreserved. - Struck 3 stale claims in
26-form-type.md(table-not-in-schema;template_requirements.template_typeCHECK; a Notes-section duplicate) that contradicted the now-live schema. - Paired with
{52.5a}(Layer-5 KG-entity schema relaxation, OQ-52-WAVE-1-A Option 3) which let32-q-a-pair.mdvalidate (nobaseline_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_PROMPTdrift ({52.6}Executor explicitly flagged it as out-of-scopeOQ-52-PROMPT-DRIFT; not fixed). - The cocoindex
CLASSIFICATION_PROMPTcontent_type-list freshness vs the 15-value snapshot (unverified — OQ-63-2). - The
primary_domain/primary_subtopicunconstrained-free-text gap on the cocoindex re-ingest path (§4.2 #3). requirement_typehaving 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’sform_type/content_typevalidators.
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.tsfull-file failure … likely ontology source-of-truth drift”) — the full-file failure was theloadOntologyCVs()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-baselined26-form-type.mdstatus + struck stale claims;{52.6}aligned itsbaseline_valuesto the live 8-keyform_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 gap | Stage affected | Severity | Evidence |
|---|---|---|---|---|
| R1 | Q_A_FORM_PROMPT enumerates 11 form_types (3 invalid); validator rejects framework/dps/gcloud → ingest-time q_a_form cataloguing failures for those procurement forms | Path-A cataloguing / Path-B form-template | HIGH / blocker | §3 finding; prompts.py:104 vs extraction.py:185 |
| R2 | test_cocoindex_prompts.py:113 asserts the legacy 11 set is present (GREEN test encoding a drifted contract) | test integrity | HIGH | §3; test-philosophy violation |
| R3 | cocoindex re-ingest classifier leaves primary_domain/primary_subtopic unconstrained free-text (no taxonomy_domains/subtopics enforcement) → drift-prone domain/subtopic on full re-ingest | Path-A classify (T7) | HIGH | §4.2 #3; CLASSIFICATION_PROMPT + ClassificationExtraction |
| R4 | requirement_type markdown CV (7) has no parity test vs its DB CHECK (7) — they happen to match now, but nothing guards future drift | Path-C / T10 | MEDIUM (latent) | §3; markdown-parity.test.ts SNAPSHOT_KEY_BY_CV_NAME omits it |
| R5 | THREE 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? | architecture | MEDIUM | §4.2 |
| R6 | markdown-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 integrity | LOW | §2 markdown-parity.test.ts:84,89-96; 26-form-type.md:6 |
| R7 | scripts/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. | architecture | LOW | §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.py → extraction.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:
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 bysync:taxonomyand currently matches.form_type(8) on Mode-1q_a_form— HIGH risk (R1). Forms whose true type is framework/DPS/G-Cloud will be prompted towardframework/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.primary_domain/primary_subtopic— HIGH risk (R3). Unconstrained free-text on the cocoindex path means a full re-ingest can populatecontent_items.primary_domain/primary_subtopicwith values that do not match any livetaxonomy_domains/taxonomy_subtopicsslug. 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”.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.
9. Open Questions — OQ-63-N
Section titled “9. Open Questions — OQ-63-N”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_PROMPTdrift fix (R1+R2), blocker. The prompt enumerates 11 form_types (3 —framework, dps, gcloud— rejected by the{52.6}validator);test_cocoindex_prompts.py:113locks 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 — regenerateQ_A_FORM_PROMPTform_type list from the snapshot (single source) AND rewritetest_q_a_form_enumerates_form_typesto 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: areframework/dps/gcloudgenuinely dropped from the form_type vocabulary (the S275 3-tier split intent), or should they be re-added toform_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_PROMPTcontent_type freshness.Q_A_FORM_PROMPTdrifted; hasCLASSIFICATION_PROMPT’scontent_typeenumeration (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 == snapshotcontent_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_subtopicenforcement 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 livetaxonomy_domains/taxonomy_subtopicsbefore 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_typeparity guard (R4). The markdown CV (7) and DB CHECK (7) match but nothing tests it. Add a parity test (markdown12-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. Wirerequirement_typeinto 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, iskb_pipeline/classify.pyretired, 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 treatkb_pipeline/classify.pyas 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 akb_pipeline/classify.pyretirement 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.pystatus (R7). A second Python CV-frontmatter parser/validator exists alongside the TSloadOntologyCVs(). Is it live (and on which flow), and does its validation agree withOntologyCVSchema? 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)”- Frame the three CVs by stage, not as a single “Path-A feed” (§4.1) — correct the brief’s framing:
requirement_typeis Path-C/T10, not ingest. - Make R1 + R2 the first, blocker-grade invariant —
Q_A_FORM_PROMPTform_type list == snapshot (8), and the prompts test loads its expectation from the snapshot. Pending OQ-63-1a direction (shrink vs grow). - 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.
- Add parity guards for
requirement_type(OQ-63-4) andCLASSIFICATION_PROMPTcontent_type (OQ-63-2) as testable invariants. - Keep ID-9 orthogonal (§5) — no PRODUCT invariant should couple CV-loading correctness to the docs-site/docubot path.
- 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. - 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.