content-model-invariants PRODUCT.md — Verifier Report
content-model-invariants PRODUCT.md — Verifier Report
Section titled “content-model-invariants PRODUCT.md — Verifier Report”Spec under review: docs/specs/content-model-invariants/PRODUCT.md
Drafter commit: c6091e96 (drafter worktree worktree-agent-a07b896726d22ca4e, branch worktree-agent-a07b896726d22ca4e, parent 0c7197e1 main)
Spec length: 190 lines; 32 numbered invariants (C-1 .. C-62 across 6 clusters); 3 §5.3 gap flags ([GAP-CMI-001..003])
Verifier: background worktree adversarial review agent (S242 W1.3)
Verifier commit base: 70336a3c on content-items-investigation
Date: 18/05/2026 (S242)
Rubric: 11-axis adversarial review per drafter brief
Verdict
Section titled “Verdict”PASS-WITH-NOTES. The spec is substantively implementation-ready and correctly implements the feature-spec pattern with forward-refs to canonical-pipeline P-1/P-2 (no restatement), CLAUDE.md gotcha enforcement on content_text_hash GENERATED ALWAYS, summary vs ai_summary, pipeline service-account UUID, and the entity-classification false-positive framing. Source-of-truth pointers are correctly assembled.
However, the spec contains two factual citation errors that compromise the no-fabrication discipline (construction-guide §5), neither of which invalidate the invariant content but both of which would mislead a downstream implementing agent:
- [GAP-CMI-003] cites
02-data-flow.md§22, a section that does not exist. The doc only contains §1-§12. Theform_typediscriminator framing the spec is pointing at actually lives at02-data-flow.mdline 22 (within §1) — looks like a “line 22” / “§22” confusion at drafting time. - [GAP-CMI-002] cites
PLAN.md §4.2 T2 subtask 9, but T2’s enumerated subtask list runs 1-8 only. Op_id is also not enumerated in the 10-item Q-OQR1-16 scope at0.9-decision-graph.md§11.3 — the upstream02-data-flow.md§5.3 framing (op_id “RATIFIED-S236, column-level schema STILL-OPEN pending the Q-OQR1-16 combined-PR migration”) is internally consistent with the spec’s GAP framing, but the specific subtask-9 pointer is a fabrication.
Both are non-blocking for implementation (the invariants themselves remain testable, and the gap-flag categories are correctly assigned) but should be fixed in a same-session edit pass to preserve the audit-trail discipline that this spec family depends on.
Two additional issues are noted as non-blocking suggestions (axis 6 + axis 7).
Per-axis findings
Section titled “Per-axis findings”Axis 1 — Brief compliance — PASS
Section titled “Axis 1 — Brief compliance — PASS”All 6 invariant clusters present:
| Cluster | Range | Invariant count | Brief target |
|---|---|---|---|
| Field-population | C-1 .. C-7 | 7 | ✓ |
| Chunking-boundary | C-10 .. C-14 | 5 | ✓ |
| Source-attribution | C-20 .. C-23 | 4 | ✓ |
| Embedding | C-30 .. C-34 | 5 | ✓ |
| Content-type taxonomy | C-40 .. C-43 | 4 | ✓ |
| Entity-classification | C-50 .. C-53 | 4 | ✓ |
| v1 scope boundaries | C-60 .. C-62 | 3 | (additional but in-brief) |
Total 32 numbered invariants — matches drafter-reported count.
Each invariant follows the “When [trigger], the system / row [outcome] (because [constraint])” shape from the canonical-pipeline P-N pattern, adapted for the feature-spec audience.
Forward-refs to canonical-pipeline P-1 + P-2 are by spec+section (Source-of-truth pointers, line 19; Summary, line 36; C-21 forward-refs to canonical-pipeline P-1 line 70; C-60 forward-refs to canonical-pipeline §2 P-10 line 112) — never restated.
Axis 2 — No-fabrication discipline — PARTIAL FAIL (2 errors)
Section titled “Axis 2 — No-fabrication discipline — PARTIAL FAIL (2 errors)”Spot-check of citations (≥6 verified directly):
| Citation | Spec invariant | Verified status |
|---|---|---|
lib/content/chunking.ts:38 (MIN_DOCUMENT_CHARS = 500) | C-10 | ✓ Line 38 reads export const MIN_DOCUMENT_CHARS = 500; |
lib/content/chunking.ts:44-53 (determineSplitLevel) | C-11 | ✓ Lines 44-53 are the determineSplitLevel function |
lib/content/chunking.ts:172-216 (merge step) | C-12 | ✓ Lines 172-216 cover the pendingMerge loop including the last-chunk-merge branch |
lib/content/chunking.ts:23-32 (ContentChunk interface) | C-13 | ✓ Lines 23-32 are the ContentChunk interface |
lib/content/chunking.ts:16-18 (code-block safety) | C-14 | ✓ Lines 16-18 are the JSDoc comment about marked lexer tokenisation |
docs/reference/SCHEMA-QUICK-REFERENCE.md §35 (content_type CHECK enum) | C-40 | ✓ §35 line 1864-1873 enumerates the 15 values exactly as quoted |
content_items.summary not ai_summary (S164b rename) | C-4 | ✓ Confirmed in SCHEMA-QUICK-REFERENCE.md line 87 (“Claude-generated summary (renamed from ai_summary in S164b)”); database.types.ts lines 668/745/822 carry summary on content_items; feed_articles.ai_summary at lines 1152/1174/1196 is the separate column. |
content_items.content_text_hash GENERATED ALWAYS | C-2 | ✓ Confirmed SCHEMA-QUICK-REFERENCE.md line 133 (“GENERATED ALWAYS — read-only, computed by Postgres from md5(normalised content); omit from INSERT/UPDATE payloads”) |
ingest_source canonical values | C-5 | ✓ Confirmed SCHEMA-QUICK-REFERENCE.md line 123 enumerates all 11 values cited (manual, url_import, upload, upload_autosplit, mcp_create, rss_feed, bid_outcome_integration, python_url, python_markdown, qa_import, batch_reclassify) per migration 20260428174512 |
Pipeline service-account UUID a0000000-0000-4000-8000-000000000001 | C-5 | ✓ Matches CLAUDE.md “Data & Architecture” gotcha verbatim |
05-qa-flow.md §2.1 (question_embedding shape) | C-34 | ✓ §2.1 line 56 confirms “question_embedding vector(1024) … embedded over question_text + alternate_question_phrasings concatenated” |
05-qa-flow.md §1.2 “0 of 395 prod q_a_pair rows” | C-41 (PLAN.md §4.7 sub-cite) | ✓ §1.2 line 30 confirms |
Citation errors (blocking for audit discipline, non-blocking for invariant validity):
-
F1 (significant — fabrication).
[GAP-CMI-003]“What is open” first sentence: “procurement-flow rows capturingform_typediscriminators per02-data-flow.md§22 framing”.02-data-flow.mdhas no §22 — verified bygrep -nE "^## |^### " docs/plans/phase-0-investigation/architecture/02-data-flow.mdreturning §1 through §12 (with subsections through §12.1). The actual line that mentions “the procurement flow capturingform_typein the extraction step” is02-data-flow.mdline 22 — inside §1 Mission. The drafter appears to have confused “line 22” with “§22”. Recommended fix: replace with02-data-flow.md§1 (line 22) — application-type coupling para. -
F2 (significant — fabrication).
[GAP-CMI-002]“What is open” sentence: “It is part of the Q-OQR1-16 combined-PR migration scope per02-data-flow.md§5.3 +0.9-decision-graph.md§11.4.1 + PLAN.md §4.2 T2 subtask 9”. PLAN.md §4.2 T2 only enumerates subtasks 1-8; no subtask 9 exists. Also: the 10-item Q-OQR1-16 scope at0.9-decision-graph.md§11.3 (lines 480-489) does not enumerate op_id — it listsapplication_types,workspaces.application_type_idFK,kb_sectionretire, procurement rename,project_id→workspace_id, form_templates rename,digests→change_reports, provenance enum,source_documents.workspace_idNULLABLE,q_a_pairsschema sketch. The upstream02-data-flow.md§5.3 line 134 itself reads “Theop_idcolumn is added tocontent_items(and potentially other cocoindex target tables) as part of the combined-PR scope per0.9-decision-graph.md§11.4.1. RATIFIED-S236, column-level schema STILL-OPEN pending the Q-OQR1-16 combined-PR migration” — this is a known upstream gap. The spec inherits the gap correctly via its [GAP-CMI-002] category but invents a “T2 subtask 9” pointer that does not exist. Recommended fix: drop ”+ PLAN.md §4.2 T2 subtask 9” and replace with “the combined-PR landing target is RATIFIED-S236 (column-level schema STILL-OPEN per02-data-flow.md§5.3 quoted text); landing in T2 follow-up subtask is named in PLAN.md but the specific subtask slot is STILL-OPEN.”
Other citations not flagged but worth noting are accurate:
02-data-flow.md§3 6-stage cocoindex topology ✓ (sections present)02-data-flow.md§5.1 N7 hybrid op_id pattern ✓02-data-flow.md§9.1 cocoindex freshness vs governance freshness ✓ (§9.1 line 214)02-data-flow.md§9.3 “re-ingesting a stale record does not make it fresh” ✓ (§9.3 line 234)02-data-flow.md§7.2 N6pipeline_runsrollup ✓- PLAN.md §4.7 (T7 acceptance criteria for soft-archive) ✓ (lines 263-270 verified)
- PLAN.md §4.7 “1 year retention” RATIFIED-S241 ✓ (line 270 verified)
- S239 closure addendum row 5 (RATIFIED-S241 = 1yr) ✓ (line 218 verified)
- Entity-type taxonomy spec §2 universal rules (Named Entity, External Reference, Policy/Procedure/Plan, Role Title, Generic Concept) ✓
Axis 3 — CLAUDE.md gotcha enforcement — PASS
Section titled “Axis 3 — CLAUDE.md gotcha enforcement — PASS”| Gotcha | Captured in | Status |
|---|---|---|
content_items.content_text_hash GENERATED ALWAYS | C-2 (line 44) — explicit (“system does NOT supply a value … any explicit write value is rejected with cannot insert a non-DEFAULT value into column "content_text_hash"”) | ✓ Verbatim error message captured |
summary NOT ai_summary | C-4 (line 48) — explicit (“This column is NOT named ai_summary — that name is reserved for feed_articles.ai_summary and is intentionally separate”) | ✓ |
classifyContent userId pipeline service-account UUID a0000000-0000-4000-8000-000000000001 | C-5 (line 50) — explicit (“created_by is the pipeline service-account UUID (a0000000-0000-4000-8000-000000000001) per CLAUDE.md”) | ✓ |
| Entity classification false-positives ≠ type errors | C-51 (line 104) — explicit, with bold callout (“The classification failure mode is false positives, not type errors.”) + quoting CLAUDE.md verbatim | ✓ Reproduces CLAUDE.md framing exactly |
Embedding-vector JSON.stringify | Not invariant-relevant at v1 (this gotcha applies to RPC params, not row-write contract); appropriately omitted | ✓ Correctly scoped out |
Additional CLAUDE.md gotcha capture worth noting:
- “
content_items.summary(notai_summary)” — captured at C-4 with the exact CLAUDE.md wording quote. ingest_sourceenum (per migration20260428174512COMMENT) — captured at C-5 with the full 11-value enumeration.
Axis 4 — Forward-ref discipline — PASS
Section titled “Axis 4 — Forward-ref discipline — PASS”Spec correctly cites canonical-pipeline P-N invariants by spec+section without restating:
| Reference | Where in spec | Restatement check |
|---|---|---|
| canonical-pipeline P-1 (external-folder canonical) | “How to use this doc” line 15, Summary line 36, C-20 line 70 | ✓ Cited as “canonical-pipeline P-1”; not restated |
canonical-pipeline P-2 (cocoindex ExtractByLlm recurring runtime adapter) | Summary line 36 | ✓ Cited as “canonical-pipeline P-2”; not restated |
| canonical-pipeline P-10 (source_documents Option α) | C-60 line 112 | ✓ Cited as “canonical-pipeline §2 P-10”; not restated |
| canonical-pipeline P-70 (T7 q_a_pair migration) | C-41 line 94, C-42 line 96 | ✓ Cited as “PLAN.md §4.7” + “P-70 target state”; not restated |
Construction-guide §5.3 framing for gap flags is explicitly invoked at line 13 (“each invariant either carries explicit ratification or — for invariants gated on a pending spike — carries a §5.3-format gap flag rather than an inline TBD”) and at line 128 (“Open content-model items captured per construction guide §5.3 format”). Each gap flag carries: category, what is open, sources checked, prerequisite work, what the spec does in the meantime — matches the §5.3 example template.
Axis 5 — Spike #1 gap-flag correctness — PASS
Section titled “Axis 5 — Spike #1 gap-flag correctness — PASS”[GAP-CMI-001] correctly captures Spike #1 dependency without pre-empting:
- Category named: “Tech-spec needed (gated on Spike #1 outcome).” (line 132) — matches the construction-guide §5.2 four-category taxonomy.
- What is open: Persistence target for chunk-position metadata — dedicated columns vs
metadatajsonb side-channel — gated onTableSchemadeclaration shape outcome. - Sources checked: 5 sources listed (chunking.ts ContentChunk shape, 02-data-flow.md §3.1, S1 spike doc §§1-2, SCHEMA-QUICK-REFERENCE.md §1, PLAN.md §4.1 T1.4 + §4.8 T8 subtask 1).
- Prerequisite work: “Spike #1 live-mount verification … closes at the start of the T8 wave (S243+) per Liam S242 ratification.”
- What the spec does in the meantime: “C-13 names the fields and binds them to the chunker output; the persistence-target binding is forward-referred to
TECH.md.”
The S243+ T8 wave start framing is correct per the S242 continuation prompt (continuation-prompt-kh-s242-main-t1-specs-and-phase-0-close.md line 117 “Spike #1 must close at the start of Wave 4 (T8 cocoindex flow scaffolding kickoff)”) and PLAN.md §4.8 T8 subtask 1.
Spike #1 is not pre-empted — C-13 leaves the persistence-target binding open, and chunk-position field semantics (heading_text, position, parent_position, etc.) are correctly bound to the lib/content/chunking.ts ContentChunk shape, which is non-speculative.
Axis 6 — Tag vocabulary discipline — PASS-WITH-NOTES
Section titled “Axis 6 — Tag vocabulary discipline — PASS-WITH-NOTES”| Tag usage | Location | Verdict |
|---|---|---|
[RATIFIED-S242] | Line 1 banner | ✓ Used correctly |
[CURRENT-CANONICAL] | Line 5 status | ✓ Used correctly |
[GAP-CMI-001..003] (custom tag family for this spec) | Lines 132, 147, 163 | ✓ Self-consistent, scoped to this spec |
[DEFERRED-v1.1] | Line 119 (“v1.1 candidates (DEFERRED — not blocking)”) — header form, no per-item tag | ✓ Acceptable header form; per-item tags absent but the section header itself carries the DEFERRED marker |
[CONDITIONAL-...] | Not used — N/A | N/A |
Inline TBD / XXX / TODO | Searched: zero instances of TBD/XXX/TODO in spec body | ✓ Discipline observed |
N1 (non-blocking suggestion). The v1.1 candidates section header uses “(DEFERRED — not blocking)” prose rather than the more grep-able [DEFERRED-v1.1] per-item tag pattern used elsewhere in the codebase (e.g. rls-pattern/PRODUCT.md P-5 “[DEFERRED-v1.1]”). Consider per-item tags on the three v1.1 candidate bullets so a grep -rn '\[DEFERRED-v1.1\]' sweep over docs/specs/ picks them up consistently.
Axis 7 — Feature-spec pattern — PASS-WITH-NOTES
Section titled “Axis 7 — Feature-spec pattern — PASS-WITH-NOTES”Per S241 critical-rule 5 + S242 continuation-prompt line 130 (“WP1 specs use Summary+Behavior feature-spec shape, NOT construction-guide pattern”):
- Summary section present (line 34, single paragraph) ✓
- Behavior section present (line 38, numbered C-N invariants) ✓
- Status / How to use / Source-of-truth pointers / Audience sections present as expected framing ✓
- No
Validation/Success criteria/Testingsections ✓ (per write-product-spec skill — these live in TECH.md)
The spec uses C-N (not P-N) for its invariants, which is correctly distinct from canonical-pipeline P-N — preserving the audit-trail rule that P-N is reserved for canonical-pipeline / construction-guide pattern specs.
N2 (non-blocking observation). The spec includes a non-standard ## §5.3 Gap flags section header which is a construction-guide-pattern affordance, not a write-product-spec pattern affordance. The write-product-spec skill (Open questions) suggests inline **Open question:** ... notes next to relevant behavior, with a “dedicated section only if there are multiple unresolved questions worth collecting” — which is satisfied here. The §5.3 framing comes from the construction-guide, not from write-product-spec. Mixing the two patterns is acceptable for a feature-spec that gates on architecture-substrate ratifications (which is exactly this case) but the spec could equally use a plain ## Open questions heading per write-product-spec — keeping [GAP-CMI-NNN] ID tags. Not blocking; flagging for consistency awareness.
Axis 8 — Embedding invariants — PASS
Section titled “Axis 8 — Embedding invariants — PASS”- Vector(1024) per text-embedding-3-large: C-30 (line 80) ✓ Cites
02-data-flow.md§3.1 + CLAUDE.md “Supabase & Schema” reference. Schema spot-checkdatabase.types.tsconfirmsembedding: vector(1024)oncontent_items. - Pre/post-T7 Q&A embedding shape difference: C-34 (line 88) ✓ Explicit: “pre-migration …
embeddingover itscontenttext per the standard pipeline. Post-T7 migration, new Q&A pairs flow toq_a_pairswithquestion_embeddingoverquestion_text + alternate_question_phrasingsper05-qa-flow.md§2.1 — a different embedding shape because the Q&A retrieval target is the question, not the answer”. This is a non-obvious invariant that downstream readers MUST know to avoid mis-querying. - Null-embedding semantics: C-32 (line 84) ✓ Correctly captures
embedding IS NULLsemantics for unembeddable rows; search RPCs skip in semantic branches. - Memoisation behaviour on re-ingest: C-31 (line 82) ✓ Embedding is NOT recomputed when content-hash matches;
embedding_tokensnot double-counted. Correctly forward-refs to C-21 op_id as the re-ingest signal.
Axis 9 — Chunking invariants — PASS
Section titled “Axis 9 — Chunking invariants — PASS”MIN_CHUNK_CHARS = 100: Verified atlib/content/chunking.ts:35(export const MIN_CHUNK_CHARS = 100;) ✓MIN_DOCUMENT_CHARS = 500: Verified atlib/content/chunking.ts:38✓- H2 default / H1 fallback: Verified at
lib/content/chunking.ts:50-51(if (headingDepths.includes(2)) return 2;/if (headingDepths.includes(1)) return 1;) ✓ - Headingless / H3+-only → single chunk: Verified at
lib/content/chunking.ts:49(if (headingDepths.length === 0) return null;) +:52(return null; // only H3+ -> no split) ✓ - Short-chunk merge with next sibling, fallback to previous: Verified at
lib/content/chunking.ts:172-216✓ — exactly the “merge with next sibling … if the last chunk is short, it merges back with the previous one” semantics C-12 describes. - Code-block heading immunity: Verified at
lib/content/chunking.ts:16-18✓
All chunking citations match the source code exactly.
Axis 10 — UK English — PASS
Section titled “Axis 10 — UK English — PASS”- “initialised” (line 52) ✓
- “behaviour” (line 106) ✓
- “organisational” (line 106) ✓
- “MUST”, “NULLABLE” (PG keywords, correctly capitalised — not a UK/US issue)
- No “color”, “behavior”, “organization”, “optimize” detected via grep.
Date format: 18/05/2026 (line 7) per UK DD/MM/YYYY ✓
Axis 11 — Location ratification correctness — PASS
Section titled “Axis 11 — Location ratification correctness — PASS”Spec correctly lands at docs/specs/content-model-invariants/PRODUCT.md (sibling spec, NOT extension of 0.9-canonical-pipeline/PRODUCT.md §1).
Trail:
- PLAN.md §4.1 T1.4 subtask 4 line 135: “Draft Q1.3-Q1.N content-model extension (location TBD per skill judgement — extend PRODUCT.md §1 or new spec).”
- PLAN.md §4.1 line 124: “(iii) Extension to
docs/specs/0.9-canonical-pipeline/PRODUCT.md§1 covering Q1.3-Q1.N content-model invariants (or a siblingdocs/specs/content-model-invariants/PRODUCT.mdif scope justifies — judgement at draft time).” - S242 continuation prompt line 115: “Q1.3-Q1.N location: drafter recommends extension to
0.9-canonical-pipeline/PRODUCT.md§1 OR newdocs/specs/content-model-invariants/PRODUCT.md. Liam ratifies the drafter’s recommendation before the agent commits.”
Spec banner correctly cites this as the “second-option resolution” (line 1). Sibling-spec path is well-motivated given: (a) 32 invariants and 3 gap-flags would balloon canonical-pipeline §1 disproportionately; (b) canonical-pipeline PRODUCT.md is documented as “historical-substrate” per PLAN.md S241 critical-rule (per S242 continuation-prompt line 122 “anti-patterns to avoid: Inlining drift fixes into 0.9-canonical-pipeline/{PRODUCT,TECH}.md”). The S241 “anti-pattern” rule explicitly forbids extending the canonical-pipeline mega-spec — making sibling-spec the only correct path, not just an optional one. The spec captures this correctly.
Findings summary
Section titled “Findings summary”| Finding | Severity | Axis | Blocking? |
|---|---|---|---|
F1 — [GAP-CMI-003] cites 02-data-flow.md §22 (no such section; meant §1 line 22) | Significant (citation fabrication) | 2 | No, but should fix in same-PR edit |
F2 — [GAP-CMI-002] cites PLAN.md §4.2 T2 subtask 9 (no such subtask) | Significant (citation fabrication) | 2 | No, but should fix in same-PR edit |
N1 — v1.1 candidates use prose marker rather than per-item [DEFERRED-v1.1] tag | Minor | 6 | No (suggestion) |
N2 — ## §5.3 Gap flags header is construction-guide pattern in a feature-spec shape | Minor | 7 | No (acceptable hybrid) |
Total: 4 findings (0 blocking, 2 significant-non-blocking, 2 minor).
The 2 significant findings are both citation errors, not invariant errors. The invariants themselves remain testable and the gap-flag categories are correctly assigned; only the specific pointer text inside the gap flags is wrong. Recommend a single-commit fix pass on the drafter’s worktree to:
- Replace
02-data-flow.md §22→02-data-flow.md §1 (line 22)(or§1 application-type-coupling para) in [GAP-CMI-003]. - Replace
PLAN.md §4.2 T2 subtask 9→ drop the subtask pointer (or replace with “PLAN.md §4.2 T2 acceptance criteria (line 147 10-item enumeration does NOT currently list op_id — upstream gap; tracked by02-data-flow.md§5.3 column-level STILL-OPEN framing)”) in [GAP-CMI-002].
The spec is otherwise ready to merge once the citation fixes land. No re-verification needed if the fixes are limited to the two specific lines flagged.
Out-of-scope acknowledgements
Section titled “Out-of-scope acknowledgements”The verifier observed but did not flag (per drafter brief — out of scope for this WP1.3 verifier pass):
- Companion TECH.md not yet drafted (acknowledged in spec line 7 + line 190 — landing post Spike #1 closure at T8 wave start S243+).
- 02-data-flow.md §5.3 op_id “column-level schema STILL-OPEN” framing is an upstream gap, not a spec-side gap. Spec correctly inherits the upstream STILL-OPEN via [GAP-CMI-002] (modulo the F2 fabrication).
- Per-application-type form_type discriminators are correctly forward-referred to
procurement-workspaces/{PRODUCT,TECH}.mdper [GAP-CMI-003] (modulo the F1 fabrication).
End of verifier report. SHA c6091e96 reviewed against rubric; verdict PASS-WITH-NOTES with 2 fix-pass citation corrections recommended pre-merge.