Skip to content

ID-45 {45.2} PRODUCT amendment — Explicit-source-on-create + SOURCE-BACKING contract

ID-45 {45.2} PRODUCT amendment — Explicit-source-on-create

Section titled “ID-45 {45.2} PRODUCT amendment — Explicit-source-on-create”

Status: PRODUCT amendment (a sibling to the ratified {45.2} PRODUCT.md, NOT a replacement). It defines the explicit-source-on-create contract that supersedes the transparent synthetic-minting assumption the just-authored {45.3} TECH.md carried at §“Item 1 → PRODUCT-amendment check” / OQ-45-11. Input: PRODUCT.md + RESEARCH.md + TECH.md (all read in full).

Ratified decision (Liam, S380). The platform is enforcing “no source-less content_items”content_items.source_document_idNOT NULL + ON DELETE RESTRICT FK to source_documents(id), mirroring the proven reference_items shape (squash:7230, :9919). Liam chose REQUIRE AN EXPLICIT SOURCE ON CREATEnot silent synthetic minting. Every content-creation path must therefore carry a real, user-/agent-supplied source provenance, surfaced in the UX wherever a human creates content. This amendment defines that contract; TECH §Item-1 must be re-specced to match (see §TECH delta for the Orchestrator).

De-identification: the V1 client is referred to only generically (the first client / the single-tenant V1 client / the handover DB). No client codename appears here.


Every path that creates a content_items row must attach a real source at create time — an uploaded file, an existing source_documents row, or a URL that mints one — so that after cutover, every content item traces to a real source_documents row and no path can mint source-less content. Where a human creates content, the source is a required, visible field; where an agent (MCP) creates content, the source is a required tool argument; where the content is born from an upload or a bid outcome, the source is the upload / the bid provenance, auto-attached (the user already supplied it by uploading / winning the bid). The synthetic-provenance-minting that TECH §Item-1 assumed is rejected: provenance is recorded from what the creator supplied, never invented to satisfy a constraint.

The ratified {45.3} TECH analysis found five write paths that today mint content_items rows that may carry no source_document_id (each uses the conditional-spread ...(source_document_id && { source_document_id }) idiom, omitting the column when absent). Under the new NOT-NULL constraint every one would 500. TECH’s first answer was to mint a synthetic source_documents evidence row transparently on the caller’s behalf (a shared content_item_ingest_with_source RPC that synthesises provenance). Liam reversed that: synthetic minting hides the truth that a content item has no real origin, which violates the platform’s provenance-recorded, files-are-truth principle (platform-direction.md principle 1; TECH §Governing priority). The amendment is needed so the Executor builds the explicit-source contract (a real UX/agent input), not the transparent-synthetic one.

Code-intelligence orientation (cited verbatim — Checker-verifiable). The five mint sites were grounded against the live code (SQL + Python are outside the TS symbol index; the TS routes were resolved via GitNexus then pinned by direct read):

  • gitnexus_query({repo:'canonical', query:'content_items insert create source_document provenance manual upload mcp'}) resolved the manual-create entry to Function:app/api/items/route.ts:POST (lines 29–452), and surfaced the human-facing create surface components/create-content/upload-tab-content.tsx:UploadTabContent (57–850) and the pipeline body scripts/cocoindex_pipeline/flow.py:_ingest_content_branch (1977–2399) in process_symbols — i.e. the UX surface the explicit-source field must land on is already in the graph.
  • gitnexus_context({repo:'canonical', uid:'Function:app/api/items/route.ts:POST'}) returned 0 incoming callers (it is the route entry; cluster [id], processes proc_11_post → CreateClient, proc_171/172/173), outgoing calls include getAuthorisedClient / authFailureResponse (auth helper pattern present), parseBody (Zod validation seam), resolveContentOwnerId. The source-attach validation lands in the parseBody seam.
  • Direct reads pinned each mint site verbatim:
    • P1 app/api/items/route.ts:202...(source_document_id && { source_document_id }) (insert at :214); schema lib/validation/schemas.ts:346 makes source_document_id .optional() and ingestion_source a narrowed web-form enum ['manual','upload','upload_autosplit'] (schemas.ts:343).
    • P2 app/api/items/batch/route.ts:267/278 — optional, omitted when absent.
    • P3 lib/mcp/tools/content.ts:590...(args.source_document_id && {...}) (insert :608). The tool already exposes a typed provenance triad source_url / source_file / source_document_id (inputSchema content.ts:394–414) and its description already says “supply one of the typed fields” — but all three are currently .optional().
    • P4 app/api/upload/route.ts:314 inserts content_items first (to get the UUID for storage_path = ${itemId}/${filename} at :366), then source_documents at :420, links via UPDATE at :444, with the sd-insert wrapped try{} and tolerated non-fatal (:447–448 “upload continues without lineage tracking”).
    • P5 app/api/procurement/[id]/outcome/integrate/route.ts:209–246 inserts content_items with no source_document_id key at all (it sets source_url: null, ingestion_source: 'bid_outcome_integration', and metadata.{source_bid_id, source_question_id} at :237–243). Confirmed: a genuine fifth source-less mint path, NOT an exempt one.
  • Schema (squash baseline read directly; database.types.ts is read-denied): content_items.source_document_id is nullable uuid, no FK today (partial index idx_content_items_source_document_id ... WHERE source_document_id IS NOT NULL, squash:8796). The target shape already exists on reference_items: source_document_id NOT NULL (squash:7230) + ON DELETE RESTRICT FK (squash:9919).

