Skip to content

WP6 B2 — Verifier Report: TECH.md Sanity Cross-check

WP6 B2 — Verifier Report: TECH.md Sanity Cross-check

Section titled “WP6 B2 — Verifier Report: TECH.md Sanity Cross-check”

Scope: Adversarial sanity-check of docs/specs/wp6-ontology-harness/TECH.md (S236 A2 commit 321b521d) against:

  • A1’s actual ontology output (docs/ontology/*.md, 29 files + README, commit e0ba727b).
  • WP-ONTO-R1 §6.3 canonical frontmatter (docs/plans/phase-0-investigation/phase-b-prerequisite-1-onthology-pipeline-feedback-investigation.md lines 694-727).
  • Existing KH conventions (lib/validation/schemas.ts, lib/validation/layer-schemas.ts, __tests__/validation/schema-db-consistency.test.ts, docs/reference/test-philosophy.md).
  • CLAUDE.md gotchas relevant to cocoindex, Vitest, Zod, sandbox, content_text_hash.
  • WP7 S9 spike (docs/plans/phase-0-investigation/0.9-spike-S9-cocoindex-idempotency.md §7.2).

FAIL — MUST REVISE BEFORE D1.

The spec captures the right architecture and the test approach is sound, but the Zod contract in §6 will reject 8 of A1’s 29 markdown files as currently authored, the consumer wire-up in §5.3 will produce TypeScript compilation errors at two existing call sites (the spec’s “expected breakage is small” understates this), and the status field — present in every A1 file and documented as load-bearing in docs/ontology/README.md — is absent from the Zod schema, so gray-matter-parsed frontmatter will silently strip it (Zod’s default object behaviour) and any future consumer that reads cv.status will get undefined. These three issues are independent regressions; D1 cannot proceed without them being addressed.

The spec is otherwise well-shaped. Most fixes are surgical. Estimated revision effort for an editor agent (or D1 directly): ≤30 min.


2.1 — Zod schema fidelity (Cross-check 1)

Section titled “2.1 — Zod schema fidelity (Cross-check 1)”

Finding 2.1.a — status field missing from OntologyCVSchema. CRITICAL.

docs/ontology/README.md lines 38-52 define the canonical frontmatter shape and explicitly list status: active | planned | needed. All 29 files carry it (grep "^status:" docs/ontology/*.md → 30 matches incl. README example). The spec’s §6 schema does NOT include this field.

Zod’s default z.object() is non-strict — unknown keys are stripped silently from the parsed result, NOT rejected. This means:

  • loadOntologyCVs() will return objects whose status property is undefined even though the markdown declared it.
  • OntologyCVSchema.parse(matter.data) will succeed without surfacing the silent-strip.
  • Any future consumer that reads cv.status (highly likely — the README documents the field as load-bearing) will get undefined. Silent failure surface.
  • The test in §5.4 case 1 (“every .md file parses + validates”) will pass green even though the field is being lost.

The spec authoritatively cites WP-ONTO-R1 §6.3 (correctly — §6.3 does NOT include status). The drift is between §6.3 and what A1 actually produced (A1 added status per §6.2 lifecycle classification table). The spec must reconcile this. Two options:

  • (a) Add status: z.enum(['active', 'planned', 'needed']) to OntologyCVSchema. Treat docs/ontology/README.md as the contract A1 implements.
  • (b) Use .strict() on OntologyCVSchema so unknown fields fail loudly. Then either (a) or strip status from the 29 A1 files. Strip is wrong — the field is genuinely useful product data.

Recommended: option (a) + .strict(). Belt-and-braces guards both directions of drift.

Finding 2.1.b — cv_name regex rejects BID_STATES. CRITICAL.

docs/ontology/14-bid-states.md line 1 has cv_name: BID_STATES (uppercase). The spec’s §6 regex /^[a-z][a-z0-9_]*$/ requires lowercase. The Zod parse() will throw with the file path, and loadOntologyCVs() will halt the test suite at first failure.

Two valid fixes:

  • (a) Editor-tier change: A1’s 14-bid-states.md rewrites cv_name: bid_states. Aligns with all 28 sibling files. The verbatim uppercase looks like an A1 typo (the README index line 105 already lists it as BID_STATES so the inconsistency is at A1, not at the spec).
  • (b) Spec relaxation: change regex to /^[a-zA-Z][a-zA-Z0-9_]*$/. Worse — sacrifices the convention.

Recommended: (a). Track as an explicit C1 editor action item.

Finding 2.1.c — BaselineValueSchema.key regex rejects 11 keys across 5 files. CRITICAL.

grep -E "^\s+- key: " docs/ontology/*.md | grep -vE "key: [a-z][a-z0-9_]*$" returns 11 hits:

FileOffending key
01-taxonomy-domains.mdTBD (uppercase)
02-taxonomy-subtopics.mdTBD
12-requirement-type.mdTBD
15-workspaces-type.mdproposal-placeholder (kebab-case)
18-entity-aliases.mdTBD
19-engineering-types.mdbid-metadata, unified-gap, filter-preset (kebab-case)
20-chunk-kind.mdheading-section, qa-block (kebab-case)
21-scope-tag.mdTBD

The TBD occurrences are placeholder rows where A1 deliberately did not enumerate the full baseline (per the README “Drafter wave” stage — values to be filled in by Verifier or Editor). The kebab-case rows in 19-engineering-types.md legitimately mirror existing TS type naming conventions (bid-metadata, unified-gap, filter-preset are real TS module names) — these will need editor judgement on whether to snake_case in the markdown or relax the regex.

Recommended:

  • (a) For the 5 TBD rows: A2 spec says BaselineValueSchema.key is required + non-empty. Either (i) A1 needs to supply real values before the loader runs, or (ii) the spec must allow an explicit “TBD” sentinel during the Drafter→Editor transition. Cleanest: spec adds BaselineValueSchema.key.refine(k => k !== 'TBD', '...') AND the editor wave clears all TBD rows. The parity test then catches re-introductions.
  • (b) For the 6 kebab-case keys: this is a real product question. Options: (i) A1 normalises to snake_case (bid_metadata, unified_gap, etc.) — but this loses fidelity with the existing TS module names; (ii) spec regex widens to /^[a-z][a-z0-9_-]*$/ (allowing hyphens). I recommend option (ii) — the existing KH conventions in lib/validation/schemas.ts:41-57 are snake_case, but taxonomy_domains already ships kebab-case names (safeguarding-child-protection, multi-academy-trusts per the snapshot file) and the spec’s parity test will reconcile against those snapshots. Forcing snake_case in the markdown will create permanent DB↔markdown drift on the kebab-case CVs. Widening the regex is the right call.

The spec must pick a position; either way, an explicit decision with supporting reasoning belongs in §6.

Finding 2.1.d — provenance_model enum mismatch with WP-ONTO-R1 §6.2 vocabulary. MEDIUM.

The spec’s §6 declares PROVENANCE_MODEL_VALUES = ['core', 'client_defined', 'hybrid']. The README lines 56-64 + WP-ONTO-R1 §6.2 use 'client' (not 'client_defined') for the open-vocabulary case. A1’s actual usage: every file uses provenance_model: core | client | hybrid (e.g. 15-workspaces-type.md:3 provenance_model: core; 02-taxonomy-subtopics.md:3 provenance_model: hybrid; no file uses client_defined).

Spec is wrong. Change PROVENANCE_MODEL_VALUES = ['core', 'client', 'hybrid'] as const to match A1 + README. Otherwise no A1 file using a client-only vocabulary will validate (none currently do, but client-tier CVs are explicitly in scope per the README’s three classifications, and any future Editor-wave CV will fail).

Finding 2.1.e — layer literal-union shape is needlessly clunky. NIT.

The spec’s §6 layer encoding:

layer: z.union(LAYER_VALUES.map((n) => z.literal(n)) as [
z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>,
z.ZodLiteral<4>, z.ZodLiteral<5>, z.ZodLiteral<6>,
]),

This works but is fragile (the cast tuple has to be re-typed manually if the layer set changes). KH convention per lib/validation/layer-schemas.ts:53 and elsewhere: z.enum(keys as [string, ...string[]]) for derived enums. For a known small numeric set, the idiomatic Zod 3.x form is:

layer: z.union([
z.literal(1), z.literal(2), z.literal(3),
z.literal(4), z.literal(5), z.literal(6),
]),

…or simpler still:

layer: z.number().int().min(1).max(6),

The latter loses the literal-union TypeScript type narrowing but matches lib/validation/schemas.ts:206 (period_days: z.number().int().min(1).max(90)) and lib/validation/guide-schemas.ts:23 (display_order: z.number().int().min(0)) — the existing KH idiom for bounded integers.

If literal-union narrowing is needed (probably yes for layer, since downstream code may switch on layer number), use the explicit z.union([...]) shape — drop the LAYER_VALUES.map(...) as [...] indirection. It saves nothing and makes layer-set changes brittle.

Same applies to related_layers — line 220-227. Same fix.

2.2 — Existing pattern alignment (Cross-check 2)

Section titled “2.2 — Existing pattern alignment (Cross-check 2)”

Finding 2.2.a — gray-matter is correct package choice. Verified via grep "gray-matter" package.json → no existing entry. Verified js-yaml and remark-gfm are present (the latter doesn’t separate frontmatter, only Markdown). The spec’s justification is sound.

Finding 2.2.b — KH idiom for runtime-derived z.enum is z.enum(keys as [string, ...string[]]). Per lib/validation/layer-schemas.ts:53. The spec mentions this idiom in passing in §5.3 (z.enum(values as [string, ...string[]])) but doesn’t enforce it as the convention in content-type-registry.ts. The Implementer should include this exact cast pattern in the registry — see §4 Drift item D-3.

Finding 2.2.c — KH as const literal-tuple pattern is the existing convention for hand-authored constants. The spec’s §5.3 acknowledges that switching from as const widens the type — but the spec leaves the migration of the two existing z.enum(VALID_CONTENT_TYPES) call sites (lib/validation/schemas.ts:264, lib/validation/ingest-schemas.ts:11) implicit. These are NOT just type-narrowing audit items; they are concrete TS compilation errors the Implementer will hit immediately. See §4 Drift item D-3.

2.3 — Test philosophy compliance (Cross-check 3)

Section titled “2.3 — Test philosophy compliance (Cross-check 3)”

PASS WITH OBSERVATIONS. The spec’s §5.4 + §7 plan satisfies the six audit criteria of docs/reference/test-philosophy.md:

  • (1) behaviour-not-implementation: ✅ asserts on parsed values, DB snapshot equality. No mock spy ordering.
  • (2) public API: ✅ exercises loadOntologyCVs() + CONTENT_TYPE_VALUES + the on-disk snapshot file.
  • (3) no internal-implementation coupling: ✅.
  • (4) coverage: ✅ exercises every CV file via iteration + every load path.
  • (5) titles read as product specs: ✅ — verbatim test titles like “every CV file parses and validates against the ontology schema” read like product invariants.
  • (6) factories: N/A — no test data is constructed; the test reads real artefacts.

Mock discipline (§5.3 of test-philosophy.md): ✅ explicitly forbidden in spec §5.4 implementation notes (“No vi.mock. No createMockSupabaseClient. No fixture stubs of the markdown.”). Correct.

Observation: test 4 (“every related_layers entry references a layer in {1, 2, 3, 4, 5, 6}”) is described in spec §5.4 as a “belt-and-braces” check the Zod schema already enforces. If the Zod schema does enforce it (which it does — see §6 LAYER_VALUES discussion in §2.1.e), this test case is strictly redundant with case 1. Either drop it (the schema-validation test 1 covers it) or rephrase to assert on the human-readable error message Zod produces — which is the only thing test 4 adds beyond test 1. The current spec text suggests both intent and uncertainty. Recommend dropping or rephrasing for clarity. Not a blocker.

2.4 — Cocoindex stub fidelity (Cross-check 4)

Section titled “2.4 — Cocoindex stub fidelity (Cross-check 4)”

PASS WITH OBSERVATIONS. The spec’s §5.5 honours the WP7 S9 §7.2 layered fn-shape (inner fns take content_text: str, not FileLike). Specific points:

  • ✅ Outer fn process_ontology_file(file: FileLike) -> ParsedCV correctly file-tier (whole-file fingerprint).
  • ✅ Inner fn parse_cv_frontmatter(content_text: str) -> dict correctly content-tier (memo invalidates only on body change).
  • ✅ Spec correctly notes “imports live inside guarded blocks or comments” — avoids cocoindex import-at-load failure. CLAUDE.md gotcha respected.
  • if __name__ == "__main__": guard prevents accidental invocation.

Observation 2.4.a: the spec’s §5.5 mentions validate_cv_against_db(content_text: str, cv_name: str) -> ValidationReport as a second inner fn. This is a real DB-validation operation that, if ever uncommented, would need DB credentials. Spec says “no DB writes” but doesn’t explicitly say “no DB reads either.” The stub is safer if it omits this fn entirely (or replaces with a pure-Python validate_cv_against_yaml(content_text: str, expected_keys: list[str]) placeholder). Implementer should not introduce a placeholder that gestures at DB access without making the credential surface explicit.

Observation 2.4.b: S9 §7.2 also recommends a version=N bump pattern for cascade invalidation when extraction logic changes. The stub doesn’t reference this, which is fine for a stub but worth noting in a header comment: ”// version= bumps NOT shown here; defer until live wiring.” Belt-and-braces for the future engineer who lifts this stub into a live flow.

PASS. gray-matter is genuinely new (verified via grep "gray-matter" package.json → 0 results). Project already uses remark-gfm (~30 KB) and js-yaml is a transitive of various Vite/Webpack tooling, but neither separates YAML frontmatter from Markdown body. gray-matter is the canonical choice; ~7 KB; MIT-licensed; published by Jon Schlinkert (well-maintained); zero runtime warnings; used by the Astro / Next.js ecosystem.

Resolution of A2 open Q3: confirmed correct dependency choice. No alternative in the existing tree solves the same problem with comparable LOC.

PASS WITH ADDITIONS. The spec’s §8 risk table covers cocoindex sandbox, DB-MD drift, type widening, loader caching, missing ontology dir, knip handling. Missing risks:

Missing risk R-A — Zod default-strip behaviour. As detailed in §2.1.a, Zod z.object() is non-strict by default. The spec’s loader will silently strip status (and any other A1-introduced field beyond §6) without any test failing. The risk table should acknowledge this and the mitigation should be “use .strict() on OntologyCVSchema so unknown fields fail loudly.” This is the single most subtle landmine in the current spec.

Missing risk R-B — “Build the thing, forget to turn it on” (CLAUDE.md general gotcha). The spec’s content-type-registry.ts exports CONTENT_TYPE_VALUES and lib/validation/schemas.ts re-exports as VALID_CONTENT_TYPES. The risk: every consumer importing VALID_CONTENT_TYPES continues to work because the re-export is in place; the registry is exercised. But what tests prove the markdown register is actually being read at runtime? The §5.4 parity test does — but only because it imports CONTENT_TYPE_VALUES directly. Add explicit acceptance criterion: “the parity test imports CONTENT_TYPE_VALUES from @/lib/ontology/content-type-registry, AND the existing __tests__/validation/schema-db-consistency.test.ts continues to pass via VALID_CONTENT_TYPES (proving the re-export chain wires end-to-end).”

Missing risk R-C — Vitest test isolation across the loader cache. The spec’s §5.2 caches the loaded array in module scope (let cached: readonly OntologyCV[] | null). Vitest by default runs test files in separate workers, so cache isolation per worker is fine — but if the loader.ts module is imported by multiple test files in the same worker (e.g. parity test + a future consumer test), the cache survives across tests. If any test mutates the parsed objects (it shouldn’t — they’re typed readonly), that mutation would leak. The spec already declares readonly so this is mostly academic, but a one-liner in §5.2 noting “the cached array is the same reference across all callers — do not mutate” is cheap insurance.

Missing risk R-D — bun run test vs bun test confusion. Spec §5.4 already mentions this. ✅ already covered. No action.

Missing risk R-E — vi.mock() hoisting. The spec’s parity test deliberately uses no mocks per §5.4 implementation notes. ✅ no action — but worth a one-line “if a future addition introduces a mock, follow vi.hoisted() per CLAUDE.md” guard.

Missing risk R-F — content_text_hash GENERATED ALWAYS. Not relevant to this spec — no content_items writes are involved. ✅ no action.

2.7 — Acceptance criteria realism (Cross-check 7)

Section titled “2.7 — Acceptance criteria realism (Cross-check 7)”

PASS WITH ONE CLARIFICATION NEEDED.

  • bun run test __tests__/lib/ontology — verifiable, command exists.
  • bun run test — full-suite green is verifiable.
  • bun lint — verifiable.
  • bun run format:check — verifiable.
  • bun run knip — verifiable.

Clarification needed (acceptance criterion 9): “All 29 markdown files referenced by the Drafter wave parse and validate.” Given the Findings 2.1.b, 2.1.c above, this criterion is currently unsatisfiable as written because at least 8 files will fail Zod validation under the spec’s current §6 schema. The Implementer needs the criterion to be either:

  • (a) “All 29 markdown files parse and validate after the editor-wave fixes” — gates D1 on a pre-merge editor pass.
  • (b) “The Zod schema accepts every A1-shipped frontmatter shape” — gates D1 on the spec being relaxed first.

Pick one. Do not leave the Implementer guessing.

Implicit verification step the spec doesn’t list: running the Implementer’s smoke-test snippet (bun run lib/ontology/loader.ts) at the end of step 3 of §10 build order. This is a one-liner test that catches missing-file or schema-rejection failures BEFORE the Vitest harness runs. Belt-and-braces, very cheap. Add to the build-order checklist.

2.8 — Out-of-scope completeness (Cross-check 8)

Section titled “2.8 — Out-of-scope completeness (Cross-check 8)”

PASS. The spec’s §11 covers: 29 markdown files (Drafter), README (Drafter), other enum migrations, GitBook, live cocoindex flow, admin UI, combined-PR items.

One thing the spec leaves implicit that the Implementer might over-build: the relationship between content-type-registry.ts and the future per-CV registries. The spec’s §5.3 says “first downstream consumer” and §3 architecture diagram shows lib/ontology/content-type-registry.ts as a single file. An over-eager Implementer might generalise this into a generic lib/ontology/registry.ts<T> factory pattern, deferring all 28 other CVs by emitting parametric helpers. Spec must explicitly say: “Do NOT generalise the registry pattern this slice. Build exactly one CV’s registry. Generalisation comes when the second CV adopts the pattern (per the test-philosophy.md §1 rule against premature abstraction).” This is a §11 addition.

2.9 — Implementer instructions clarity (Cross-check 9)

Section titled “2.9 — Implementer instructions clarity (Cross-check 9)”

PASS WITH NOTES. The §10 build order is sequential and explicit. Pre-flight checks are correct. Build order respects dependencies. Quality-gate checklist is comprehensive.

Ambiguity 9.a: §10 step 7 says “Convert literal-union narrowing to runtime .includes(...) where needed.” For the two existing z.enum(VALID_CONTENT_TYPES) calls (lib/validation/schemas.ts:264, lib/validation/ingest-schemas.ts:11), the fix is NOT .includes() — the call sites are inside Zod schemas where .includes() doesn’t apply. The fix is z.enum(VALID_CONTENT_TYPES as readonly [string, ...string[]]). The spec must enumerate this distinction or D1 will be confused. See Drift item D-3.

Ambiguity 9.b: the §10 escalation triggers list “Drafter wave hasn’t produced the 29 markdown files.” This is listed as a halt-condition, but the spec doesn’t give D1 the go/no-go criterion for the Drafter-wave findings (status field, BID_STATES, TBD keys, kebab keys). D1 should NOT silently widen the spec’s regexes to make broken Drafter output pass. The clear D1 rule is: “If the Drafter output violates the spec’s §6 contract, halt and surface to main session — Liam decides whether to relax the spec or fix the Drafter output.” Add this as an explicit escalation trigger.

Ambiguity 9.c: the spec says (line 312) “git reset —hard main.” For a worktree-isolated agent, this is correct per CLAUDE.md. But this branch (content-items-investigation) is the parent branch the implementer will be on; reset-to-main destroys the WP6 commits. The spec MUST say “git reset —hard content-items-investigation” or “git pull —rebase origin content-items-investigation” — not main. The current text invites D1 to wipe out the WP6 chain.


Q1 — Parity test fail-fast vs accumulate-and-report. A2 default: accumulate.

Section titled “Q1 — Parity test fail-fast vs accumulate-and-report. A2 default: accumulate.”

Verdict: A2’s default is correct. Accumulate-and-report is the right call for KH dev workflow. Reasoning:

  1. Vitest’s per-expect reporting already accumulates within a single it() block when assertions are independent. The §5.4 test cases are independent invariants — failing one does not invalidate the others. Reporting all at once gives the developer a complete picture.
  2. The Drafter→Editor→Implementer wave structure specifically separates concerns: Editor wave should see ALL drift items at once, not one-at-a-time across N test runs. Fail-fast would force N iterations of “fix one, re-run, fix next.”
  3. The existing __tests__/validation/schema-db-consistency.test.ts already uses accumulate (sets up missing/extra arrays then asserts toHaveLength(0) with the array contents in the failure message). The new test should mirror this exact pattern.

Caveat: within a single CV file’s frontmatter, Zod stops at the first issue. That’s fine — fixing one Zod issue per file per iteration is the natural unit. The accumulation is across-file, not within-file.

No spec edit needed. A2 default stands.

Q2 — Cocoindex DB target table omitted from this slice. Confirm scope decision is documented adequately.

Section titled “Q2 — Cocoindex DB target table omitted from this slice. Confirm scope decision is documented adequately.”

Verdict: documented adequately. Spec §11 explicitly excludes “Live cocoindex flow (engine boot, DB writes, scheduled execution).” Spec §5.5 explicitly says “we deliberately defer (a) and (b) to a later session because they need migration design + the broader application_types work that lands in the combined PR (Q-OQR1-16).” Both citations are clear.

One small addition recommended: in §5.5’s “Why a stub now” paragraph, add an explicit statement that the future DB target table will land alongside the application_types migration (Q-OQR1-16), so the future engineer doesn’t independently propose a table-design migration. Cross-link to the canonical place that owns the application_types decision.

Q3 — gray-matter ~7 KB new dep — verify size + check for existing alternative.

Section titled “Q3 — gray-matter ~7 KB new dep — verify size + check for existing alternative.”

Verdict: dependency choice is correct. Verified via grep "gray-matter" package.json → no existing entry. Verified js-yaml is not a top-level dep (it’s transitive). Verified remark-gfm is present but does not separate frontmatter from Markdown body. gray-matter is the standard ecosystem choice (used by Astro, Next.js MDX, Hugo plugins) and is ~7 KB minified. No comparable alternative in the existing tree.

A2’s choice stands. No spec edit needed.


4. Drift items requiring TECH.md edit (for editor agent or D1)

Section titled “4. Drift items requiring TECH.md edit (for editor agent or D1)”

Concrete change actions, ordered by criticality. Each has a single-line summary then the diff intent.

D-1 (CRITICAL) — Add status to OntologyCVSchema and use .strict().

Section titled “D-1 (CRITICAL) — Add status to OntologyCVSchema and use .strict().”

Change site: §6 (the Zod shape, lines ~191-232).

Why: Per Finding 2.1.a. Without this, loadOntologyCVs() silently strips a field every A1 file declares.

Diff intent (paste into §6 schema):

  • Add STATUS_VALUES = ['active', 'planned', 'needed'] as const; near line 189.
  • Add status: z.enum(STATUS_VALUES), to OntologyCVSchema.
  • Append .strict() to OntologyCVSchema = z.object({ ... }).strict(); so unknown fields fail loudly going forward.
  • Add STATUS_VALUES to the §5.1 exports list.

D-2 (CRITICAL) — Reconcile cv_name regex with A1’s BID_STATES + BaselineValueSchema.key regex with kebab-case + TBD keys.

Section titled “D-2 (CRITICAL) — Reconcile cv_name regex with A1’s BID_STATES + BaselineValueSchema.key regex with kebab-case + TBD keys.”

Change site: §6 (the Zod shape, lines ~191-232).

Why: Per Findings 2.1.b and 2.1.c. Currently 8+ files will fail validation.

Two-track fix:

  • (a) Update the spec to widen BaselineValueSchema.key regex to ^[a-z][a-z0-9_-]*$ (allow hyphens). Reason: A1 has 6 legitimate kebab-case keys mirroring TS module names.
  • (b) Add an explicit C1 editor-wave action: “Rewrite 14-bid-states.md line 1 from cv_name: BID_STATES to cv_name: bid_states.” Track in §10 escalation list.
  • (c) Add an explicit C1 editor-wave action: “Replace all 5 key: TBD rows with real values OR remove the placeholder rows.” Track in §10 escalation list. Spec keeps the regex strict; A1’s TBD rows are the bug.

Document both decisions in §6 with reasoning.

D-3 (CRITICAL) — Document the two z.enum(VALID_CONTENT_TYPES) call-site fixes verbatim.

Section titled “D-3 (CRITICAL) — Document the two z.enum(VALID_CONTENT_TYPES) call-site fixes verbatim.”

Change site: §5.3 (“Migration of lib/validation/schemas.ts”) + §10 step 7.

Why: Per Finding 2.2.c + Ambiguity 9.a. The spec hand-waves this with “Convert literal-union narrowing to runtime .includes(...) where needed” — wrong fix for the two real call sites.

Diff intent (replace §5.3 final paragraph):

“After widening, two existing call sites in lib/validation/schemas.ts:264 and lib/validation/ingest-schemas.ts:11 use z.enum(VALID_CONTENT_TYPES). The Zod enum() constructor requires readonly [string, ...string[]], not readonly string[]. Apply this exact change at both sites: z.enum(VALID_CONTENT_TYPES as readonly [string, ...string[]]). The cast is safe because CONTENT_TYPE_VALUES is constructed from a non-empty baseline_values array (Zod schema enforces .min(1) on baseline_values). Same fix applies to any future consumer using z.enum(...). The literal-tuple cast is the canonical KH idiom — see lib/validation/layer-schemas.ts:53.”

Also rewrite §10 step 7 to point to this guidance instead of saying “.includes().”

D-4 (CRITICAL) — Fix provenance_model enum to match A1 + README.

Section titled “D-4 (CRITICAL) — Fix provenance_model enum to match A1 + README.”

Change site: §6 (line ~187: PROVENANCE_MODEL_VALUES).

Why: Per Finding 2.1.d. A1 + README use client, not client_defined. Currently no A1 file uses client_defined, so the bug is latent — but new editor-wave CVs WILL use client and will fail.

Diff:

  • Change PROVENANCE_MODEL_VALUES = ['core', 'client_defined', 'hybrid'] as const; to PROVENANCE_MODEL_VALUES = ['core', 'client', 'hybrid'] as const;.

D-5 (HIGH) — Fix worktree reset target in §10 pre-flight.

Section titled “D-5 (HIGH) — Fix worktree reset target in §10 pre-flight.”

Change site: §10 line 312.

Why: Per Ambiguity 9.c. The current text says “git reset —hard main” — this destroys the WP6 commit chain on the content-items-investigation branch.

Diff:

  • Change “git reset —hard main” to “git reset —hard origin/content-items-investigation” (or “git pull —rebase” if the local already has the commits). Add a one-line note: “the WP6 work lives on content-items-investigation, not main.”

D-6 (HIGH) — Add explicit “do not generalise the registry” out-of-scope item.

Section titled “D-6 (HIGH) — Add explicit “do not generalise the registry” out-of-scope item.”

Change site: §11.

Why: Per Cross-check 2.8.

Diff:

  • Append: “Generalising the content-type-registry.ts pattern into a parametric registry.ts<T> for all 28 other CVs. Build exactly the content_type registry this slice. The second CV adopting the pattern (a future session) is when generalisation earns its complexity per docs/reference/test-philosophy.md §2 anti-premature-abstraction discipline.”

D-7 (HIGH) — Add Zod default-strip + cache-mutation + version-cascade risks to §8.

Section titled “D-7 (HIGH) — Add Zod default-strip + cache-mutation + version-cascade risks to §8.”

Change site: §8.

Why: Per Cross-check 2.6, missing risks R-A, R-C, and (less important) the cocoindex version cascade reminder.

Diff: add three rows to the §8 table per the missing-risk descriptions in §2.6 above.

Section titled “D-8 (MEDIUM) — Simplify layer and related_layers schema shape.”

Change site: §6 lines 206-228.

Why: Per Finding 2.1.e. The current shape works but is needlessly complex.

Diff intent: replace z.union(LAYER_VALUES.map(...) as [...]) with explicit z.union([z.literal(1), z.literal(2), ..., z.literal(6)]). Drop the cast indirection.

Section titled “D-9 (MEDIUM) — Drop or rephrase test case 4 (related_layers in {1..6}).”

Change site: §5.4 + §7 test plan table.

Why: Per Cross-check 2.3 observation. The test is strictly redundant with case 1 if the schema enforces the layer set.

Diff intent: either delete case 4 entirely OR rephrase as “rejects related_layers entries outside {1..6} with an actionable error message” — and assert on the Zod issue’s message field (this would be the only thing case 4 adds beyond case 1).

D-10 (MEDIUM) — Tighten cocoindex stub to remove validate_cv_against_db.

Section titled “D-10 (MEDIUM) — Tighten cocoindex stub to remove validate_cv_against_db.”

Change site: §5.5.

Why: Per Cross-check 2.4 observation 2.4.a. The stub gestures at DB access without making the credential surface explicit.

Diff intent: drop the validate_cv_against_db(content_text, cv_name) -> ValidationReport inner fn from the stub OR replace with validate_cv_against_yaml(content_text: str, expected_keys: list[str]) -> ValidationReport (pure-Python, no DB).

D-11 (LOW) — Add the Implementer’s smoke-test step to §10 build order.

Section titled “D-11 (LOW) — Add the Implementer’s smoke-test step to §10 build order.”

Change site: §10 step 3.

Why: Per Cross-check 2.7. A bun run lib/ontology/loader.ts (with the smoke-test snippet baked in) catches issues before Vitest fires. Cheap.

D-12 (LOW) — Acceptance criterion 9 needs to gate on the editor-wave outcome.

Section titled “D-12 (LOW) — Acceptance criterion 9 needs to gate on the editor-wave outcome.”

Change site: §9 acceptance criterion 9.

Why: Per Cross-check 2.7 clarification.

Diff intent: rewrite as “All 29 markdown files parse and validate against the spec’s §6 schema, after applying the editor-wave actions listed under D-2.” Removes ambiguity for D1.


5. Things the Implementer MUST do beyond what the spec says

Section titled “5. Things the Implementer MUST do beyond what the spec says”

These are concrete actions D1 must perform that the spec leaves implicit:

  1. Apply .strict() to OntologyCVSchema so unknown fields fail loudly (per D-1 fix). Without this, the silent-strip behaviour will hide future drift indefinitely.
  2. Cast VALID_CONTENT_TYPES as readonly [string, ...string[]] at both existing z.enum() call sites (lib/validation/schemas.ts:264, lib/validation/ingest-schemas.ts:11) per D-3. The spec’s “literal-union narrowing → .includes() substitution” advice does NOT apply at these sites — they’re inside Zod schemas.
  3. Verify A1’s editor-wave fixes have landed before running the loader: at minimum cv_name: BID_STATEScv_name: bid_states and the 5 key: TBD rows replaced or removed. Per D-2 + escalation-trigger update.
  4. Run a one-shot smoke test (bun run lib/ontology/loader.ts with a try-catch + process.exit(1) snippet) BEFORE running the Vitest suite, so the loader’s full-corpus validation surfaces in <1 second instead of buried inside test reporters.
  5. Verify the existing __tests__/validation/schema-db-consistency.test.ts still passes after the consumer wire-up. The spec’s acceptance criteria say “no regressions from the consumer wire-up” — this is the specific test that proves the re-export chain is intact end-to-end.
  6. Verify the snapshot file is fresh before writing the parity test (bun run sync:taxonomy if .content_types | length ≠ 15). The §10 pre-flight step 3 says this; D1 must actually do it (not just check the file exists).
  7. If the editor-wave fixes are not yet in place when D1 launches: halt, do NOT widen the spec’s regexes locally to make broken Drafter output pass. Surface to main session for a Liam ruling on whether the spec or the Drafter output is the source of truth.

Guardrails against over-scoping:

  1. MUST NOT generalise the content-type-registry.ts pattern into a parametric registry.ts<T> factory for all 28 other CVs. Build exactly one CV’s registry. Premature abstraction is the named anti-pattern in docs/reference/test-philosophy.md. Per D-6.
  2. MUST NOT migrate any other enum (VALID_PLATFORMS, VALID_REVIEW_ACTIONS, VALID_DIGEST_TYPES, etc.) in this slice. Spec §2 + §11 explicitly defer these. Doing extras breaks the slice budget and the review surface.
  3. MUST NOT actually invoke the cocoindex stub (scripts/ontology-sync/parse-flow.py) — no python parse-flow.py, no CI workflow, no engine boot. The stub is a typed file artefact; running it requires the cocoindex install which CLAUDE.md flags as dangerouslyDisableSandbox: true.
  4. MUST NOT introduce vi.mock() in the parity test — spec §5.4 explicitly forbids it; doing so would defeat the parity-with-real-files invariant.
  5. MUST NOT silently widen the §6 Zod regex constraints to make broken Drafter output pass. If the Drafter output violates the spec, halt and escalate.
  6. MUST NOT delete the existing __tests__/validation/schema-db-consistency.test.ts even though the new test partially overlaps. The old test exercises the existing VALID_CONTENT_TYPES import path and proves the re-export chain works. Removing it loses end-to-end wire-up coverage.
  7. MUST NOT touch any files outside lib/ontology/*, lib/validation/schemas.ts, __tests__/lib/ontology/*, scripts/ontology-sync/parse-flow.py, package.json, bun.lock. Spec §10 quality-gate checklist line 6 enumerates the exact set; deviations are scope creep.
  8. MUST NOT git-reset to main — the WP6 work lives on content-items-investigation. Per D-5.

End of report. Authored by S236 Wave B2 verifier sub-agent against TECH.md commit 321b521d. Surfaces 3 critical blocking issues (D-1, D-2, D-3, D-4), 3 high-severity issues (D-5, D-6, D-7), 3 medium issues (D-8, D-9, D-10), and 2 low-priority polish items (D-11, D-12). Recommend: route to C1 editor wave for spec revision before D1 implementer launch. Estimated revision time: ≤30 min for an editor agent given the explicit diff intents above.