Skip to content

ID-69 — Client corpus & AI-readiness onboarding model: cross-workspace canonical ingest

ID-69 — Client corpus & AI-readiness onboarding model: cross-workspace canonical ingest

Section titled “ID-69 — Client corpus & AI-readiness onboarding model: cross-workspace canonical ingest”

{69.4} PLAN artefact. This is the decomposition phase of the spec-driven lifecycle: it formalises and sequences the slices the {69.3} TECH Planner identified into implementable {69.5+} Subtasks. It does not re-decide mechanism (that is settled in TECH) and does not author the implementation (that is the Executor). Every Subtask maps to one or more numbered PRODUCT invariants (BI-1..BI-13) and to the corresponding TECH Proposed change.

  • Task: ID-69 “Client corpus & AI-readiness onboarding model — cross-workspace canonical ingest”.
  • Predecessors (read in full): {69.1} RESEARCH.md (S290, ratified), {69.2} PRODUCT.md (S291, ratified — 13 invariants BI-1..BI-13), {69.3} TECH.md (S291, ratified — 13 Proposed changes, 1:1 with BI-1..BI-13, 4 slices + an optional follow-up).
  • Authored: S291 (31/05/2026), fresh Planner instance (Q-PLANNER-2: not the {69.2}/{69.3} author), agent worktree off local branch canonical-pipeline-setup.
  • Status: Draft, pending ratification.
  • Language: UK English throughout.

ID-69 v1 closes the canonical-association ingest gap operator-side and establishes the AI-readiness model — but it ships no schema and no new feature code on the v1 path. The v1 junction-write driver (app/api/items/[id]/workspaces POST) already exists and already implements the BI-3 idempotent composite-key contract (insert + 23505-benign, admin/editor gate). TECH §Migration plan is explicit: “the v1 implementation is operator- driver tests + Python upsert-discipline tests + one doc, not schema or new feature code”. v1 therefore decomposes into characterisation / regression tests + one client-readable doc that pin the existing contract so the deferred v1.1 ingest-side writer can later reuse it unchanged (the load-bearing BI-4 constraint).

This is a deliberately small decomposition (4 mandatory Subtasks + 1 optional follow-up). The TECH Planner estimated 2-4h total. None of the four mandatory slices depend on one another — they are independently verifiable against disjoint surfaces (TS route, Python flow re-ingest, Python flow declares, a markdown doc). That makes them maximally parallelisable.

Architecture decisions (inherited from TECH — not re-decided here)

Section titled “Architecture decisions (inherited from TECH — not re-decided here)”
  • v1 is operator-side only. No ingest-side multi-workspace manifest writer, no ingest-side include/exclude filter, no source_documents.workspace_id write, no upload-route coupling, no ontology hardening, no onboarding UI. All of these are v1.1 / out-of-scope (PRODUCT Non-goals; TECH Q1/Q2; this branch’s resolver is single-valued today).
  • The junction contract is already shipped (operator-side). BI-3 needs no route code change — only a characterisation test proving the contract. The v1.1 ingest-side writer must pass the same assertions, so the test must characterise the contract, not the route’s incidental shape (BI-4).
  • BI-6 re-ingest preservation is automatic via the stable uuid5 identity + declare_row UPSERT discipline (TECH Q3) — guarded by a regression test asserting parent-row UPDATE-not-delete + junction-count invariance, not by runtime code.
  • No DDL migration (content_item_workspaces already has the composite PK, FK cascades, and RLS). No proxy.ts change (no new public route).
{69.5} TS operator-route contract test (BI-3, BI-12) ─┐
{69.6} Python re-ingest upsert regression test (BI-6) ─┤ all independent
{69.7} Python BI-1/BI-2 ingest assertions (BI-1, BI-2) ─┤ (disjoint surfaces)
{69.8} Client-readable readiness doc (BI-7/9/10) ─┘
{69.9} OPTIONAL sb()-harden route inserts (Follow-up) ── depends on {69.5}

All four mandatory Subtasks ({69.5}–{69.8}) have no sibling dependencies — they touch disjoint files and can run fully in parallel. The optional follow-up {69.9} depends on {69.5} only (the contract test must exist and pass before the route insert is refactored, so the refactor is regression-guarded).

