Skip to content

RECOVERED findings — 7 failed StructuredOutput agents (run wf_da6fa678-4c7)

RECOVERED findings — 7 failed StructuredOutput agents (run wf_da6fa678-4c7)

Section titled “RECOVERED findings — 7 failed StructuredOutput agents (run wf_da6fa678-4c7)”

==========================================================================================

E1 pipeline_runs completed-status provenance

Section titled “E1 pipeline_runs completed-status provenance”

(agent a1c58c5715f1fab28; 5 StructuredOutput attempts; best=5424 chars; fields=[‘id’, ‘question’, ‘verdict’])

{ “id”: “E1”, “question”: “What writes pipeline_runs.status=‘completed’ (the ~395/496 prod rows with 0 content_chunks) — E2E debris, a cron, or the pipeline — and do any current tests require a ‘completed’ value?”, “verdict”: “The ‘completed’ rows are overwhelmingly cron + MCP-tool heartbeat rows, NOT ingestion-pipeline content writes. Of 397 completed rows (prod zjqbrdctesqvouboziae, 498 total — this reconciles the doc’s “~395/496”: completed≈395 and total≈496, both drifted +2 since authoring), 220 (55%) are the daily review_cadence cron, 49 more are other governance/quality crons (publish_classify 42, quality_score 3, content_gaps 2, coverage_alert 2), 121 (30%) are mcp_create_content_item (manual/dogfooding MCP writes = the “debris” leg), 5 are form_draft_all, and ONLY 2 are the real ingestion pipeline (kh_canonical_pipeline, the only ones with op_id) — and those 2 ingestion runs produced 0 content_chunks, which is the genuine “pipeline never completed a content write” gap. Owner hypothesis is confirmed AND refined: it is the review_cadence cron + MCP debris, not E2E debris from the content pipeline; crons emit ‘completed’ even with items_processed=0, so ‘completed’ is decoupled from content_chunks by design. Multiple CURRENT integration/E2E tests assert on status=‘completed’, most notably the cocoindex ingestion E2E gate which BLOCKS until status=‘completed’ — so ‘completed’ is load-bearing and asserted-upon.\nSQL (zjqbrdctesqvouboziae) status counts: completed=397, failed=97, in_progress=2, completed_with_errors=1, running=1; total=498; content_chunks=0.\nSQL breakdown of completed by pipeline_name: review_cadence=220 (op_id/created_by all NULL, 2026-06-13..06-28 daily 03:45 UTC), mcp_create_content_item=121 (06-18..06-21), publish_classify=42, form_draft_all=5, quality_score=3, content_gaps=2, coverage_alert=2, kh_canonical_pipeline=2 (the only rows with op_id set; 0 chunks).\nCron writer path: app/api/cron/review-cadence/route.ts:39 imports recordPipelineRun; :110-131 calls it with pipelineName:‘review_cadence’. Helper lib/pipeline/record-run.ts:222 does supabase.from(‘pipeline_runs’).insert({status, …}); JSDoc example (:155-158) literally uses pipelineName:‘content_gaps’ status:‘completed’. Vercel cron schedule vercel.json:63 path /api/cron/review-cadence (also content-gaps, quality-score, coverage-alerts, datapath-watch).\nMCP/debris writer: lib/mcp/tools/content.ts:456,653,678,761,810 emit pipelineName:‘mcp_create_content_item’ (manual content_item creation via MCP tool, not the ingestion flow).\nIngestion-pipeline writer: scripts/cocoindex_pipeline/flow.py:3410 sets flow_status:PipelineRunStatus=‘completed’ (default), terminal emit :3868 _emit_pipeline_run_webhook(status=flow_status); fn def flow.py:837-858 POSTs to the Vercel webhook (“the ONLY path used to land pipeline_runs rows” for the sidecar) — pipeline_name=kh_canonical_pipeline.\nTests REQUIRE ‘completed’: tests/integration/cocoindex/test-helpers.ts:365-430 pollPipelineRunCompleted polls until pipeline_runs.status=‘completed’ for op_id (the ingestion E2E success gate); tests/integration/content-owner-id-and-pipeline-run.integration.test.ts:271-305 expect(data!.status).toBe(‘completed’); tests/integration/pipeline-runs-admin-update.integration.test.ts:160,169; tests/integration/queue/procurement-draft-all.integration.test.ts:682,699; tests/deploy/onprem/monitor/datapath-watch.test.ts:57,116,150,182 (datapath-watch monitor keys off status=‘completed’).\nrecordPipelineRun terminal-status contract: lib/pipeline/record-run.ts:210-214 isTerminal includes ‘completed’; :138 ‘completed’ => no Sentry (healthy path) — confirms crons legitimately write ‘completed’ as a heartbeat, independent of any content write.\nTreat status=‘completed’ as a heartbeat/observability signal, NOT a content-write success signal — it is dominated by the review_cadence cron and MCP debris, and the cocoindex E2E gate (pollPipelineRunCompleted) currently PASSES on a run that wrote 0 content_chunks (exactly the 2 kh_canonical_pipeline rows). For id-134 §9 / Task C the promotion gate must NOT rely on status=‘completed’ alone: strengthen the {127.4} assertion set to additionally require content_chunks>0 WITH embeddings (and source_documents/q_a_pairs/reference_items deltas) scoped to the run’s op_id — which is precisely the v3 §9 direction. When content_items is eliminated, the mcp_create_content_item writer (lib/mcp/tools/content.ts) and the cron pipeline_names need no schema change, but the prod table should be truncated of the 121 mcp_create_content_item + cron debris rows before any release-gate baseline is captured. No owner decision is created by this finding; it confirms the §9 reframe.\nid-134 §9 / Task C promotion-confidence E2E gate (content_chunks>0 assertion)\nokf-record-model-v3 §9 (the “0 content_chunks on prod despite 33 content_items + 496 pipeline_runs” gap statement — numbers now 0/33/498)\nEMPIRICAL_ANSWERED\nfalse\nhigh\n\n” }

