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_id→ NOT NULL + ON DELETE RESTRICT FK tosource_documents(id), mirroring the provenreference_itemsshape (squash:7230, :9919). Liam chose REQUIRE AN EXPLICIT SOURCE ON CREATE — not 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.
Summary
Section titled “Summary”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.
Problem
Section titled “Problem”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 toFunction:app/api/items/route.ts:POST(lines 29–452), and surfaced the human-facing create surfacecomponents/create-content/upload-tab-content.tsx:UploadTabContent(57–850) and the pipeline bodyscripts/cocoindex_pipeline/flow.py:_ingest_content_branch(1977–2399) inprocess_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], processesproc_11_post → CreateClient,proc_171/172/173), outgoing calls includegetAuthorisedClient/authFailureResponse(auth helper pattern present),parseBody(Zod validation seam),resolveContentOwnerId. The source-attach validation lands in theparseBodyseam.- Direct reads pinned each mint site verbatim:
- P1
app/api/items/route.ts:202—...(source_document_id && { source_document_id })(insert at :214); schemalib/validation/schemas.ts:346makessource_document_id.optional()andingestion_sourcea 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 triadsource_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:314insertscontent_itemsfirst (to get the UUID forstorage_path = ${itemId}/${filename}at :366), thensource_documentsat :420, links via UPDATE at :444, with the sd-insert wrappedtry{}and tolerated non-fatal (:447–448 “upload continues without lineage tracking”). - P5
app/api/procurement/[id]/outcome/integrate/route.ts:209–246insertscontent_itemswith nosource_document_idkey at all (it setssource_url: null,ingestion_source: 'bid_outcome_integration', andmetadata.{source_bid_id, source_question_id}at :237–243). Confirmed: a genuine fifth source-less mint path, NOT an exempt one.
- P1
- Schema (squash baseline read directly;
database.types.tsis read-denied):content_items.source_document_idis nullableuuid, no FK today (partial indexidx_content_items_source_document_id ... WHERE source_document_id IS NOT NULL, squash:8796). The target shape already exists onreference_items:source_document_id NOT NULL(squash:7230) +ON DELETE RESTRICTFK (squash:9919).
Goals / Non-goals
Section titled “Goals / Non-goals”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).
Behavior
Section titled “Behavior”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.
A. The explicit-source contract, per path
Section titled “A. The explicit-source contract, per path”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_idon the create schema becomes a required-one-of rule (source_document_idXORsource_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_idas 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).
B. The post-cutover guarantee
Section titled “B. The post-cutover guarantee”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.
Validation method per invariant
Section titled “Validation method per invariant”| ES | Group | Validation method (concrete check) |
|---|---|---|
| ES-1 | A contract | Across 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-2a | A P1 | POST /api/items with no source → validation error before insert (no 500, no synthetic row); with a source → item created, fk resolves. |
| ES-3 | A P2 | Every batch-split child carries the parent upload’s source_document_id; no per-child source prompt. |
| ES-4 / ES-4a | A P3 | mcp_create with none of source_url/source_file/source_document_id → tool error naming the three fields; with one → item created, fk resolves. |
| ES-5 | A P4 | Post-upload item has non-null source_document_id; sd-insert failure → request fails, no surviving source-less item (non-fatal branch removed). |
| ES-6 | A P5 | Conditional 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-7 | B post-cutover | count(content_items WHERE source_document_id IS NULL) = 0; NULL insert raises NOT NULL; bad-fk insert raises FK violation. |
| ES-8 | B post-cutover | Audit: every content-mint site routes through a source-attaching path; a source-less insert fails at the constraint. |
| ES-9 | C cutover | App fixes (ES-2..ES-6) merged + live before the constraint migration (TECH §R2); no source-less create possible in the window. |
| ES-10 | C cutover | Full re-walk lands zero source-less content items; parent INV-8/9/22 pass; pipeline behaviour unchanged. |
| ES-11 | D UX | Create form shows a required source control; submit-without-source → inline text validation message. |
| ES-12 | D UX | No raw colour classes on the source control/error; error uses role="alert" + aria-describedby (mirrors existing source-URL error). |
| ES-13 | D UX | Upload/batch confirmation displays the attached source filename/title; no redundant source prompt. |
| ES-14 | D UX | New 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 asource_documentsrow representing the bid-outcome provenance (e.g.sd:bid_outcome:{question_id}, carryingsource_bid_id/source_bid_name/source_question_textfrom 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-outcomesource_documentsrow — 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.tsxand 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 existingsource_document_id), confirm the URL mints asource_documentsrow (keyedsd:{url}, matching the pipeline’s URL seed and thereference_ingestprecedent) 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 samesd:{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):
-
TECH §“Item 1 → Phase A” / the shared
content_item_ingest_with_sourceRPC. 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_idto link an existing row, ORsource_url/source_fileto mint one keyedsd:{url}/sd:{file}), mintssource_documentsfirst only from real provenance, and rejects a call with no provenance (it no longer “synthesises a filename” or usesgen_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. -
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.mdratifies 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). -
TECH §Item-1 P1 (
POST /api/items). Add: the create schema’ssource_document_id.optional()(schemas.ts:346) becomes a required-one-of (source_document_idXOR a mint-from-source_url), validated in theparseBodyseam, returning a 400 validation error (not a 500) when absent — per ES-2/ES-2a. New required UX field (ES-11..ES-13). -
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. -
TECH §Item-1 P2. Add the explicit “auto-attach the parent upload’s
source_document_idto every split child” contract (ES-3) — P2 is not prompted-source; it inherits the upload’s source. -
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).
-
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_documentsrow from the existing route metadata, or (b) P5 carve-out. TECH must not pre-decide. -
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.idvalues are placeholders ({45.x}) — the Orchestrator assigns the next free local ids;dependenciesreference those assigned ids.description≤250 chars /testStrategy≤300 chars (budget-gated); detail overflow lives indetails.
[ { "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)”- 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.
- 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. - 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.
- 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.