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.
0. The headline (read this first)
Section titled “0. The headline (read this first)”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.
-
content_items elimination is right and “cheaper than it appears” — for the AI/MCP value path. The MCP
findtool 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) -
The v2 record-identity recipe was wrong, and you already half-fixed it years ago. v2 proposed switching
source_documentsto acontent_hashseed and mintingqa:question_hashfor Q&A. Both are retracted.source_documentskeepsrel_pathby 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-nativegen_random_uuid()PK and a fully-built many-raw→one-master merge model. There is nothing to “switch.” (§2) -
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 (theresource:/# Citationspointers 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) -
“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 offcontent_item_idontosource_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) -
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)
-
Governance/freshness/lifecycle should become its own shared facet, not land on source_documents alone. A polymorphic
record_lifecyclefacet (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 — whilegovernance_configstays exactly as-is (per-domain policy). (§5) -
{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.)
1. The three-layer model (refined)
Section titled “1. The three-layer model (refined)”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 +# Citationsbody section carrycanonical://<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:2111content,:2529sidecar); 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_rowUPSERTs in place. - Live schema confirms rel_path is the sole idempotency mechanism:
source_documentshas only a PK onid— NOUNIQUE(storage_path), NOUNIQUE(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_NSconstant (flow.py:1640) is correct. content_hashstays 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_idtoday (flow.py:2138-2159writes ~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 tierq_a_extractions, NOT ofq_a_pairs.flow.py:2270(content) and:2568(sidecar) seed it intoqa_target, which is mounted on tableq_a_extractions(flow.py:3429-3433). KEEP it — it is the exact analogue ofsd:{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 PK —q_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 usesq_a_pairs.superseded_by(:805-808); cross-file/cross-form dedup isq_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 liveUNIQUE(source_url),source_url NOT NULL,idhas 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_urlMUST remain onreference_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 claimed | v3 corrected | |
|---|---|---|
| Hard freeze point | ID-45’s first DB write | First publication of the client-owned OKF bundle (Task A) |
| Why | re-ingest mints first rows whose grain hardens | full-replace + deterministic uuid5 ⇒ L-records is disposable & reproducible; you can re-seed pre-launch any number of times |
| Owner of the irreversible decision | Task B | Task 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:
- FREEZE the seed STRING formats —
sd:{rel_path},ri:{source_url},qa:{rel_path}:{idx}— and the_KH_PIPELINE_DOC_NSconstant, 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, everycanonical://<table>/<uuid>citation silently orphans.) - The
resource:URI scheme (owned by Task A — recommendcanonical://<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.id | Enforcement | Action | Target |
|---|---|---|---|
content_chunks.content_item_id | unenforced (no FK) | RE-PARENT (ADD source_document_id col — CC has none today) | source_document_id |
entity_mentions.content_item_id | unenforced (no FK) | RE-PARENT (ADD source_document_id col) | source_document_id |
entity_relationships.source_item_id | SET NULL | RE-PARENT | source_document_id |
classification_disputes.content_item_id | CASCADE | RE-PARENT | source_document_id |
ingestion_quality_log.content_item_id | CASCADE | RE-PARENT | facet owner / SD |
feed_articles.content_item_id | SET NULL | RE-PARENT | reference_item_id (verify writers) |
verification_history.content_item_id | CASCADE | MOVE with governance | record_lifecycle owner (§5) |
content_history.content_item_id | SET NULL | DROP or move | git/OKF history + SD parent_id chain |
read_marks.content_item_id | CASCADE | DROP | IMS reading-tracker |
content_item_workspaces.content_item_id | CASCADE | DROP | ratified (owner §A) |
content_items.superseded_by (self) | SET NULL | DROP | full-replace removes in-DB supersession |
citations.cited_content_item_id | CASCADE | HARD — 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 view | v3 corrected call (current head) |
|---|---|---|
content / text | — | SD extracted_text (already there) + CC retrieval grain |
content_type | — | DECOMPOSE: q_a_pair→QA; article/blog/pdf/note/document→SD/RI classification; case_study/policy/certification/methodology/capability/product_description→L-concepts type discriminators |
embedding, embedding_model | — | EMB-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_by | — | → source_documents.uploaded_by (SD has no created_by) |
updated_by / updated_at | — | net-new (SD has neither) |
Q&A cols (answer_standard, answer_advanced) | — | DROP — pure duplicates; q_a_pairs already has them |
source_url | drop from CI; keep on RI | DROP 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_document | DROP — 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_items | NET-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 |
platform | may/may not be needed | DROP — IMS vestige, no writer, bound only in browse filter UI |
source_domain | may/may not be needed | DROP — derivable from source_url |
layer | — | DROP (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 / history | — | keep current on facet if used; DROP history cols |
citation_count | — | DERIVE (rollup over citations) |
| freshness/lifecycle/governance/ownership family | — | → record_lifecycle facet (§5), NOT inline on SD |
archived_at/by, archive_reason | — | facet (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:
hybrid_search— RETURNS 22 columns but the MCPfindtool 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 acrosssource_documents/content_chunks/q_a_pairs/reference_items, AND thewin_statsscoring CTE keyed oncited_kind = 'content_item'must re-anchor./api/search/route.ts:85-88passes RPC rows wholesale, so its clients (use-search.ts:34,use-browse-data.ts:642) see whatever the rewrite emits.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 oncited_kind='content_item'. Re-anchor top_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_ratederives outcome fromform_templates.outcomeviaform_outcome_types.counts_toward_win_rate, whilehybrid_search.win_statsderives it fromworkspaces.domain_metadata->>'outcome'— two different outcome sources for one concept. Pick the canonical source before re-anchoring or the bug propagates.find_related_items— NOT dead (callerapp/item/[id]/page.tsx:58). Re-anchor or split per-type; drops theuser_tagsIMS-vestige column.filter_by_keywords— the LIVE variant(search_terms text[])RETURNSSETOF uuid(calleruse-browse-data.ts:183), low effort. TheSETOF content_itemsvariant 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.
3.5 Where dedup is ACTUALLY valuable
Section titled “3.5 Where dedup is ACTUALLY valuable”- 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 addcontent_text_hashto SD. - The IMS dedup tooling (
find_exact_duplicates,find_duplicate_pairs,dedup_statusstamping incontent.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.
4. The minimal record model
Section titled “4. The minimal record model”The smallest typed-entity set in L-records that satisfies every persona, with no generic content_item:
| # | Entity | Role | Notes vs v2 |
|---|---|---|---|
| 1 | source_documents | Provenance substrate + version lineage + record-identity seed (rel_path) | KEEPS rel_path; gains classification cols (net-new) |
| 2 | q_a_extractions → q_a_pairs | Raw extraction tier (path-idempotent) → master canonical answers (opaque DB PK) | Two-tier merge model = ratify-as-built; re-parent extractions to source_document_id |
| 3 | reference_items | External evidence, one-shot, citeable, deliberately governance-free | unchanged; ri:{source_url} settled |
| 4 | content_chunks | Retrieval grain, re-parented to source_document_id | RAG reduced, not removed |
| 5 | EMB-STORE | Dedicated polymorphic embeddings (owner_kind,owner_id,model,vector) | net-new; indexes records AND concepts (§7) |
| 6 | citations | Polymorphic; extended to {q_a_pair, reference_item, source_document, concept} | CITE-EXT (§3.4) |
| 7 | entity_mentions + entity_relationships | Closed-ontology KG, STAYS in DB (owner §F) | re-parent write-sites to source_document_id; NOT “leaving the DB” |
| 8 | record_lifecycle facet | Shared 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:
| Axis | Home | owner_kind set | Notes |
|---|---|---|---|
| Supersession | inline self-FK | {source_document, q_a_pair, reference_item} | superseded_by stays inline on each typed record (RI gains it) |
| Review / Governance | cold facet | {source_document, q_a_pair} | reference_item EXCLUDED — BI-16 holds for THIS axis only |
| Freshness / Validity | cold facet | {source_document, reference_item} | q_a_pair EXCLUDED (the inverse of the governance axis) |
| Contradiction | edge table | {reference_item} | pairwise contradiction edges, not a per-record column |
Q&A drops the freshness clock entirely — 0 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.mdfrontmatter value cannot power it — it REQUIRES queryable columns. q_a_pairsALREADY 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_statusis NOT NULL and load-bearing in q_a_search/dedup). A facet UNIFIES the review queue / cadence / governance crons /whats_in_my_queueacross 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 filterspublication_statuson 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_tofor opt-in cert-expiry only; the RI lifecycle axes are DEFERRED to the Intelligence-domain track. entity_mentionsalso 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.
6. Ontology pass (owner §H)
Section titled “6. Ontology pass (owner §H)”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_itemsis encoded as THE Layer-5 “Knowledge artefact” ontology class (phase-b-feedback §5:482);content_typeis 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 FKscontent_items_id(extraction.py:261-264).- The LIVE semantic linter is the Pydantic gate in
extraction.py, NOT the markdown register: HARD-rejectcontent_type/form_type(field_validators readingtaxonomy_snapshot.json) +entity_type/relationship/form_format(hardcoded Literals); SOFT-WARNprimary_domain/primary_subtopic/secondary(counter + log). Allowed sets come fromtaxonomy_snapshot.json(DB-derived), kept in lockstep with the register bymarkdown-parity.test.ts. - The owner’s
allowed_types/allowed_relationsliterally do not exist (grep = 0 across repo + docs-site).entity_type(12) andrelationship(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_modelcore/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/ontologywhich no longer exists (register relocated to docs-site at ID-68.27); the loader was inverted (markdown-parity.test.ts:250-251asserts 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-vocabularyCV is an AUDIENCE axis, NOT the OKF 3-layer model — do not conflate.
The 9-item pass:
- Rewrite the README boundary rule so client-canonical knowledge routes to
{source_documents, q_a_pairs, reference_items}+ L-concepts, notcontent_items+content_item_workspaces. - Add a NEW 3-layer-model CV (L-raw / L-records / L-concepts), distinct from the audience-axis
03-layer-vocabulary. - 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. - Re-home the ~7 content_items-coupled column CVs onto
source_documents. - Promote
entity_type(12) +relationship(10) into NEW Layer-5/6 KG ontology CVs with per-value provenance — this is the literalallowed_types/allowed_relationsthe 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. - Add an OKF concept-frontmatter ontology + validator (concept
typeset + 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. - Fix the register repo-split: pick ONE source of truth (repoint
loader.tsat the docs-site register viaKH_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. - 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_contextfirst (5 flow_context tests referencecontent_items_id). - Sweep
content_itemsprose across the ~10 referencing CVs; refresh the stalewp6-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); extractorsextract_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 viadeclare_rowvs the example’s Neo4j viadeclare_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_idontosource_document_id(flow.py:2245chunks,:2381mentions,:2456relationships). 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-2run_web_pass()crawls seed URLs (host allowlist + depth) → enriches + createsreferences/<slug>;regenerate_indexes()buildsindex.md. - Lift-and-shift: the Source protocol,
prompts/(reference_instruction.md,web_ingestion_instruction.md), the bundle writer, and theviewer/. Note: the “sources/base.py” pattern is the reference_agent’s, NOT ours — we have noscripts/cocoindex_pipeline/sources/package (our sources arelocalfs.walk_dir+ a hand-rolledurl_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_filelands 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_filewith 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:/# Citationsat 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-21self-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) NORreference_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-requirementsskill 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, FKtemplate_requirement_id+form_template_id). - “Q&A pairs complete it” = T10 matching (
template-coverage.ts) + drafting →form_responses(source_content_ids) → filled back totemplate_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):
- 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 producescontent_chunksWITH 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 explicitqa_sidecarmapping (flow.py:2019-2029). - The DB seed — ALREADY built (
seed-synthetic-corpus.ts: 6 synthetic workspaces + ~30 questions- manifest emitter). Live staging
synthetic_ws=6; prodsynthetic_ws=0→ run it on prod too (owner R-2 resolved YES: “the Platform exists for E2E/dogfooding”).
- manifest emitter). Live staging
- A repeatable assertion set — expected row deltas per route/format after a walk
(
form_templatesN,source_documentsN,content_items-successors N,content_chunks> 0 with embeddings,reference_itemsfrom feed,pipeline_runsin_progress+terminal withstage_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.
10. Decision register (v3)
Section titled “10. Decision register (v3)”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.
| # | Decision | v3 call | Reversibility | Status |
|---|---|---|---|---|
| 3-LAYER | L-raw / L-records / L-concepts | Ratify (§1). Two of three layers client-owned; concepts cite records. | EXPENSIVE to reverse once built | RATIFIED (owner §A) |
| D5-v2 (SEED) | Record-identity seeds | source_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 write | REVISED — replaces v2 §2.3 |
| SEED-CONTRACT | Freeze seed STRING formats + _KH_PIPELINE_DOC_NS + add a test | Declare at ID-45; gate before Task-A bundle publication. | CHEAP to declare; FREEZE at publication | NEW — ratify |
| QA-DBONLY | Retire the {59.x} Q&A sidecar __qa__/*.md round-trip | Recommend 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-REPARENT | Add q_a_extractions.source_document_id + backfill | Before content_items drop (its source_content_item_id dies). | CHEAP | NEW — ratify |
| CI-DECOMP | Eliminate content_items | Fully 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 migration | RATIFIED (owner §A); current-head-corrected |
| CITE-EXT | Extend citations.cited_target_kind | Add {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-FIX | Resolve dual outcome source | get_content_win_rate (form_templates.outcome) vs hybrid_search.win_stats (workspaces.domain_metadata) — pick canonical before re-anchoring to q_a_pair. | CHEAP | NEW — ratify |
| (b) M2M | Drop content_item_workspaces | Reconfirmed; query-time scope_tag overlap. | CHEAP | RATIFIED (owner §A) |
| EMB-STORE | Dedicated polymorphic embeddings store | NET-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-FACET | Governance/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). | MEDIUM | NEW — supersedes v2 GOV-HOST; per-axis per D7 |
| EXTRACT-REPARENT | Entities STAY in DB; re-parent 3 write-sites | content_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). | CHEAP | NEW — corrects v2 §8 risk 5 |
| PRODUCER | OKF producer = wiki declare_file + reference_agent | NOT 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 EXPENSIVE | REVISED |
| ONTOLOGY-PASS | 9-item ontology re-alignment | Rewrite 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. | MEDIUM | NEW — ratify (owner §H) |
| content_templates | DROP with content_items | Vestigial (0 rows, unread). Re-home the skeleton idea to OKF concept-type templates (Task A). | CHEAP | NEW — ratify (owner §I) |
| FORMS | No 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 gate | NOT “shrink.” Seam-coverage file tree + DB seed (run on prod) + re-runnable release gate. Defer golden-snapshot freeze until after Task B. | CHEAP | REVISED — supersedes v2 {127.4} |
| SRC-URL | Drop source_url from content_items only | KEEP on SD (URL-ingest, load-bearing) + RI (NOT NULL). | CHEAP | RATIFIED (owner §Misc) |
| FULL-REPLACE | Full-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 |
| RAG | Reduce, don’t remove | Keep vector index (EMB-STORE) over bundle + long tail. | n/a | RATIFIED (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).
11. Task breakdown + sequencing
Section titled “11. Task breakdown + sequencing”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
impactbefore touchinghybrid_search,get_content_win_rate,_ExtractionStamp, governance symbols; ast-dataflowcolumn-reads/-writesto 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).
Sequencing
Section titled “Sequencing” ┌───────────────────────────────────────────────────────────────────┐ │ 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 parallelThe 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.
12. Biggest risks
Section titled “12. Biggest risks”-
Citation contract break during content_items elimination (HIGH).
cite_contentstill writescited_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_ratevshybrid_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. -
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. -
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.
-
Seed-contract drift orphans bundle citations (MEDIUM, irreversible-flavoured). Re-ingest reproduces uuids only if BOTH the seed strings AND
_KH_PIPELINE_DOC_NSare unchanged. If anyone “tidies” them after a bundle ships, everycanonical://<table>/<uuid>silently orphans. Mitigation: a frozen-contract test asserting the seed strings + namespace value, gated before Task-A publication. -
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).
-
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.tsagainst the new vector space. -
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.
-
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. -
Forms-matching blast radius under-counted (MEDIUM).
fetchContentForMatching+form_responses.source_content_ids+form_questions.matched_content_idsall 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. -
{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.