Goals. Define the explicit-source contract per path (what the creator supplies, the UX/agent surface, the validation/error when absent), the post-cutover guarantee (every content_items row traces to a real source_documents row), and how this composes with the blank-preview-branch cutover — all as numbered, testable Behavior invariants the Checker can verify against acceptance.

Non-goals. This amendment does not re-specify the run/cutover invariants (INV-1..33 in the parent PRODUCT.md stand unchanged); does not own the constraint migration’s DDL mechanics, the ON DELETE semantics (OQ-45-8 / TECH), the cross-target-commit-ordering smoke (OQ-45-9), or the app-fix-before-constraint sequencing (TECH §R2) — those are TECH/PLAN. It owns the create-time contract only. It does not decide the keep-set, and does not touch the pipeline write path (the pipeline is already source-backed — flow.py:2114/:2817 — so it is source-backed by construction and needs no explicit-source UX).


Invariants carry stable IDs ES-1..N (Explicit-Source) so they do not collide with the parent PRODUCT’s INV-1..33; the Checker and the re-specced TECH reference them. They are grouped: A — the create-time contract per path, B — the post-cutover guarantee, C — composition with the cutover, D — UX / accessibility.

ES-1 — Universal create-time invariant (the contract). No content-creation path produces a content_items row without a source_document_id that resolves to a real source_documents row. “Real” means the source_documents row’s provenance traces to something the creator actually supplied — an uploaded file, an existing source document, a URL they entered, or a bid outcome they won — never a row invented solely to satisfy the constraint. Acceptance: across P1–P5, every created content_items row has a non-null source_document_id; the referenced source_documents row carries a provenance key (source_url, storage_path/filename, or a typed bid-outcome marker) derived from the creator’s input, not a placeholder.

ES-2 — P1 manual create (POST /api/items) requires an explicit source. A human creating content manually MUST attach exactly one source before the item can be created: either (a) select an existing source_documents row (passing its id), or (b) enter a source URL (which mints/links a source_documents row keyed on that URL), or (c) arrive with an uploaded file’s source already attached (the upload-then-create flow, which routes through P4/P2). Acceptance: a POST /api/items body that supplies none of {a valid source_document_id, a source_url} is rejected with a validation error (not a 500, not a silently-minted synthetic row); a body supplying one of them creates an item whose source_document_id resolves.

  • ES-2a — validation, not a 500. The “no source” case fails at the request-validation boundary with a clear, user-facing message (e.g. “A source is required: attach a file, select a source document, or enter a source URL.”), before the insert is attempted. The current .optional() source_document_id on the create schema becomes a required-one-of rule (source_document_id XOR source_url-that-mints).

ES-3 — P2 batch / upload-autosplit (POST /api/items/batch) auto-attaches the upload’s source. Content split from an uploaded document already has a source — the upload’s source_documents row. The batch path MUST attach that same source_document_id to every split child; it does not prompt the user for a fresh source per child. Acceptance: every batch-created child carries the parent upload’s source_document_id; the user is not asked to supply a source per split row (the file upload was the explicit source). This counts as “explicit” because the user uploaded the file.

ES-4 — P3 MCP create (mcp_create) requires an explicit source argument. An agent creating content MUST supply a source: the tool’s provenance triad (source_url / source_file / source_document_id) becomes a required-one-of input rather than three optionals. Acceptance: an mcp_create call with none of the three provenance fields is rejected by the tool with a clear error instructing the agent to supply one (the error names the three fields); a call supplying one creates an item whose source_document_id resolves. The tool description’s existing “supply one of” guidance becomes an enforced contract, not advisory.

  • ES-4a — agent-legible error. The rejection message is actionable by an LLM caller: it states that a source is required and lists source_url, source_file, source_document_id as the accepted ways to supply it (mirroring the existing tool-desc wording at content.ts:340) — so the agent can retry without a human.

