Version. 0.1 · Date. 2026-08-15 · Author. Claude Code (drafting, from [B-93] + scoping-notes/loomworks-b93-fk-census-scoping-note-v0_1) / Marvin Percival (approving).
Target. /Users/dunin7/loomworks-engine only (the discard, the policy declaration, the fence test). Baseline. Engine 1adcd17. CC confirms at Step 0.
Companion to. [B-93] (two named FKs, twice-predicting — now three), the census note, [B-98]'s completion (the pairing's other half).
Charter. Autonomous regime. Status. DRAFT — awaiting the gate. Do not execute.
engagements, 4 handled, and a third blocker already live before anyone hit it (companion_notifications, on E0129 today, found by enumeration instead of by toll). Four entries each added after a block is a pattern, not a list — a fifth would arrive the same way. The fence is what breaks that cycle, and it is the fourth application of the source-contract pattern: the AST test (CR-204), the source-scrape (CR-205), the kinds fence (CR-213), and this — a contract that must hold over source constructs, enforced against the source (here, the schema), not against exercised paths.delete / null / active_only forces someone to decide what each FK MEANS for a candidate rather than reacting to a 500. The declaration is where the thinking happens; the fence catches an undeclared FK, but a policy chosen carelessly passes the fence happily. The twenty declarations below are decisions, made at drafting-or-build with reasons, not defaults filled to green the test.
A. The policy declaration (the point). One module (engagement/candidate_discard_policy.py or inline in candidate_discard.py — Step 1 decides placement): a mapping from every FK that references engagements(id) to its candidate-discard policy, each with a one-line reason:
delete — rows about a thing being unmade (the endpoint's own nothing-to-preserve rationale): memory_events, memberships, engagement_api_keys, uploaded_files (the four already handled, now declared), conversation_turns (blocker 1), companion_notifications (blocker 3), and any others the Step-1 read rules into this class.null — stale pointers to a thing being unmade: host_account.current_engagement_id (blocker 2 — a focus on an unmade thing is stale by definition).active_only — a declared assertion that this table cannot reference a candidate, with the reason (e.g. contributors — registered at commit; the render/manifestation/shape pipeline — room production is post-commit; host_account.personal_engagement_id — personal engagements are created active). Each active_only is a claim, checked against code at Step 1, not a default — and a wrong one converts to a one-line policy change the day its table proves reachable, caught by the very 500 the delete/null classes no longer produce, or preferably by review before then.
B. The discard executes the policy. discard_candidate_engagement iterates the declaration: deletes the delete class, nulls the null class, touches nothing declared active_only — then the row. Order respects any FK-to-FK dependencies (Step 1 verifies; membership_designations already cascades).
C. The fence (the cycle-breaker). A test reads the live schema's referencing FKs (information_schema / SQLAlchemy metadata) and fails when any FK referencing engagements lacks a declared policy — a new FK added anywhere in the system without a discard decision is a failing test at its author's desk, not a 500 on a cleanup months later. Plus behavior tests: discard succeeds for a candidate WITH turns, WITH a notification, WITH a focus pointing at it (the three fixtures the suites never had — each observed failing first); the fence-of-the-fence: an active engagement's rows are never touched by any discard path.
D. Nothing else. The HTTP route's shape, auth, and refusal-for-committed behavior unchanged; no surface change; [B-100]/[B-94]/[B-97] untouched.
active_only declarations become standing claims with named reasons — reviewable, cheap to amend, and each one a place where the thinking is recorded rather than deferred.The fence (schema-read, fails on undeclared FK — observed failing trivially before the declaration exists); discard-with-turns; discard-with-notification; discard-with-focus (each currently 500s — the observed-failing state is production truth); the active-untouched fence; policy-execution order. Existing discard/hard-delete suites re-run.
Kind C expected: discard plumbing + a declaration module; the seed path untouched (candidate seeds live on the ADMIN log and their events are already in the delete class via memory_events). Checked at Step 1; a null finding is an entry.
| Step | What | Mode |
|---|---|---|
| 0 | Pre-flight — baseline, clean tree. | Auto |
| 1 | The census re-run against code (the write paths for each of the 27 — every active_only claim verified, not assumed; per the arc: probe what can be probed); placement decision; tests written, observed failing. | Auto |
| 2 | The declaration + the discard rewrite + the fence. | Auto |
| 3 | Full suite; ruff; mypy (message-level; the baseline gate remembered); the no-delete static-check allow-list updated if the new module carries DELETEs. | Auto |
| 4 | Eye-test: E0129 discarded live — three blockers, one 204, the walk arc's residue gone; verified absent; focus confirmed cleared not orphaned. | Auto |
| A | Checkpoint. | Checkpoint |
| 5 | Tag; push; watch (verdict read directly); redeploy engine. | Auto |
Execute CR-2026-215 v0.1 at change-requests/cr-2026-215-loomworks-
candidate-discard-policy-fence-v0_1.md in loomworks-record. Engine
repo only. Confirm baseline first.
§0: the declaration is where the thinking happens — every
active_only verified against code, not defaulted; the fence breaks
the added-after-a-block cycle (source-contract pattern, fourth
application); E0129 is the specimen and its discard cleans the
walk arc's residue. Halt at Checkpoint A.
DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-215 — candidate discard, policy and fence — v0.1 — 2026-08-15 Twenty-seven references, one decision each. The fence catches the undeclared; the declaration is where the thinking happens.