Skip to content

OKF / Record-Model Reframe — v3 (Synthesis: owner round-3 direction folded in)

OKF / Record-Model Reframe — v3 (Synthesis: owner round-3 direction folded in)

Section titled “OKF / Record-Model Reframe — v3 (Synthesis: owner round-3 direction folded in)”

Supersedes: okf-record-model-v2.md + okf-record-model-v2-CRITIC-CORRECTIONS.md. Authoritative input: okf-v3-owner-feedback.md (owner round-3). Where this doc and v2 disagree, the owner feedback and the 7 grounded lenses win. Audience: Product owner (non-developer). Plain-English first; file:line evidence inline. Grounded against (this round): live DB (project zjqbrdctesqvouboziae); current-head flow.py / extraction.py / promote-corpus.ts; the docs-site ontology register; the Google reference_agent + cocoindex self-updating-wiki + docs-to-KG references; and the real first-client corpus at /Users/liamj/Documents/development/local-fs-client/corpus (Phew Design Ltd). Date: 2026-06-28.


Your round-3 direction is correct and it makes the whole programme cheaper and lower-risk than v2 implied — but in three specific places v2 (and the critic) were simply working from stale ground truth, and the corrections change what work exists, not whether to do it.

  1. content_items elimination is right and “cheaper than it appears” — for the AI/MCP value path. The MCP find tool maps only 8 of hybrid_search’s 22 returned columns (lib/mcp/tools/search.ts:191-202), and all 8 have clean homes. So the output contract for the core value path is small. The real cost is concentrated and nameable: a moderate rewrite of 4 RPC bodies (not their shapes) + a citation re-anchor + the deletion (not rewrite) of the legacy IMS browse/item UI. (§3)

  2. The v2 record-identity recipe was wrong, and you already half-fixed it years ago. v2 proposed switching source_documents to a content_hash seed and minting qa:question_hash for Q&A. Both are retracted. source_documents keeps rel_path by design (owner §B; the critic’s collision blocker is now moot because the switch is abandoned, and single-tenant dissolves the namespace half). The Q&A “identity problem” was a tier conflation: qa:{rel_path}:{idx} is the PK of the raw extraction tier (q_a_extractions), which we KEEP; the master tier (q_a_pairs) already has an opaque DB-native gen_random_uuid() PK and a fully-built many-raw→one-master merge model. There is nothing to “switch.” (§2)

  3. The irreversibility moves. v2/critic said the record-identity grain “freezes at ID-45’s first write.” Under full-replace re-ingest + deterministic uuid5, the L-records DB is disposable and reproducible — re-ingesting the same gated corpus with the same recipe reproduces the same uuids, so you can re-seed any number of times pre-launch. The true point-of-no-return is the first publication of the client-owned OKF bundle (the resource: / # Citations pointers pin record uuids into a durable client-owned artefact). That irreversibility is owned by Task A (producer emission), not Task B (DB write). (§2.4, §10, §11)

  4. “Adopt cocoindex docs-to-knowledge-graph” is a misnomer — we are ahead of it on extraction. We already run a richer closed-ontology entity+relationship extractor than the example, write to Postgres (not Neo4j, which aligns with owner §F), and deliberately avoid ExtractByLlm. The §F work is re-parenting 3 extraction write-sites off content_item_id onto source_document_id — a pipeline edit, not an adoption. The .md-writing reference is the self-updating-wiki (localfs.declare_file), and the agent logic is the Google reference_agent two-pass. (§7)

  5. The OKF producer + EMB-STORE are net-new and honestly-sized. No OKF/markdown producer exists anywhere in the codebase (grep = zero pipeline hits). The genuinely bespoke build is a Source adapter over our L-records DB; the prompts/bundle-writer/viewer/Source-protocol are lift-and-shift; the real cost is porting the reference_agent agent-loop from Google ADK+Gemini to Anthropic. The dedicated embeddings store is net-new (neither cocoindex reference provides one) and we own its polymorphic design. (§7)

  6. Governance/freshness/lifecycle should become its own shared facet, not land on source_documents alone. A polymorphic record_lifecycle facet (owner_kind ∈ {source_document, q_a_pair}) makes the cross-type expiry sweep ONE set-based UPDATE and unifies the review queue across documents and canonical answers — while governance_config stays exactly as-is (per-domain policy). (§5)

  7. {127.4} is NOT “shrink to a tiny fixture” — that mis-read its purpose. It is the Platform promotion-confidence E2E corpus, sized by seam coverage (md+PDF+DOCX binary extractors, the forms route, a content route that produces content_chunks+embeddings, the feed/URL slice). Its DB half is already built; the missing piece is the local-fs file tree + a repeatable release gate that closes the live “0 content_chunks on prod” gap. (§9)

A theme runs through all seven: this is a pre-launch code/schema refactor with zero prod data lock-in (governance_config = 0 rows, content_items prod = 33 E2E-debris rows with 0 chunks, q_a_pairs = 0). Pre-launch is the cheapest possible moment to do it.

Two-DB clarification (D11, owner 28/06/2026). The figures above (content_items = 33, content_chunks = 0, q_a_pairs = 0, reference_items = 3) are the Platform DB (zjqbrdctesqvouboziae, near-empty). Client PROD (rovrymhhffssilaftdwd, “canonical-phew”) currently holds pre-OKF debris — 631 content_items (incl. 396 content_type='q_a_pair'-typed), 926 content_chunks, 55 og:image thumbnails — which id-45’s full-replace re-ingest (cocoindex full walk) WIPES & rebuilds from the gated corpus, NOT a data migration. So “zero prod data lock-in” means the DB is disposable and regenerated via wipe-and-replace (this covers CLIENT-PROD), NOT that the client DB is empty. (D11 dissolved by owner — client-prod is not live / has no real users.)


RATIFIED (owner §A). The refinement vs v2 is in the pointers and the irreversibility seat, not the shape.

┌──────────────────────────────────────────────────────────────────────────────┐
│ L-raw — THE CLIENT'S RAW SOURCES owner: THE CLIENT (stays put) │
│ local-fs / Notion / Gmail / .docx / .pdf / council DPIAs / the live website. │
│ The authoritative provenance + audit + citation anchor. Canonical never hosts │
│ it. BUT: the corpus is GATED — we define the source-doc structure (at least │
│ for the first client) and the Q&A format; the pipeline monitors it for change.│
└───────────────▲────────────────────────────────────────────────────────────────┘
│ cocoindex pipeline derives ↓ (delta-only; per-file content-hash memoised)
│ records POINT BACK to L-raw via source locator / "Source of Information"
┌───────────────┴────────────────────────────────────────────────────────────────┐
│ L-records — CANONICAL DB RECORDS owner: CANONICAL (operated) │
│ source_documents (provenance + version chain + RECORD-IDENTITY SEED) │
│ q_a_extractions (raw tier) → q_a_pairs (master/canonical answers) │
│ reference_items (external evidence, one-shot) content_chunks (retrieval grain)│
│ entity_mentions + entity_relationships (closed-ontology KG, STAYS in DB §F) │
│ citations (polymorphic) record_lifecycle facet (§5) EMB-STORE (§7) │
│ op_id stamps which RUN wrote each row (audit, not identity). │
│ High-cardinality, churny rows live HERE. Disposable: full-replace rebuilds it. │
└───────────────▲────────────────────────────────────────────────────────────────┘
│ 2nd-pass enrichment agent distills ↓ (Pass-1 from L-records, Pass-2 from gated L-raw)
│ concepts POINT to records via `resource:` URI + `# Citations` (record uuids)
┌───────────────┴────────────────────────────────────────────────────────────────┐
│ L-concepts — THE OKF CONCEPT BUNDLE owner: THE CLIENT (private git repo) │
│ A distilled KNOWLEDGE MAP: ~30-50 .md concept files (topics, products, certs, │
│ company, case studies, metrics, datasets, playbooks) + index.md + log.md. │
│ Concept-identity = the file path. Concepts CITE records; never STORE them. │
│ A Q&A pair does NOT appear here (it is a record, not a concept). Fits one │
│ context window (~86k for Phew) → load wholesale; vector index for the long tail. │
└──────────────────────────────────────────────────────────────────────────────┘

The only legitimate cross-layer pointers:

  • L-records → L-raw (provenance): reference_items.source_url, source_documents.storage_path + content_hash, and the per-Q&A “Source of Information” (observed verbatim in the Phew corpus).
  • L-concepts → L-records (citation): a concept’s resource: field + # Citations body section carry canonical://<table>/<uuid> pointer values. This is the only place a Canonical record id touches the client-owned bundle, and it is a pointer value, never the concept’s identity key.

Two of three layers are client-owned (L-raw and L-concepts); only L-records is Canonical-operated. This is what makes “we don’t extract your value / you keep your canonical files” literally true.

Naming caution (carried from Lens E): the existing ontology CV 03-layer-vocabulary is an AUDIENCE/DEPTH axis (sales_brief / bid_detail / company_reference / research), NOT this L-raw/L-records/L-concepts model. Pick distinct terminology so prompts/classifiers that read content_items.layer are not confused with the OKF layer model.


2. Record-identity v3 (replaces v2 §2.3 entirely)

Section titled “2. Record-identity v3 (replaces v2 §2.3 entirely)”

The v2 recipe carried a tier conflation and an over-generalised “rel_path is the offender” claim. Both are corrected. The corrected model is mostly ratify-as-built, not design-in-Task-B.

2.1 source_documents — KEEP rel_path (v2’s content_hash switch FULLY RETRACTED)

Section titled “2.1 source_documents — KEEP rel_path (v2’s content_hash switch FULLY RETRACTED)”
  • Seed = uuid5(_KH_PIPELINE_DOC_NS, "sd:" + rel_path) (flow.py:2111 content, :2529 sidecar); URL branch = uuid5(… "sd:" + url) (flow.py:2940). This is idempotency-in-place by design (flow.py:1630-1640): re-ingesting the same file mints the same PK → declare_row UPSERTs in place.
  • Live schema confirms rel_path is the sole idempotency mechanism: source_documents has only a PK on id — NO UNIQUE(storage_path), NO UNIQUE(content_hash). The seed string IS the idempotency.
  • The critic’s BLOCKER-1 (two identical-content files collide under a content_hash seed) is moot because the switch is abandoned. Single-tenant (owner §B: each tenant has its own DB + pipeline) dissolves the namespace half — the global _KH_PIPELINE_DOC_NS constant (flow.py:1640) is correct.
  • content_hash stays a stored audit/diff column (plain file hash, is_generated = NEVER), feeding id-117 diff + idempotency-of-bytes checks. It is NOT the PK seed.
  • The pipeline does not write version / parent_id today (flow.py:2138-2159 writes ~9 cols only), so an edited same-path file overwrites in place. There is no version-threading logic yet — consistent with owner §B “idempotency-in-place.” A version-threading feature would be a deliberate future build, not implied by this model.

2.2 Q&A — the master PK is ALREADY DB-only opaque; the “switch” was a tier conflation

Section titled “2.2 Q&A — the master PK is ALREADY DB-only opaque; the “switch” was a tier conflation”

This is the single biggest correction to v2/critic.

  • qa:{rel_path}:{idx} is the PK of the RAW tier q_a_extractions, NOT of q_a_pairs. flow.py:2270 (content) and :2568 (sidecar) seed it into qa_target, which is mounted on table q_a_extractions (flow.py:3429-3433). KEEP it — it is the exact analogue of sd:{rel_path} (idempotency-in-place over the gated corpus). v2’s proposal to drop it / replace with a question hash is wrong: a question hash re-collides on merge and re-mints on edit.
  • q_a_pairs (the master/canonical tier) is ALREADY DB-only with an opaque PKq_a_pairs.id DEFAULT gen_random_uuid(); the promoter inserts then reads the DB-assigned id back (lib/q-a-pairs/promote-corpus.ts:391-406). No path/hash derivation exists for the master PK, and nothing external pins it. There is nothing to switch. Keep it non-deterministic across rebuilds — opacity is a feature (full-replace re-mints; nothing external depends on a stable master uuid).
  • The many-raw→one-master merge anchor is BUILT, not hypothetical: q_a_extractions.promoted_to_pair_id (FK → q_a_pairs.id) is the CAS link (promote-corpus.ts:295-296, 417-421); the joint (promoted_to_pair_id, source_document_id) keys the pair (:292-293); re-merge/version-migration uses q_a_pairs.superseded_by (:805-808); cross-file/cross-form dedup is q_a_pair_dedup_proposals (curator-approved write only, qa_dedup_proposer.py:40-44). This is exactly what lets a master survive moving across library files (Company-Wide ↔ LMS / Audit / Website) and versions (v4.2→v4.4) — its identity is opaque and DB-internal, so re-grouping only re-points extractions.

The genuine “Q&A as DB-only” decision the owner is reaching for (§D): it is NOT a PK change (master is already DB-only). It is the decision to RETIRE the {59.x} Q&A sidecar file round-trip (promote-corpus.ts:540-566, which materialises promoted pairs back to the corpus as __qa__/*.md and sets q_a_pairs.source_document_id = sdUuid5(relPath)). Recommend retiring it: a Q&A pair is a record (owner §D), never a concept, so writing it as a file in the client-owned bundle contradicts the concept≠record split. Keep q_a_pairs.source_document_id as pure-DB provenance. Confirm owner understanding (§D): YES — a Q&A pair is a record, never an OKF concept, never in a bundle.

2.3 reference_items — KEEP ri:{source_url} (settled, constraint-enforced)

Section titled “2.3 reference_items — KEEP ri:{source_url} (settled, constraint-enforced)”
  • Seed = uuid5(_KH_PIPELINE_DOC_NS, "ri:" + item.url) (flow.py:2941), with live UNIQUE(source_url), source_url NOT NULL, id has NO DB default (the seed is mandatory), source_document_id NOT NULL (the sd+ri evidence pair). RI is one-shot / never re-extracted.
  • Owner §Misc: source_url MUST remain on reference_items. Settled — no open decision.

2.4 The reframed irreversibility (this is the important change)

Section titled “2.4 The reframed irreversibility (this is the important change)”
v2/critic claimedv3 corrected
Hard freeze pointID-45’s first DB writeFirst publication of the client-owned OKF bundle (Task A)
Whyre-ingest mints first rows whose grain hardensfull-replace + deterministic uuid5 ⇒ L-records is disposable & reproducible; you can re-seed pre-launch any number of times
Owner of the irreversible decisionTask BTask A (the resource:/# Citations pointers pin record uuids into a durable client-owned artefact)

Client-prod is in scope of “disposable” (D11, owner 28/06/2026). The re-seedability is not a Platform-only convenience. Client PROD (rovrymhhffssilaftdwd) is not live / has no real users; its current content is pre-OKF debris, and id-45’s full-replace re-ingest (cocoindex full walk) WIPES & rebuilds it from the gated corpus — a regeneration, not a migration. “Disposable L-records” therefore covers client-prod via wipe-and-replace, not merely Platform’s emptiness.

What must be RATIFIED at ID-45 (as a stable CONTRACT, not an irreversible DB fact), so the Task-A producer can rely on it:

  1. FREEZE the seed STRING formats — sd:{rel_path}, ri:{source_url}, qa:{rel_path}:{idx} — and the _KH_PIPELINE_DOC_NS constant, so re-ingest keeps reproducing citable uuids. Add a test asserting the seed strings + namespace value; gate it BEFORE Task-A bundle publication. (If anyone “tidies” the seed strings or namespace after a bundle ships, every canonical://<table>/<uuid> citation silently orphans.)
  2. The resource: URI scheme (owned by Task A — recommend canonical://<table>/<uuid>).

The one genuine pre-ID-45 SCHEMA item (reversible, not irreversible): re-parent q_a_extractions to source_document_id (add the column + backfill from extraction_metadata.rel_path / sdUuid5), since its current source_content_item_id link dies with content_items. q_a_pairs already carries source_document_id, so the master tier is unaffected.


3. content_items decomposition (current-head-corrected)

Section titled “3. content_items decomposition (current-head-corrected)”

Direction RATIFIED (owner §A/§C: “100%, but cheaper than it appears”). The corrections below are all current-head facts that v2/critic got stale or wrong.

3.1 Corrected FK inventory — 10 enforced + 2 unenforced (NOT 11+2)

Section titled “3.1 Corrected FK inventory — 10 enforced + 2 unenforced (NOT 11+2)”

source_document_diffs was already DROPPED (id-117); v2 §3.2’s FK table is stale. Live pg_constraint confrelid = 'content_items' = 10 enforced (all convalidated = true) + 2 unenforced bare-uuid columns.

FK referencing content_items.idEnforcementActionTarget
content_chunks.content_item_idunenforced (no FK)RE-PARENT (ADD source_document_id col — CC has none today)source_document_id
entity_mentions.content_item_idunenforced (no FK)RE-PARENT (ADD source_document_id col)source_document_id
entity_relationships.source_item_idSET NULLRE-PARENTsource_document_id
classification_disputes.content_item_idCASCADERE-PARENTsource_document_id
ingestion_quality_log.content_item_idCASCADERE-PARENTfacet owner / SD
feed_articles.content_item_idSET NULLRE-PARENTreference_item_id (verify writers)
verification_history.content_item_idCASCADEMOVE with governancerecord_lifecycle owner (§5)
content_history.content_item_idSET NULLDROP or movegit/OKF history + SD parent_id chain
read_marks.content_item_idCASCADEDROPIMS reading-tracker
content_item_workspaces.content_item_idCASCADEDROPratified (owner §A)
content_items.superseded_by (self)SET NULLDROPfull-replace removes in-DB supersession
citations.cited_content_item_idCASCADEHARD — extend contract§3.4 (CITE-EXT)

Note (Lens C sequencing): the chunk/entity re-parent needs the NEW source_document_id column AND the pipeline writer (flow.py) writing that parent from day one of the full-replace re-ingest, because content_items will not exist. Verify flow.py chunk/entity stages reference the right parent before ID-45.

3.2 Column homes — the honest reality (v2’s “mostly already on SD” is FALSE)

Section titled “3.2 Column homes — the honest reality (v2’s “mostly already on SD” is FALSE)”

source_documents is a lean 21-column provenance table. It has NO classification, NO freshness/lifecycle, NO governance, NO title (filename/original_filename only), NO created_by/updated_by/verified_*. So “re-home onto SD” = ADD ~25-30 net-new columns (or a 1:1 sidecar) — it is NOT “columns already mostly there.”

content_items column(s)Owner’s §C viewv3 corrected call (current head)
content / textSD extracted_text (already there) + CC retrieval grain
content_typeDECOMPOSE: q_a_pair→QA; article/blog/pdf/note/document→SD/RI classification; case_study/policy/certification/methodology/capability/product_descriptionL-concepts type discriminators
embedding, embedding_modelEMB-STORE (§7); CC already carries one inline
classification family (primary_domain, primary_subtopic, ai_keywords, summary, classified_at, …)net-new on SD (SD has none today). D1/E2/F4: the pipeline today writes ONLY content_type/primary_domain/primary_subtopic/title (flow.py:2173-2195). PERSIST classification_confidence/classification_reasoning/secondary_domain/secondary_subtopic onto SD — all consumed by review/quality/forms/provenance, but extracted-then-discarded or app-side-only today, so extend flow.py to write them. DROP classification_model (dead, 0 stored-value consumers)
created_bysource_documents.uploaded_by (SD has no created_by)
updated_by / updated_atnet-new (SD has neither)
Q&A cols (answer_standard, answer_advanced)DROP — pure duplicates; q_a_pairs already has them
source_urldrop from CI; keep on RIDROP from CI (moot once table gone); KEEP on SD (URL-ingest provenance) + RI (NOT NULL)
content_text_hash (GENERATED md5 of normalised text)could SD carry it?NO — low value over SD content_hash + rel_path. Migrate normalised-text dedup to q_a_pairs (see §3.5)
author_name→ source_documentDROP — IMS vestige, no pipeline writer, only in hybrid_search RETURNS + a +0.02 author boost. Route to SD only IF the ontology declares a real author attribute
thumbnail_url→ reference_itemsNET-NEW on reference_items (D4) — ship empty nullable col now; the 55 old client-prod og:image values are WIPED by the full-replace re-ingest, no backfill. Optionally re-wire the existing og:image extractor (lib/intelligence/content-extractor.ts:276) into the NEW reference ingest so RIs get thumbnails going forward
platformmay/may not be neededDROP — IMS vestige, no writer, bound only in browse filter UI
source_domainmay/may not be neededDROP — derivable from source_url
layerDROP (D5) — audience axis, never written by the SD/QA pipeline (flow.py:2515); belongs to Guides. Only the source_documents column is dropped — the 03-layer-vocabulary CV concept is RETAINED in the ontology (re-framed as the Guides audience axis, id-133)
quality_score / historykeep current on facet if used; DROP history cols
citation_countDERIVE (rollup over citations)
freshness/lifecycle/governance/ownership familyrecord_lifecycle facet (§5), NOT inline on SD
archived_at/by, archive_reasonfacet (SD already has archived_at/by)
IMS social cols (starred, user_tags, priority, brief, detail, reference, file_path, source_file)DROP

Key correction: of the four columns the owner asked about, three are NOT “a move onto an existing column” — author_name/platform/source_domain are DROP; thumbnail_url is now NET-NEW on reference_items (D4, ship empty). There is no requirement for “~58 cols” (owner §C) — confirmed; most are IMS vestige drops.

3.3 The 4 search-RPC bodies + their TS callers (the real consumer cost)

Section titled “3.3 The 4 search-RPC bodies + their TS callers (the real consumer cost)”

Owner is RIGHT for the AI/MCP surface, and the honest scope is:

  1. hybrid_search — RETURNS 22 columns but the MCP find tool maps only 8 (search.ts:191-202), all with clean homes ⇒ small output contract for the value path. The work is the RPC BODY: a single-table scan becomes a polymorphic UNION across source_documents/content_chunks/q_a_pairs/reference_items, AND the win_stats scoring CTE keyed on cited_kind = 'content_item' must re-anchor. /api/search/route.ts:85-88 passes RPC rows wholesale, so its clients (use-search.ts:34, use-browse-data.ts:642) see whatever the rewrite emits.
  2. get_content_win_rate — NOT dead, NOT migrated (owner belief incorrect). 2 live callers (procurement.ts:544, app/api/items/[id]/effectiveness/route.ts:81). Body keys on cited_kind='content_item'. Re-anchor to p_q_a_pair_id + cited_kind='q_a_pair' (the reusable answer is what wins/loses a bid). Resolve the latent inconsistency first: get_content_win_rate derives outcome from form_templates.outcome via form_outcome_types.counts_toward_win_rate, while hybrid_search.win_stats derives it from workspaces.domain_metadata->>'outcome' — two different outcome sources for one concept. Pick the canonical source before re-anchoring or the bug propagates.
  3. find_related_items — NOT dead (caller app/item/[id]/page.tsx:58). Re-anchor or split per-type; drops the user_tags IMS-vestige column.
  4. filter_by_keywords — the LIVE variant (search_terms text[]) RETURNS SETOF uuid (caller use-browse-data.ts:183), low effort. The SETOF content_items variant is a no-op stub to delete.

Plus the dedup family (find_duplicate_pairs, find_exact_duplicates + content.ts/supersession.ts) re-targeted to q_a_pairs (§3.5).

3.4 The one hard re-home: citations (CITE-EXT), partly pre-built

Section titled “3.4 The one hard re-home: citations (CITE-EXT), partly pre-built”

citations already carries dormant cited_q_a_pair_id + cited_q_a_pair_version and a CHECK (citations_cited_one_of_chk) enforcing exactly-one-of {content_item, q_a_pair}. But owner belief is incorrect: cite_content does NOT yet source from a dedicated table — it still writes cited_kind:'content_item' (procurement.ts:463-464; auto-citer draft-stream/route.ts:330-331).

CITE-EXT = extend cited_target_kind to add {reference_item, source_document, concept}; add cited_reference_item_id / cited_source_document_id (or a generic cited_target_id); rewrite the CHECK; re-point all writers (procurement.ts:463, draft-stream:330) + readers (items/route.ts:667-668, effectiveness/route.ts:120-121) + both win-rate consumers off cited_kind='content_item'. DB-record targets use the §2 record uuid; concept targets use the concept path.

Citation-target framing (D8, reframed). CITE-EXT activates q_a_pair as a citation target inside the DB, but the client-owned bundle (Task-A L-concepts) cites only {source_document, reference_item, concept} — a Q&A pair’s bundle-level citation stays DB-internal. The driver for NOT citing q_a_pairs from a bundle is that bundles are not record-based by design (a concept cites provenance, not canonical answers), NOT that the opaque master uuid would orphan on re-ingest. A Q&A pair could be cited if it were authoritative, but its provenance source (the source_document / reference_item it derives from) is the better, more durable citation. Conclusion unchanged: bundle anchors = {source_document, reference_item, concept}; q_a_pair citation is DB-internal.

  • VALUABLE for q_a_pairs — the corpus’s own “44 masters from 123 Q&As” merge IS normalised-question dedup; migrate a normalised question hash there (matching the existing two-tier merge model).
  • Largely VESTIGIAL for source_documents — SD already gets file-level idempotency from content_hash + rel_path. Single-tenant + a gated corpus WE structure means accidental byte-identical dupes are a curation-time concern, not a runtime DB-dedup need. Do NOT add content_text_hash to SD.
  • The IMS dedup tooling (find_exact_duplicates, find_duplicate_pairs, dedup_status stamping in content.ts/supersession.ts) is personal-KB heritage — port to q_a_pairs, do not blindly re-home onto documents.

3.6 The legacy IMS browse/item UI = ELIMINATION, not rewrite

Section titled “3.6 The legacy IMS browse/item UI = ELIMINATION, not rewrite”

components/content/content-card.tsx, content-row.tsx, item-detail/metadata-sidebar.tsx, editor-view.tsx, browse/filter-panel.tsx, app/item/[id]/item-detail-client.tsx bind the dropped IMS columns (thumbnail_url/author_name/source_domain/user_tags/priority). This is where the critic’s “not a silent DROP” caveat actually bites — but it is removal of a deprecated surface, not contract preservation. Scope these as deletions.

3.7 Can content_items be fully eliminated? YES.

Section titled “3.7 Can content_items be fully eliminated? YES.”

Pipeline core → SD + CC; IMS cols → DROP; Q&A cols → DROP (dupes of q_a_pairs); URL → RI; governance/lifecycle → record_lifecycle facet; distilled content_types → L-concepts. No role remains that the typed records + facet + concepts cannot absorb.


The smallest typed-entity set in L-records that satisfies every persona, with no generic content_item:

#EntityRoleNotes vs v2
1source_documentsProvenance substrate + version lineage + record-identity seed (rel_path)KEEPS rel_path; gains classification cols (net-new)
2q_a_extractionsq_a_pairsRaw extraction tier (path-idempotent) → master canonical answers (opaque DB PK)Two-tier merge model = ratify-as-built; re-parent extractions to source_document_id
3reference_itemsExternal evidence, one-shot, citeable, deliberately governance-freeunchanged; ri:{source_url} settled
4content_chunksRetrieval grain, re-parented to source_document_idRAG reduced, not removed
5EMB-STOREDedicated polymorphic embeddings (owner_kind,owner_id,model,vector)net-new; indexes records AND concepts (§7)
6citationsPolymorphic; extended to {q_a_pair, reference_item, source_document, concept}CITE-EXT (§3.4)
7entity_mentions + entity_relationshipsClosed-ontology KG, STAYS in DB (owner §F)re-parent write-sites to source_document_id; NOT “leaving the DB”
8record_lifecycle facetShared governance/freshness/lifecycle (§5)replaces v2’s “land on SD only”

A 4th, distinct entity family — forms (operational data): form_templates / form_template_fields / form_questions / form_responses / template_completions (workspace-scoped) + form_template_requirements (global catalogue). Forms are a consumer of the 3 record types, not one of them and not a concept (§8).

Computed, not stored: “where-are-we-exposed”, freshness coverage, “what’s in queue”, guide coverage/gaps — aggregates over the facet + citations, not a stored content type.

Real-corpus validation: the Phew corpus resolves into exactly Q&A pairs, source_documents (docx/pdf), and URL references, plus a hand-built proto-OKF concept map (the “BID RESPONSE TOPIC INDEX”, ~30 topics). The client already performed the concept≠record split by hand.


5. Governance schema (owner §E, GOV-HOST) — promote to a shared facet

Section titled “5. Governance schema (owner §E, GOV-HOST) — promote to a shared facet”

RATIFIED — PER-AXIS owner_kind (D7, owner 28/06/2026; REVERSES the uniform single-facet design described below). The lifecycle is NOT one facet over a single owner_kind set. Each axis carries its own home and its own owner_kind set:

AxisHomeowner_kind setNotes
Supersessioninline self-FK{source_document, q_a_pair, reference_item}superseded_by stays inline on each typed record (RI gains it)
Review / Governancecold facet{source_document, q_a_pair}reference_item EXCLUDED — BI-16 holds for THIS axis only
Freshness / Validitycold facet{source_document, reference_item}q_a_pair EXCLUDED (the inverse of the governance axis)
Contradictionedge table{reference_item}pairwise contradiction edges, not a per-record column

Q&A drops the freshness clock entirely0 net-new q_a_pairs columns; reuse the existing inline valid_to for opt-in cert-expiry surfaced to the user (not a swept freshness clock).

The RI freshness/validity/supersession/contradiction work is DEFERRED to the Intelligence-domain track (follows id-130) — tracked, NOT built in id-131. Expiry-data intent: see bl-90 / docs/research/human-validation-auto-metadata-research.md.

The narrative below (uniform owner_kind ∈ {source_document, q_a_pair}) is retained for its hot/cold-split reasoning and consumer-rewrite scope, but is superseded on the owner_kind axis-set by the per-axis table above.

RECOMMENDATION: do NOT land it on source_documents only (v2’s call). Promote freshness + review-cadence + governance-review + ownership into its OWN shared, polymorphic facet table (e.g. record_lifecycle / governance_state), keyed (owner_kind, owner_id) with owner_kind ∈ {source_document, q_a_pair} and a denormalised domain for the policy join — landing alongside governance_config (which stays unchanged as the per-domain POLICY table). reference_item is EXCLUDED by design (BI-16), enforced by the owner_kind CHECK.

Why a facet beats SD-only:

  • The expiry sweep / recalculate_all_freshness() becomes ONE set-based UPDATE over the facet instead of N per-type UPDATEs or being blind to Q&A lifecycle. The owner names the queryable expiry sweep (Priya/O6) as the driving requirement; a single queryable lifecycle store is the cleanest substrate. A .md frontmatter value cannot power it — it REQUIRES queryable columns.
  • q_a_pairs ALREADY has a divergent lifecycle subset; SD-only would freeze Q&A into a permanently parallel governance model (drift, two review surfaces) or drop Q&A lifecycle (regression — publication_status is NOT NULL and load-bearing in q_a_search/dedup). A facet UNIFIES the review queue / cadence / governance crons / whats_in_my_queue across documents AND canonical answers.

Why a facet beats per-record columns: per-table forces recalculate_all_freshness, the review queue, cadence stats into polymorphic UNIONs across SD+QA (the exact pain the critic flagged for hybrid_search), plus duplicated CHECK/enum/index definitions and drift risk.

The central sub-decision — SPLIT by axis (hot/cold), do not move monolithically:

  • Cold (cron/queue-driven, cross-cutting) → the facet: freshness, freshness_checked_at, previous_freshness, lifecycle_type, expiry_date, next_review_date, review_cadence_days, governance_review_status, governance_review_due, governance_reviewer_id, verified_at, verified_by, content_owner_id, domain.
  • Hot (read-path filters) → KEEP INLINE on typed records: publication_status, superseded_by, valid_from, valid_to (q_a_search filters publication_status on EVERY query; q_a_pairs already has these NOT NULL). Do not push these behind a facet join.
  • DERIVE, do not store: quality_score (cron-computed), citation_count (rollup).

Keep governance_config exactly as-is (per-domain policy, UNIQUE(domain)). The facet carries a denormalised domain (= owner record’s primary_domain, set at write time) so the existing policy→state join in the quality-score / freshness-transitions / review-cadence crons keeps working in one hop. Add a write-time sync (trigger or pipeline) so facet.domain == owner.primary_domain after reclassification.

Codify BI-16 as a schema invariant: the facet’s owner_kind CHECK MUST NOT include reference_item — absence of a facet row = governance-free, stronger than today’s convention.

Honest consumer-rewrite scope (~60 TS files): 3 crons (quality-score, freshness-transitions, review-cadence), app/api/governance/review, app/api/review/{queue,cadence,stats}, app/api/source-documents/[id]/send-to-review, MCP governance.ts + review.ts (whats_in_my_queue governance facet), 3 DB fns (recalculate_all_freshness, get_freshness_breakdown, get_review_breakdown_stats). Convert the freshness recalc fn and the review queue first. Re-parent the CASCADE sidecars (verification_history, ingestion_quality_log) to the facet owner; review_assignments is already filter-based (filter_domains/filter_content_types /filter_freshness) — extend filter_content_types to span owner_kind.

Open sub-decisions for the owner:

  • RESOLVED (D7) — per-axis, not one facet. Q&A drops the freshness clock entirely: freshness/validity = {source_document, reference_item}; review/governance = {source_document, q_a_pair}; supersession (inline) = {source_document, q_a_pair, reference_item}; contradiction (edge table) = {reference_item}. See the RATIFIED block at the top of §5. Q&A reuses inline valid_to for opt-in cert-expiry only; the RI lifecycle axes are DEFERRED to the Intelligence-domain track.
  • entity_mentions also carries a JSON expiry path (metadata->>'expiry_date' swept in freshness-transitions). Decide whether entities join the facet or stay on the metadata path.
  • FK enforcement idiom: per-kind nullable FKs + exactly-one CHECK (mirror citations_cited_one_of_chk) vs (owner_kind, owner_id) + validating trigger.

The ontology DEEPLY encodes content_items and must be re-aligned to the three layers. This is a first-class workstream.

Current ground truth (corrects the stale TECH spec):

  • content_items is encoded as THE Layer-5 “Knowledge artefact” ontology class (phase-b-feedback §5:482); content_type is literally “the shapes a content_items row can take” (04-content-type.md:63); the README boundary rule routes all client-canonical knowledge → content_items (README.md:76); ~10 CVs reference content_items columns; the extraction stamp FKs content_items_id (extraction.py:261-264).
  • The LIVE semantic linter is the Pydantic gate in extraction.py, NOT the markdown register: HARD-reject content_type/form_type (field_validators reading taxonomy_snapshot.json) + entity_type/relationship/form_format (hardcoded Literals); SOFT-WARN primary_domain/primary_subtopic/secondary (counter + log). Allowed sets come from taxonomy_snapshot.json (DB-derived), kept in lockstep with the register by markdown-parity.test.ts.
  • The owner’s allowed_types/allowed_relations literally do not exist (grep = 0 across repo + docs-site). entity_type (12) and relationship (10) are hardcoded Python Literals (extraction.py:378-391, 423-434) mirrored to TS by a parity guard, with NO register entry and NO provenance metadata.
  • The register DOES capture core-vs-client (provenance_model core/client/hybrid, client_extensible, editable_via, per-value provenance) — 22 core / 9 hybrid / 2 client. This maps 1:1 to owner §H.
  • Register repo-split is STILL PRESENT: loader.ts:29 ONTOLOGY_DIR = REPO_ROOT/docs/ontology which no longer exists (register relocated to docs-site at ID-68.27); the loader was inverted (markdown-parity.test.ts:250-251 asserts the dir is absent and the loader throws); the build was sidestepped to read a frozen 4-CV repo fixture (ontology-cv-baselines.json). Two partial sources + a dead bridge.
  • The existing 03-layer-vocabulary CV is an AUDIENCE axis, NOT the OKF 3-layer model — do not conflate.

The 9-item pass:

  1. Rewrite the README boundary rule so client-canonical knowledge routes to {source_documents, q_a_pairs, reference_items} + L-concepts, not content_items + content_item_workspaces.
  2. Add a NEW 3-layer-model CV (L-raw / L-records / L-concepts), distinct from the audience-axis 03-layer-vocabulary.
  3. Decompose 04-content-type: split the 15-value enum across record classifications (article/blog/pdf/note→SD/RI), the q_a_pair migration-out, and L-concept type discriminators (case_study/policy/certification/methodology/product_description→concepts). This is a PRODUCT call, not mechanical — mis-splitting hardens at bundle publication. Coordinate with §3.
  4. Re-home the ~7 content_items-coupled column CVs onto source_documents.
  5. Promote entity_type(12) + relationship(10) into NEW Layer-5/6 KG ontology CVs with per-value provenance — this is the literal allowed_types/allowed_relations the owner wants, and the spine of the §G extraction. Highest-value new artefact. Extend (do not bypass) the existing Python↔TS parity guard — promoting to a register adds a THIRD source.
  6. Add an OKF concept-frontmatter ontology + validator (concept type set + required keys type/title/description/timestamp + resource: URI scheme + tags). Net-new; evaluate lifting the Google okf-skills validator rather than building from zero. This gates L-concept writes exactly as the Pydantic gate gates extraction writes.
  7. Fix the register repo-split: pick ONE source of truth (repoint loader.ts at the docs-site register via KH_PRIVATE_DOCS_DIR, OR retire the loader and make an expanded repo fixture authoritative) + document the lockstep. The current dead-loader + 4-CV-fixture + taxonomy_snapshot triple is a drift hazard.
  8. Re-parent the extraction stamp content_items_id → source_document_id (extraction.py:261-264), in lockstep with §3. Run gitnexus impact on _ExtractionStamp / stamp_extraction_base / flow_context first (5 flow_context tests reference content_items_id).
  9. Sweep content_items prose across the ~10 referencing CVs; refresh the stale wp6-ontology-harness/TECH.md §3 (still says 29 CVs / public-repo / live-loader).

Keep the HARD-reject vs SOFT-WARN distinction in the rewritten register: content_type (and successors) HARD-reject; open dimensions (domain/subtopic) SOFT-WARN. The pass changes WHAT is gated and WHERE the stamp points, not the gate’s enforcement semantics.

Sequencing: gate the homes-dependent items (3,4,8) AFTER Task B’s decomposition decisions; author the doc-side items (1,2) in parallel (they unblock the producer).


7. Producer architecture (owner §G) — cocoindex docs-to-KG + enrichment

Section titled “7. Producer architecture (owner §G) — cocoindex docs-to-KG + enrichment”

The central grounding answer: “adopt docs-to-knowledge-graph” is a misnomer in three of its four parts. Here is the corrected mapping.

7.1 Extractor (a) — KEEP ours + re-parent (NOT adopt docs-to-KG)

Section titled “7.1 Extractor (a) — KEEP ours + re-parent (NOT adopt docs-to-KG)”
  • We already extract entities+relationships from source docs and persist a KG: entity_mentions (flow.py:2375), entity_relationships (flow.py:2460), content_chunks (flow.py:2238); extractors extract_entity_mentions + extract_relationships (extraction.py:1087-1145).
  • We are ahead of the example: (i) we use the Anthropic SDK directly, NOT cocoindex ExtractByLlm (deliberate, flow.py:85-87); (ii) a closed 12-entity/10-predicate domain ontology + cross-doc Stage-5 resolution vs the example’s open concept/verb vocabulary; (iii) we write Postgres relational tables via declare_row vs the example’s Neo4j via declare_record/declare_relation.
  • Literally adopting docs-to-KG would REGRESS us (lose ExtractByLlm-avoidance, lose the closed ontology, lose Stage-5 resolution, pull in Neo4j against owner §F).
  • The §F work is re-parenting 3 write-sites off content_item_id onto source_document_id (flow.py:2245 chunks, :2381 mentions, :2456 relationships). A pipeline edit. Scope in Task B.
  • Owner §F risk corrected: entities STAY in the DB; the v2 “entities may leave the DB” risk is dropped.

7.2 Producer (b) — self-updating-wiki declare_file (NOT docs-to-KG) + reference_agent

Section titled “7.2 Producer (b) — self-updating-wiki declare_file (NOT docs-to-KG) + reference_agent”
  • No OKF/markdown producer exists (grep okf/bundle/concept.md/enrichment/reference_agent = zero pipeline hits). 100% net-new.
  • The correct .md-writing reference is the self-updating-wiki blog (localfs.declare_file(output_dir / f'{name}.md', markdown, create_parent_dirs=True)), a native cocoindex FILE target, incremental via memo — delta-only OKF regeneration for free, compatible with our “no out-of-band DDL” rule (it writes files, not DDL). docs-to-KG writes graph nodes only, no .md.
  • The agent logic is the Google reference_agent two-pass: Pass-1 enrich_concept() per concept from a pluggable Source protocol (list_concepts/read_concept_raw/sample_rows) → write_concept_doc; Pass-2 run_web_pass() crawls seed URLs (host allowlist + depth) → enriches + creates references/<slug>; regenerate_indexes() builds index.md.
  • Lift-and-shift: the Source protocol, prompts/ (reference_instruction.md, web_ingestion_instruction.md), the bundle writer, and the viewer/. Note: the “sources/base.py” pattern is the reference_agent’s, NOT ours — we have no scripts/cocoindex_pipeline/sources/ package (our sources are localfs.walk_dir + a hand-rolled url_source.py).
  • The single genuinely bespoke piece is a Source adapter over our L-records DB (typed tables + the entity graph). Concept granularity (product / topic / cert / company / metric) is a PRODUCT decision driven by the id-71 strawman + the client’s hand-built topic index — not liftable.
  • Framework decision (the real cost of “lift-and-shift”): port the agent-loop from Google ADK+Gemini to the Anthropic tool-use surface we already run (claude-opus, extraction.py:71) for provider consistency. Make this an explicit Task-A line item. Lift-and-shift on Gemini is the faster fallback if the port slips.
  • Git “knowledge sync” is OUTSIDE cocoindex: declare_file lands files locally; a separate git writer produces the client-owned private-repo commits + point-in-time rollback (round-1 step 4). Ownership/timing (per-run vs batched) is unspecified — a Task-A decision.
  • Gate declare_file with the 12-entity/10-relation ontology + the OKF concept-frontmatter validator (§6) as a semantic linter on every concept write (owner §H).
  • Constrain Pass-2 web enrichment to the GATED authoritative corpus (owner §B), NOT the open web — wire the reference_agent’s host-allowlist + depth-limit + path-filter knobs to the client’s authoritative sources only. Point concept resource: / # Citations at Task-B’s record uuids — the only place a Canonical id enters the client-owned bundle.

7.3 EMB-STORE (owner §G) — NET-NEW, we own the design

Section titled “7.3 EMB-STORE (owner §G) — NET-NEW, we own the design”

Neither cocoindex reference provides an embeddings store (docs-to-KG is pure graph; the wiki has none). We already embed (text-embedding-3-large, dim 1024, flow.py:997-998) but store inline across 6 scattered vector columns (content_items.embedding, content_chunks.embedding, q_a_pairs.question_embedding, reference_items.embedding, form_template_requirements.requirement_embedding, + company_profiles.company_embedding as TEXT, an existing inconsistency to absorb). Build a dedicated polymorphic store (owner_kind ∈ {source_document, content_chunk, q_a_pair, reference_item, concept}, owner_id, model, vector) so ONE store serves both L-records hybrid_search AND L-concept bundle traversal (serves owner §A “reduce-not-remove RAG”). Keep declaring the pgvector index via Supabase migration, NOT cocoindex declare_vector_index (deliberately avoided, flow.py:3668-3673).

Sequencing dependency: Task B (EMB-STORE) → Task A. The polymorphic store must be ratified BEFORE the producer relies on it for concept-vector indexing; if it lands after, Pass-1 has no vector index for the long-tail RAG retention.


8. content_templates (owner §I) + forms (owner §J)

Section titled “8. content_templates (owner §I) + forms (owner §J)”

8.1 content_templates — vestigial; DROP with content_items

Section titled “8.1 content_templates — vestigial; DROP with content_items”
  • DB table = 0 rows, read by nothing at runtime. The live feature is 5 hardcoded markdown-skeleton constants (lib/content/content-templates.ts) that pre-fill the legacy manual content_item create form (app/item/new → POST /api/items). hooks/use-content-templates.ts:11-21 self-documents “Phase 2 (future): will query the content_templates database table.”
  • Keyed to the OLD content_type taxonomy (policy/case_study/capability/methodology/qa-pair), 4/5 of which are L-concepts in v3. MCP/pipeline-invisible.
  • DROP it with the content_items elimination. Do NOT repurpose for record authoring — a markdown body skeleton fits NEITHER source_documents (authored by ingesting a file) NOR reference_items (authored by providing a URL). It only fits q_a_pairs (and that template is blank).
  • Re-home the idea, not the table: the policy/case-study/methodology skeletons are proto OKF concept-type templates — model them as type-discriminated concept templates owned by the Task-A producer. If AI-tooling record-authoring scaffolds are wanted, split by type: source_document = ingest a file (no template); reference_item = provide a URL (no template); q_a_pair = a minimal authoring shape (question + answer_standard + answer_advanced + scope_tag[]). Owner §C: removing the content_item abstraction makes this seeding “much clearer” — lean into that, don’t re-template.

8.2 Forms — NO manifest needed; the owner’s §J model is ALREADY BUILT

Section titled “8.2 Forms — NO manifest needed; the owner’s §J model is ALREADY BUILT”

The owner’s 4-step model maps 1:1 onto the id-52 Path A/B/C system:

  • “user provides a form” = upload/ingest → form_templates (instance) + form_template_fields (Path B deterministic extractor, no LLM; the id-80 fork ensures forms get ZERO content rows).
  • “cataloguing skill maps it” = Path C catalogue-form-requirements skill promoting instance fields → form_template_requirements (GLOBAL, workspace-agnostic, embedding-bearing, human-confirmed per-row; 96 rows live). This IS the reusable “form definition” — DERIVED, not hand-authored as a manifest.
  • “added to the platform” = catalogue + form_questions (workspace-scoped working copy, FK template_requirement_id + form_template_id).
  • “Q&A pairs complete it” = T10 matching (template-coverage.ts) + drafting → form_responses (source_content_ids) → filled back to template_completions.

So forms need no separate structure manifest. The only “manifest” in play is the pipeline’s workspace-map route tag (id-80, .kh-workspace-map.json), which is ingest-ROUTING config for the gated corpus path — irrelevant to manually-uploaded forms (ingest_source='manual'). Keep these two senses of “manifest” distinct.

The ONE required v3 change: forms completion currently matches against the content_items bucket (template-coverage.ts:519-527 fetchContentForMatching SELECTs FROM content_items incl. content_type='q_a_pair'). Eliminating content_items REQUIRES re-pointing this matching engine to q_a_pairs (primary match) + reference_items (optional secondary). source_documents is DROPPED from the match pool — provenance-only, NOT a match candidate (D2/E5: SD has no embedding and no answer-grained content; resolve any document hit via content_chunks to the parent SD as a citation/provenance link only). form_responses.source_content_ids / form_questions.matched_content_ids (uuid[]) become q_a_pair/reference_item ids. This SIMPLIFIES forms (match a requirement against canonical Q&A answers, not a generic bucket). It is the forms-side of the CITE-EXT consumer rewrite — fold into Task B.

Preserve the blank-vs-answered fork (id-80 ratified): answered forms remain Path-A knowledge (→ q_a_pairs); blank instruments remain Path-B. The refactor must not collapse this distinction.

Calibration risk: matching thresholds (0.55/0.35) and cosineSimilarity were tuned against content_items embeddings. If EMB-STORE changes the vector space, re-calibrate or matching silently degrades.


9. {127.4} promotion-confidence E2E (owner §K) — reframe (CHANGES Task C)

Section titled “9. {127.4} promotion-confidence E2E (owner §K) — reframe (CHANGES Task C)”

v2’s “shrink to a tiny fixture” was a mis-read. Reframe {127.4} from “assemble synthetic corpus + manifest” to “Platform promotion-confidence E2E corpus + repeatable release gate” — its purpose is to prove the SAME immutable vX.Y.Z image + migration set runs end-to-end on the Platform rig BEFORE it is pinned to a client (platform-to-client-promote.md §2-5), NOT to validate id-45 content quality.

What {127.4} actually needs (three halves):

  1. The local-fs file tree (the only missing piece) under local-fs-platform/corpus, authored to cover EVERY seam the promote changes only by config: md + PDF + DOCX (binary extractors = highest image-bump risk), the forms route (→form_templates), a content route that produces content_chunks WITH embeddings, and the feed/URL slice (→FeedUrlSource → /extract trafilatura, the #1 Platform↔client config delta per BI-4). Ship the mandatory root .kh-workspace-map.json (loaded unconditionally, flow.py:3366-3378; filename stays .kh-, bl-368 out of scope). Avoid the __qa__/ reserved prefix or carry an explicit qa_sidecar mapping (flow.py:2019-2029).
  2. The DB seed — ALREADY built (seed-synthetic-corpus.ts: 6 synthetic workspaces + ~30 questions
    • manifest emitter). Live staging synthetic_ws=6; prod synthetic_ws=0 → run it on prod too (owner R-2 resolved YES: “the Platform exists for E2E/dogfooding”).
  3. A repeatable assertion set — expected row deltas per route/format after a walk (form_templates N, source_documents N, content_items-successors N, content_chunks > 0 with embeddings, reference_items from feed, pipeline_runs in_progress+terminal with stage_counts.source_walk ≥ N), asserting rows land ONLY in the Platform DB. Re-runnable on EVERY release — this is what converts dogfooding into promotion confidence and closes the live “0 content_chunks on prod despite 33 content_items + 498 pipeline_runs” gap (the Platform prod pipeline has NEVER completed an end-to-end content write).

Gate filter (E1) — status='completed' is NOT a content-write signal. Of the 498 Platform pipeline_runs, the ~395 completed rows are dominated by the review_cadence cron (220) + mcp_create_content_item debris (121) + other governance crons; only kh_canonical_pipeline runs carry op_id (just 2, both with 0 content_chunks). So the promotion gate MUST filter pipeline_name='kh_canonical_pipeline' AND op_id IS NOT NULL (and additionally assert content_chunks > 0 WITH embeddings scoped to the run’s op_id), NOT bare status='completed' — crons emit completed as a heartbeat even with items_processed=0.

Two-DB caveat: all figures here (0 content_chunks / 33 content_items / 498 pipeline_runs) are the Platform DB (zjqbrdctesqvouboziae). Client PROD (rovrymhhffssilaftdwd) is a separate DB and is out of scope for the Platform promotion gate (BI-3: no client data on Platform).

Scale = COMPLETE SEAM COVERAGE, not “tiny” and not “elaborate”: ~6-10 files (one per route×format×notable-branch) + the 6 synthetic workspaces + ~30 questions + 1-2 feed rows + one deliberate UnmappedPath/COALESCE-edge file so graceful-degradation paths are gated too. v2’s “2-3 edge shapes” UNDER-covers (misses binary extraction + feed + embeddings — the very seams that break on a release).

Is synthetic the right vehicle? YES. The real Phew corpus is policy-ineligible for the Platform pipeline (BI-3: no client data on Platform; ID-68 IP separation). Promotion confidence REQUIRES a deterministic, owned, byte-stable input so the only thing that varies between two promote runs is the image/migration/app under test. Hold the line: Platform = synthetic only; id-45 = real corpus on the empty preview branch.

Three non-conflatable E2E lanes (promotion confidence = their COMPOSITION):

  • id-128 Lane A — APP Playwright, SQL-fixture/ephemeral-branch, no worker (GitHub Actions vs Supabase branch).
  • id-128 Lane B — pipeline-as-system (the ~44 cocoindex integration specs + synthetic-corpus STRUCTURE + real /walk).
  • {127}/{127.4} — the DEPLOYED pipeline (real Coolify app+worker+Traefik+webhook+image vX.Y.Z) over the Platform DB on IONOS — a RELEASE/DEPLOY-time gate, not a CI test.

The {127.4} corpus is the single shared input to Lane B and the Platform E2E (seed-synthetic-corpus.ts is already shared) and is the upstream artefact the golden snapshot consumes (one /walk → frozen content_items+embeddings → replayed in Lane A). Author the file tree ONCE; all consumers point at it.

DEFER the golden-snapshot FREEZE until after the OKF record-model refactor (Task B). A snapshot frozen on today’s content_items shape is invalidated the moment content_items is eliminated and content_chunks re-parents. The corpus FILES are shape-stable and can be built now; the frozen OUTPUT must wait for the post-refactor typed-record shape.

Decide corpus ownership: a hand-authored, local-only corpus cannot gate CI or a teammate’s promote. Decide committed/vendored vs local before relying on it as a repeatable release gate.


Reversibility: CONTRACT-FREEZE (hardens at first bundle publication, Task A) / EXPENSIVE (large change) / CHEAP (reversible pre-launch). Full-replace re-ingest ⇒ no data migration throughout.

#Decisionv3 callReversibilityStatus
3-LAYERL-raw / L-records / L-conceptsRatify (§1). Two of three layers client-owned; concepts cite records.EXPENSIVE to reverse once builtRATIFIED (owner §A)
D5-v2 (SEED)Record-identity seedssource_documents KEEPS rel_path (content_hash switch RETRACTED); q_a_extractions KEEPS qa:{rel_path}:{idx}; q_a_pairs master PK is ALREADY opaque DB-native (ratify-as-built); reference_items KEEP ri:{source_url}.CONTRACT-FREEZE at bundle publication (Task A), NOT ID-45 DB writeREVISED — replaces v2 §2.3
SEED-CONTRACTFreeze seed STRING formats + _KH_PIPELINE_DOC_NS + add a testDeclare at ID-45; gate before Task-A bundle publication.CHEAP to declare; FREEZE at publicationNEW — ratify
QA-DBONLYRetire the {59.x} Q&A sidecar __qa__/*.md round-tripRecommend retire (a Q&A pair is a record, never a concept). Keep q_a_pairs.source_document_id as DB provenance.CHEAP (pre-launch)NEW — ratify
QA-REPARENTAdd q_a_extractions.source_document_id + backfillBefore content_items drop (its source_content_item_id dies).CHEAPNEW — ratify
CI-DECOMPEliminate content_itemsFully eliminate (§3). 10 enforced + 2 unenforced FKs (NOT 11+2); SD gains ~25-30 net-new classification cols; IMS cols DROP; legacy browse/item UI = ELIMINATION.EXPENSIVE as code; ZERO data migrationRATIFIED (owner §A); current-head-corrected
CITE-EXTExtend citations.cited_target_kindAdd {reference_item, source_document, concept}; activate q_a_pair; re-point writers/readers + both win-rate consumers. cite_content does NOT yet use a dedicated table (owner belief corrected).EXPENSIVE (consumer-contract)RATIFIED (owner §A)
WINRATE-FIXResolve dual outcome sourceget_content_win_rate (form_templates.outcome) vs hybrid_search.win_stats (workspaces.domain_metadata) — pick canonical before re-anchoring to q_a_pair.CHEAPNEW — ratify
(b) M2MDrop content_item_workspacesReconfirmed; query-time scope_tag overlap.CHEAPRATIFIED (owner §A)
EMB-STOREDedicated polymorphic embeddings storeNET-NEW (no cocoindex ref covers it). owner_kind/owner_id/model/vector; indexes records AND concepts; absorbs the 6 scattered vector cols + the company_profiles TEXT inconsistency. pgvector index via migration, not declare_vector_index. Task B → Task A dependency.CHEAP-MEDIUM (additive)NEW — ratify
GOV-FACETGovernance/freshness/lifecycle → per-axis homes (D7)Replaces v2 “SD-only” AND the uniform single-facet design: per-axis owner_kind — supersession inline {source_document, q_a_pair, reference_item}; review/governance cold facet {source_document, q_a_pair} (BI-16 here only); freshness/validity cold facet {source_document, reference_item} (q_a_pair EXCLUDED); contradiction edge table {reference_item}. Q&A drops the freshness clock (0 net-new cols; reuse inline valid_to). Denormalised domain; hot/cold split; governance_config unchanged. RI lifecycle axes DEFERRED to the Intelligence-domain track (follows id-130; ref bl-90).MEDIUMNEW — supersedes v2 GOV-HOST; per-axis per D7
EXTRACT-REPARENTEntities STAY in DB; re-parent 3 write-sitescontent_chunks/entity_mentions/entity_relationships content_item_id → source_document_id (flow.py:2245/2381/2456). NOT “adopt docs-to-KG”; NOT “entities leave the DB” (owner §F).CHEAPNEW — corrects v2 §8 risk 5
PRODUCEROKF producer = wiki declare_file + reference_agentNOT docs-to-KG for the .md writer. Lift-and-shift Source protocol/prompts/viewer; bespoke = Source adapter; real cost = ADK+Gemini → Anthropic port. Git knowledge-sync OUTSIDE cocoindex. Pass-2 gated to authoritative corpus.Decision CHEAP; build EXPENSIVEREVISED
ONTOLOGY-PASS9-item ontology re-alignmentRewrite boundary rule; new 3-layer CV; decompose content_type; promote entity_type/relationship to register CVs with provenance; OKF concept-frontmatter validator; fix loader repo-split; re-parent extraction stamp.MEDIUMNEW — ratify (owner §H)
content_templatesDROP with content_itemsVestigial (0 rows, unread). Re-home the skeleton idea to OKF concept-type templates (Task A).CHEAPNEW — ratify (owner §I)
FORMSNo manifest; re-point matching to typed records§J model already built (id-52 Path A/B/C). Re-point fetchContentForMatching + uuid[] arrays off content_items to q_a_pairs/RI/SD. Preserve blank-vs-answered fork.MEDIUM (matching rewrite)NEW — ratify (owner §J)
{127.4}Promotion-confidence E2E corpus + repeatable gateNOT “shrink.” Seam-coverage file tree + DB seed (run on prod) + re-runnable release gate. Defer golden-snapshot freeze until after Task B.CHEAPREVISED — supersedes v2 {127.4}
SRC-URLDrop source_url from content_items onlyKEEP on SD (URL-ingest, load-bearing) + RI (NOT NULL).CHEAPRATIFIED (owner §Misc)
FULL-REPLACEFull-replace re-ingest WIPES & rebuilds CLIENT-PROD from the gated corpus (also runs on the empty preview branch)Confirmed; “zero prod lock-in” = the DB is disposable/regenerated, NOT that client-prod is empty (it holds pre-OKF debris — 631 items / 926 chunks / 55 thumbnails). D11 dissolved by owner.n/a (mechanism)CONFIRMED
RAGReduce, don’t removeKeep vector index (EMB-STORE) over bundle + long tail.n/aRATIFIED (owner §A)

Dropped from prior register: v2 §2.3 content_hash/question_hash seed recipe → D5-v2; v2 “entities may leave the DB” risk → EXTRACT-REPARENT; v2 {127.4} “shrink drastically” → promotion-confidence reframe; D5 frontmatter-id (already superseded in v2).


Four Tasks. Task B is the foundation everything depends on; Task A owns the bundle-publication irreversibility.

Task B — L-records refactor (content_items decomposition) — FOUNDATION

Section titled “Task B — L-records refactor (content_items decomposition) — FOUNDATION”

Scope: eliminate content_items (§3); re-home the 10+2 FKs; CITE-EXT (§3.4) + WINRATE-FIX; build the EMB-STORE (§7.3); build the record_lifecycle governance facet (§5) with the hot/cold split; re-parent content_chunks/entity_mentions/entity_relationships to source_document_id (§7.1); re-parent q_a_extractions to source_document_id; retire the {59.x} Q&A sidecar; drop content_item_workspaces + content_templates; re-point forms matching (fetchContentForMatching + uuid[] arrays) to typed records (§8.2); migrate the 4 search-RPC bodies + their TS callers (§3.3); delete the legacy IMS browse/item UI (§3.6). Declare the SEED-CONTRACT (freeze seed-string formats

  • namespace + test). Owns: CI-DECOMP, CITE-EXT, EMB-STORE, GOV-FACET, the seed-contract declaration. Depends on: nothing upstream. Use gitnexus impact before touching hybrid_search, get_content_win_rate, _ExtractionStamp, governance symbols; ast-dataflow column-reads/-writes to pin every consumer site. Size (honest): the largest Task. ~60 TS files for governance alone; 4 RPC bodies; the EMB-STORE + facet are net-new schema; the forms-matching re-point + IMS-UI deletion are real surfaces. But ZERO data migration (full-replace) and the value-path output contract is small (8 of 22 columns).

Task A — L-concepts + producer (the OKF bundle) — OWNS BUNDLE-PUBLICATION IRREVERSIBILITY

Section titled “Task A — L-concepts + producer (the OKF bundle) — OWNS BUNDLE-PUBLICATION IRREVERSIBILITY”

Scope: the client-owned OKF git bundle for Phew; the 2-pass producer (self-updating-wiki declare_file mechanism + reference_agent two-pass agent logic); the bespoke Source adapter over L-records; the ADK+Gemini → Anthropic agent-loop port (explicit line item; Gemini fallback); concept schema (type discriminated by tags); index.md/log.md; the resource: URI scheme (recommend canonical://<table>/<uuid>); the git “knowledge sync” writer (outside cocoindex); the OKF concept-frontmatter validator (evaluate lifting okf-skills); a vector index over the bundle (via EMB-STORE); Pass-2 web enrichment gated to the authoritative corpus. Owns: the resource-URI scheme + the FIRST bundle publication (the true point-of-no-return); concept-granularity (PRODUCT decision from id-71 + the client’s topic index). Depends on: Task B (record uuids for resource:/citations; EMB-STORE for the bundle vector index). Producer scaffolding + framework port can start in parallel; the pointer contract waits on B. Size (honest): Source adapter (bespoke, moderate) + framework port (the real “lift-and-shift” cost, moderate-large) + git layer + validator. The prompts/bundle-writer/viewer are lift-and-shift (small).

Task D — Ontology pass (three-layer alignment + semantic linter)

Section titled “Task D — Ontology pass (three-layer alignment + semantic linter)”

Scope: the 9-item pass (§6) — boundary-rule rewrite, new 3-layer CV, content_type decomposition, re-home column CVs to SD, promote entity_type/relationship to register CVs with provenance, OKF concept-frontmatter validator, fix the loader repo-split (single source of truth), re-parent the extraction stamp, prose sweep + TECH.md refresh. Owns: the allowed_types/allowed_relations register (the semantic linter that gates BOTH Task-B extraction and Task-A concept writes); core-vs-client-configurable provenance for the KG + concept layers. Depends on: Task B for the homes-dependent items (content_type decomposition, column re-homes, extraction-stamp re-parent). Doc-side items (boundary rule, 3-layer CV) author in parallel — they unblock Task A.

Task C — {127.4} promotion-confidence E2E corpus + repeatable gate

Section titled “Task C — {127.4} promotion-confidence E2E corpus + repeatable gate”

Scope: author the seam-coverage local-fs file tree under local-fs-platform/corpus (md+PDF+DOCX + forms route + content-with-chunks+embeddings + feed/URL + a graceful-degradation edge file) + the mandatory root manifest; run seed-synthetic-corpus.ts on Platform PROD; build the repeatable expected-row-delta release gate ({127.11}/{127.13}); point id-128 Lane B + the golden snapshot at the SAME corpus; decide corpus ownership (committed vs local). Depends on: nothing to build the FILES. Defer the golden-snapshot freeze until after Task B (the frozen OUTPUT is content_items-shape-coupled).

┌───────────────────────────────────────────────────────────────────┐
│ Task B — L-records refactor (FOUNDATION) │
│ eliminate content_items · EMB-STORE · GOV-FACET · CITE-EXT · │
│ re-parent extraction · retire qa-sidecar · DECLARE seed-contract │
└───────────────┬───────────────────────────────────────────────────┘
│ provides: record uuids · EMB-STORE · L-records schema · stable seed-contract
┌───────────────────────────────────────────────────────────────────┐
│ Task A — L-concepts + 2-pass producer │
│ OWNS the FIRST bundle publication = true point-of-no-return │
│ (scaffolding + ADK→Anthropic port start in parallel with B) │
└───────────────────────────────────────────────────────────────────┘
Task D — Ontology pass: doc-side (boundary rule, 3-layer CV) ∥ B; homes-dependent items AFTER B;
provides the semantic linter gating B-extraction AND A-concept-writes
Task C — {127.4}: FILES anytime; golden-snapshot freeze AFTER B
ID-45 (Phew re-ingest): on the empty preview branch; consumes the REAL corpus + Task B's recipe;
ratifies (does not freeze) the seed-contract
goose G4 (current branch): NOT blocked — headless consumes the same MCP verbs; proceeds in parallel

The single sequencing constraint that bites (reframed): it is NO LONGER “Task B before ID-45’s first write.” Under full-replace + deterministic uuid5 the DB is re-seedable pre-launch. The binding constraint is the SEED-CONTRACT (frozen seed strings + namespace + resource-URI scheme) must be ratified before Task A publishes the first client-owned bundle, because that durable artefact pins record uuids. Task B → Task A on the EMB-STORE dependency is the other hard edge.


  1. Citation contract break during content_items elimination (HIGH). cite_content still writes cited_kind='content_item'; the CHECK only knows {content_item, q_a_pair}. If CITE-EXT and the record-uuid re-anchor land out of step, every citation loses its anchor. Plus the latent dual outcome-source bug (get_content_win_rate vs hybrid_search.win_stats) propagates if not resolved first. Mitigation: ratify CITE-EXT + WINRATE-FIX together in Task B; pre-launch when no prod citations exist.

  2. Governance facet consumer-rewrite under-scoped (HIGH). ~60 TS files + 3 crons + 3 DB fns + the review/governance/MCP surfaces. If under-scoped it leaks past ID-45. The freshness recalc fn and the review queue are load-bearing — convert first. Mitigation: gitnexus impact + ast-dataflow column-reads/writes before touching governance_review_status/freshness/lifecycle_type.

  3. The producer framework port is the real “lift-and-shift” cost (MEDIUM-HIGH). reference_agent is ADK+Gemini; porting the agent-loop to Anthropic is moderate, currently un-scoped work and is the true cost behind “lift-and-shift.” If underestimated, Task A slips. Mitigation: explicit Task-A line item; Gemini fallback if the port slips; the bespoke Source adapter is the only other genuinely new piece.

  4. Seed-contract drift orphans bundle citations (MEDIUM, irreversible-flavoured). Re-ingest reproduces uuids only if BOTH the seed strings AND _KH_PIPELINE_DOC_NS are unchanged. If anyone “tidies” them after a bundle ships, every canonical://<table>/<uuid> silently orphans. Mitigation: a frozen-contract test asserting the seed strings + namespace value, gated before Task-A publication.

  5. Concept-granularity + content_type decomposition are PRODUCT calls that harden at publication (MEDIUM). Which typed records become which concept types, and which content_type values become L-concepts vs record classifications vs drop — mis-splitting hardens when the first bundle ships. Mitigation: drive both from the id-71 strawman + the client’s hand-built topic index; coordinate §3 (Task B) ↔ §6 (Task D) ↔ §7.2 (Task A).

  6. EMB-STORE lands after the producer (MEDIUM, sequencing). If the polymorphic store is not ratified in Task B before Task A relies on it, Pass-1 has no vector index for the long-tail RAG retention (owner §A). Also re-tunes the forms-matching thresholds (0.55/0.35). Mitigation: enforce Task B → Task A on EMB-STORE; re-calibrate template-coverage.ts against the new vector space.

  7. Golden-snapshot freeze vs the OKF refactor collide (MEDIUM). A snapshot frozen on today’s content_items shape is invalidated the moment content_items is eliminated and content_chunks re-parents. Mitigation: build the {127.4} corpus FILES now; freeze the OUTPUT only after Task B.

  8. Single-source-of-truth ambiguity in the ontology (MEDIUM). Post repo-split: register in docs-site (33 CVs), build reads a 4-CV repo fixture, runtime gate reads taxonomy_snapshot.json (DB-derived), loader dead. The pass must pick ONE authority + lockstep mechanism or drift recurs; promoting entity_type/relationship to a register adds a THIRD source unless it extends the existing parity guard.

  9. Forms-matching blast radius under-counted (MEDIUM). fetchContentForMatching + form_responses.source_content_ids + form_questions.matched_content_ids all reference content_items; form completion breaks if not re-pointed. Mitigation: include forms in the Task-B consumer-rewrite inventory; decide whether matching reads q_a_pairs only or a polymorphic union.

  10. {127.4} as a hand-authored local-only corpus is fragile as a release gate (LOW-MEDIUM). It cannot gate CI or a teammate’s promote if it lives on one operator’s Mac. Mitigation: decide committed/vendored vs local; the corpus must be reproducible to certify a promote.