ES-5 — P4 upload (POST /api/upload) is source-first and fatal-on-failure. The upload is the explicit source. The path MUST mint the source_documents row and set the content item’s source_document_id such that the content item is never persisted source-less, and a source_documents insert failure is fatal (the request fails; no source-less content item is left behind). Acceptance: after an upload, the created content_items row has a non-null source_document_id resolving to the upload’s source_documents row; if the source_documents insert fails, the request returns an error and no content_items row survives (today’s “non-fatal, continues without lineage” branch at upload/route.ts:447–448 is removed). The storage-path ordering constraint (the item UUID feeds storage_path) is a TECH concern; the observable invariant is “no surviving source-less content item, ever”.

ES-6 — P5 bid-outcome integration (outcome/integrate) — provenance is the bid (see ESCALATION). A won bid’s content promoted to the KB is born in the DB — its provenance is the form_response/bid, not an uploaded file. The route already carries the bid provenance (metadata.source_bid_id, metadata.source_question_id, ingestion_source='bid_outcome_integration' at route.ts:230–241). The required outcome is that the promoted content item is source-backed; the mechanism is an OPEN QUESTION for Liam (§Open questions → OQ-ES-1): either (a) the path mints a source_documents row representing the bid-outcome provenance (keyed e.g. sd:bid_outcome:{question_id}, carrying the bid id/name/question as its provenance metadata — making the bid itself the “source document”), OR (b) P5 is the one sanctioned exception to ES-1 and the constraint admits a documented carve-out for born-in-DB content. Acceptance is conditional on the OQ-ES-1 decision — until Liam decides, ES-6 is stated on the outcome (“the bid outcome’s content traces to a recorded provenance”) and the mechanism is deferred. This amendment does not silently pick (a) or (b).

ES-7 — Every content item traces to a real source document (post-cutover). After the constraint lands and the cutover completes, zero content_items rows have a null source_document_id, and every non-null source_document_id resolves to an existing source_documents PK. This is the DB-enforced superset of the parent PRODUCT’s INV-22 (which verified linkage by uuid5 derivation because the FK was dropped); post-amendment the linkage is constraint-enforced, not only derivation-checked. Acceptance: SELECT count(*) FROM content_items WHERE source_document_id IS NULL = 0; an attempted INSERT ... (source_document_id = NULL) raises NOT NULL; an insert with a non-existent FK raises the FK violation.

ES-8 — The contract holds for every future create path, not just P1–P5. Any content item born after cutover is either pipeline-ingested (source-backed by construction — the cocoindex flow always mints sd then writes ci.source_document_id, flow.py:2114/ :2817) or created via P1–P5 (now each requiring/attaching an explicit source). There is no third category of source-less creation. Acceptance: an audit of content-mint sites finds every one routes through a source-attaching path; a new mint site added later that omits source-backing fails the constraint at insert time (the constraint is the backstop the contract sits on top of).

C. Composition with the blank-preview-branch cutover

Section titled “C. Composition with the blank-preview-branch cutover”

ES-9 — The contract is enforced from the cutover moment, on an empty table. The constraint lands during the parent PRODUCT’s INV-6 zero-row window (every pipeline write target empty), so the NOT-NULL addition is backfill-free (TECH §Item-1 Phase B). Post-cutover, the table starts populated only by the re-walk (pipeline, source-backed by construction). The first human/agent create after cutover is already under the explicit-source contract — there is no transition period where source-less creates are tolerated. Acceptance: the app-path fixes (ES-2..ES-6) are merged and live before the constraint migration lands (TECH §R2 ordering); a create attempted between the two is covered by the app fix, so no source-less create can slip through the window.

ES-10 — No regression to the pipeline source-backing. The pipeline write path is unchanged in behaviour by this amendment — it already attaches a real source_documents row per file/URL (the sd:{rel_path} / sd:{url} seeds, flow.py:2039/:2775). The only pipeline change is the schema-declaration tighten (CONTENT_ITEMS_SCHEMA["source_document_id"] nullable=True → False, flow.py:1170), which is a declaration match, not a logic change. Acceptance: a full re-walk lands zero source-less content items (already true; now also constraint-enforced) — the parent PRODUCT’s INV-8/INV-9/INV-22 pass unchanged.