==========================================================================================

E2 classification-col pipeline-writer audit

Section titled “E2 classification-col pipeline-writer audit”

(agent a30da2aac7266a780; 5 StructuredOutput attempts; best=6188 chars; fields=[‘id’, ‘question’, ‘verdict’])

{ “id”: “E2”, “question”: “For each of the ~25-30 classification cols M3 ADDs to source_documents, does the cocoindex pipeline CURRENTLY write it onto content_items, or is it written-elsewhere / dead / never-written?”, “verdict”: “The cocoindex pipeline writes ONLY 3 classification cols onto content_items: content_type, primary_domain, primary_subtopic (plus title, derived from suggested_title) — flow.py:2173-2195. Every other classification-family col (secondary_domain, secondary_subtopic, ai_keywords, summary, suggested_title-as-its-own-column, classification_confidence, classification_reasoning, classified_at, updated_by) is written ONLY by the SEPARATE app-side TS classifier lib/ai/classify.ts::classifyContent, NOT the pipeline. captured_date and summary_data are not classification outputs at all (captured_date = ingest timestamp set by app item/upload routes; summary_data = the separate summarise feature). classification_model is genuinely dead — 0 of 33 prod rows, no writer anywhere — the one true vestige that should NOT be ported.\nflow.py:2173-2195 ci_target.declare_row (the pipeline’s ONLY content_items writer) sets from classification: content_type (2183), title (2181, = suggested_title ?? filename-stem), primary_domain (2192), primary_subtopic (2193) — and nothing else from the family.\nextraction.py:451-468 ClassificationExtraction CORE fields = content_type, primary_domain, primary_subtopic, suggested_title, classification_confidence, secondary_classifications (list[str]), rationale. The model has NO ai_keywords, summary, secondary_domain/secondary_subtopic, classified_at, or classification_model. classification_confidence + rationale ARE extracted by the pipeline but DISCARDED (never reach the declare_row).\nlib/ai/classify.ts:1685-1700 updateData writes the full app-side family to content_items: primary_domain, primary_subtopic, secondary_domain, secondary_subtopic, ai_keywords, summary, suggested_title, classification_confidence, classification_reasoning, classified_at, updated_by. classifyContent has many LIVE callers (app/api/upload/route.ts:1080, app/api/items/route.ts:523, items/[id]/route.ts:798, items/batch/route.ts:315, cron/classification-quality:148, lib/mcp/tools/ai.ts:64, governance.ts:499) — it is a real, currently-wired production writer, not dead.\nflow.py:2961-2988 ri_target.declare_row (reference_items, NOT content_items) writes summary=item.summary (feed-declared RSS summary) + primary_domain/primary_subtopic; this is the feed/reference path, so the content_items.summary column is never written by the pipeline.\nSQL content_items (project zjqbrdctesqvouboziae, 33 rows total): primary_domain/subtopic non-sentinel=13, secondary_domain/subtopic=10, ai_keywords=13, summary=13, suggested_title=13, classified_at=13, classification_confidence=13, classification_reasoning=13, captured_date=13, updated_by=3, summary_data=1, classification_model=0. The 13-row cohort = app-side classifyContent output.\nclassification_model: only READ at lib/provenance/item-provenance.ts:121/136/140 (item.classification_model ?? env_default), zero writers in lib/ app/ scripts/; 0 prod rows. captured_date writers = app routes only: app/api/items/route.ts:212, items/batch/route.ts:246, procurement/…/outcome/integrate/route.ts:248 (all = new Date(), i.e. ingest time). summary_data writers = lib/ai/summarise.ts:233 + app/api/summaries/generate/route.ts (summarise feature). updated_at = PG default now()/trigger (flow.py:1267-1269 omits it deliberately).\nTECH.md (id-131) line 290 M3 list confirms the ~25-30 cols; PRODUCT.md:181-182 + RESEARCH.md:242-243 + okf-record-model-v3.md:258 all state the classification family is net-new on SD (“SD carries none today”).\nPort-as-real-pipeline-output (3): content_type, primary_domain, primary_subtopic — these the cocoindex pipeline already emits; they are genuine pipeline outputs and map straight onto SD. Port-but-FLAG-as-not-yet-pipeline-written (8): secondary_domain, secondary_subtopic, ai_keywords, summary, suggested_title, classification_confidence, classification_reasoning, classified_at — these are real classification data but are produced ONLY by the app-side lib/ai/classify.ts, which is NOT the cocoindex pipeline (and partly not even in the pipeline’s ClassificationExtraction shape: ai_keywords/summary/secondary_domain/secondary_subtopic are absent from the Pydantic model; confidence+rationale are extracted then discarded). If content_items is eliminated and the app-side classifier is retired/absorbed (collapse-list §5.1 RATIFIED-RETIRE for batch-reclassify), these 8 SD columns will be net-new-and-never-written UNLESS extraction.py + flow.py are extended to emit them — call this out in §3.2 as an M3 prerequisite, not a free ADD. Non-classification (2): captured_date (ingest timestamp — port as an SD ingest/captured field, owned by the write-path not the classifier) and summary_data (separate summarise feature — port only if that feature is retained). True VESTIGE / DO-NOT-PORT (1): classification_model — 0 rows, no writer, read-only with env fallback; drop it (or only add if a deliberate model-stamp is wanted going forward). updated_at/updated_by are audit cols (DB-defaulted / set by whatever writer), not classification signals.\nid-131 §3.2 column homes / M3 (id131_sd_classification_cols)\nid-131 TECH.md:290 M3 migration — reclassify the ~16 ADD cols into {pipeline-written:3, app-side-classifier-only:8, non-classification:2, dead:1, audit:2}\nid-132 okf-concept-producer / classifier absorption — extraction.py ClassificationExtraction must gain ai_keywords/summary/secondary_* + flow.py must persist confidence/reasoning if the app-side classify.ts is retired\nEMPIRICAL_ANSWERED\ntrue\nhigh\n\n” }