The remaining PRODUCT invariants are review-only / negative invariants folded into the relevant Subtask’s acceptance rather than given their own Subtask, to keep the decomposition tight and avoid no-op Subtasks:

  • BI-4 (driver-agnostic contract) — review checklist; verified inside {69.5} acceptance (the test characterises the composite-key contract, not the route).
  • BI-5 (many workspaces per record) — verified inside {69.5} (two POSTs → ≥2 junction rows, first intact, GET returns both).
  • BI-8 (association explicit, never inferred) — review-only; folded into {69.7} acceptance (no folder/classification→workspace path).
  • BI-11 (dedup-agnostic in v1) — review-only; folded into {69.5} acceptance (association additive/idempotent only, no merge/dedup).
  • BI-13 (ID-69 gates ID-45/T7) — Task-level dependency the Orchestrator records on T7’s dependencies[]; NOT a Subtask (already actioned per the brief). Stated here for the Checker; no Subtask owns it.

Phase 1: Contract characterisation + regression (parallel)

Section titled “Phase 1: Contract characterisation + regression (parallel)”
  • {69.5} TS operator-route contract test (BI-3, BI-4, BI-5, BI-11, BI-12)
  • {69.6} Python re-ingest upsert-discipline regression test (BI-6)
  • {69.7} Python BI-1/BI-2 ingest + ast-dataflow regression assertions (BI-1, BI-2, BI-8)
  • {69.8} Client-readable file-source readiness contract doc (BI-7, BI-9, BI-10)
  • bun run test green for the new operator-route contract test ({69.5}).
  • python3 -m pytest scripts/tests/ green for the re-ingest regression ({69.6}) and the BI-1/BI-2 ingest assertions ({69.7}).
  • Readiness doc ({69.8}) reviewed as client-readable (no UI, no DB sources, defers UX to v1.1).
  • Checker confirms BI-4 (driver-agnostic) and BI-13 (gating stated) hold.

Phase 2: Optional tidy (after {69.5}, non-blocking)

Section titled “Phase 2: Optional tidy (after {69.5}, non-blocking)”
  • {69.9} OPTIONAL — sb()/tryQuery()-harden the operator route inserts (Follow-up; non-load-bearing for BI-3).
  • All four mandatory acceptance criteria met; specs and tests aligned.
  • Optional {69.9} either landed (with {69.5} still green) or deferred to the v1.1 deferral register.

{69.5} — TS operator-route contract test (BI-3/BI-4/BI-5/BI-11/BI-12)

Section titled “{69.5} — TS operator-route contract test (BI-3/BI-4/BI-5/BI-11/BI-12)”

Description. Add a Vitest characterisation test for the existing app/api/items/[id]/workspaces POST asserting it is the BI-3 idempotent composite-key junction-write contract: admin/editor-gated, idempotent on (content_item_id, workspace_id), 23505→benign 409. No route code change.

Scope. S (1 test file). Code-touching (TS) → Executor tool-discipline applies (read-only impact on the route under test; no symbol edit, but run the discipline to confirm the test does not require a route change).

Files. New __tests__/api/items/workspaces-contract.test.ts (mirror the existing __tests__/api layout; confirm exact dir at implement time). Under test: app/api/items/[id]/workspaces/route.ts POST (:60-211 per TECH) — auth gate :65-66 (getAuthorisedClient(['admin','editor']) + authFailureResponse(auth)), assign insert :170-173, 23505-benign block :176-181, GET via get_item_workspaces RPC :21-57. Mock pattern: createMockSupabaseClient() from __tests__/helpers/mock-supabase.ts.

What to assert (characterise the CONTRACT, not the route shape — BI-4):

  1. Associate (content_item_id, workspace_id) twice → exactly one junction row; second call benign (409, no duplicate, no unhandled throw) [BI-3].
  2. Two POSTs for one content_item_id to two distinct workspace_ids → ≥2 junction rows, first intact, GET returns both [BI-5].
  3. admin and editor associate succeed; reviewer/viewer rejected at the auth boundary via authFailureResponse(auth) (assert auth.success is the checked field, not auth.authorised) [BI-12].
  4. Association is additive/idempotent only — no merge/dedup/remove path exercised [BI-11].

Constraints carried (do NOT do). Do not add a batch endpoint, do not change the route, do not couple to the upload route, do not write source_documents.workspace_id. The v1.1 ingest-side writer must pass these same assertions — so phrase assertions against the composite-key contract, not against route internals (BI-4).