D. UX and accessibility (P1, P4, P2 surfaces — Warm Meridian)

Section titled “D. UX and accessibility (P1, P4, P2 surfaces — Warm Meridian)”

ES-11 — The source field is a first-class, visible part of the create form. On the manual-create surface (the create-content form; components/create-content/provenance-fieldset.tsx is the existing Provenance fieldset home, which today has an optional Source URL field but no source-document selector), the source becomes a required affordance: a labelled control to select an existing source document or enter a source URL, marked required, with the requirement communicated by text/label (not colour alone — WCAG 2.1 AA; never colour alone for meaning). Acceptance: the create form shows a required source control; submitting without a source surfaces an inline, text-based validation message adjacent to the control.

ES-12 — Warm Meridian + semantic tokens for the source affordance and its error. The required-field marker, the inline error, and any “source attached” confirmation use semantic design tokens only (per warm-meridian-implementation-spec.md and the project-wide semantic tokens only, never raw Tailwind colours invariant). The error state mirrors the existing sourceUrlError pattern in provenance-fieldset.tsx:84–92 (role="alert", aria-invalid, aria-describedby, text-destructive semantic token) so the new required-source error is consistent with the established field-error idiom. Acceptance: no raw colour classes on the source control or its error; the error is exposed to assistive tech via role="alert" + aria-describedby exactly as the existing source-URL error is.

ES-13 — Auto-attached sources are shown, not hidden (P2/P4). Where the source is auto-attached (P4 upload, P2 batch-split — the user supplied it by uploading), the UX shows the attached source (filename / document title) rather than presenting an empty required field. The user is not asked to re-supply what they already gave. Acceptance: the upload/batch confirmation surface displays the attached source document’s filename/title; there is no redundant “select a source” prompt on these paths.

ES-14 — UK English throughout. All new copy (labels, errors, tool-arg descriptions) uses UK English (“colour”, “organisation”, DD/MM/YYYY) and avoids client codenames. Acceptance: new strings are UK-spelled; no client codename appears in any label/error.


ESGroupValidation method (concrete check)
ES-1A contractAcross P1–P5, each created content_items row has non-null source_document_id resolving to a source_documents row whose provenance key derives from creator input (not a placeholder).
ES-2 / ES-2aA P1POST /api/items with no source → validation error before insert (no 500, no synthetic row); with a source → item created, fk resolves.
ES-3A P2Every batch-split child carries the parent upload’s source_document_id; no per-child source prompt.
ES-4 / ES-4aA P3mcp_create with none of source_url/source_file/source_document_id → tool error naming the three fields; with one → item created, fk resolves.
ES-5A P4Post-upload item has non-null source_document_id; sd-insert failure → request fails, no surviving source-less item (non-fatal branch removed).
ES-6A P5Conditional on OQ-ES-1: promoted bid-outcome item traces to a recorded provenance (minted bid-outcome source_documents row) OR the documented carve-out applies.
ES-7B post-cutovercount(content_items WHERE source_document_id IS NULL) = 0; NULL insert raises NOT NULL; bad-fk insert raises FK violation.
ES-8B post-cutoverAudit: every content-mint site routes through a source-attaching path; a source-less insert fails at the constraint.
ES-9C cutoverApp fixes (ES-2..ES-6) merged + live before the constraint migration (TECH §R2); no source-less create possible in the window.
ES-10C cutoverFull re-walk lands zero source-less content items; parent INV-8/9/22 pass; pipeline behaviour unchanged.
ES-11D UXCreate form shows a required source control; submit-without-source → inline text validation message.
ES-12D UXNo raw colour classes on the source control/error; error uses role="alert" + aria-describedby (mirrors existing source-URL error).
ES-13D UXUpload/batch confirmation displays the attached source filename/title; no redundant source prompt.
ES-14D UXNew strings UK-spelled; no client codename in labels/errors.

Coverage: every ES-1..14 has a concrete validation method. No invariant is aspirational.


Open questions / decisions that gate full ratification