==========================================================================================

E4 declare_vector_index avoidance rationale

Section titled “E4 declare_vector_index avoidance rationale”

(agent a780069a9c03dc40e; 5 StructuredOutput attempts; best=3807 chars; fields=[‘id’, ‘question’, ‘verdict’])

{ “id”: “E4”, “question”: “Why does the pipeline deliberately AVOID cocoindex declare_vector_index? Are embeddings added but the pgvector index managed via Supabase migration instead?”, “verdict”: “Yes, the owner’s reading is exactly right. The pipeline DOES compute and write embeddings; it just does not let cocoindex create the search index. Stage-4 of the flow embeds every document’s text with OpenAI’s text-embedding-3-large (a 1024-number vector) and stores that vector in the row alongside the document. Separately, the pgvector index that makes those vectors searchable (an HNSW cosine index) is created by a hand-written Supabase migration, not by cocoindex. The reason is a governance rule: cocoindex’s declare_vector_index would silently run a CREATE INDEX schema-change command from inside the pipeline, bypassing the safeguard (managed_by=USER) that otherwise stops cocoindex from altering DB structure, and the project’s hard rule is that ALL schema/DDL changes go through Supabase migrations only while the pipeline writes row DATA exclusively.\nflow.py:997-998 — EMBEDDING_MODEL=‘text-embedding-3-large’, EMBEDDING_DIMENSIONS=1024; the pipeline computes the embedding itself\nflow.py:1135-1166 — embed_content_text() wraps LiteLLMEmbedder.embed() and returns a list[float] vector encoded by the pgvector encoder on the ‘embedding’ ColumnDef\nflow.py:2166 and 2184 — Stage-4 block: ‘embedding = await embed_content_text(content_text)’ then the value is passed into the content_items declare_row payload as the ‘embedding’ field (embeddings ARE persisted, not a None stub)\nflow.py:3662-3673 — rationale comment: the HNSW index is NOT declared here, it is migration-owned (idx_content_items_embedding); declare_vector_index would issue out-of-band CREATE INDEX DDL via a vector_index attachment NOT gated by managed_by=USER (the USER gate only suppresses table/column DDL), conflicting with the ‘DDL via Supabase CLI migrations only’ rule and the row-only target contract, so the route is deliberately avoided. See the ID-49.2 journal OQ.\nsupabase/migrations/20260617130000_squash_baseline.sql:8722 — CREATE INDEX idx_content_items_embedding … USING hnsw (embedding vector_cosine_ops); the index is created by a Supabase migration, confirming it is migration-managed (also idx_content_chunks_embedding:8654, idx_reference_items_embedding:9210)\nscripts/tests/test_cocoindex_flow_embedding.py:16-21 and the dedicated test test_ingest_file_does_not_declare_vector_index (line 294) lock in that ingest_file declares the embedding VALUE only and never calls declare_vector_index on any target\nConfirm to the owner: YES — embeddings are written by the pipeline today, and the index is intentionally created by Supabase migration so there is one single reviewable path for all database-structure changes. This is not a limitation, it is a deliberate separation of duties (pipeline = data, migrations = schema). For v3 §7.3 EMB-STORE, carry the exact same pattern forward: the new polymorphic embeddings store should have its pgvector/HNSW index declared in a Supabase migration, and the producer/pipeline should write only the vector VALUES — do NOT use cocoindex declare_vector_index, because it escapes the managed_by=USER DDL gate. No design change needed; this is an already-settled, test-enforced convention.\nid-131 §5.1 EMB-STORE (owner question)\nokf-record-model-v3 §7.3 EMB-STORE\nEMPIRICAL_ANSWERED\nfalse\nhigh\n\n” }