Tool-discipline (code-touching TS). Before any edit: gitnexus_impact({target: "POST", direction: "upstream"}) scoped to the route (or gitnexus_context({name: "..."}) on the handler) to confirm read-only blast radius; use ast-dataflow callers/references if a call-chain question arises; run gitnexus_detect_changes() before commit to confirm only the new test file is affected. bun run test (not bun test). Tests verify real behaviour per docs/reference/test-philosophy.md.

Spec slices. PRODUCT BI-3 / BI-4 / BI-5 / BI-11 / BI-12; TECH Proposed-changes BI-3 / BI-4 / BI-5 / BI-11 / BI-12, Central design “the contract”, Q2.


{69.6} — Python re-ingest upsert-discipline regression test (BI-6)

Section titled “{69.6} — Python re-ingest upsert-discipline regression test (BI-6)”

Description. Add a Python regression test in scripts/tests/test_cocoindex_flow_write_path.py proving a changed-bytes re-ingest re-stamps the same content_item_id, UPDATEs (not deletes) the content_items parent row, and leaves any existing content_item_workspaces junction rows count-invariant. Asserts the upsert discipline that makes BI-6 automatic; catches an accidental delete-and-reinsert that would FK-cascade the junction.

Scope. S (1 test file). Python → grep + gitnexus_context (Python is gitnexus-indexed, NOT ast-dataflow).

Files. scripts/tests/test_cocoindex_flow_write_path.py (existing home; 0 junction refs today per TECH). Under test: scripts/cocoindex_pipeline/flow.py ingest_file identity content_item_id = uuid.uuid5(_KH_PIPELINE_DOC_NS, f"ci:{rel_path}") (:1240), content_items declare/upsert (:1266), Stage-6 deterministic-PK UPSERT comment (:1233-1238). Schema FK content_item_workspaces.content_item_id → content_items.id ON DELETE CASCADE (migration :5361); composite PK :4318.

What to assert. ingest a file → associate it (operator path or a direct junction insert in the test fixture, mirroring the contract) → re-ingest the same file with changed bytes → assert: (a) content_item_id unchanged; (b) the content_items row was UPDATEd, not duplicated and not delete+reinserted (critical — a delete+reinsert would cascade the junction away); (c) the junction row count for that content_item_id is invariant across re-ingest.

Constraints carried. No runtime code change to flow.py (the guard is a test, TECH Q3). Do NOT add the v1.1 ingest-side junction writer — the resolver is single-valued on this branch and the multi-workspace carrier is deferred.

Tool-discipline (Python). grep over scripts/cocoindex_pipeline/ + gitnexus_context({name: "ingest_file"}) / gitnexus_context({name: "resolve_workspace"}) for orientation (both verified LOW structural risk in TECH). python3 -m pytest scripts/tests/. Use PYTHONUNBUFFERED=1 so output is visible. classifyContent/service-account UUIDs must be valid UUIDs (a0000000-0000-4000-8000-000000000001 for the pipeline service account) if the fixture invokes classification. Tests verify real behaviour per docs/reference/test-philosophy.md.

Spec slices. PRODUCT BI-6; TECH Proposed-change BI-6, Q3, Risks “Re-ingest cascade silently drops associations”.


{69.7} — Python BI-1/BI-2 ingest + ast-dataflow regression assertions (BI-1/BI-2/BI-8)

Section titled “{69.7} — Python BI-1/BI-2 ingest + ast-dataflow regression assertions (BI-1/BI-2/BI-8)”

Description. Add assertions that the canonical ingest declares content_items with no intrinsic workspace and never sets source_documents.workspace_id, confirming association rides content_item_workspaces only. Combine a Python integration assertion (post-ingest source_documents.workspace_id IS NULL while the junction holds the pair; a zero-junction record is still complete) with an ast-dataflow regression check that no canonical-path TS writer touches source_documents.workspace_id.

Scope. S (1 Python test file + 1 ast-dataflow assertion). Python primary; the ast-dataflow check is a tooling assertion, not a TS edit.

Files. scripts/tests/test_cocoindex_flow_write_path.py (or a sibling test module — confirm at implement time). Under test: flow.py content_items declare :1266 (no workspace dependency; resolve_workspace runs later at :1433 for Path B only), source_documents declare :1247-1257 (no workspace_id), classification declare :1278-1279 (LLM-derived, never workspace). Schema: no content_items.workspace_id column (migration :3511).