Section titled “Open questions / decisions that gate full ratification”
  • OQ-ES-1 (P5 bid-outcome — BEFORE the P5 fix; ESCALATED to Liam). A won bid’s promoted content is born in the DB — its provenance is the form_response/bid, not an uploaded file. Does the explicit-source contract apply by (a) minting a source_documents row representing the bid-outcome provenance (e.g. sd:bid_outcome:{question_id}, carrying source_bid_id / source_bid_name / source_question_text from the route’s existing metadata as the source document’s provenance — making the bid the “source”), OR (b) is P5 the one sanctioned exception to ES-1 (the constraint admits a documented carve-out for born-in-DB content, e.g. a reserved sentinel source-document, or P5 is exempted from NOT-NULL via a different mechanism)? TECH recommendation note (informational, NOT a decision): option (a) keeps the NOT-NULL constraint uniform (no carve-out, no nullable escape hatch) and the route already has every field needed to mint a faithful bid-outcome source_documents row — so (a) is the lower-complexity, constraint-uniform path. But this is a provenance-semantics call (is a bid a “source document”?), so it is Liam’s to make, not the Planner’s. Until OQ-ES-1 is resolved, ES-6 is stated on the outcome only and the P5 impl Subtask is blocked.

  • OQ-ES-2 (P1/P2 relationship — clarify before P1/P2 build). Confirm the upload-then-create UX flow: does manual create (P1) ever happen with a fresh file upload inline (in which case the upload mints the source and P1 auto-attaches it, like P2), or is P1 strictly “select-existing-source / enter-URL” and all file-origin creates go through P4/P2? This determines whether P1’s required-source control needs an inline-upload option or only select/URL. (Grounding: upload-tab-content.tsx and the create-content tabs suggest upload and manual-write are separate tabs — so P1 is likely select/URL-only, but confirm.) Not run-blocking; clarifies the P1 UX surface.

  • OQ-ES-3 (URL-entered source on P1 — minting semantics). When a P1/P3 creator supplies a source_url (not an existing source_document_id), confirm the URL mints a source_documents row (keyed sd:{url}, matching the pipeline’s URL seed and the reference_ingest precedent) so the fk resolves — i.e. a URL is a first-class source, not just a metadata string. (The pipeline already does this for URL content; the create paths should reuse the same sd:{url} keying for idempotency.) Likely yes; confirm so the Executor reuses the keying rather than inventing a new one.


{45.3} TECH delta (the precise changes the TECH needs — for the Orchestrator to route)

Section titled “{45.3} TECH delta (the precise changes the TECH needs — for the Orchestrator to route)”

This amendment reverses the transparent-synthetic-minting assumption in TECH §Item-1. The TECH fix-pass must make these changes (TECH invariant mapping preserved — Item 1 still maps to INV-8/9/22, now also ES-1..ES-10):

  1. TECH §“Item 1 → Phase A” / the shared content_item_ingest_with_source RPC. The RPC must not synthesise provenance for source-less callers. Re-spec it as a source-required seam: it takes the creator-supplied provenance (source_document_id to link an existing row, OR source_url/source_file to mint one keyed sd:{url} / sd:{file}), mints source_documents first only from real provenance, and rejects a call with no provenance (it no longer “synthesises a filename” or uses gen_random_uuid() for “manual rows with no natural key” — that branch is deleted). The “manual rows with no URL/file key → gen_random_uuid()” fallback in the current TECH (§Item-1 Phase-A shared-seam bullet) is removed.

  2. TECH §“PRODUCT-amendment check” (the escalation paragraph) + OQ-45-11. Resolve in favour of mandatory-explicit-source (this amendment). Replace the “This TECH assumes transparent synthetic minting (no PRODUCT amendment)” conclusion with “PRODUCT amendment PRODUCT-source-backing.md ratifies explicit-source-on-create; the seam is source-required, not synthesising.” OQ-45-11 is now answered (explicit-source wanted) and can be marked resolved, replaced by OQ-ES-1 (P5 mechanism).

  3. TECH §Item-1 P1 (POST /api/items). Add: the create schema’s source_document_id .optional() (schemas.ts:346) becomes a required-one-of (source_document_id XOR a mint-from-source_url), validated in the parseBody seam, returning a 400 validation error (not a 500) when absent — per ES-2/ES-2a. New required UX field (ES-11..ES-13).

  4. TECH §Item-1 P3 (mcp_create). Add: the inputSchema provenance triad (content.ts:394–414) becomes required-one-of (currently three .optional()); the tool returns a structured error naming the three fields when none supplied — per ES-4/ES-4a.

  5. TECH §Item-1 P2. Add the explicit “auto-attach the parent upload’s source_document_id to every split child” contract (ES-3) — P2 is not prompted-source; it inherits the upload’s source.

  6. TECH §Item-1 P4. Keep the source-first reorder + fatal-on-failure (TECH already specs this correctly) but bind it to ES-5 and note the observable invariant is “no surviving source-less content item, ever” (remove the non-fatal branch at upload/route.ts:447–448).

  7. TECH §Item-1 P5 + new OQ-ES-1. Replace the “mints a synthetic sd evidence row” instruction with the OQ-ES-1 escalation: the P5 impl Subtask is blocked until Liam chooses (a) mint a bid-outcome source_documents row from the existing route metadata, or (b) P5 carve-out. TECH must not pre-decide.

  8. TECH §Testing. Add per-path behaviour tests for the rejection case (P1/P3 create with no source → 400/tool-error, not a silently-minted row) — the explicit-source contract’s negative test is new vs the synthetic-minting framing (which had no rejection path).

