ID-59 {59.25} PRODUCT — Q&A markdown-sidecar canonical model (sidecar-emit promoted to v1)
ID-59 {59.25} PRODUCT — Q&A markdown-sidecar canonical model
Section titled “ID-59 {59.25} PRODUCT — Q&A markdown-sidecar canonical model”Status: PRODUCT draft — authored 20/06/2026 by a FRESH planner instance (Q-PLANNER-2). This is a SEPARATE feature from the two sibling PRODUCT specs in this dir (
PRODUCT.mdper-UC write-back +edit_intent;PRODUCT-qa-corpus-promotion.mdroute-i promotion); it does NOT overwrite either. Inputs read in full:PRODUCT.md(esp the three “Q&A file-sidecar materialisation — DEFERRED-v1.1” notes at :140, :189, :147), the ratifiedPRODUCT-qa-corpus-promotion.md(INV-1..16) +TECH-qa-corpus-promotion.md(R1promoteCorpusExtractions()as-built), the built code (lib/q-a-pairs/promote-corpus.ts,lib/edit-intent/write-back.ts,app/api/q-a-pairs/[id]/route.ts), the cocoindex flow (scripts/cocoindex_pipeline/flow.py,adapters.py,extraction.py), and the canonical architecture/decision docs (05-qa-flow.md§4–§5,decision-graph.mdQ2.9/Q3.2/:43).This PRODUCT is a RATIFIED REVERSAL (Liam, S380). Markdown sidecar-emit was always the INTENDED canonical Q&A form but is currently ratified DEFERRED-v1.1 (
decision-graph.md:43records “Markdown sidecar emit = DEFERRED-v1.1, not reopened to v1”;05-qa-flow.md §4.4reaffirms the deferral). Liam has ratified promoting sidecar-emit to v1 and specifying it NOW, before ID-45. The required doc-corrections are surfaced as INTENTS in §Doc-update intents (the Orchestrator routes them to the Curator / docs-site lane).Sequence note (load-bearing): the sidecar file convention + walk skip/route gate (INV-1..INV-6) is the gate that ID-45
{45.3}TECH references (the re-ingest walk must know to skip/route Q&A sidecars, not mint junk content_items). The convention MUST be frozen by this PRODUCT before{45.3}TECH is ratified.{45.3}is being authored in parallel — this is a cross-Task ordering dependency tracked at Task level, not a sibling dep.
Summary
Section titled “Summary”The markdown sidecar file becomes the canonical form of a Q&A pair: an in-platform Q&A
edit, a corpus promotion, and a re-walk all converge on one .md file in the source folder,
and the q_a_pairs row + q_a_extractions row are DERIVED projections of that file. This
realises the Wikipedia Principle for Q&A (one golden record, many views) and resolves the
multiple-representations problem — one canonical file projects to two DB rows, it does not
add a third representation. The change promotes sidecar-emit from DEFERRED-v1.1 to v1.
Problem
Section titled “Problem”The platform has shipped the two-tier Q&A model DB-side but left the sidecar FILE — the form the model was designed around — unbuilt, so today the DB rows ARE the canonical Q&A form with no file behind them, which is the multiple-representations hazard the sidecar pattern was meant to dissolve:
- Corpus promotion ends at the DB.
promoteCorpusExtractions()(lib/q-a-pairs/promote-corpus.ts:170) inserts theq_a_pairsrow, CAS-links the extraction, embeds, publishes — and STOPS. There is no file emit. (gitnexus_context:promoteCorpusExtractions— 1 direct callerapp/api/q-a-pairs/promote-corpus/route.ts:POST; outgoing callsretireSupersededPairs,embedAndPublish,tryQuery,safeErrorMessage— no filesystem write in the call graph.) - UC6 user-direct Q&A revision is explicitly KH-DB-only by design.
app/api/q-a-pairs/[id]/route.ts:11-13carries the comment “KH-DB-ONLY (INV-4): this revision path writes ONLY to Postgres. The file-sidecar materialisation for Q&A is DEFERRED-v1.1 — there is no file write here, by design.” The route UPDATEsq_a_pairsand the trigger snapshots history; no.mdis written. - File write-back exists ONLY for content_items.
writeBackFileFirst()(lib/edit-intent/write-back.ts:131) is the file-first + compensating-restore adapter for the UC1/UC4 content-item leg — it writes the edited bytes to the EXACTsource_documents.storage_path(theuuid5PK seed) and restores on DB-leg failure. (gitnexus_context:writeBackFileFirst— 3 direct callerspatchHandler/runSweep/rollbackSweep; participates in flowsproc_85_post,proc_86_post.) This is the proven pattern the Q&A sidecar leg REUSES; it has no Q&A caller today. - The pre-walk hazard is the reason sidecar-emit was deferred, and it is real. A Q&A
.mddropped inCOCOINDEX_SOURCE_PATHmatches_TEXT_EXTENSIONS = {".md", ".markdown", ".txt"}(scripts/cocoindex_pipeline/adapters.py:62) →convert_binary_to_markdown→_ingest_content_branch(flow.py:2003), which runs the FULL content branch (extract_classification+extract_qa_form+extract_entity_mentions+extract_relationships,flow.py:2016-2027) and mintscontent_items/source_documents/content_chunks/entity_mentions. So an un-gated Q&A sidecar would mint junk content_items — the S297 BUG-B hazard, the exact reasonedit_intentwas put on a column not a sidecar (per{59.1}RESEARCH + Liam 08/06). - The fork that the gate slots into already exists.
_ingest_file_body(flow.py:1900) already computes a route ONCE before either write path runs:resolve_route(manifest, rel_path)returnsroute ∈ {content, forms}(flow.py:1917-1962), dispatching to_ingest_form_branchor_ingest_content_branch. A Q&A sidecar is a natural THIRD route at this fork — extraction-only, no content_items mint. There is also a precedent for a reserved file being skipped from the content walk:_WORKSPACE_MANIFEST_FILENAME = ".kh-workspace-map.json"(flow.py:1619). - The inner two-tier idempotency contract is already half-built.
extract_qa_form(content_text: str) -> QAFormExtraction(extraction.py:1062) is the inner, content-hash-keyed extractor the05-qa-flow.md §5.2layered shape mandates (extract_q_a(content_text: str) → ExtractedQa); it already satisfies the COCO.10 idempotency gate (memo keyed oncontent_text, notFileLike). What is missing is the outer sidecar-file tier and the emit legs that write the file. q_a_pairshas NO linkage anchor to a sidecar today. The live schema (supabase/migrations/20260617130000_squash_baseline.sql:7128-7165) hasid,question_text,answer_standard,answer_advanced,scope_tag,anti_scope_tag,source_workspace_id,origin_kind,publication_status,superseded_by,valid_from/valid_to,created_at/updated_at,alternate_question_phrasings,question_embedding,edit_intent,source_form_response_id,source_question_id. There is NOsource_document_idand NOsidecar_storage_path. The linkage column is greenfield (INV-8).q_a_pairslive count = 0 (the zero-row window the column rides).
gitnexus orientation: existing symbols promoteCorpusExtractions/writeBackFileFirst/extract_qa_form are the three integration points — the sidecar-emit legs attach to the first, reuse the second's pattern, and feed the third. Not greenfield at the symbol level; greenfield only for the new linkage column and the walk-routing gate.
Goals / Non-goals
Section titled “Goals / Non-goals”Goals. Promote markdown sidecar-emit to v1. Make the sidecar .md file the canonical Q&A
form, with q_a_pairs + q_a_extractions as derived projections. Freeze the sidecar file
convention + walk skip/route gate (the ID-45 {45.3} gate). Add the q_a_pairs↔sidecar
linkage anchor. Classify the DB-only lifecycle columns that a re-walk MUST NOT clobber. Emit
sidecars on corpus promotion and on the user-facing Q&A write paths. Prove the round-trip
(in-platform edit → file → re-extract → DB reconverges with no drift). Reconcile sidecar
FILES on UC8 merge (not just the DB superseded_by).
Non-goals (v1). Sidecars for content_items (that is the existing writeBackFileFirst
content leg — UC1/UC4 — already shipped; this spec only REUSES its pattern for Q&A). The
edit_intent arbitration substrate (sibling PRODUCT.md — sidecar-emit is single-actor on
the promotion path and rides the existing UC6 arbitration on the user-direct path). The
DDL/migration mechanics (a {59.26} TECH concern) — this PRODUCT specifies the behaviour the
columns + convention enable. SharePoint/Notion sidecar sync (the source binding is LocalFS
for v1 per Q2.10). A user-facing “sidecar files” management UI — the sidecar is
AI-invisible infrastructure (INV-22), not a surfaced feature.
Figma: none provided. This is backend + pipeline + data-model work with no new visual surface; the only adjacent UI is the existing Q&A revision-history surface (bl-304, classified stays-backlog in §Satellite disposition).
Behavior
Section titled “Behavior”“User” here is fourfold. (a) The operator running corpus promotion / re-ingest (who must see sidecars emitted and the round-trip reconverge). (b) The admin/editor revising a Q&A pair in-platform (whose edit must reach the canonical file). (c) The cocoindex walk (the “code that reads the model”) which must route a Q&A sidecar correctly and never mint junk content_items. (d) The downstream consumer of
q_a_search(who must see no drift after a round-trip). Behaviour is specified so each can rely on it.
A. The canonical model (the Wikipedia Principle for Q&A)
Section titled “A. The canonical model (the Wikipedia Principle for Q&A)”-
The sidecar file is the canonical Q&A form. For every Q&A pair that has a sidecar (INV-7 scope), the markdown
.mdfile in the source folder is the single golden record. Theq_a_pairsrow and theq_a_extractionsrow are DERIVED projections of that file: one canonical file → exactly two DB projections. The platform MUST NOT treat the DB rows as a third independent source of truth — on any conflict between the file content and the round-trippable DB fields (INV-9), a re-walk of the file is authoritative for those fields. -
The sidecar carries the round-trippable Q&A content, nothing else. A sidecar file’s canonical payload is exactly the fields a re-walk reconstructs (INV-9 “carried” set):
question_text,answer_standard,answer_advanced(when present),alternate_question_phrasings,scope_tag,anti_scope_tag. It MUST NOT carry the DB-only lifecycle state (INV-9 “not-carried” set) — those are never written to the file and never read back from it. -
Re-walk converges, does not duplicate. Re-walking a sidecar that has already been promoted reconstructs/updates the SAME
q_a_pairsprojection (via the existingpromoted_to_pair_ididempotency anchor on the extraction, plus the new linkage anchor INV-8), never a duplicate pair. This is the existingpromoteCorpusExtractions()idempotency contract (PRODUCT-qa-corpus-promotion.mdINV-5/INV-8) extended to the file-seeded re-walk.
B. Sidecar file convention + the walk skip/route gate (THE ID-45 GATE — freeze first)
Section titled “B. Sidecar file convention + the walk skip/route gate (THE ID-45 GATE — freeze first)”-
A Q&A sidecar is identified by a frozen, reserved convention — NOT by being a generic
.md. The convention MUST be unambiguous to the walk so a Q&A sidecar is NEVER routed down the content branch. The convention is frozen by this PRODUCT as: a reserved sub-directory prefix under the source root (recommended__qa__/— a double-underscore sentinel dir, mirroring the existing.kh--prefixed reserved-file precedent atflow.py:1619), with the Q&A sidecar files inside it. A path resolving under the reserved prefix routes to the Q&A-sidecar branch; a path outside it is generic content as today.- Why a reserved subdir (recommendation + justification). It is the lowest-ambiguity,
lowest-collision option and slots directly into the EXISTING
resolve_route(manifest, rel_path)fork (flow.py:1921) as a third route value alongsidecontent/forms— no new interception point, no per-file content sniff. A reserved extension (e.g..qa.md) would still match the.mdsuffix in_TEXT_EXTENSIONSand require changing the suffix-matching inadapters.py:62, which is shared with generic content routing — a riskier blast radius. A frontmatter discriminator requires READING file bytes before routing (defeating the pre-walk skip and re-introducing the content-sniff the reserved dir avoids). The reserved subdir wins on all three axes. - Open question (OQ-25-1): confirm the reserved-prefix string (
__qa__/proposed) and whether it is manifest-declared (an entry in.kh-workspace-map.jsonrouting the prefix toqa_sidecar, consistent with the existing manifest-prefix routing) or a hard-coded sentinel. Needs Liam ratification — this is THE string ID-45{45.3}freezes against.
- Why a reserved subdir (recommendation + justification). It is the lowest-ambiguity,
lowest-collision option and slots directly into the EXISTING
-
A Q&A sidecar NEVER mints content-branch rows. When the walk encounters a path under the reserved prefix, it MUST NOT create
content_items,content_chunks, orentity_mentionsfor it (the S297 BUG-B junk-content hazard), nor any content embedding. It mints exactly onesource_documentsrow — the INV-8 linkage anchor (PKuuid5("sd:" + rel_path), per TECH P1) — and otherwise runs ONLY the Q&A-extraction tier (INV-6). This is the load-bearing skip-rule: a re-ingest run over a folder containing N Q&A sidecars adds ZERO content_items for those N files. -
A Q&A sidecar runs the two-tier extraction shape (COCO.10-safe). The walk routes a reserved-prefix path to an outer sidecar-file tier (keyed on the file handle / byte change) that calls the EXISTING inner
extract_qa_form(content_text: str)extractor (extraction.py:1062), which is content-hash-keyed and hits memo on metadata-only edits (the05-qa-flow.md §5.2layered-fn requirement, COCO.10). A metadata-only touch of a sidecar (mtime/owner change, no content change) MUST NOT re-run the LLM extraction. The inner tier UPSERTsq_a_extractionsexactly as the content path’sextract_qa_formcall does today; the difference is only the absence of the content_items legs (INV-5).
C. Linkage anchor on q_a_pairs
Section titled “C. Linkage anchor on q_a_pairs”-
Which origin_kinds get sidecars. Sidecar emit is v1 for
origin_kind='extracted_from_corpus'(corpus promotion, thepromoteCorpusExtractions()leg) andorigin_kind='curated_explicit'(manual authoring / UC6 user-direct revision — theapp/api/q-a-pairs/[id]route). It is NOT v1 fororigin_kind='derived_from_form_response'(UC5 promotion,app/api/q-a-pairs/promote) — form-response-derived pairs keep their existing KH-DB-only resting state (draft, no file) and are evaluated for sidecar materialisation in v1.1 once the corpus + curated legs are proven.origin_kind='imported_legacy'is the one-shot Phew migration and gets no sidecar (historical). This scopes the emit leg to TWO sites: the promote-corpus loop and the user-direct Q&A route — NOT the UC5 form-response route.- Rationale.
extracted_from_corpuspairs are FILE-SEEDED by definition (their extraction came from a walked source), so emitting a sidecar closes their round-trip.curated_explicitpairs are the ones a human edits in-platform — the canonical-file promise only holds if their edit reaches a file.derived_from_form_responsepairs are KH-DB-native (born from a form response, no source file), so a sidecar would be inventing a file identity as a side effect — exactly the source-layout-freeze hazard UC1’s source-less guard already refuses (write-back.ts:178). Deferring them to v1.1 keeps v1’s emit legs to the two paths where a file is genuinely canonical.
- Rationale.
-
The linkage anchor is a
source_document_id uuidonq_a_pairs(uuid5-derived, FK-LESS). A new nullableq_a_pairs.source_document_id uuidcolumn links a pair to its sidecar’ssource_documentsrow, without a real cross-target foreign key — consistent with the landed cocoindex-write-model R1 (the content_items→source_documents FK was already DROPPED, migration20260602073942/ BUG-E, because the cocoindex autocommit write model cannot satisfy cross-target FKs — seewrite-back.ts:96). The id is theuuid5of the sidecar’s source-relative path (the same derivation the content path uses,flow.py:1915uuid5(_KH_PIPELINE_DOC_NS, "ci:" + rel_path)-analogue for the sidecar namespace), so the linkage is stable across re-walks.source_document_id IS NULLmeans “no sidecar yet” (pre-emit, or aderived_from_form_responsepair).- Why this over a
sidecar_storage_path text. Auuid5-derivedsource_document_idis (a) FK-pattern-consistent with the rest of the cocoindex write model, (b) stable: it does not break if the human-readable path is later reorganised within the reserved dir (the05-qa-flow.md §5.5path-stability caveat — UUID-keyed naming survives folder reorg), and (c) it reaches the actualstorage_pathvia thesource_documentsrow (one indirection) rather than duplicating a mutable path string onq_a_pairs. A baresidecar_storage_path textwould denormalise a path that the source-binding can rename, re-introducing the drift the UUID key avoids. Recommendsource_document_id. - This column rides the zero-row window:
q_a_pairslive count = 0, so theALTERis zero-backfill (mirrors theedit_intentpre-cutover discipline in siblingPRODUCT.mdINV-13). The DDL itself is a{59.26}TECH concern.
- Why this over a
D. Lifecycle columns NOT round-tripped (the S233 gate)
Section titled “D. Lifecycle columns NOT round-tripped (the S233 gate)”-
DB-only lifecycle state is reconstructed-but-not-clobbered on re-walk. A re-walk of a sidecar reconstructs the carried set (INV-2:
question_text,answer_standard,answer_advanced,alternate_question_phrasings,scope_tag,anti_scope_tag) from the file, but MUST NOT overwrite the not-carried DB-only set:publication_status,superseded_by,source_workspace_id,question_embedding,edit_intent,valid_from/valid_to,created_at/updated_at, and the linkagesource_document_iditself. A re-walk that finds an existing pair (via INV-3/INV-8) updates ONLY the carried fields and leaves every not-carried field untouched. This is the explicit S233 SIDECAR-REOPENED gate (“lifecycle-only columns explicitly classified as not-round-tripped”,decision-graph.md:190) — re-ratified here as INV-9.- Confirmation against the brief’s proposed set: the brief proposed
{publication_status, superseded_by, source_workspace_id, question_embedding, edit_intent, valid_from/valid_to, created_at/updated_at}. This PRODUCT confirms that set and ADDSsource_document_id(the linkage anchor must not be clobbered by its own re-walk) — the decision-graph’s historical example namedconfidence/governance_review_statuswhich are not live columns on the currentq_a_pairsschema, so they are dropped; the live equivalents arequestion_embedding(the expensive derived signal) andpublication_status.
- Confirmation against the brief’s proposed set: the brief proposed
-
question_embeddingis a derived signal, never written to the file and never read back. Because a re-walk reconstructsquestion_textbut MUST NOT clobberquestion_embedding, a metadata-only or carried-field-only re-walk does NOT re-embed (the embedding stays valid). A re-walk that CHANGESquestion_text(a real content edit) leaves the stale embedding in place and the existing self-heal eligibility (linked-but-unembedded, or re-embed-on-question-change) is the path that refreshes it — embedding refresh is a derived-projection concern, not a sidecar-content concern. The platform MUST NOT publish a pair whosequestion_embeddingis stale-but-non-NULL in a way that makesq_a_searchreturn a wrong-vector match; the re-embed trigger on aquestion_textchange is the{59.26}TECH’s concern to wire, surfaced here as the invariant it must satisfy.- Open question (OQ-25-2): on a re-walk where
question_textchanged, is the embedding refreshed eagerly in the same walk, or marked-stale for the next promotion/embed pass? Recommend mark-stale-then-reconverge (consistent with the existing linked-but-unembedded self-heal inpromote-corpus.ts), so the walk stays cheap. Needs Liam ratification.
- Open question (OQ-25-2): on a re-walk where
E. Emit legs (where the file gets written)
Section titled “E. Emit legs (where the file gets written)”-
Corpus-promotion emit leg. When
promoteCorpusExtractions()promotes an extraction to a newextracted_from_corpuspair, it ALSO emits the sidecar.mdfile to the reserved prefix at the pair’s derivedstorage_path, writing the carried set (INV-2), and sets the pair’ssource_document_idlinkage (INV-8). The emit reuses thewriteBackFileFirst-pattern (file-first + compensating-restore,write-back.ts): the file write and theq_a_pairsprojection are presented as one atomic promotion outcome — on file-write failure the pair is not left published-without-a-file; on DB-leg failure the file is restored. A promotion that cannot write its sidecar is a recorded promotion failure in the batch summary (extending the existingPromotionSummaryshape), NOT a silent DB-only pair.- Open question (OQ-25-3): does emit run inside the existing promote loop (per-pair,
adding a
sidecar_failedcount toPromotionSummary), or as a separate post-promote pass over freshly-promoted pairs? Recommend in-loop per-pair (the linkage + file are written in the same logical step as the pair, mirroring the existing CAS-then-embed ordering). Needs Liam ratification — bounds thePromotionSummaryshape change.
- Open question (OQ-25-3): does emit run inside the existing promote loop (per-pair,
adding a
-
User-direct emit leg (UC6 /
curated_explicit). When an admin/editor revises a Q&A pair viaPATCH /api/q-a-pairs/[id], and that pair has a sidecar (INV-7 scope +source_document_id IS NOT NULL), the route ALSO writes the revised carried-set bytes back to the sidecar file — reusing thewriteBackFileFirstfile-first + compensating-restore adapter exactly as the content-item route does. The DB UPDATE (withedit_intent, via the existing arbitration path) and the file write are one atomic save: on either leg’s failure the user sees one failure state and neither leg is left applied in isolation. This REPLACES the current explicit “NO file write (DEFERRED-v1.1)” behaviour (app/api/q-a-pairs/[id]/route.ts:11-13) — that comment + behaviour is the v1.1 deferral this spec reverses.- A
curated_explicitpair with NO sidecar yet (source_document_id IS NULL) — e.g. a manually-authored pair created before this feature — is handled per INV-13.
- A
-
First-edit materialisation for source-less curated pairs. When an admin/editor revises a
curated_explicitpair that has no sidecar (source_document_id IS NULL), the platform materialises a sidecar on that first edit: it writes a new.mdto the reserved prefix and setssource_document_id, so the pair becomes file-canonical from that edit forward. This is DISTINCT from the content_item source-less guard (write-back.ts:178, which refuses to auto-create a source_document for a content_item) because a Q&A pair under the reserved prefix does NOT mint a content_items identity (INV-5) — the source-layout-freeze hazard the content guard protects against does not apply to a Q&A sidecar. The user experience is a normal save; the sidecar appears transparently.- Open question (OQ-25-4): alternatively, defer materialisation of pre-existing
source-less
curated_explicitpairs to a one-shot backfill pass (leave first-edit as DB-only). Recommend materialise-on-first-edit (no backfill, no separate pass; the pair becomes canonical exactly when a human next touches it). Needs Liam ratification.
- Open question (OQ-25-4): alternatively, defer materialisation of pre-existing
source-less
F. UC8 file-side merge semantics (fold {59.15})
Section titled “F. UC8 file-side merge semantics (fold {59.15})”- Merging two pairs reconciles their sidecar FILES, not just the DB
superseded_by. When UC8 cross-workspace dedup merges twoq_a_pairs(the surviving pair references the superseded one viasuperseded_by, as the existing retirement pass does), the merge MUST also reconcile the two sidecar FILES so the folder does not retain a live file for a retired pair. The surviving pair keeps its sidecar; the superseded pair’s sidecar is retired so a subsequent re-walk does not resurrect the merged-away pair as a fresh extraction.
- The file-side retirement is delete-on-merge (recommended), with archive-rename as the
considered alternative. Deleting the superseded sidecar is the clean realisation of “one
golden record” — the merged-away content now lives in the survivor. The risk (losing the
pre-merge file) is covered by
q_a_pair_history(the DB snapshot of the superseded pair survives) + the existingsuperseded_bylineage, so the file deletion is not a data-loss event. Leave-and-mark is REJECTED — a left-behind sidecar would be re-walked into a fresh extraction → a fresh pair, resurrecting the duplicate UC8 just merged (the exact loop the merge exists to close). - Open question (OQ-25-5): delete-on-merge vs archive-rename (move the superseded sidecar out of the reserved prefix into a non-walked__qa_archive__/). Recommend delete-on-merge (history table is the durable archive). Needs Liam ratification + is gated on UC8 ratification + Spike#10 — see INV-21.
- {59.15} is folded into this amendment. The previously-parked
{59.15}(UC8 dedup file tie-in) is subsumed by INV-14: its scope is the file-side merge semantics specified here. It inherits INV-14’s gating (UC8 ratification + Spike#10, INV-21) and does not start until those clear.
G. Round-trip proof (UC1-Candidate-A, COCO.10 idempotency)
Section titled “G. Round-trip proof (UC1-Candidate-A, COCO.10 idempotency)”-
An in-platform Q&A edit round-trips with no DB drift. The canonical-model contract is provable end-to-end: an admin/editor edits a Q&A pair in-platform (INV-12) → the carried bytes are written to the sidecar file → a subsequent cocoindex re-walk of that file re-extracts via
extract_qa_form→ theq_a_pairsprojection reconverges to the same carried-field values WITHOUT clobbering any not-carried field (INV-9). The observable invariant: for a metadata-only-equivalent edit (the carried fields unchanged after the write→re-walk loop), the re-walk is a no-op onq_a_pairs(the COCO.10 memo short-circuits the inner extractor); for a real carried-field edit, the post-re-walk pair equals the in-platform edit, byte-for-byte on the carried set, with all not-carried fields preserved. -
The round-trip is idempotent N times. Running write→re-walk→write→re-walk over an unchanged pair produces a stable fixpoint: the
q_a_pairsprojection does not oscillate, accumulate history-row churn beyond the genuine edits, or drift the embedding. This is the COCO.10 contract on metadata-only edits (05-qa-flow.md §5.2) made testable at the pair level.
H. Roles, scoping, AI-invisibility
Section titled “H. Roles, scoping, AI-invisibility”-
Authorisation. Sidecar emit on corpus promotion follows the existing
promoteCorpusExtractionsoperator guard (getAuthorisedClient(['admin','editor']), failures viaauthFailureResponse). Sidecar write-back on the user-direct path follows the existingPATCH /api/q-a-pairs/[id]admin/editor guard. No new public route; no addition toproxy.tspublicRoutes. Viewers/reviewers cannot trigger an emit or a write-back. -
Workspace scoping. Q&A sidecars are corpus-level (the Q&A corpus is shared across workspaces,
05-qa-flow.md §1.1); the sidecar files live in the corpus source folder, not a workspace-partitioned location. An emit MUST NOT leak a pair across a workspace boundary the acting user cannot otherwise reach; the existingq_a_pairsRLS andq_a_searchscope-tag overlap filter continue to govern visibility identically to a sidecar-less pair. -
Path stability across reorganisation. A sidecar’s
source_document_id(INV-8) is the stable identity; if a sidecar file is renamed/moved within the reserved prefix during a folder reorganisation, the re-walk re-keys on the new path’suuid5and thepromoted_to_pair_id/source_document_idlinkage must reconcile rather than mint a duplicate pair (the05-qa-flow.md §5.5path-stability caveat). The naming convention under the reserved prefix SHOULD preserve the UUID-keyed name so a reorg does not trigger spurious re-extraction. -
UC8 file-merge gating. The UC8 file-side merge (INV-14/INV-15) is gated on UC8 ratification + Spike#10 (dedup substrate) — tracked as cross_doc_links at Task level, NOT sibling subtask deps. The corpus-promotion emit (INV-11), user-direct emit (INV-12), convention/gate (INV-4..6), linkage column (INV-8), lifecycle classification (INV-9), and round-trip proof (INV-16/17) do NOT depend on UC8 and can land first.
-
AI-invisible infrastructure. The sidecar file, the
extract_qa_formLLM extraction, and the embedding are infrastructure — they are NOT surfaced as a user-facing “AI feature” or “sidecar files” management surface. A consumer ofq_a_searchand an admin editing a Q&A pair both experience the pair exactly as if the data was always there (per the AI-visibility policy); the only neutral provenance signal remainsorigin_kind. The user editing a Q&A pair does not see “this writes a markdown file” — they see a save.
Open questions (collected)
Section titled “Open questions (collected)”- OQ-25-1 (INV-4): Confirm the reserved-prefix string (
__qa__/proposed) + whether it is manifest-declared or a hard-coded sentinel. This is the string ID-45{45.3}freezes against — highest-priority ratification. - OQ-25-2 (INV-10): On a re-walk where
question_textchanged — refresh embedding eagerly in-walk, or mark-stale-then-reconverge? Recommend mark-stale. - OQ-25-3 (INV-11): Corpus-promotion emit in-loop (per-pair,
sidecar_failedcount) vs a separate post-promote pass? Recommend in-loop. - OQ-25-4 (INV-13): Source-less
curated_explicitpairs — materialise-on-first-edit (recommended) vs one-shot backfill pass? - OQ-25-5 (INV-14): UC8 superseded sidecar — delete-on-merge (recommended) vs archive-rename to a non-walked dir? (Gated on UC8 ratification + Spike#10.)
Doc-update intents (for the Orchestrator → Curator / docs-site lane)
Section titled “Doc-update intents (for the Orchestrator → Curator / docs-site lane)”These docs currently record the DEFERRED-v1.1 state this PRODUCT reverses. They MUST be corrected when this amendment is ratified (the Planner does NOT edit them — surfaced as intents):
05-qa-flow.md §4.4(“Markdown sidecar on promotion — deferred v1.1”) — update from DEFERRED-v1.1 to v1, sidecar-emit promoted per this PRODUCT; cite the S380 ratification.05-qa-flow.md §4.1— corrects a STALE reference: it cites the sidecar path assource_documents.original_path; the live column issource_documents.storage_path(there is nooriginal_pathcolumn — same correction the siblingPRODUCT.mdmade as S-a). Update tostorage_path.decision-graph.md:43(Q1.3/Q3.1/Q3.2 row: “Markdown sidecar emit = DEFERRED-v1.1, not reopened to v1”) — crosswalk to RATIFIED-v1-S380; the sidecar-emit reversal.decision-graph.md:190(Q2.9 SIDECAR-REOPENED-S233, Q3.2 trace) — record that the S233 gates (OQ10 markdown shape locked + S9 idempotency confirmed + lifecycle columns classified not-round-tripped) are RE-RATIFIED and SATISFIED: markdown is the v1 input/output shape (OQ10),extract_qa_formis content-hash-keyed (S9/COCO.10,extraction.py:1062), and the not-round-tripped column set is classified at INV-9.
End of PRODUCT. Behaviour invariants INV-1..INV-22. Companion {59.26} TECH
(write-tech-spec) is the next chain step: the walk skip/route gate (a third resolve_route
branch + reserved-prefix routing), the q_a_pairs.source_document_id migration, the two emit
legs (promoteCorpusExtractions + PATCH /api/q-a-pairs/[id] reusing writeBackFileFirst),
the not-round-tripped column enforcement on re-walk, and the round-trip golden-fixture proof —
one Proposed-change per invariant. Freeze INV-4 before {45.3} ratifies.