Skip to content

T8 PLAN — Cocoindex flow scaffolding decomposition

T8 PLAN — Cocoindex flow scaffolding decomposition

Section titled “T8 PLAN — Cocoindex flow scaffolding decomposition”

Spec slug: cocoindex-flow-scaffolding (single spec — RATIFIED-S252 Wave 1) Subtask: ID-28.4 (PLAN, succeeds {28.3 TECH}, precedes implementation Subtasks {28.5+} per Q-PLANNER-2 fresh-per-Subtask discipline) Parent Task: ID-28 — T8 cocoindex flow scaffolding + Cloud Run sidecar deploy (docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md §4.8) Critical-path position: Wave 4. Gates: T1 (Q-EX2 contract ratified) + T2 (Q-OQR1-16 combined-PR migration SHIPPED-S247) + T6 (q_a_extractions schema SHIPPED-S250 migration 20260520225456_t6_q_a_pairs_full_schema.sql) + T3 (RLS-pattern apply SHIPPED-S239). Gates T7 (Phew Q&A first-ingest) per RATIFIED-S243. Author: task-planner (opus-4-7, isolation=worktree) — FRESH INSTANCE per Q-PLANNER-2 (NOT the {28.1} RESEARCH / {28.2} PRODUCT / {28.3} TECH author).


This PLAN.md decomposes the ratified Wave 1–3 spec chain into TM-shape implementation Subtasks {28.5+} for S253+ dispatch. The spec chain itself closed at S252 Wave 3 with:

  • {28.1} RESEARCH done (S252 Wave 1, 8/8 OQ defaults accepted) — docs/specs/id-28-cocoindex-flow-scaffolding/RESEARCH.md.
  • {28.2} PRODUCT done (S252 Wave 2, 4/4 P-OQ defaults accepted, Checker PASS_WITH_NOTES) — docs/specs/id-28-cocoindex-flow-scaffolding/PRODUCT.md. 27 Behaviour invariants across 6 Areas A–F.
  • {28.3} TECH done (S252 Wave 3, 5/5 T-OQ defaults accepted, Checker PASS_WITH_NOTES + nit-fixes landed at ebbcba2c) — docs/specs/id-28-cocoindex-flow-scaffolding/TECH.md. 9 Proposed changes (P-1..P-9) mapped 1:1 to 27 PRODUCT invariants via coverage matrix at TECH §2.10. Effort estimate at TECH §5 = ~34 h total.

This PLAN.md is decomposition-only. No new behaviour, no new Proposed changes, no new schema. Every Subtask body cross-links to its PRODUCT inv + TECH P-change so the Executor never needs to re-read the spec chain top-to-bottom — the details field IS the dispatch brief.

Sibling decomposition discipline (Q-PLANNER-2 forcing function applied): every Subtask’s dependencies array references other Subtasks of Task ID-28 only. No cross-Task deps (no 28.X → 29.X or similar). The TECH spec gates documented at the head are already-closed Task-level dependencies (Task.dependencies = ["1", "2", "3", "6"] already declared on Task ID-28’s record); they do not appear at Subtask level.

Canonical references:

  • docs/specs/id-28-cocoindex-flow-scaffolding/RESEARCH.md — ratified S252 Wave 1.
  • docs/specs/id-28-cocoindex-flow-scaffolding/PRODUCT.md — ratified S252 Wave 2, 27 invariants.
  • docs/specs/id-28-cocoindex-flow-scaffolding/TECH.md — ratified S252 Wave 3, 9 P-changes + 5 T-OQ defaults + ~34 h estimate.
  • docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md §4.8 — T8 detail in the broader canonical-pipeline roadmap.
  • lib/validation/task-list-schema.tsSubtaskSchema shape + sibling-only superRefine enforcement.
  • kh-sdlc-workflow.md §3.3 (sibling-only deps), §3.4 (25-Subtask soft ceiling), §6.3 (Subtask state machine).

10 implementation Subtasks ({28.5} through {28.14}), well within the 25-Subtask soft ceiling. Integer ids per TM convention restart at 1 within Task ID-28’s Subtask namespace; the four spec-authoring Subtasks already consumed ids 1–4, so implementation ids begin at 5.

Each row in the table below maps to a TM-shape Subtask record appended to docs/reference/task-list.json under Task ID-28’s subtasks array. The details column here is a one-paragraph précis; the full load-bearing dispatch brief lives in the JSON details field.