Everything else in TECH §Item-1 (the constraint migration Phase B, the nullable=True→False flow.py flip, the ON DELETE RESTRICT recommendation / OQ-45-8, the cross-target-commit smoke / OQ-45-9, the app-fix-before-constraint ordering / R2) is unchanged by this amendment.


Proposed implementation Subtasks (ledger-write INTENTS — Orchestrator applies on MAIN)

Section titled “Proposed implementation Subtasks (ledger-write INTENTS — Orchestrator applies on MAIN)”

These are proposed records for the Orchestrator to add via bun scripts/ledger-cli.ts add-subtasks 45 --file -. The Planner does not write the ledger. Dependencies are sibling-only (within ID-45). Ordering honours TECH §R2 (all app-path fixes land + merge BEFORE the constraint migration) and the OQ-45-9 FK-smoke prod-gate on the constraint Subtask. id values are placeholders ({45.x}) — the Orchestrator assigns the next free local ids; dependencies reference those assigned ids. description ≤250 chars / testStrategy ≤300 chars (budget-gated); detail overflow lives in details.

[
{
"id": "P1",
"title": "Require explicit source on POST /api/items (manual create)",
"description": "Make manual create require an explicit source: select an existing source_documents row (source_document_id) or enter a source_url that mints one. Reject a no-source body with a 400 validation error before insert. New required UX source control.",
"details": "ES-2/ES-2a/ES-11/ES-12. File: app/api/items/route.ts (mint site :202, insert :214). Schema lib/validation/schemas.ts:346 — change source_document_id .optional() to a required-one-of rule (source_document_id XOR source_url-that-mints), validated in the parseBody seam (route POST at :29). On no source: return 400 with a user-facing message ('A source is required: attach a file, select a source document, or enter a source URL.') BEFORE the insert — never a 500, never a synthetic row. When source_url supplied (no source_document_id), mint a source_documents row keyed sd:{url} (reuse pipeline keying — see OQ-ES-3) and link it. UX: components/create-content/provenance-fieldset.tsx — add a required source control (select-existing-source OR source URL), mirror the existing sourceUrlError pattern (:84-92, role='alert' + aria-describedby + text-destructive semantic token). Semantic tokens only, UK English, no client codename. Routes through the shared source-required RPC if the seam Subtask lands first (sibling dep).",
"status": "pending",
"dependencies": [],
"testStrategy": "Behaviour test (test-philosophy.md): POST /api/items with no source -> 400 validation error, zero content_items rows written; POST with source_document_id -> item created, source_document_id resolves; POST with source_url -> source_documents row minted (sd:{url}) and linked. bun run test."
},
{
"id": "P3",
"title": "Require explicit source argument on mcp_create (MCP content tool)",
"description": "Make the mcp_create provenance triad (source_url / source_file / source_document_id) a required-one-of input instead of three optionals. Reject a call with none of the three with a structured, agent-legible error naming the three fields.",
"details": "ES-4/ES-4a. File: lib/mcp/tools/content.ts (inputSchema triad :394-414, insert :608, mint guard :590). Change the three .optional() provenance fields to a required-one-of (Zod refine across the triad). On none-supplied: return a tool error stating a source is required and listing source_url/source_file/source_document_id (mirror the existing tool-desc wording at :340). When source_url/source_file supplied without source_document_id, mint a source_documents row (sd:{url} / sd:{file}) and link it (reuse pipeline keying, OQ-ES-3). Routes through the shared source-required RPC if the seam Subtask lands first (sibling dep). UK English in the new error/arg copy; no client codename.",
"status": "pending",
"dependencies": [],
"testStrategy": "Behaviour test: mcp_create with no provenance field -> tool error naming source_url/source_file/source_document_id, zero rows written; with source_document_id -> item created, fk resolves; with source_url -> source_documents minted + linked. bun run test."
},
{
"id": "P2",
"title": "Auto-attach the upload source to batch / autosplit children",
"description": "Batch-split children (POST /api/items/batch) inherit the parent upload's source_document_id automatically — no per-child source prompt. Every split child is created source-backed by construction from the upload's source_documents row.",
"details": "ES-3/ES-13. File: app/api/items/batch/route.ts (mint :267, insert :278). The batch path already receives the upload's sourceDocumentId (component passes it; see components/create-content/upload-tab-content.tsx props sourceDocumentId). Set source_document_id on EVERY split child from the parent upload's source_documents row; remove the conditional-spread omission. UX (ES-13): the upload/batch confirmation surface shows the attached source filename/title (no redundant 'select a source' prompt). Semantic tokens, UK English.",
"status": "pending",
"dependencies": [],
"testStrategy": "Behaviour test: a batch create with N split children writes N content_items each carrying the parent upload's source_document_id; none source-less; no per-child source prompt rendered. bun run test."
},
{
"id": "P4",
"title": "Make POST /api/upload source-first and fatal-on-sd-failure",
"description": "Reorder upload so the content item is never persisted source-less, and make a source_documents insert failure fatal (request fails, no surviving source-less content item). Removes the current non-fatal 'continues without lineage' branch.",
"details": "ES-5. File: app/api/upload/route.ts (ci insert :314 currently FIRST, sd insert :420, link UPDATE :444, non-fatal try/catch :447-448). Reorder to source-first: mint source_documents BEFORE the content_items insert OR keep the UUID-first ordering but guarantee the ci row carries source_document_id at insert time (the itemId feeds storage_path=${itemId}/${filename} at :366 — Executor picks the minimal reorder: e.g. mint sd with a placeholder storage_path then update post-upload, or sequence the storage upload before the ci/sd pair). Make the sd-insert FATAL: on failure, return an error and ensure NO content_items row survives (today's :447-448 non-fatal branch is removed; if a ci row was written first it must be deleted on sd failure). Observable invariant: no surviving source-less content item, ever.",
"status": "pending",
"dependencies": [],
"testStrategy": "Behaviour test: successful upload -> content_items row has non-null source_document_id resolving to the upload's source_documents row; simulated sd-insert failure -> request returns error AND zero surviving content_items rows for that upload. bun run test."
},
{
"id": "P5",
"title": "BLOCKED — bid-outcome integrate source-backing (awaits OQ-ES-1)",
"description": "Source-back the bid-outcome promotion (POST /api/procurement/[id]/outcome/integrate). BLOCKED on OQ-ES-1: mint a bid-outcome source_documents row from existing route metadata, OR a sanctioned P5 carve-out. Liam decides the provenance semantics before this builds.",
"details": "ES-6 + OQ-ES-1. File: app/api/procurement/[id]/outcome/integrate/route.ts (content_items insert :209-246, NO source_document_id key today). Route already carries metadata.source_bid_id / source_question_id / source_question_text (:237-243) and ingestion_source='bid_outcome_integration' (:230). DO NOT BUILD until Liam resolves OQ-ES-1: option (a) mint a source_documents row keyed sd:bid_outcome:{question_id} carrying the bid provenance as its metadata, set source_document_id; option (b) documented carve-out for born-in-DB content. TECH must not pre-decide. This Subtask stays pending/blocked; surface the OQ in the dispatch.",
"status": "pending",
"dependencies": [],
"testStrategy": "After OQ-ES-1 resolved: behaviour test that a bid-outcome integrate creates a content_items row whose source_document_id traces to the recorded bid provenance (option a) OR is admitted by the documented carve-out (option b). bun run test."
},
{
"id": "SEAM",
"title": "Source-required content-mint seam (shared RPC, no synthetic minting)",
"description": "Introduce the shared source-required write seam (content_item_ingest_with_source RPC, reference_ingest pattern) that mints source_documents first from REAL provenance and rejects no-provenance calls. No synthetic/gen_random_uuid fallback.",
"details": "TECH delta items 1-2. New migration: SECURITY DEFINER PL/pgSQL RPC public.content_item_ingest_with_source(...) mirroring public.reference_ingest (squash:4555-4616). Given content payload + provenance (source_document_id to link, OR source_url/source_file to mint sd:{url}/sd:{file}), mint source_documents FIRST then insert content_items with the fk — one transaction, idempotent on the deterministic PK where a natural key exists. REJECT a call with no provenance (the synthetic-filename / gen_random_uuid 'manual rows with no key' branch from the original TECH is DELETED). Carry SET search_path = public, extensions and REVOKE EXECUTE ... FROM anon. P1-P4 route through it (sibling deps). Optional: if the Executor prefers per-route inline sd-first over a shared RPC, that is a TECH-level call — but the no-synthetic-minting rule is binding regardless.",
"status": "pending",
"dependencies": [],
"testStrategy": "Behaviour/integration test: RPC with source_url mints source_documents (sd:{url}) + content_items linked, one txn; RPC with existing source_document_id links it; RPC with NO provenance raises/rejects (no row written, no synthetic sd). anon cannot EXECUTE. bun run test + live smoke."
},
{
"id": "CONSTRAINT",
"title": "content_items.source_document_id NOT NULL + RESTRICT FK (after path fixes)",
"description": "Land the NOT NULL + ON DELETE RESTRICT FK on content_items.source_document_id during the INV-6 zero-row window, AFTER all path fixes merge. Flip the cocoindex schema declaration nullable False. Prod-gated on the OQ-45-9 FK-smoke.",
"details": "TECH Item-1 Phase B + ES-7/ES-9/ES-10, R2 ordering, OQ-45-8, OQ-45-9. MUST land AFTER P1+P2+P3+P4 merge (and P5 resolved or carved out) — sibling deps on all path-fix Subtasks (R2: a constraint before the path fixes 500s every source-less create in prod). New migration: ALTER TABLE public.content_items ALTER COLUMN source_document_id SET NOT NULL; ADD CONSTRAINT content_items_source_document_id_fkey FOREIGN KEY (source_document_id) REFERENCES public.source_documents(id) ON DELETE RESTRICT (OQ-45-8 — RESTRICT recommended; SET NULL is invalid on a NOT-NULL column). Flip CONTENT_ITEMS_SCHEMA['source_document_id'] nullable=True->False (flow.py:1170). Promote the partial index (squash:8796) to full (optional). Regen database.types.ts --schema public,api. PROD-GATE (OQ-45-9): the constraint MUST NOT merge to prod until the Preview-branch live smoke proves the pipeline lands ZERO FK violations across a full walk (sd commits before ci for every file). cat supabase/.temp/project-ref before any push.",
"status": "pending",
"dependencies": ["P1", "P2", "P3", "P4", "SEAM"],
"testStrategy": "Live Preview-branch smoke (the oracle): full walk lands 0 source-less content_items, 0 FK violations (OQ-45-9 gate). Post-constraint: INSERT with NULL source_document_id raises NOT NULL; INSERT with bad fk raises FK violation; count(content_items WHERE source_document_id IS NULL)=0. supabase-types-parity CI green. bun run test."
}
]

