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 branchcanonical-pipeline-setup. - Status: Draft, pending ratification.
- Language: UK English throughout.
Overview
Section titled “Overview”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_idwrite, 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
uuid5identity +declare_rowUPSERT 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_workspacesalready has the composite PK, FK cascades, and RLS). Noproxy.tschange (no new public route).
Dependency graph
Section titled “Dependency graph”{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.
Subtask list
Section titled “Subtask list”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)
Checkpoint: v1 contract pinned
Section titled “Checkpoint: v1 contract pinned”-
bun run testgreen 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).
Checkpoint: complete
Section titled “Checkpoint: complete”- 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.
Subtask details
Section titled “Subtask details”{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):
- Associate
(content_item_id, workspace_id)twice → exactly one junction row; second call benign (409, no duplicate, no unhandled throw) [BI-3]. - Two POSTs for one
content_item_idto two distinctworkspace_ids → ≥2 junction rows, first intact, GET returns both [BI-5]. - admin and editor associate succeed; reviewer/viewer rejected at the auth
boundary via
authFailureResponse(auth)(assertauth.successis the checked field, notauth.authorised) [BI-12]. - 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.
- Schema/shape:
content_itemshas noworkspace_id; a canonical record queried back with zero junction rows still hascontent_text,embedding,source_document_id(complete un-associated record) [BI-1]. - Post-ingest
source_documents.workspace_id IS NULLwhilecontent_item_workspacesholds the(content_item_id, workspace_id)pair [BI-2]. - ast-dataflow regression:
bun scripts/ast-dataflow-cli.ts column-writes --table source_documents --column workspace_idshows no canonical-path writer [BI-2]. Also confirm (review/grep) no folder-path→workspace orprimary_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.
- The file-source readiness checklist — consumable by a non-developer client admin; supported formats, “AI-ready canonical form” before staging [BI-10].
- Base-path stability as a required operator obligation + the identity-fork consequence of drift (and that drift breaks manifest resolution too) [BI-7].
- 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]. - 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()”.
Sibling-only dependency verification
Section titled “Sibling-only dependency verification”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.
Parallelisation
Section titled “Parallelisation”| Wave | Subtasks | Rationale |
|---|---|---|
| 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. |
25-Subtask ceiling
Section titled “25-Subtask ceiling”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.
Risks and mitigations
Section titled “Risks and mitigations”| Risk | Impact | Mitigation |
|---|---|---|
| {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. | Med | Subtask 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). | High | Subtask 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). | Med | Constraints 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. | Low | Marked OPTIONAL / non-blocking; details require the {69.5} contract test to stay green and the unique-violation→benign-409 path to be explicitly preserved. |
Open questions
Section titled “Open questions”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.
Changelog
Section titled “Changelog”| Date | Session | Note |
|---|---|---|
| 31/05/2026 | S291 | {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). |