What to assert.

  1. Schema/shape: content_items has no workspace_id; a canonical record queried back with zero junction rows still has content_text, embedding, source_document_id (complete un-associated record) [BI-1].
  2. Post-ingest source_documents.workspace_id IS NULL while content_item_workspaces holds the (content_item_id, workspace_id) pair [BI-2].
  3. ast-dataflow regression: bun scripts/ast-dataflow-cli.ts column-writes --table source_documents --column workspace_id shows no canonical-path writer [BI-2]. Also confirm (review/grep) no folder-path→workspace or primary_domain/primary_subtopic→workspace mapping exists [BI-8].

Constraints carried. Negative invariants — assert absence, add no new writer. Do NOT introduce any source_documents.workspace_id write (architecturally wrong, PRODUCT Non-goals: never).

Tool-discipline. Python: grep + gitnexus_context. The ast-dataflow column-writes call is the BI-2 regression assertion itself — record its output in the test journal. Python is gitnexus-indexed not ast-dataflow, so also grep the Python flow for any workspace coupling. python3 -m pytest scripts/tests/ (PYTHONUNBUFFERED=1). Tests verify real behaviour per docs/reference/test-philosophy.md.

Spec slices. PRODUCT BI-1 / BI-2 / BI-8; TECH Proposed-changes BI-1 / BI-2 / BI-8.


{69.8} — Client-readable file-source readiness contract doc (BI-7/BI-9/BI-10)

Section titled “{69.8} — Client-readable file-source readiness contract doc (BI-7/BI-9/BI-10)”

Description. Author a new, client-readable AI-readiness checklist for file sources: what enters the corpus, in what shape, and what a client must do to a plain-file source before staging. Names base-path stability as an operator obligation (with the identity-fork consequence) and the clean-staged-folder discipline (no ingest filter in v1; raise-on-unsupported unchanged). No UI; defers database sources and onboarding UX to v1.1.

Scope. S (1 doc, no code).

Files. New docs/specs/ID-69-client-corpus-onboarding/READINESS.md (client-readable, distinct from the internal ingest mechanics in TECH). Reference (do not modify): adapters.py:74-77 raise-on-unsupported (stays unchanged for v1); COCOINDEX_SOURCE_PATH base path (ID-66 owns the volume); manifest at source_path/.kh-workspace-map.json.

What the doc must contain.

  1. The file-source readiness checklist — consumable by a non-developer client admin; supported formats, “AI-ready canonical form” before staging [BI-10].
  2. Base-path stability as a required operator obligation + the identity-fork consequence of drift (and that drift breaks manifest resolution too) [BI-7].
  3. Clean-staged-folder discipline: only AI-ready supported files staged; v1 adds no ingest-side include/exclude filter; the pipeline today raises on unsupported formats (adapters.py:74-77); note the v1.1 skip-not-raise upgrade without implementing it [BI-9].
  4. Explicit deferral: database sources and onboarding UX → v1.1.

Constraints carried. No UI deliverable (watch C-7). No ingest-side filter / no allowlist (BI-9 / OQ-69-4 are v1.1). Scope to file sources only. UK English, DD/MM/YYYY dates.

Tool-discipline. None (doc-only, no symbols). Confirm no code change ships with this Subtask (a reviewer verifies adapters.py:74-77 is unchanged).

Spec slices. PRODUCT BI-7 / BI-9 / BI-10; TECH Proposed-changes BI-7 / BI-9 / BI-10; RESEARCH §6 (AI-ready canonical form).


{69.9} — OPTIONAL: sb()/tryQuery()-harden operator route inserts (Follow-up)

Section titled “{69.9} — OPTIONAL: sb()/tryQuery()-harden operator route inserts (Follow-up)”

Description. Optional, non-load-bearing tidy: refactor the operator route’s assign and create+assign inserts to use sb() / tryQuery() from @/lib/supabase/safe instead of the raw client with manual error.code handling, for consistency with the no-silent-Supabase-failures discipline. Behaviour unchanged (it is already not silent — it checks error.code).

Scope. S (1 route file). Code-touching (TS) → Executor tool-discipline applies. Non-blocking — may be deferred to the v1.1 deferral register; flag, do not force.

Files. app/api/items/[id]/workspaces/route.ts assign insert (:170-173) and create+assign insert (:145-150). Helpers: @/lib/supabase/safe (sb() / tryQuery()); composite responses via warningsEnvelope() if a composite shape results.