Subtask IDTitleOne-line descriptionSibling depsTECH P-change coveredPRODUCT inv(s) coveredEffort
28.5Migration — op_id column propagation + indexes on 4 tablesDDL migration adding op_id uuid NULL + B-tree partial index on content_items, q_a_extractions, source_documents, pipeline_runs. IF NOT EXISTS-guarded; no PL/pgSQL functions; no anon-EXECUTE pattern needed. BLOCKER for op_id-consuming Subtasks.[]P-4 (migration only)Inv-11, Inv-12 (column substrate)~1.5 h
28.6Cloud Run sidecar Service deploy infra (4 per-tenant Services + Docling pre-warm)4 per-tenant Service manifests (cloudrun/services/{prod,staging}-{kpf,phew}-cocoindex.yaml), new cloudbuild-cocoindex.yaml, Docling pre-warm script (cloudrun/cocoindex-prewarm.py), requirements.txt pins, .github/workflows/cloud-run-deploy.yml extension. ~5.3 GB image budget (log+warn per T-OQ4). pullmd remains separate per O-Q3.[]P-1Inv-6, Inv-7, Inv-8, Inv-9, Inv-10~6 h
28.7Layered fn-shape per-MIME adapters (scripts/cocoindex_pipeline/adapters.py)Outer-tier @coco.fn(memo=True) convert_binary_to_markdown(file) + three inner-tier extractors: _docling_to_markdown(bytes), _pullmd_to_markdown(url), _passthrough_markdown(str). Pattern mirrors scripts/ontology-sync/parse-flow.py. Inner-tier signatures take bytes/str — never FileLike — so memoisation key is content-hash.[]P-3Inv-4 (idempotency via memoisation)~3 h
28.8Cocoindex flow.py scaffolding — 6-stage canonical pipelineNew scripts/cocoindex_pipeline/{__init__.py, flow.py, __main__.py}. Six stages wired flow-scope per 02-data-flow.md §3.1 + Q-EX2 TECH §3.1 verifier B-3 (NOT @coco.fn-wrapped). KH_PIPELINE_APP = coco.AppConfig(name="kh_pipeline", main_fn=app_main). localfs.walk_dir(live=True, recursive=True). Three mount_table_target(managed_by=ManagedBy.USER) calls for content_items / q_a_extractions / source_documents. Empty-source idle-mode behaviour per O-Q8. content_text_hash omitted from TableSchema (GENERATED ALWAYS per CLAUDE.md).[5, 6, 7]P-2Inv-1, Inv-2, Inv-3, Inv-5~5 h
28.9op_id flow-scope stamping (Python) + record-run.ts opId extensionAugment 28.8’s flow.bind_target() calls to pass op_id=flow["op_id"] so cocoindex stamps each UPSERTed row. Edit lib/pipeline/record-run.ts to add opId?: string | null to RecordPipelineRunParams and include op_id: opId ?? null in the sb() insert payload.[5, 8]P-4 (code half)Inv-11, Inv-12, Inv-14, Inv-15~1.5 h
28.10Inv-13 v1 substrate — RAISE LOG structured-log emission per Postgres UPSERTPer P-OQ1 ratification: audit_log table population DEFERRED-v1.1. v1 substrate is coco.logger.info(...) lines per UPSERT carrying {op_id, table, row_id, operation}. No new KH-side log-shipping plumbing — Cloud Run structured-log surface picks up JSON-formatted lines automatically. Inv-13 retained as v1.1 contract. T-OQ1 v1.1 follow-up routed to backlog (separate ID, post-T8 close).[8]P-5Inv-13 (v1 deferral substrate)~1 h
28.11/api/internal/pipeline-runs/record Vercel route + Python webhook emitter + record-run.ts stageCountsNew app/api/internal/pipeline-runs/record/route.ts (POST, CRON_SECRET auth per T-OQ2 reuse). New _emit_pipeline_run_webhook(...) in scripts/cocoindex_pipeline/flow.py (HTTP POST via aiohttp to PIPELINE_RUN_WEBHOOK_URL env var, mounted via Secret Manager from 28.6). Extend RecordPipelineRunParams with stageCounts?: Record<string, number> so per-stage counters land in pipeline_runs.result JSON column. publicRoutes check in proxy.ts for the new route per CLAUDE.md gotcha.[6, 8, 9]P-7Inv-16, Inv-17, Inv-18 (helper-path discipline)~4 h
28.12ExtractByLlm integration (cross-link to Q-EX2; flow.py edits + 3 integration tests)Cross-link only — DO NOT restate Q-EX2 Pydantic class shapes. scripts/cocoindex_pipeline/flow.py imports the three ExtractionOutput discriminated-union variants + three instruction prompts from scripts/cocoindex_pipeline/{extraction.py, prompts.py} modules authored by T1.2 (already landed). Flow-scope invocation pattern per Q-EX2 TECH §3.1 verifier B-3 (doc["markdown"].transform(ExtractByLlm(...)), NOT @coco.fn-wrapped).[8]P-6Inv-20, Inv-21, Inv-22~2 h
28.13Failure-mode wiring — retry / error-classes / structured-log emission / no-partial-write enforcementDocument retry policy explicitly per P-OQ2 (cocoindex defaults: 3 retries, exponential backoff, 1 s base — no custom config). New lib/pipeline/error-classes.ts exporting 6-class enum as TS literal union + Zod enum. Augment _emit_pipeline_run_webhook() in flow.py to emit status='failed' + populated errorClass per Inv-25 vocabulary. Per-stage coco.logger.error(...) emissions with {op_id, stage, error_class, content_items_id, redacted_error_message} JSON shape. Per-row UPSERT atomicity for Inv-27 per T-OQ5 (cocoindex-native; flow-scope transaction wrapping rejected as anti-cocoindex).[8, 11]P-8Inv-23, Inv-24, Inv-25, Inv-26, Inv-27~4 h
28.14Integration test suite — 22 tests covering 27 invariants + Inv-18/Inv-19 unit guardsNew __tests__/integration/cocoindex/ directory + fixtures/ sub-folder (~10 synthetic test files for failure-injection). test-helpers.ts with pollContentItemsFor(), assertPipelineRunMetrics(), injectLlmFailure(), scaleSidecarToZero() shared utilities. One test file per failure mode + happy path per TECH §2.10 coverage matrix. Inv-18 code-discipline guard via ast-dataflow callers query against raw pipeline_runs.insert. Inv-19 to_regclass('public.pipeline_failures') IS NULL guard. Tests run via bun run test:integration (real Anthropic + Supabase per CLAUDE.md; dangerouslyDisableSandbox: true for cocoindex sub-process).[6, 8, 9, 10, 11, 12, 13]P-9Cross-cutting (all 27 invariants)~6 h

