Skip to content

TECH — OKF ontology three-layer pass + semantic linter (allowed_types/allowed_relations register)

TECH — OKF ontology three-layer pass + semantic linter

Section titled “TECH — OKF ontology three-layer pass + semantic linter”

Task: ID-133 (Task D in the OKF v3 programme). Tier = TECH+PLAN — reports/okf-record-model-lineage/okf-record-model-v3.md §6 is the settled product intent (owner round-3 ratified 28/06/2026); this artefact is the technical design; {133.2} PLAN decomposes it. Depends (coordination, not hard-blocking): ID-131 (Task B, L-records refactor) for the DB homes the column CVs re-home onto. ID-133’s doc-side items unblock ID-132 (Task A, producer). Gates: the rewritten register + the two new semantic linters gate BOTH ID-131 extraction writes (the Pydantic gate) AND ID-132 concept writes (the new concept-frontmatter validator). Audience: Product owner (non-developer) + Executors. Plain-English first; file:line evidence inline. UK English throughout.


The KH ontology register (33 controlled-vocabulary markdown files + a Zod schema + a runtime Pydantic gate + parity guards) deeply encodes the content_items god-table that ID-131 eliminates. ID-133 re-aligns the ontology to the OKF three-layer model (L-raw / L-records / L-concepts, v3 §1) and promotes the closed entity/relationship vocabularies into a first-class allowed_types / allowed_relations register with per-value core-vs-client provenance — the artefact the owner asked for, which today does not exist. It is a nine-item pass (v3 §6). The pass changes what is gated and where the extraction stamp points, NOT the gate’s enforcement semantics: HARD-reject for closed enums (content_type and successors), SOFT-WARN for open dimensions (domain/subtopic) is preserved exactly.

Code-intelligence orientation (verbatim — Checker: confirm this ran)

Section titled “Code-intelligence orientation (verbatim — Checker: confirm this ran)”

Per the binding rule (.gitnexus/CLAUDE.md, .ast-dataflow/CLAUDE.md), outputs are cited verbatim, not paraphrased.

  • gitnexus_query({query: 'ontology controlled vocabulary loader parity guard entity_type relationship', repo:'canonical'}) returned (definitions, abridged to the load-bearing hits):
    • Function:scripts/cocoindex_pipeline/extraction.py:extract_relationships (1116–1144)
    • Class:scripts/cocoindex_pipeline/extraction.py:RelationshipExtraction (398–435)
    • Class:scripts/tests/test_cocoindex_extraction.py:TestEntityTypeParity (880–908) + Method:…TestEntityTypeParity.test_python_literal_matches_ts_constant (883–908)
    • Method:scripts/tests/test_cocoindex_prompts.py:TestPromptsEnumeratesEnums.test_entity_mention_enumerates_12_entity_types (235–254)
      • …test_relationship_enumerates_10_relationship_types (256–274)
    • Function:scripts/cocoindex_pipeline/extraction.py:_load_canonical_form_types (99–129)
  • gitnexus_context({name: 'loadOntologyCVs', repo:'canonical'})Function:lib/ontology/loader.ts:loadOntologyCVs (42–75); incoming: {} (no production caller — only the parity test imports it); outgoing.accesses: [issues]; processes: []. Confirms the loader is a leaf with no runtime consumer (item 7 evidence).
  • gitnexus_context({name: 'OntologyCVSchema', repo:'canonical'}) → ambiguous: Function: and Const: both at lib/ontology/schemas.ts:104 (the superRefine wrapper + its exported const). The symbol the spec extends.
  • gitnexus_impact({target: '_ExtractionStamp', direction:'upstream', repo:'canonical', includeTests:true})risk: HIGH, impactedCount: 46, summary.direct: 16, byDepthCounts {1:16, 2:19, 3:11}, affected_modules: [{name:'Cocoindex_pipeline', hits:3, impact:'direct'}]. This is the blast radius behind item 8; the CODE re-parent is owned by ID-131 (see below), ID-133 owns only the ontology-doc side.

ccc fallback was not required — gitnexus resolved every symbol the spec mandates. Not a greenfield surface.

Current state — the four artefacts and the repo-split (the drift hazard)

Section titled “Current state — the four artefacts and the repo-split (the drift hazard)”