What to assert. The {69.5} contract test still passes unchanged after the refactor (idempotency + 23505-benign + role gate preserved). The 23505-benign-409 semantics must survive the sb()/tryQuery() migration — verify the unique-violation code path is still detected and mapped to a benign 409, not swallowed or re-raised.

Constraints carried. Behaviour-preserving only. Must not regress BI-3 / BI-12. No barrel re-exports — import sb/tryQuery directly from @/lib/supabase/safe.

Tool-discipline (code-touching TS). gitnexus_impact({target: "POST", direction: "upstream"}) on the route handler before edit; warn if HIGH/CRITICAL (expected LOW — single route, callers are HTTP clients). ast-dataflow references for any helper signature question. gitnexus_detect_changes() before commit. bun run test (re-run {69.5} contract test) green before commit.

Spec slices. TECH Follow-ups (“sb()/tryQuery()-harden the operator route inserts”); Risks “Operator route insert bypasses sb()”.


All Subtask dependencies are sibling-only (within ID-69):

  • {69.5}, {69.6}, {69.7}, {69.8}dependencies: [] (independent).
  • {69.9}dependencies: [5] (sibling {69.5} only).

No cross-Task Subtask dependency surfaced. BI-13 (ID-69 gates ID-45/T7) is a Task-level dependency the Orchestrator records on T7’s dependencies[] — it is deliberately not encoded as a Subtask dependency (per the dispatch brief, already actioned). No Task-split / Task-merge escalation is required.

WaveSubtasksRationale
Wave 1{69.5}, {69.6}, {69.7}, {69.8}Fully independent; disjoint surfaces (TS route test / Python re-ingest test / Python declares test / markdown doc). Maximal parallelism.
Wave 2 (optional){69.9}Depends on {69.5} (contract test must exist + pass to regression-guard the route refactor). Non-blocking — may be deferred.

4 mandatory + 1 optional = 5 of 25. Well within the soft cap. No Task-boundary problem. The small count reflects that v1 ships no schema and no new feature code — the junction-write driver already exists operator-side; v1 pins it with tests + one doc.

RiskImpactMitigation
{69.5} characterises the route’s incidental shape instead of the BI-4 contract, so the v1.1 ingest-side writer cannot reuse the same assertions.MedSubtask details mandate asserting the composite-key contract (idempotent upsert + 23505-benign + role gate), not route internals. Checker verifies BI-4 against the test.
{69.6} asserts row identity but misses the delete-and-reinsert cascade (the real BI-6 hazard).HighSubtask details make the parent-row-UPDATE assertion explicit and critical; assert junction-count invariance across re-ingest, which fails loudly on a cascade.
{69.8} drifts into UI/onboarding-UX scope (C-7 v1.1) or ingest-filter scope (BI-9 v1.1).MedConstraints carried: no UI, no ingest filter, file sources only, defers DB + UX to v1.1; Checker confirms client-readability + scope.
{69.9} taken as mandatory and its sb() migration silently swallows the 23505-benign path.LowMarked OPTIONAL / non-blocking; details require the {69.5} contract test to stay green and the unique-violation→benign-409 path to be explicitly preserved.

None blocking. Mechanism questions (Q1 v1.1 manifest carrier, Q2 operator surface exactness, Q3 re-ingest preservation) were resolved in TECH and are not re-opened here. The v1.1 ingest-side junction driver + workspace_ids carrier remain on the deferral register (docs/themes/canonical-pipeline/reference/deferral-register.md, OQ-69-1 option a) and are explicitly NOT ID-69 v1 Subtasks.

DateSessionNote
31/05/2026S291{69.4} PLAN authored by a fresh Planner against ratified {69.2} PRODUCT + {69.3} TECH. Decomposed v1 into 4 mandatory Subtasks ({69.5} TS operator-route contract test BI-3/4/5/11/12; {69.6} Python re-ingest upsert regression BI-6; {69.7} Python BI-1/BI-2 ingest + ast-dataflow assertions BI-1/2/8; {69.8} client-readable file-source readiness doc BI-7/9/10) + 1 optional non-blocking follow-up ({69.9} sb()-harden route inserts). All sibling-only deps; {69.9} depends on {69.5}. BI-13 is a Task-level dep (not a Subtask). 5 of 25 ceiling. Wave 1 = {69.5}–{69.8} parallel; Wave 2 = optional {69.9}. No schema, no new v1 feature code — tests + one doc pin the already-shipped operator-side contract for unchanged v1.1 reuse (BI-4).