Total: 10 Subtasks, ~34 h (matches TECH §5 estimate exactly). Per-Subtask effort: 6+5+4+4+3+2+1.5+1.5+1+6 = 34 h.


Text-form ASCII (acyclic, sibling-only):

28.5 (op_id migration)
├──────────────────┐
│ │
┌──────────▼ ▼──────────┐
│ 28.9 (op_id stamping + record-run.ts)
│ │
│ ▼
│ (consumed by 28.11)
28.6 (Cloud Run sidecar) ┐
│ │
│ │
▼ │
28.8 (flow.py 6-stage) ◄──┤ 28.7 (per-MIME adapters)
▲ │ │ │ │ │ ▲
│ │ │ │ │ │ │ (28.7 has no sibling deps —
│ │ │ │ │ │ │ flow.py imports the module)
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 28.10 28.12 28.11 │
│ (audit (Q-EX2 (webhook│
│ logs) cross) route) │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ 28.13 │
│ │ │ (failure) │
│ │ │ │ │
│ ▼ ▼ ▼ │
└─► 28.14 (integration test suite — depends on 28.6,8,9,10,11,12,13)

Direct dependency edges (DAG, 19 edges total):

  • 28.5 → 28.8 (op_id columns exist so flow.py TableSchema declarations validate at runtime)
  • 28.5 → 28.9 (op_id stamping requires the columns)
  • 28.6 → 28.8 (sidecar env vars COCOINDEX_SOURCE_PATH, IMAGE_SHA, secrets mounted)
  • 28.6 → 28.11 (sidecar mounts PIPELINE_RUN_WEBHOOK_URL + CRON_SECRET)
  • 28.6 → 28.14 (integration tests need a deployable Service)
  • 28.7 → 28.8 (flow.py imports convert_binary_to_markdown from adapters.py)
  • 28.8 → 28.9 (stamping edits the flow.py bind_target calls authored in 28.8)
  • 28.8 → 28.10 (audit-log emission lines live in flow.py)
  • 28.8 → 28.11 (webhook emitter helper lives in flow.py)
  • 28.8 → 28.12 (Q-EX2 integration edits the flow.py extractor calls)
  • 28.8 → 28.13 (failure-mode wiring edits flow.py retry + error-class emission)
  • 28.8 → 28.14 (integration tests exercise flow.py end-to-end)
  • 28.9 → 28.11 (webhook emitter passes op_id from stamping)
  • 28.9 → 28.14 (op-id stamping integration test)
  • 28.10 → 28.14 (audit-log shipping integration test)
  • 28.11 → 28.13 (failure-mode webhook emits status='failed' via the route from 28.11)
  • 28.11 → 28.14 (record-run integration test)
  • 28.12 → 28.14 (Q-EX2 contract-honour integration test)
  • 28.13 → 28.14 (failure-mode integration tests)