Sibling-only dependency note. All deps above are within ID-45 (sibling Subtasks), so the sibling-only constraint is honoured — no cross-Task dep is needed. The CONSTRAINT Subtask depends on the five path-fix/seam siblings (R2 app-fix-before-constraint). The P5 Subtask is blocked on OQ-ES-1 (a Liam decision, not a sibling dep) — the Orchestrator should hold it pending and not dispatch until OQ-ES-1 resolves; whether CONSTRAINT waits on P5 depends on the OQ-ES-1 outcome (if P5 is carved out as an exception, CONSTRAINT may not depend on it; if P5 mints a bid-outcome source, it must — flag for the Orchestrator to set the CONSTRAINT → P5 dep once OQ-ES-1 lands).


Findings for the Checker (alignment notes)

Section titled “Findings for the Checker (alignment notes)”
  1. This is an amendment, not a replacement. Parent PRODUCT.md INV-1..33 stand unchanged; ES-1..14 are additive and grounded on the same write surface. ES-7 is the constraint-enforced superset of INV-22 (which was derivation-only because the FK was dropped); no contradiction — INV-22’s derivation check still holds, ES-7 adds the constraint backstop.
  2. Reverses the TECH synthetic-minting assumption deliberately. TECH §“PRODUCT-amendment check” / OQ-45-11 explicitly flagged that if Liam wants mandatory-explicit-source, a {45.2} amendment is needed first — this is that amendment. The §TECH delta is the precise re-spec the Orchestrator routes back to a TECH fix-pass.
  3. P5 is surfaced as an OQ, not silently decided (OQ-ES-1) — per the brief’s explicit instruction to escalate the born-in-DB hard case rather than pick a mechanism. The P5 impl Subtask is authored as blocked.
  4. Code-intelligence orientation is cited verbatim (GitNexus query/context outputs + direct file:line reads for all 5 paths + the schema shape) per the planner code-intel binding rule — the Checker can re-run the cited tools to verify.