The register is NOT a single source of truth today; it is one human source + three derived artefacts + a dead bridge:

  1. Human source of truth — the register markdown (PRIVATE, since ID-68.27): ${KH_PRIVATE_DOCS_DIR}/src/content/docs/ontology/*.md (33 CV files + README.md + index.md). The boundary rule (README.md:74–87, the Q1/Q1a/Q1b/Q2/Q3/Q4 decision procedure) routes client-canonical knowledge → content_items (README.md:76) and forbids new content_type values for routing (README.md:87). 04-content-type.md:63 defines content_type as “the shapes a content_items row can take” (15 values; capability already ratified-S237 to retire to 14). 03-layer-vocabulary.md is the audience axis (sales_brief/bid_detail/company_reference/research), explicitly NOT the OKF layer model (README provenance rollup: 22 core / 9 hybrid / 2 client).
  2. Runtime gate — the Pydantic semantic linter in scripts/cocoindex_pipeline/extraction.py (the LIVE linter, NOT the markdown register):
    • HARD-reject content_type via _validate_content_type field-validator (470–481), reading _VALID_CONTENT_TYPES (97) loaded from taxonomy_snapshot.json (_load_canonical_content_types, 84–94).
    • HARD-reject form_type (_validate_form_type, 304–315) + form_format Literal (285) + the entity_type Literal (378–391, 12 values) + the relationship Literal (423–434, 10 values).
    • SOFT-WARN primary_domain/primary_subtopic/secondary_classifications via _surface_out_of_taxonomy_classification model-validator (483–532): bumps a counter + logs, never raises, always returns self (Inv-6/7). This is the enforcement-semantics line item 8/9 must not move.
  3. DB-derived runtime listscripts/tests/fixtures/taxonomy_snapshot.json (regenerated from the live DB by bun run sync:taxonomy); holds content_types (15), form_types (8), domains, subtopics.
  4. Public lockstep fixture + codegen__tests__/fixtures/ontology/ontology-cv-baselines.json (the frozen 4-CV baseline: content_type, platform, requirement_type, form_type). scripts/generate-content-type-values.ts reads the fixture (NOT the now-removed docs/ontology/) and writes lib/ontology/content-type-values.generated.ts; lib/ontology/content-type-registry.ts re-exports it; wired predev/prebuild (package.json:13–18). lib/validation/schemas.ts re-exports the tuple as VALID_CONTENT_TYPES.
  5. The dead bridgelib/ontology/loader.ts:29 ONTOLOGY_DIR = join(REPO_ROOT, 'docs', 'ontology') points at a directory that no longer exists; loadOntologyCVs (43–76) fail-louds if it is absent (46–51). The parity test __tests__/lib/ontology/markdown-parity.test.ts inverts this into a privacy tripwire: it asserts existsSync(ONTOLOGY_DIR) === false AND loadOntologyCVs() throws. So the loader has no production caller (confirmed: loadOntologyCVs incoming: {}) and exists only as a tripwire.

Parity guards (the lockstep mechanism):

  • __tests__/lib/ontology/markdown-parity.test.ts — asserts the public fixture, the generated tuple, and taxonomy_snapshot.json agree (the dead-loader tripwire lives here too).
  • scripts/tests/test_cocoindex_extraction.py:881–909 TestEntityTypeParity.test_python_literal_matches_ts_constant — asserts the Python entity_type Literal == TS VALID_ENTITY_TYPES (lib/validation/schemas.ts, extracted by _extract_ts_string_array, 861). There is NO equivalent register source in this guard, and NO Python↔TS parity test for relationship — relationship parity is only indirectly covered by the prompt-enumeration test (test_cocoindex_prompts.py:256–274). The TS relationship vocabulary lives in the ExtractedRelationship/ExtractedEntity unions in lib/ai/classify.ts:653–666.

The stale wp6 spec: ${KH_PRIVATE_DOCS_DIR}/src/content/docs/specs/wp6-ontology-harness/TECH.md §3 (29–69) still describes “29 CV files”, the live docs/ontology/*.md register, and a live runtime loader — all three superseded (33 CVs; private register; dead loader).

The Layer-5 framing to correct: …/phase-0-investigation/phase-b-prerequisite-1-onthology-pipeline-feedback-investigation.md §5 (481–482) records BOTH source_documents AND content_items as “Layer 5 ontology class”, with content_items = the “Knowledge artefact”. Eliminating content_items retires the artefact but the Layer-5 framing must move to the typed records + L-concepts.

What ID-131 owns (NOT ID-133 — the boundary)

Section titled “What ID-131 owns (NOT ID-133 — the boundary)”

The DB column moves and the extraction.py CODE re-parent are ID-131’s; ID-133 documents them in the register and adjusts the CVs to match the new homes. From id-131/TECH.md:

  • M3 id131_sd_classification_cols (290): ADD ~25–30 classification + hot cols to source_documents (content_type, primary_domain, primary_subtopic, secondary_domain, secondary_subtopic, ai_keywords text[], summary, suggested_title, classified_at, classification_confidence, classification_reasoning, classification_model, captured_date, summary_data jsonb, updated_by, updated_at, publication_status NOT NULL DEFAULT 'published' INLINE).
  • M1a id131_record_lifecycle_facet (287) + GOV-FACET hot/cold split: the freshness/lifecycle/ governance family → the record_lifecycle facet (NOT source_documents); publication_status/ valid_*/superseded_by stay INLINE.
  • M2 id131_extract_reparent (289): re-parent content_chunks/entity_mentions/entity_relationships/ classification_disputes/q_a_extractions content_item_idsource_document_id, in lockstep with the extraction stamp extraction.py:261–264 (_ExtractionStamp, HIGH). This is the code re-parent for item 8. ID-133 does not touch extraction.py:261–264; it documents it.
  • M4b CITE-EXT — citations extended to {q_a_pair, reference_item, source_document, concept}.

The two cross-cutting decisions (made here)

Section titled “The two cross-cutting decisions (made here)”

Decision A — single source of truth + lockstep (item 7, resolves v3 risk 8)

Section titled “Decision A — single source of truth + lockstep (item 7, resolves v3 risk 8)”