Acyclicity check: every edge points forward in id (lower → higher). No back-edges. Topological sort: 28.5, 28.6, 28.7, 28.8, 28.9, 28.10, 28.11, 28.12, 28.13, 28.14 — direct id-order is a valid execution order.

Sibling-only verification: every dependency integer is in {5, 6, 7, 8, 9, 10, 11, 12, 13, 14} (the sibling id set within Task ID-28). No cross-Task dep expressed. TaskSchema.superRefine validation passes.

Parallelisation opportunities (if S253+ dispatch wants to fan out):

  • Wave A (no deps): 28.5, 28.6, 28.7 can run in parallel from cold-start.
  • Wave B (after Wave A): 28.8 only (single critical-path node).
  • Wave C (after 28.8): 28.9, 28.10, 28.12 can run in parallel.
  • Wave D (after 28.9 + Wave C): 28.11 (deps: 28.6, 28.8, 28.9).
  • Wave E (after Wave D): 28.13 (deps: 28.8, 28.11).
  • Wave F (after Wave E + Wave C): 28.14 — single Subtask absorbing the full surface (deps: 28.6, 28.8, 28.9, 28.10, 28.11, 28.12, 28.13).

6-wave dispatch shape; biggest parallel fan-out is 3 (Wave A) and 3 (Wave C). Comfortable for the orchestrator’s max-2-parallel-Executor pattern.


Per-Subtask + total:

SubtaskEffortCumulative
28.5 op_id migration~1.5 h1.5 h
28.6 Cloud Run sidecar deploy~6 h7.5 h
28.7 per-MIME adapters~3 h10.5 h
28.8 flow.py 6-stage~5 h15.5 h
28.9 op_id stamping + record-run.ts~1.5 h17 h
28.10 audit-log v1 substrate~1 h18 h
28.11 webhook route + emitter + stageCounts~4 h22 h
28.12 ExtractByLlm cross-link~2 h24 h
28.13 failure-mode wiring~4 h28 h
28.14 integration test suite~6 h34 h
Total~34 h

Cross-check vs TECH §5: matches exactly. No effort drift introduced during decomposition.

Per-Subtask sizing (per planning-and-task-breakdown skill sizing guide):

  • XS (≤1.5 h): 28.5, 28.9, 28.10 — single file or two coupled changes.
  • S (2–3 h): 28.7, 28.12 — one module + tests.
  • M (4–5 h): 28.8, 28.11, 28.13 — one feature slice spanning 2–3 files.
  • L (6 h): 28.6, 28.14 — multi-file infrastructure (deploy manifests + workflow edits) and the full test suite respectively.
  • XL (>6 h): none. No Subtask exceeds 6 h, comfortably under the 2-hour-agent-session × 3-agent-session ceiling.