==========================================================================================

(agent ae8fa327b3041eee4; 5 StructuredOutput attempts; best=5161 chars; fields=[‘id’, ‘question’, ‘verdict’])

{ “id”: “E5”, “question”: “v3 §8.2 re-points forms matching to “q_a_pairs (primary) + reference_items + source_documents”. Does form matching/filling genuinely need SD+RI, or only q_a_pairs?”, “verdict”: “Matching genuinely needs q_a_pairs (primary) and CAN legitimately include reference_items, but source_documents should NOT be a direct match target. The matcher is fundamentally an embedding cosine search that feeds draftable text into an LLM: q_a_pairs (question_embedding + answer_standard/advanced) and reference_items (embedding + body/summary + taxonomy) both satisfy this and carry citeable answer content, whereas source_documents has NO embedding column and no answer-grained content (only a whole-file extracted_text dump + provenance cols). The correct grain for matching document bodies is content_chunks (embedding-bearing), surfacing the parent source_document only as a citation/provenance link. So v3 §8.2’s third leg is over-stated: re-point to “q_a_pairs (primary) + reference_items”, with source_documents demoted to a provenance/citation-resolution target (via content_chunks), not a vector-match source.\n<parameter name=“evidence”>LIVE forms matcher reads only content_items and ALL content_types: app/api/procurement/[id]/questions/match/route.ts:131 calls rpc(‘search_for_form_response’); body at supabase/migrations/20260617130000_squash_baseline.sql:5102-5121 = ‘FROM content_items ci … WHERE ci.embedding IS NOT NULL AND (1 - (ci.embedding <=> query_embedding)) > 0.25’ (no content_type filter).\nMatcher consumes embedding (hard-required), title ILIKE boost (squash_baseline:5107), ai_keywords boost (:5109). Drafter consumes content/suggested_title/content_type/summary: lib/domains/procurement/draft-response.ts:94-114 selects ‘id, suggested_title, content, content_type, summary’ from content_items; same in app/api/procurement/[id]/responses/[rId]/route.ts:103-109.\nfetchContentForMatching (the file named in the brief) is the SEPARATE coverage/gap engine, also content_items-backed: lib/domains/procurement/form-templating/template-coverage.ts:519-527 selects ‘id, content, brief, detail, title, suggested_title, primary_domain, primary_subtopic, content_type, ai_keywords, embedding’ from content_items.\nLive schema (SQL, project zjqbrdctesqvouboziae): source_documents columns = id, filename, …, content_hash, version, parent_id, storage_path, status, extracted_text, extraction_metadata, … source_url — NO embedding, NO title/summary/content. reference_items HAS embedding + title + body + summary + primary_domain + primary_subtopic. q_a_pairs HAS question_embedding + answer_standard + answer_advanced + publication_status. content_chunks HAS embedding + content.\nLive content_items pool (non-archived): article=21 (1 embedded), q_a_pair=10 (10 embedded), note=2 (2 embedded); the RPC requires embedding IS NOT NULL, so the actually-matchable pool is 13 rows, 10 of them q_a_pairs.\nid-130 spec is q_a_pairs-only for match candidates and never names RI or SD: PRODUCT.md:426 + :436 (“match candidates come from the corpus-level q_a_pairs”); TECH.md:522 (“Match candidates still come from corpus-level q_a_pairs (guardrail)”).\nNo live form-matching data exists yet to migrate: form_questions.matched_content_ids and form_responses.source_content_ids are both empty (SQL returned []), so this is a forward design call, matching v3 Risk #9 (okf-record-model-v3.md:870-873) which itself defers “whether matching reads q_a_pairs only or a polymorphic union”.\nRecommended: re-point the matcher to a q_a_pairs-primary + reference_items union and DROP source_documents as a direct match source. Rationale: only those two carry an embedding AND draftable text; SD has neither. If document-body matching is desired, add content_chunks (embedding + content) as the doc grain and resolve hits to the parent source_document as a CITATION/provenance link, not a match candidate. Note a real rewrite caveat: q_a_pairs lacks title/ai_keywords/primary_domain/subtopic, so the matcher’s title-ILIKE, keyword-overlap and taxonomy tiers go dark for Q&A (it degrades to pure question_embedding cosine) — the rewrite must either accept vector-only for Q&A or derive those signals. Alternative (v3’s literal text): keep SD in the union, but it would only ever match via a joined content_chunks embedding, so naming “source_documents” as a match source is misleading and should be reworded to “content_chunks → source_documents provenance”.\nid-131 §8.2 forms re-point (FORMS / fetchContentForMatching + source_content_ids/matched_content_ids uuid[])\nid-131 Risk #9 (forms-matching polymorphic-union decision)\nid-130 §B-20 / T-B20 composer match-candidate source (corpus q_a_pairs guardrail)\nDESIGN_DECISION_NEEDED\ntrue\nhigh\n\n” }

==========================================================================================

(agent a3e6bb6466e5d9ebc; 5 StructuredOutput attempts; best=5820 chars; fields=[‘classification’, ‘confidence’, ‘id’, ‘ownerDecisionNeeded’, ‘question’, ‘verdict’])

{ “id”: “E7”, “question”: “Define the required manual q_a_pair authoring shape (BI-33, deferred) and where provenance comes from when it is not a source_document.”, “verdict”: “A manual q_a_pair authoring shape is fully supported by the EXISTING q_a_pairs schema with zero migration: write directly to q_a_pairs (opaque gen_random_uuid PK) with question_text (NOT NULL), answer_standard (NOT NULL), answer_advanced (nullable), scope_tag text[] (default ’{}’), publication_status ‘draft’, and an origin_kind that records the manual origin. Provenance is a discriminated union keyed by origin_kind, NOT a mandatory source_document_id: source_document_id is NULLABLE and is set ONLY when the hand-authored pair derives from a known ingested document; when there is no source document the provenance is the origin_kind value itself (the human is the origin) plus optional source_workspace_id — exactly as the live form-response path already does (origin_kind=‘derived_from_form_response’ with source_document_id NULL and form-lineage FKs instead). EP8 (ep8-qa-docx-import-ui-spec) is the bulk .docx IMPORT surface and writes to the doomed content_items table (content_type=‘q_a_pair’), so it is a useful field reference but the WRONG target table under v3 — it is not the single-pair manual authoring form the owner now requires.\n<parameter name=“evidence”>ep8 spec located: /Users/liamj/Documents/development/knowledge-hub-docs-site/src/content/docs/specs/ep8-qa-docx-import-ui-spec.md (v5, S209) — §2 Goal 1 + §7.1 payload write to content_items with content_type=‘q_a_pair’ (legacy IMS god-table, eliminated in v3); it is admin bulk-docx import wrapping scripts/import_bid_library.py, NOT a manual single-pair form.||Live q_a_pairs schema (project zjqbrdctesqvouboziae): id uuid DEFAULT gen_random_uuid() (opaque); question_text NOT NULL; answer_standard NOT NULL; answer_advanced nullable; scope_tag text[] NOT NULL DEFAULT ’{}’; anti_scope_tag text[] NOT NULL DEFAULT ’{}’; alternate_question_phrasings text[] NOT NULL DEFAULT ’{}’; origin_kind text NOT NULL DEFAULT ‘curated_explicit’; publication_status text NOT NULL DEFAULT ‘draft’; source_document_id uuid NULLABLE; source_form_response_id/source_question_id/source_form_template_id/source_workspace_id all uuid NULLABLE; question_embedding vector nullable.||CHECK constraints: q_a_pairs_origin_kind_check ∈ {extracted_from_corpus, curated_explicit, derived_from_form_response, imported_legacy}; q_a_pairs_publication_status_check ∈ {draft, in_review, published, archived}.||Create path 1 (corpus pipeline): lib/q-a-pairs/promote-corpus.ts:382-392 sets origin_kind:‘extracted_from_corpus’, publication_status:‘draft’; provenance source_document_id set later via emitCorpusSidecar at :606-635 = sdUuid5(relPath).||Create path 2 (form-response, the no-source-document precedent): app/api/q-a-pairs/promote/route.ts:178-197 sets origin_kind:‘derived_from_form_response’ with provenance from source_form_response_id/source_question_id/source_form_template_id/source_workspace_id and NO source_document_id.||Edit path: app/api/q-a-pairs/[id]/route.ts (USER-DIRECT PATCH) materialises a sidecar + sets source_document_id=sdUuid5(relPath) on first edit only if NULL (:276-286).||NO manual create path exists: grep across lib/app/scripts shows zero writers for origin_kind ‘curated_explicit’ (the DEFAULT) or ‘imported_legacy’, and zero export async function POST in app/api/q-a-pairs. The only generic create verb (app/item/new -> POST /api/items -> lib/mcp/tools/content.ts) writes content_items, removed by BI-33/BI-34.||RLS: q_a_pairs_insert WITH CHECK auth.role() ∈ {authenticated, service_role} — any authenticated user may insert (admin gate is route-level, not DB).||BI-33 = RATIFIED DEFER: PRODUCT.md:334-344 (shape = question + answer_standard + answer_advanced + scope_tag[], opaque PK + recorded provenance origin); TECH.md:503-507 (scaffold OUT of ID-131, target id-71 {71.16}/ID-59/ID-69); .user-scratch/main_session_output.md:1068 + :1277 TECH-carry note recommends the later scaffold add a distinct ‘manually_authored’ origin_kind for lineage auditability; v3 §2.2 recommends RETIRING the Q&A sidecar file round-trip (a Q&A pair is a record, never a file/concept).\n<parameter name=“recommendation”>Scope a new ID-131 sub-task “manual q_a_pair authoring” = a POST /api/q-a-pairs route (getAuthorisedClient editor/admin) + minimal form that INSERTs directly to q_a_pairs. Authoring fields: question_text, answer_standard (both required), answer_advanced (optional), scope_tag[] (optional, default []), publication_status=‘draft’ (review-before-publish, mirroring the form-response path), question_embedding populated post-insert. Provenance: source_document_id is OPTIONAL/nullable — populate only if the author cites an ingested document; otherwise leave NULL and let origin_kind carry the provenance, optionally stamping source_workspace_id for where-authored context. RECOMMENDED design call: add a distinct ‘manually_authored’ origin_kind value (one-line CHECK migration) rather than overloading the ‘curated_explicit’ default, so hand-authored pairs are auditably distinguishable from the default; ALTERNATIVE is to reuse ‘curated_explicit’ (zero migration, it is already the writer-less default). Do NOT write a sidecar file for these (v3 §2.2: keep q_a_pairs.source_document_id as pure-DB provenance, retire the corpus sidecar round-trip). Do NOT resurrect EP8’s content_items target — port only its field mapping (question->title, answer_standard/advanced) onto q_a_pairs.\n<parameter name=“affects”>id-131 new sub-task (manual q_a authoring) / BI-33”, “classification”: “NEW_SUBTASK”, “ownerDecisionNeeded”: true, “confidence”: “high” }

==========================================================================================

E8 OKF external alignment (Google knowledge-catalog)

Section titled “E8 OKF external alignment (Google knowledge-catalog)”

(agent a5902f733beb53682; 5 StructuredOutput attempts; best=6301 chars; fields=[‘id’, ‘question’, ‘verdict’])

{ “id”: “E8”, “question”: “Is Canonical using OKF correctly? Was knowledge-catalog toolbox/enrichment/src (fileset.ts) reviewed for our enrichment agent, or only the BigQuery agent? Could fileset.ts benefit us over our Source-adapter/sources.py plan? Any OKF-correctness gap?”, “verdict”: “Our id-132 design IS OKF-conformant with no hard correctness gap, but the TypeScript toolbox/enrichment/src (and fileset.ts) was NOT reviewed: every spec plus the v3 doc grounds exclusively on the Python okf/src/reference_agent, whose only concrete Source is BigQuery (sources/bigquery.py). The question’s premise is a mis-framing — fileset.ts is NOT an alternative to sources.py; it is MarkdownFileset, a list/read/keyword-search-over-a-markdown-directory MCP tool used as an enrichment-CONTEXT source, while the TS toolbox’s actual asset-source is BigQuery-via-mdcode. The true analogue of our planned l_records.py Source adapter is the Python sources/base.py, which our spec already lifted correctly. fileset.ts offers a modest, optional win: a framework-agnostic ready-made pattern for our Pass-2 gated-corpus reader (our L-raw is a local markdown filesystem, which MarkdownFileset fits more cleanly than the web-fetch adaptation TECH currently plans).\ngrep for toolbox|fileset|kcenrich|kcagent|kcmd|mdcode|md-fileset|MarkdownFileset across all 4 spec dirs (id-131..134) + .user-scratch/okf-record-model-v3.md = ZERO hits → toolbox/enrichment was never reviewed\nid-132 PRODUCT.md:59-67 and TECH.md:40-68 ground only on reference_agent (“the reference_agent sources/base.py pattern is theirs, not ours”); grep -rcil reference_agent on id-132/.md matches both files → only the Python reference_agent (BigQuery its sole concrete Source) was reviewed\nGitHub repo tree shows TWO distinct enrichment impls: Python okf/src/reference_agent/sources/{base.py,bigquery.py} (Source ABC, ADK+Gemini, two-pass, bundle/ writer, viewer/, web/fetcher.py) vs TypeScript toolbox/enrichment/src/{agent/, tools/md/fileset.ts} (separate, ADK+Gemini-2.5-flash, MCP+SKILL.md harness, metadata-as-code via mdcode/kcmd)\nfileset.ts = class MarkdownFileset(root) with listContents/readFile/searchContents over a .md directory tree (path-traversal-safe safePath); toolbox README exposes it as md-fileset MCP server tools list_fileset_contents/read_fileset_file/search_fileset_content via a SKILL.md — it is enrichment CONTEXT, not the primary asset Source (primary assets come from BigQuery via mdcode kcmd pull)\nokf/src/reference_agent/sources/base.py: Source ABC methods are list_concepts() / read_concept() / sample_rows(); read_concept_raw is the agent-tool wrapper in tools/source_tools.py. Our TECH.md:206-208 labels the protocol method read_concept_raw — minor naming imprecision (tool name vs ABC method)\nOKF SPEC.md (v0.1): concept = one markdown doc, concept-ID = file path; only type is required frontmatter; resource=recommended URI of underlying asset; # Citations + index.md/log.md are optional conventions; consumers MUST tolerate missing optional fields / unknown types / broken links\nid-132 alignment: v3 §1 “Concept-identity = the file path”; PRODUCT.md:33-37,210-225 (index.md/log.md, resource: URI, # Citations, BI-12 required frontmatter type+title+description+timestamp+resource+tags, BI-13 validator gate, “evaluate lifting Google’s okf-skills validator”) — all match OKF; BI-12 is a producer-side SUPERSET of SPEC’s type-only minimum, which is OKF-legal for a producer\nPass-2 plan today: PRODUCT.md:244-245 + TECH.md:216-227 require Pass-2 enrichment “gated to the authoritative corpus, NOT the open web,” implemented as a url_source.py/web-fetch adaptation + charset_normalizer + host-allowlist/depth knobs; our gated L-raw corpus is a LOCAL filesystem (Phew corpus at /Users/liamj/…/local-fs-client/corpus) → MarkdownFileset is a more natural fit for that local reader\nconcept→concept cross-links: acknowledged at PRODUCT.md:139 (“inbound concept→concept references” re-pointed via log.md on moves) but body-level markdown bundle-relative link mechanics (OKF cross-linking convention / blog “join paths”) are under-specified; OKF does not require cross-links (consumers tolerate absence) → quality opportunity, not a conformance gap\nTreat as a low-stakes enhancement, not a blocker. (1) OKF-correctness: CONFIRMED conformant — keep the producer-side strict validator (BI-12/BI-13) but add one forward note that any future Canonical OKF CONSUMER (MCP reading the bundle) must be permissive per SPEC’s consumer model (tolerate missing optional fields / unknown types / broken links), since our write-gate is stricter than the read contract should be. (2) fileset.ts: do NOT adopt it as a Source adapter (wrong role); DO add a one-line note to id-132 TECH that the Pass-2 gated-corpus reader should lift the MarkdownFileset list/read/search pattern (a ~120-line framework-agnostic Node fs module) instead of bending a web-fetcher to a local path — lift the class/algorithm, not the ADK+Gemini harness (we standardise on Anthropic tool-use). Caveat: MarkdownFileset only handles .md; docx/pdf in L-raw are already extracted into source_documents.extracted_text and read via the Source adapter, so fileset only covers the raw-markdown slice. (3) Fix the cosmetic protocol-naming slip in TECH.md:206-208 (read_concept is the ABC method; read_concept_raw is the tool). (4) Optionally tighten the concept→concept markdown cross-link / join-path convention in TECH to fully exploit OKF’s (optional) linking model.\nid-132 PRODUCT.md (Pass-2 gated-corpus reader source; OKF consumer-permissiveness forward note)\nid-132 TECH.md:206-208 (Source-protocol method naming read_concept vs read_concept_raw) and TECH.md:216-227 (Pass-2 reader: consider lifting MarkdownFileset over web-fetch adaptation)\nEMPIRICAL_ANSWERED\nfalse\nhigh\n\n” }

==========================================================================================

E11 dead-loader: DB-based register feasibility

Section titled “E11 dead-loader: DB-based register feasibility”

(agent a29e107229dffdc11; 5 StructuredOutput attempts; best=6577 chars; fields=[‘affects’, ‘classification’, ‘confidence’, ‘id’, ‘ownerDecisionNeeded’, ‘question’, ‘recommendation’, ‘verdict’])

{ “id”: “E11”, “question”: “id-133 BI-7 / Decision A retires the dead lib/ontology/loader.ts and makes the docs-site register/fixture authoritative. Owner pushback: could the register instead be DB-based (core) plus client rather than retired, and is that a viable third option that beats Decision A?”, “verdict”: “DB-based core plus client is a viable, well-grounded option and in fact already exists and is authoritative for the open/hybrid CVs (taxonomy_domains, taxonomy_subtopics, layer_vocabulary are DB tables with a provenance column plus a recommend-then-accept workflow; taxonomy_snapshot.json is derived from them). But it does NOT beat Decision A for BI-7’s actual scope, for two separable reasons. First, the loader being retired is the dead markdown reader (gitnexus shows no incoming callers and no process participation); it is dead under every storage model because the runtime already reads the build-time generated tuple for closed enums or the DB-derived snapshot for open dims, never the markdown, so a DB register makes it more dead not resurrected. Second, the register the owner actually names, entity_type and relationship (allowed_types/allowed_relations), is product-CLOSED at v1 (Decision B sets client_extensible false), enforced by hardcoded Python Literals plus DB CHECK constraints, so a provenance-bearing register table would buy zero v1 payoff while adding a further source to keep in lockstep, the exact v3-risk-8 drift hazard Decision A collapses. The owner is right in principle but for the extensible CVs, which already use the DB pattern; the markdown register is correctly reserved for the closed/core CVs.\n<parameter name=“evidence”>[“gitnexus context loadOntologyCVs repo canonical: no incoming callers, no process participation; confirmed dead with no production caller”, “lib/ontology/loader.ts:29 ONTOLOGY_DIR resolves to docs/ontology which no longer exists (register went private at ID-68.27); loader survives only as a parity-test tripwire”, “id-133 TECH.md lines 139-155 Decision A: CHOSEN is retire loadOntologyCVs and ONTOLOGY_DIR; REJECTED is repointing the loader at the docs-site register (adds a reader, reintroduces a node:fs private read, does not remove drift). Authority-chain table lines 158-164: markdown is human source, snapshot/fixture/generated-tuple/code-Literals are derived mirrors held by parity guards”, “SQL on zjqbrdctesqvouboziae: DB already has taxonomy_domains 7 rows, taxonomy_subtopics 34 rows, layer_vocabulary 4 rows, taxonomy_sync_state 1 row. taxonomy_domains and taxonomy_subtopics carry NOT-NULL provenance plus recommended_by, recommended_at, accepted_at columns: a DB-backed core-plus-client register with a recommend-then-accept workflow. All current provenance values are baseline (core)”, “scripts/generate-taxonomy-snapshot.ts lines 52-74 select taxonomy_domains and taxonomy_subtopics FROM the live DB into taxonomy_snapshot.json, so for the open dimensions the DB IS the source of truth and the snapshot is the derived mirror (the inverse of closed enums where markdown is the source)”, “phase-b-prerequisite-1-onthology-pipeline-feedback-investigation.md sections 2.5 and 6, plus phase-b-prerequisite-1-onthology-pipeline.md lines 356-359 and 523: Liam ratified Option (c) hybrid with provenance (S235 Q-OQR1-11, adopt across all hybrid vocabularies), explicitly mirroring the taxonomy_domains.provenance precedent, DB table authoritative for runtime config and code registry kept as contract. The owner’s instinct is the ratified pattern for HYBRID CVs”, “SQL CHECK constraints: entity_mentions_entity_type_check enumerates 12 entity types and entity_relationships_relationship_type_check enumerates 10 predicates, so the KG vocab already has a DB footprint; entity_aliases_provenance_check (core/client/recommended) shows the provenance pattern already spread. No allowed_types or allowed_relations register TABLE exists”, “scripts/cocoindex_pipeline/extraction.py lines 378-391 (entity_type) and 423-434 (relationship) are hardcoded Pydantic Literals: a closed HARD-reject runtime gate, not read from DB or snapshot. id-133 TECH.md lines 201-204 Decision B sets these provenance core, client_extensible false, closed at v1, client-extension research-deferred”, “id-133 TECH.md line 447 v3-risk-8: promoting the KG vocabularies adds a third source (MEDIUM), mitigated by parity guards binding register, fixture, Python Literal and TS const; a DB register would be yet another source under the same hazard”]”, “recommendation”: “KEEP Decision A / BI-7 as-is: retire the dead markdown loader (orthogonal to and unaffected by the storage-model question) and keep the markdown register as the human source-of-truth for the closed/core CVs. RE-FRAME for the owner rather than treating the pushback as a defect: the DB-based core-plus-client register he wants already exists and is authoritative for the genuinely client-extensible CVs (taxonomy_domains, taxonomy_subtopics, layer_vocabulary, provenance baseline-core plus recommend/accept client, snapshot derived from DB). The markdown register is deliberately reserved for the closed/core CVs that are HARD-rejected and product-closed at v1 (content_type, entity_type, relationship). The owner’s named target, entity_type and relationship, is the lever: it is product-CLOSED at v1 (Decision B), so a DB register buys nothing now and adds a lockstep source. RECOMMENDED: ratify Decision A and record the forward path explicitly. If and when the product decides entity_type/relationship should be client-extensible (the research-deferred call in Decision B), promote them to the SAME DB-backed provenance pattern as taxonomy_domains (per ratified S235 Q-OQR1-11), NOT to the markdown register; Decision B’s optional per-value provenance keys (provenance_model, client_extensible, editable_via) are the forward-compat bridge because they are exactly the columns that future table would carry. ALTERNATIVE if the owner wants client-extensible entity types at v1: make entity_type/relationship hybrid now, but that is a product-scope expansion (custom KG ontology per client) with real cost in the cocoindex Pydantic gate (the Literal must become a runtime DB-read), not a storage-of-a-dead-loader question, and should be costed as its own subtask.”, “affects”: [ “id-133 BI-7 / Decision A”, “id-133 Decision B (per-value provenance keys as DB forward-compat bridge)”, “okf-record-model-v3 section 6 ontology pass” ], “classification”: “DESIGN_DECISION_NEEDED”, “ownerDecisionNeeded”: true, “confidence”: “high” }