DECISION: the private docs-site register is THE single human source of truth; every other artefact is a DERIVED mirror kept in lockstep by parity guards. Retire the dead loader; expand the public fixture into the complete public mirror of the runtime-relevant CVs; document the derive-and-lockstep chain.

Rationale + rejected alternative:

  • The owner-flagged drift hazard is the triple (dead loader + partial 4-CV fixture + taxonomy_snapshot.json) with no declared authority. The fix is to name ONE authority and make everything else derived, not to add a fourth reader.

  • REJECTED: repoint loader.ts at the docs-site register via KH_PRIVATE_DOCS_DIR. The codegen (generate-content-type-values.ts) already solved the client-bundle node:fs problem by reading the public frozen fixture instead of the loader; the loader has zero production callers (gitnexus incoming: {}). Repointing it would (a) reintroduce a private-sibling node:fs read into the public repo’s server/test runtime, throwing locally whenever KH_PRIVATE_DOCS_DIR is unset, and (b) not change any runtime behaviour because nothing consumes the loader. It adds a reader without removing drift.

  • CHOSEN: retire loadOntologyCVs + ONTOLOGY_DIR (lib/ontology/loader.ts). Replace the dead-loader tripwire in markdown-parity.test.ts with a same-intent privacy tripwire that does not import the loader (assert the public docs/ontology/ dir is absent directly). The authority chain becomes explicit and is documented in README.md + the wp6 TECH refresh (item 9):

    RoleArtefactKept in lockstep by
    Human source of truth${KH_PRIVATE_DOCS_DIR}/…/ontology/*.md(authored by hand; docubot lane)
    Public mirror (CI-visible)__tests__/fixtures/ontology/ontology-cv-baselines.json (EXPANDED: + entity_type, + relationship)docs-site “parity-guard twin” (ID-68.27 follow-up)
    DB-derived runtime listscripts/tests/fixtures/taxonomy_snapshot.jsonbun run sync:taxonomy
    Build-time client tuplelib/ontology/content-type-values.generated.tsgenerate:content-type-values (predev/prebuild)
    Code Literals (KG CVs)extraction.py Literals + schemas.ts/classify.ts conststhe parity guards below

    Drift in any derived artefact fails a parity guard at build/CI. The register markdown is never read at runtime (correct — it is the human contract; the machine reads the snapshot/fixture/Literals).

This decision is a prerequisite for item 5 (promoting the KG vocabularies to the register adds a third source for entity_type/relationship — the fixture is where that third source lives publicly, and the parity guard is extended to bind all three).

Reframe — markdown register vs DB-backed register, for THESE CVs (owner round-4 pushback, E11/D6): the owner asked whether the register should instead be DB-backed (core) + client rather than retired. The answer is that a DB-backed core+client register ALREADY EXISTS and is authoritative for the genuinely client-extensible CVs — taxonomy_domains / taxonomy_subtopics / layer_vocabulary are DB tables carrying a provenance column + a recommend-then-accept workflow (recommended_by / accepted_at), with taxonomy_snapshot.json derived FROM them (the DB is the source of truth for the open dimensions). The markdown register is deliberately reserved for the closed/core CVs that are HARD-rejected and product-closed at v1 (content_type, entity_type, relationship). For BI-5’s named targets — entity_type (12) + relationship (10) — a DB register buys nothing now: they are closed at v1 (Decision B client_extensible: false) AND prompt-coupled (hardcoded Pydantic Literals mirrored into the extraction prompts), so a provenance-bearing table would only add a further lockstep source — the exact drift hazard Decision A collapses. Forward bridge: IF the product later makes them client-extensible (the Decision B research-deferred call), promote them to the SAME DB-backed provenance pattern as taxonomy_domains (ratified S235 Q-OQR1-11), NOT to the markdown register — Decision B’s optional per-value keys (provenance_model / client_extensible / editable_via) are exactly the columns that future table would carry, so they are the forward-compat bridge.

id-63 lineage (F3 — faithful continuation, not a fresh start): ID-133 BUILDS ON the original id-63 ontology-pipeline plans (phase-b-prerequisite-1-onthology-pipeline*.md). Decision A (markdown register authoritative) IS the original Q-OQR1-12 “auto-fresh register” vision; Decision B (per-value provenance) extends Q-OQR1-11; the entity_type / relationship CORE-closed posture and the HARD/SOFT enforcement split are id-63’s contributions carried forward unchanged. The ONE superseded thread is id-63’s original “auto-fresh register / ID-9 → Astro auto-sync” mechanism: it is replaced here by the hand-authored-contract + parity-guard + docubot lane model — the docubot lane (docs upkeep automated IN the docs-site repo) is the SUCCESSOR to the auto-fresh intent, and the id-63 → ID-9/Astro auto-sync linkage is explicitly SUPERSEDED/CLOSED. Recording this keeps the lineage readable as a continuation, not an abandonment.

Decision B — the allowed_types / allowed_relations register schema (item 5, the highest-value artefact)

Section titled “Decision B — the allowed_types / allowed_relations register schema (item 5, the highest-value artefact)”

DECISION: promote entity_type (12) and relationship (10) into two NEW register CVs — 34-entity-type.md (Layer 5, Ontology) and 35-relationship.md (Layer 6, Knowledge Graph) — with per-value provenance, by EXTENDING BaselineValueSchema (lib/ontology/schemas.ts:33–49) with three OPTIONAL per-value keys.

The register already carries CV-level provenance (provenance_model core/client/hybrid, client_extensible, editable_via) and per-value provenance (core/client/recommended). The owner’s ask (“allowed_types/allowed_relations with per-value core-vs-client provenance”) needs the core-vs-client distinction per value so a future client-defined entity type can be marked extensible while the 12 shipped types stay immutable. Concrete schema extension (backward-compatible — all keys optional, so the existing 33 CVs are unaffected and continue to validate):

// lib/ontology/schemas.ts — BaselineValueSchema (extended)
const BaselineValueSchema = z.object({
key: z.string().min(1).regex(/^([a-z][a-z0-9_-]*|TBD)$/, ''),
label: z.string().min(1),
provenance: z.enum(PROVENANCE_VALUES), // existing: core | client | recommended
definition: z.string().min(1).optional(), // existing
// NEW (ID-133) — per-value provenance for KG-ontology CVs. Optional ⇒ existing CVs unaffected.
provenance_model: z.enum(PROVENANCE_MODEL_VALUES).optional(), // core | client | hybrid
client_extensible: z.boolean().optional(),
editable_via: z.enum(EDITABLE_VIA_VALUES).optional(),
});

34-entity-type.md frontmatter: layer: 5, provenance_model: hybrid, client_extensible: true, editable_via: database_migration, status: active, baseline_values: = the 12 types each with provenance: core, provenance_model: core, client_extensible: false, editable_via: database_migration (closed at v1, client-extension research-deferred — mirrors content_type’s “closed for v1” posture). 35-relationship.md frontmatter: layer: 6, otherwise identical, baseline_values: = the 10 predicates.

Schema note: the existing OntologyCVSchema.superRefine (105–137) requires baseline_values for every non-Layer-5 CV and allows (does not require) them on Layer 5. 34-entity-type.md (Layer 5) carries baseline_values (allowed) and 35-relationship.md (Layer 6) carries them (required) — both validate under the current rule with no superRefine change. Only BaselineValueSchema is extended.

Per-value provenance is the spine of the §G extraction and the contract ID-132’s concept producer and ID-131’s extraction gate both read.


Proposed changes — one section per v3 §6 item (BI-1 … BI-9)

Section titled “Proposed changes — one section per v3 §6 item (BI-1 … BI-9)”

Items are numbered BI-1…BI-9 to match v3 §6’s nine-item pass, giving {133.2} PLAN and the Checker a one-to-one mapping. Doc-side items (BI-1, BI-2) author in parallel — they unblock ID-132. Homes-dependent items (BI-3, BI-4, BI-8) gate on ID-131’s M2/M3 decisions.

BI-1 — rewrite the README boundary rule (doc-side; unblocks ID-132)

Section titled “BI-1 — rewrite the README boundary rule (doc-side; unblocks ID-132)”

File: ontology/README.md:70–88 (“Where does new data live?”). Rewrite the Q1/Q1a/Q1b/Q2/Q3/Q4 procedure so client-canonical knowledge routes to {source_documents, q_a_pairs, reference_items} + L-concepts, NOT content_items + content_item_workspaces:

  • Q1a (client-canonical knowledge): no longer ”→ content_items”. Split by record type: a unit of usable knowledge is either a canonical answer (q_a_pairs), a provenance-bearing document body (source_documents + content_chunks retrieval grain), or — when it is a distilled concept, not a record — an L-concept in the OKF bundle. Drop the content_item_workspaces association sentence (M2M dropped, ID-131); workspace scoping is query-time scope_tag overlap.
  • Q1b (external evidence): unchanged → reference_items (33-reference-item.md).
  • Q2 (raw original): unchanged → source_documents.
  • Corollary (1) (README.md:87): keep “never solve routing with a new content_type value”, but reword so content_type is “editorial shape within a source_documents classification”, not “within Q1a”.
  • Add a one-line pointer to the new 36-three-layer-model.md (BI-2) and clarify L-concepts are client-owned (NOT a DB table).
  • Update the README provenance/file-index tables (130–223) to add rows 34/35/36 and adjust the rollups (core/hybrid counts shift; content_items-keyed “Where to look first” entries re-point to source_documents).

BI-2 — NEW 3-layer-model CV, distinct from the audience axis (doc-side; unblocks ID-132)

Section titled “BI-2 — NEW 3-layer-model CV, distinct from the audience axis (doc-side; unblocks ID-132)”

NEW file: ontology/36-three-layer-model.md. Layer 1 CV, provenance_model: core, client_extensible: false, editable_via: database_migration, baseline_values: = l_raw / l_records / l_concepts (3 values, each provenance: core). Body: the v3 §1 definitions (L-raw = client raw sources, client-owned; L-records = Canonical DB; L-concepts = client OKF bundle) + the legitimate cross-layer pointers (L-records→L-raw provenance; L-concepts→L-records canonical://<table>/<uuid> citation). Anti-conflation guard (v3 Lens E): the CV body MUST state in a ## Notes bullet that this is the OKF storage-layer axis and is ORTHOGONAL to 03-layer-vocabulary (the audience/depth axis, sales_brief/bid_detail/…). Add a reciprocal note to 03-layer-vocabulary.md:74 (“Three orthogonal axes”) naming the new fourth, storage-layer axis so prompts/classifiers reading …layer are never confused. Do NOT rename 03-layer-vocabulary or its layer_vocabulary cv_name.

BI-3 — decompose 04-content-type across three destinations (homes-dependent; coordinate ID-131 §3 + ID-132 concept-type set)

Section titled “BI-3 — decompose 04-content-type across three destinations (homes-dependent; coordinate ID-131 §3 + ID-132 concept-type set)”

File: ontology/04-content-type.md. The 15-value closed enum splits three ways (a PRODUCT call — mis-splitting hardens at first bundle publication, v3 risk 5; drive from the id-71 strawman + the client’s hand-built topic index):

  • Record classifications → stay as source_documents.content_type (re-homed off content_items by ID-131 M3): article, blog, pdf, note, research, document, other. These remain the closed content_type enum, now describing a source_documents row’s editorial shape.
  • q_a_pair → migration-OUT: q_a_pair leaves content_type entirely (q_a_pairs is a peer Layer-5 class, 32-q-a-pair.md; it carries no content_type column). Record the migration-out in 04’s Notes (the 04-content-type.md:71 “once q_a_pairs is promoted… historic rows migrate” note becomes “done”).
  • L-concept type discriminators → move to the concept-frontmatter ontology (BI-6): case_study, policy, compliance, methodology, certification, capability, product_description become the L-concept type set, NOT content_type values. (capability is already ratified-S237 to retire — fold the retirement into the concept-type set decision.)
  • Coordination: the resulting source_documents.content_type value set MUST equal ID-131 M3’s content_type column CHECK and the taxonomy_snapshot.json:content_types regenerated after the DB change; the L-concept type set MUST equal ID-132’s concept type discriminators. The split is a joint ratification across ID-131 §3 / ID-133 BI-3 / ID-132 — flag for the owner before the fixture + snapshot are frozen.
  • Keep the HARD-reject: the trimmed source_documents.content_type stays a closed CHECK enforced by the Pydantic _validate_content_type (470–481) reading the regenerated snapshot.

BI-4 — re-home the content_items-coupled column CVs onto their ID-131 homes (homes-dependent)

Section titled “BI-4 — re-home the content_items-coupled column CVs onto their ID-131 homes (homes-dependent)”

The brief’s “~7 column CVs onto source_documents” is, precisely, ~11 CVs across FOUR homes (cite ID-131 M3/M1a/GOV-FACET). For each CV, update the ## Relationships + ## Consumers sections to point at the new home and the database.types.ts projected column:

CV filecontent_items column(s)New home (ID-131)
01-taxonomy-domains.md, 02-taxonomy-subtopics.mdprimary_domain/primary_subtopic/secondary_*source_documents.* (M3); q_a_pairs has none — note Finding 2 (win-rate derives QA domain via record_lifecycle.domain)
10-publication-status.mdpublication_statussource_documents.publication_status INLINE (M3); already on q_a_pairs
13-ingest-source.mdingest_source/ingestion_sourcesource_documents.ingest_source
29-cocoindex-source-kind.mdcocoindex_source_kindsource_documents.* (planned col → SD)
06-lifecycle-type.md, 08-freshness.md, 09-governance-review-status.mdlifecycle_type, freshness/previous_freshness/expiry_date, governance_review_statusrecord_lifecycle facet COLD fields (M1a) — NOT source_documents
07-dedup-status.mddedup_status/content_text_hashq_a_pairs (dedup is QA-valuable, v3 §3.5); content_text_hash DROPS (no SD home)
21-scope-tag.mdscope_tag/anti_scope_tagq_a_pairs + query-time overlap (application-surfacing axis)
05-platform.mdplatformDROP (IMS vestige, no writer, v3 §3.2) — mark the CV status: planned→deprecation note
03-layer-vocabulary.mdlayerRESOLVED (owner round-4, D5): NO source_documents.layer column — layer is retired from the SD/QA homes (the cocoindex SD/QA pipeline never writes content_items.layer, flow.py:2515). But KEEP the 03-layer-vocabulary CV — do NOT deprecate/retire it. Re-frame its OWNERSHIP to the Guides application type: it is the Guides audience/depth axis, matched against guide_sections.expected_layer. Update the CV’s ## Relationships/## Consumers to name the Guides home, not source_documents.

16-extraction-method.md, 20-chunk-kind.md, 25-application-type.md, 19-engineering-types.md need only prose touch-ups (BI-9), not re-homing.

BI-5 — promote entity_type + relationship into the allowed_types / allowed_relations register (the highest-value artefact)

Section titled “BI-5 — promote entity_type + relationship into the allowed_types / allowed_relations register (the highest-value artefact)”

Per Decision B. Two NEW CV files + a schema extension + a parity-guard extension:

  • NEW ontology/34-entity-type.md (Layer 5) and ontology/35-relationship.md (Layer 6) with the 12 + 10 baseline values, each carrying per-value provenance/provenance_model/client_extensible/editable_via (all core/false at v1). Source the values verbatim from the Python Literals (extraction.py:378–391, 423–434) so the register == Literal at authoring time.
  • Extend BaselineValueSchema (lib/ontology/schemas.ts:33–49) with the three optional per-value keys (Decision B code block). This is a PUBLIC-repo change — run gitnexus_impact on BaselineValueSchema before editing (low risk: consumed only by OntologyCVSchema + the loader being retired). No barrel re-export; direct import.
  • EXTEND the parity guard (do NOT add a third unguarded source): add the entity_type + relationship baseline_values to __tests__/fixtures/ontology/ontology-cv-baselines.json (Decision A), and EXTEND scripts/tests/test_cocoindex_extraction.py:TestEntityTypeParity to assert Python Literal == TS const == fixture baseline_values (currently it asserts only Python == TS, 905). Add a NEW TestRelationshipParity class (none exists today) asserting the same triple for relationship against lib/ai/classify.ts:653–666 (the ExtractedRelationship union) — extract the TS union with the existing _extract_ts_string_array helper (861) or a sibling for the union shape.
  • The docs-site “parity-guard twin” (ID-68.27 follow-up) keeps the fixture == register markdown in the private repo. The lockstep is: register markdown (human) → fixture (public CI) → Python Literal + TS const (runtime), all three bound by the extended guard.

BI-6 — OKF concept-frontmatter ontology + validator (gates L-concept writes; ID-132 consumes)

Section titled “BI-6 — OKF concept-frontmatter ontology + validator (gates L-concept writes; ID-132 consumes)”

NEW register CV + NEW validator module. This is the L-concept analogue of the Pydantic extraction gate: it gates concept writes exactly as _validate_content_type gates extraction writes.

  • NEW ontology/37-concept-type.md (Layer 5) — the L-concept type set (the BI-3 discriminators: case_study/policy/compliance/methodology/certification/product_description + any id-71-derived additions), provenance_model: hybrid, per-value provenance as in BI-5. Plus a ## Notes definition of the required concept frontmatter keys (type, title, description, timestamp), the resource: URI scheme (canonical://<table>/<uuid>, owned by ID-132), and the tags convention.
  • NEW validator: a Zod schema ConceptFrontmatterSchema in lib/ontology/concept-schema.ts (sibling to schemas.ts; direct import, no barrel) validating: type ∈ the concept-type set, required keys present, resource: matches the canonical://<table>/<uuid> pattern, tags: string[]. Reuse gray-matter (^4.0.3, already used at loader.ts:61) to parse concept .md frontmatter, exactly as the register loader does. Evaluate lifting the Google okf-skills validator rather than building from zero — but the KH idiom is gray-matter + Zod (mirrors lib/ontology/schemas.ts), so the recommendation is to author a thin KH-native Zod schema and borrow only the okf-skills rule set (required keys, URI shape), not its runtime. Record the lift-vs-build call in the CV Notes.
  • ID-132 wires ConceptFrontmatterSchema into the producer’s declare_file write path (the gate). ID-133 owns the CV + the schema module; ID-132 owns the call site. HARD-reject semantics (a malformed concept fails the write) mirror the content_type gate.

BI-7 — fix the loader repo-split (single source of truth)

Section titled “BI-7 — fix the loader repo-split (single source of truth)”

Per Decision A. Concretely:

  • Retire lib/ontology/loader.ts (loadOntologyCVs + ONTOLOGY_DIR) — gitnexus confirms no production caller (incoming: {}). Run gitnexus_detect_changes before commit to confirm the only affected consumer is the parity test.
  • Update markdown-parity.test.ts: replace the loadOntologyCVs-based tripwire (the case “the public docs/ontology/ register is gone and the loader fails loudly”) with a loader-free privacy tripwire that asserts the public docs/ontology/ directory is absent directly (preserve the privacy-regression intent; drop the fail-loud-loader half, which no longer has a loader to assert against). Keep the fixture ↔ snapshot ↔ generated-tuple parity cases unchanged.
  • The codegen (generate-content-type-values.ts) already reads the fixture — no change beyond the fixture expansion in BI-5.
  • Document the Decision-A authority chain in README.md (replace the stale “Cross-links (will exist after Wave D / Ratifier)” + “Docs-site auto-update” loader references, 89–125) and in the wp6 TECH refresh (BI-9).

BI-8 — extraction-stamp re-parent: ontology-doc side (code re-parent owned by ID-131)

Section titled “BI-8 — extraction-stamp re-parent: ontology-doc side (code re-parent owned by ID-131)”

The CODE change content_items_id → source_document_id at extraction.py:261–264 (_ExtractionStamp) is ID-131’s M2 / G-PIPELINE (id-131/TECH BI-14/BI-15), lockstep with flow.py:2245/2381/2456/2271. gitnexus_impact confirms it is HIGH (16 direct, 46 total) — ID-131’s Executor re-runs impact at edit time and updates the 3 stamped subclasses + the ~4–5 flow_context/test_cocoindex_* fixtures in lockstep. ID-133 owns only the ontology-doc reflection:

  • Update any register CV / README prose that asserts the extraction stamp FKs content_items_id (the phase-b-feedback §5 framing, swept in BI-9) to say source_document_id.
  • Add a ## Notes bullet to 34-entity-type.md + 35-relationship.md (the KG CVs whose mentions/ relationships are stamped) recording that the stamp parent is source_document_id post-ID-131, and that the entity/relationship write-sites stay in the DB (entities do NOT leave the DB, v3 §7.1 / id-131 BI-14).
  • Cross-reference ID-131 M2 so the Checker can confirm the lockstep, not duplicate the migration.

BI-9 — prose sweep + refresh the stale wp6 TECH.md

Section titled “BI-9 — prose sweep + refresh the stale wp6 TECH.md”
  • Prose sweep across the ~14 register files that reference content_items/content_item_workspaces (live counts: 13-ingest-source.md ×15, 33-reference-item.md ×9, 32-q-a-pair.md ×6, README.md ×5, 21-scope-tag.md ×5, 04-content-type.md ×4, 03-layer-vocabulary.md ×4, 02 ×4, 06/01 ×3, 29/25/16/10/09/08/07/05 ×2, 20/19 ×1). Re-point each content_items/content_items.<col> mention to the BI-4 home; preserve historical-context mentions where they explain provenance (mark them “(pre-ID-131)”).
  • Correct the Layer-5 framing: phase-b-prerequisite-1-onthology-pipeline-feedback-investigation.md §5 (481–482) — content_items “Knowledge artefact / Layer 5 ontology class” row is retired; the Layer-5 classes are now source_documents (+ classification) and q_a_pairs, with L-concepts the distilled map.
  • Refresh specs/wp6-ontology-harness/TECH.md §3 (29–69): “29 CVs” → “37 CVs (33 + entity_type + relationship + three-layer-model + concept-type)”; “live docs/ontology/*.md register / runtime loader” → the Decision-A authority chain (private register + public fixture + snapshot + retired loader); the §5.2 loader section → “retired (ID-133)”.
  • 04-content-type.md:111 + README.md “(planned)” cross-link rows that reference markdown-parity.test.ts asserting “15 values” → update to the post-BI-3 trimmed set.

Enforcement-semantics invariant (applies across BI-3, BI-5, BI-6, BI-8)

Section titled “Enforcement-semantics invariant (applies across BI-3, BI-5, BI-6, BI-8)”

The pass changes WHAT is gated and WHERE the stamp points, NOT the gate’s enforcement semantics. Preserve:

  • HARD-reject for closed enums: content_type (trimmed, BI-3), form_type, form_format, entity_type (BI-5), relationship (BI-5), concept type (BI-6) — a violation RAISES (_validate_content_type pattern; ConceptFrontmatterSchema rejects).
  • SOFT-WARN for open dimensions: primary_domain/primary_subtopic/secondary_classifications — the _surface_out_of_taxonomy_classification model-validator (483–532) is UNTOUCHED (it must keep always returning self, never raising). Do NOT add a field_validator to these dimensions.

Behaviour-first (test-philosophy.md); bun run test (never bun test) for TS, python3 -m pytest scripts/tests/ for Python. Per-item mapping:

  • BI-1/BI-2/BI-9 (doc-side): markdown-parity.test.ts (post-BI-7) validates every CV frontmatter against OntologyCVSchema; the new 36-three-layer-model.md + the rewritten README must parse + validate. Add a fixture case asserting 36’s baseline_values keys = [l_raw, l_records, l_concepts]. A grep-based test (CI) asserting zero un-annotated content_items references survive the sweep in the register dir (allow (pre-ID-131)-tagged historical mentions).
  • BI-3: parity — after ID-131 trims the DB CHECK + bun run sync:taxonomy, markdown-parity.test.ts asserts 04-content-type.md baseline (via the fixture) == taxonomy_snapshot.json:content_types == generated tuple. Python TestContentTypeParity (991+) asserts the runtime _VALID_CONTENT_TYPES == the trimmed snapshot. A test asserting q_a_pair is ABSENT from the trimmed set.
  • BI-4: schema-touch validation only at the doc layer (the DB moves are ID-131-tested). Assert each re-homed CV’s frontmatter still validates; the grep sweep (BI-9) covers the re-point.
  • BI-5 (highest-value): the EXTENDED TestEntityTypeParity (triple-bind Python == TS == fixture) + the NEW TestRelationshipParity; both fail on any drift between the Literal, the TS const, and the register fixture. A Zod test asserting 34/35 validate under the extended BaselineValueSchema and that per-value provenance_model/client_extensible/editable_via round-trip. A backward-compat test asserting the existing 33 CVs still validate (optional keys absent).
  • BI-6: unit tests for ConceptFrontmatterSchema: accepts a well-formed concept (type in set, required keys, resource: canonical://q_a_pairs/<uuid> shape, tags); REJECTS a bad type, a missing required key, and a malformed resource: URI (HARD-reject parity with the content_type gate).
  • BI-7: markdown-parity.test.ts post-edit: the loader-free privacy tripwire asserts the public docs/ontology/ dir is absent; bun run build + bun run test pass with loader.ts removed (gitnexus_detect_changes confirms only the test consumed it).
  • BI-8: no ID-133 code test (ID-131 owns the stamp tests, id-131/TECH §Testing — 4 stale fixtures updated, flow_context tests). ID-133’s Checker confirms the register prose names source_document_id and cross-references ID-131 M2.
  • Enforcement-semantics invariant: a Python test asserting a junk entity_type/relationship still RAISES (invalid_enum) and a junk primary_domain still SOFT-WARNs (row written, counter bumped) — proves the pass did not move the HARD/SOFT line.

Regression gate: full bun run test + python3 -m pytest scripts/tests/ after the public-repo edits (schema + parity guards + loader retirement) land.


  1. content_type / concept-type mis-split hardens at first bundle publication (MEDIUM — v3 risk 5). BI-3
    • BI-6 partition one enum across three destinations; the L-concept half pins into ID-132’s bundle. Mit: joint owner ratification across ID-131 §3 / BI-3 / ID-132 BEFORE the fixture + snapshot are frozen; drive from the id-71 strawman + the client’s hand-built topic index.
  2. Promoting the KG vocabularies adds a third source (MEDIUM — v3 risk 8). Mit: Decision A + the EXTENDED parity guard bind register-fixture == Python Literal == TS const; CI fails on drift. Do NOT add the register as an unguarded fourth reader.
  3. Stamp re-parent is HIGH-risk and cross-Task (MEDIUM). gitnexus_impact _ExtractionStamp = HIGH (16 direct). Mit: the code change is ID-131-owned (M2/G-PIPELINE); ID-133 touches only doc prose. The Checker confirms the cross-reference, not a duplicate migration. ID-133 must not edit extraction.py:261–264.
  4. 03-layer-vocabulary36-three-layer-model conflation (MEDIUM — v3 Lens E). Two “layer” axes. Mit: BI-2 reciprocal anti-conflation Notes on both CVs; do NOT rename layer_vocabulary; the storage axis is a NEW CV, the audience axis is untouched.
  5. layer column has no declared ID-131 home (RESOLVED — D5). Mit: resolved by the owner (round-4): no source_documents.layer column; layer is retired from the SD/QA homes (the pipeline never writes content_items.layer, flow.py:2515). The 03-layer-vocabulary CV is KEPT and re-framed to the Guides application type (audience/depth axis, matched against guide_sections.expected_layer) — see BI-4.
  6. Loader retirement breaks an unseen consumer (LOW). Mit: gitnexus incoming: {} + a mandatory gitnexus_detect_changes pre-commit; bun run build (client-bundle) + full test gate.

Empirical-verification note (OQ-3 — Q-EX2)

Section titled “Empirical-verification note (OQ-3 — Q-EX2)”

No NEW external-library symbols are introduced by this pass. Verified 28/06/2026 against pinned versions:

  • gray-matter@^4.0.3 (package.json) — matter() already import-and-called at lib/ontology/loader.ts:61; BI-6 reuses the same call in concept-schema.ts. PRESENT (existing call site).
  • zod@^4.4.3 (package.json) — z.object/z.enum/z.boolean/.optional()/.superRefine already in use at lib/ontology/schemas.ts; BI-5 extends BaselineValueSchema, BI-6 adds ConceptFrontmatterSchema with the same surface. PRESENT (existing call site).
  • pydantic==2.12.5 (requirements.txt) — field_validator/model_validator/Literal already in use at scripts/cocoindex_pipeline/extraction.py; ID-133 does NOT edit the Pydantic gate (BI-8 is ID-131’s). PRESENT (existing call site). The Google okf-skills validator (BI-6) is referenced for its RULE SET only (required keys, URI shape), not imported as a runtime dependency — the recommendation is a KH-native gray-matter+Zod validator, so no new third-party symbol is taken. Scope per shared-discipline §Empirical verification: external-library symbols only; no new ones here.

Quality bars (inherited by every Executor)

Section titled “Quality bars (inherited by every Executor)”

Semantic design tokens only (no raw UI in this Task, but applies to any surface); UK English + DD/MM/YYYY; auth.success + authFailureResponse(auth) for any new route (none expected); sb()/tryQuery() for any Supabase access (none expected — ID-133 is register + schema + validators, no feature DB calls); no barrel re-exports (direct imports for concept-schema.ts, the extended schemas.ts); TanStack Query only for any fetch; bun run test (never bun test); behaviour-first tests (test-philosophy.md) for the parity-guard and validator Subtasks. gitnexus discipline: gitnexus_impact before editing BaselineValueSchema / removing loader.ts; gitnexus_detect_changes before commit. NEVER edit supabase/types/database.types.ts by hand (BI-3/BI-4 column homes are ID-131’s regenerated types).


  • The docs-site “parity-guard twin” (ID-68.27 follow-up) must add entity_type + relationship + concept-type to its private-side fixture lockstep when BI-5/BI-6 land.
  • layer re-home is RESOLVED (D5): no source_documents.layer column — retired from the SD/QA homes; the 03-layer-vocabulary CV is kept and re-framed to the Guides application type (guide_sections.expected_layer). Not an ID-131 column-add.
  • Client-extensible entity/relationship types (per-value client_extensible: true) are research-deferred to v2, mirroring content_type’s “closed for v1” posture — the schema supports it now; the admin path is future.