Zero open questions carry forward from Wave 3 ratification. Full OQ provenance traceable from this PLAN.md alone:

  • Wave 1 (8 OQs): O-Q1 source_documents.op_id ALTER as T8 follow-up; O-Q2 Cloud Run Service (not Job); O-Q3 pullmd separate per AGPL; O-Q4 pre-warm Docling model layer; O-Q5 LiteLLM dependency pin + WIF auth; O-Q6 LMDB ephemeral re-fingerprint per S14; O-Q7 CocoInsight DEFERRED-v1.1 (resolved as note in PRODUCT.md §1 + TECH.md §3 out-of-scope — no impl Subtask); O-Q8 T8 ships empty source-binding; T7 stages files post-T8 stable.
  • Wave 2 (4 P-OQs): P-OQ1 audit_log v1 RAISE LOG only (v1.1 table substrate deferred); P-OQ2 cocoindex retry defaults (3 retries, exponential backoff, 1s base); P-OQ3 cocoindex LMDB DLQ + structured-log shipping + pipeline_runs.status='failed' rollup (no new KH table); P-OQ4 both latency framings (≤120s 35-file corpus AND p95 ≤30s per file).
  • Wave 3 (5 T-OQs): T-OQ1 Inv-13 v1.1 → backlog; T-OQ2 CRON_SECRET reuse for webhook auth; T-OQ3 4 per-tenant Services (prod/staging × kpf/phew); T-OQ4 log+warn image-size budget; T-OQ5 per-row atomicity for Inv-27.

All 17 ratifications are baked into the Subtask details fields as already-decided choices, not open OQs.

Risks (decomposition-level, not implementation-level — implementation risks live in the per-Subtask details):

RiskLikelihoodMitigation in decomposition
28.6 Cloud Run image-size budget overrun (Docling+cocoindex+transitive >5.3 GB target)Low–MediumT-OQ4 ratified log+warn (not fail-build). Real measurement at first deploy informs whether to ratchet to hard-enforce in T13 (separate Task).
28.8 flow.py + 28.7 adapters drift on import surface (function signatures don’t match between authoring agents)Low28.7 has no sibling deps and authors convert_binary_to_markdown(file: FileLike) -> str per TECH P-3 first. 28.8 imports against that signature. If parallel-dispatched (Wave A), orchestrator must lock 28.7’s signature before kicking off 28.8 (Wave B gate).
28.9 op_id stamping coupling to 28.8 flow.py stateLow28.9 explicitly edits flow.bind_target() calls authored in 28.8. Sequential 28.8 → 28.9 is hard-enforced via dependency edge.
28.11 proxy.ts publicRoutes regression (new internal API route silently redirects to /login)MediumSubtask details calls out CLAUDE.md gotcha + names the existing /api/cron/* pattern as precedent. Acceptance test exercises the route via direct HTTP POST (not via authenticated browser flow) so the failure mode is surfaced.
28.14 integration test flakiness (Cloud Run cold-start variance, Anthropic API quota)MediumTests skip if kh-cocoindex-pipeline-{tenant} Service is unreachable (per TECH §P-9 gotchas) so local-dev failure modes are bounded; CI runs against staging Service which has min_instances=1 so cold-start is bounded.

No cross-Task escalations. Decomposition stays within Task ID-28’s sibling space.


  • docs/specs/id-28-cocoindex-flow-scaffolding/RESEARCH.md — ratified S252 W1, 8 OQ defaults.
  • docs/specs/id-28-cocoindex-flow-scaffolding/PRODUCT.md — ratified S252 W2, 27 invariants + 4 P-OQ defaults.
  • docs/specs/id-28-cocoindex-flow-scaffolding/TECH.md — ratified S252 W3, 9 P-changes + 5 T-OQ defaults + ~34 h estimate.

§6.2 Sibling specs (already ratified, NOT restated)

Section titled “§6.2 Sibling specs (already ratified, NOT restated)”
  • docs/specs/id-36-cocoindex-extraction-contract/{PRODUCT,TECH}.md — Q-EX2 ExtractByLlm contract (T1.1+T1.2). Pydantic class shapes + validation behaviour + instruction prompts. 28.12 cross-links here.
  • docs/specs/id-36-cocoindex-ledger-api/TECH.md — TS-facing ledger API DEFERRED-v1.1. v1 substrate is pipeline_runs rollup via 28.11.
  • docs/specs/id-56-content-model-invariants/ — Q1.3-Q1.N (T1.4). Consumed by cocoindex flow at extraction time.
  • docs/specs/id-38-rls-pattern/{PRODUCT,TECH}.md — P-1..P-5 + T-1..T-5. P-5 [DEFERRED-v1.1] is the rationale for P-OQ1 audit_log table population deferral (28.10 substrate choice).
  • docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md §3 (dependency graph — T8 critical-path position) + §4.8 (T8 detail).
  • docs/plans/phase-0-investigation/architecture/02-data-flow.md §3 (6-stage topology) + §4 (Cloud Run sidecar) + §5 (op_id hybrid) + §7.2 (recordPipelineRun rollup) + §10.4 (pipeline_failures DO-NOT-BUILD) + §10.5 (anti-patterns).
  • docs/plans/phase-0-investigation/architecture/03-tech-stack.md §5 (cocoindex substrate) + §6 (Docling) + §7 (pullmd retention).
  • docs/plans/phase-0-investigation/0.9-decision-graph.md §11.4.1 (N7 op_id hybrid ratification).
  • .github/workflows/cloud-run-deploy.yml — existing Cloud Run deploy baseline (WIF + per-tenant gcloud run jobs replace + Secret Manager mount loop). 28.6 extends.
  • cloudrun/cloudbuild.yaml — existing buildpack build (~3.3 GB current). 28.6 adds second cloudbuild-cocoindex.yaml config.
  • cloudrun/jobs/{prod,staging}-{kpf,phew}.yaml — per-tenant Jobs manifest convention. 28.6 mirrors as Services.
  • lib/pipeline/record-run.ts — existing recordPipelineRun() helper. 28.9 + 28.11 extend with opId + stageCounts.
  • spike/cocoindex_s1/probe_managed_by_user.py — canonical live-wiring shape. 28.8 mirrors.
  • scripts/ontology-sync/parse-flow.py — canonical layered fn-shape stub. 28.7 mirrors.
  • kh-sdlc-workflow.md §3.3 (sibling-only Subtask deps), §3.4 (25-Subtask soft ceiling — 10/25 here), §6.3 (Subtask state machine: PLAN sets pending initial state at append; Orchestrator transitions to done after Checker PASS).
  • lib/validation/task-list-schema.tsSubtaskSchema shape + TaskSchema.superRefine sibling-only validation.
  • .claude/skills/planning-and-task-breakdown/SKILL.md — Anthropic planning-and-task-breakdown skill body (vertical-slicing + dependency-graph + sizing guidance applied).

§6.5 CLAUDE.md gotchas baked into Subtask details

Section titled “§6.5 CLAUDE.md gotchas baked into Subtask details”

Subtask details fields reference the relevant CLAUDE.md gotchas inline so the Executor inherits the discipline without re-reading the full CLAUDE.md:

  • 28.5: .temp/project-ref drift verification pre-db push; IF NOT EXISTS idempotency; dangerouslyDisableSandbox: true for CLI; no PL/pgSQL → no anon-EXECUTE pattern needed (but documented for v1.1 follow-up).
  • 28.6: Bun fetch hangs on HTTP 204 through sandbox proxy; cocoindex requires dangerouslyDisableSandbox: true for pre-warm script.
  • 28.7: cocoindex localfs.walk_dir defaults recursive=False — explicit True; layered fn-shape per S9 §7.2 (inner-tier signatures take bytes/str never FileLike).
  • 28.8: content_items.content_text_hash GENERATED ALWAYS — omit from TableSchema; cocoindex requires dangerouslyDisableSandbox: true.
  • 28.9: classifyContent userId must be a UUID (pipeline service account); REST PATCH on wrong UUID silently no-ops — verify by re-querying.
  • 28.10: coco.logger.info() JSON-formatted lines for Cloud Run structured-log ingest.
  • 28.11: recordPipelineRun() from @/lib/pipeline/record-run — never raw insert; new public endpoint must be added to proxy.ts publicRoutes; silent failures: use sb() / tryQuery() from @/lib/supabase/safe; getAuthorisedClient() returns { success } not { authorised }.
  • 28.12: import-only — no new behaviour; Q-EX2 contract is the source of truth.
  • 28.13: silent failures prevention via sb() / tryQuery(); ESLint rules local/no-unchecked-supabase-error + local/no-silent-promise-catch enforce.
  • 28.14: Vitest naming *.integration.test.ts under __tests__/integration/cocoindex/; bun run test not bun test; dangerouslyDisableSandbox: true for cocoindex sub-process; tests skip on missing-Service rather than fail (local-dev hygiene).

End of PLAN.md. Next: Liam ratification gate (10 implementation Subtasks {28.5..28.14} + dependency graph + ~34 h effort) → Orchestrator appends to docs/reference/task-list.json (if not already by this dispatch) → S253+ dispatch begins per the Wave A → Wave E shape in §3.