DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path foray-reference/loomworks-candidate-discard-and-the-record-v0_2.md

Loomworks — open design question: candidate discard and the record — v0.2

Version. 0.2 Date. 2026-08-21 Status. Open design question. Not answered here. Filed to be posed, not resolved. Markdown primary — the consumer is Claude Code and change-request drafting. Changes from v0.1. Ground-truthed against engagement/candidate_discard.py and candidate_discard_policy.py at engine 143fd6b, rather than posed from the policy file's comments alone. The central premise changes: this is not an undeclared deletion path. It is an explicitly ruled carve-out with a stated doctrine, an allow-list entry in a static no-delete check, and a fence test (§1). v0.1's framing is preserved and corrected at §7. What remains open is different from what v0.1 asked, and larger in one place and smaller in another (§2). Three consequences that did not exist on 2026-08-20 are added: the two-records ruling (§3), a live schema question about agent_lifecycle_events (§4), and the credential interaction (§5). v0.1 stands as sibling. Filed under. Ruling 2, item A, Operator, 2026-08-20.


1. What is actually there — the carve-out is declared, not accidental

engagement/candidate_discard.py's own module docstring:

> This file is allow-listed in the no-delete static check (tests/test_no_delete_statements.py). The CR §7.2 carves out an explicit exception to R-A28 / R-B21's non-erasure discipline: candidate engagements are pre-commitment (no human Commit per R-A3 has occurred), so there is nothing to preserve.

So the mechanism is ruled, documented, fenced, and reasoned. The doctrine is coherent and worth stating in its own terms:

> Non-erasure attaches at Commit. Before a human Commit, there is no committed record to preserve.

That is one of the two answers v0.1 posed — the first one — and it is not merely available, it is already the ruled position. v0.1 asked which of two readings holds without knowing that one had been chosen and built.

The policy table is stronger than v0.1 implied. candidate_discard_policy.py requires a declared decision — delete, null, or active_only — for every foreign key referencing engagements, with a fence test that reads the live schema and fails when a new FK arrives undeclared. Its own header records why: the old discard handled four tables, each added after a production block, and four entries added after blocks is a pattern, not a list. active_only requires cited code evidence, and the header warns that a forced policy is worse than an undeclared FK, because it passes the fence.

This is the fourth application of the source-contract pattern — alongside the AST scan, the source-scrape, and the kinds fence. It is the same discipline CR-2026-232's baseline used, built earlier.

2. What is actually open — and it is not what v0.1 asked

v0.1 asked whether candidate material is Memory. That is ruled: it is pre-commitment and deletable. Accepting that ruling, one thing remains, and the code conflates two claims:

The discard leaves no trace anywhere. The candidate's rows go, the engagement row goes, and no record survives that a discard occurred, who performed it, or when. The act is invisible even though the actor is known — the route enforces created_by_person_id against the caller before invoking it.

A discard is a governance act. A person unmade a thing. Under the standing note filed 2026-08-21, loomworks-standing-note-two-records-and-what-crosses-between-them-v0_1 §3, its boundary test answers cleanly: a discard is not knowledge about an engagement's subject; it is a fact about what the substrate did. It has an actor, a timestamp, and a target, and it cannot be disagreed with — it either happened or it did not.

So the operational record is where a discard record belongs, and that answer did not exist when v0.1 was written. It also dissolves the shape problem v0.1 could not resolve: an event in Memory about a discarded candidate has no engagement to attach to once the engagement row is gone. A row in the operational record has no such difficulty.

This is the question, restated: should the act of discarding be recorded in the operational record, given that the contents are ruled deletable?

And a second one behind it: if yes, what does the row carry? The engagement id of a row that no longer exists is a dangling reference by construction — which is exactly the N4 shape FORAY now permits, and worth deciding deliberately rather than by default.

3. The registry disposition follows either way

candidate_engagement_discarded is held at loomworks-anchor-priority-registry-dispositions-v0_1 §3 pending this question.

Under either answer it is not a Memory event. If the act goes unrecorded, the entry names something that will never be written. If the act is recorded, it is recorded in the operational record, and _ANCHOR_PRIORITY is consulted by Memory's write path.

So it joins the family already holding agent_identity_minted and the four grant-lifecycle kinds: entries pre-registered against a destination the implementation did not use. That family now has six members and the lesson is the same: pre-registering an event kind commits to a destination as well as a name.

4. A live schema question this note cannot answer

Raised because it may be a latent failure, and it is not this note's to resolve.

agent_lifecycle_events (migration 0115, CR-2026-231) carries session_id and engagement_id columns. It does not appear in CANDIDATE_DISCARD_POLICIES. Meanwhile agent_sessions.engagement_id is declared delete — CR-2026-227 item 4 ruled it so deliberately, because mint has no engagement-state gate and a candidate can carry sessions.

So discarding a candidate deletes agent session rows that grant-lifecycle rows may reference. Three possibilities, and they are materially different:

  1. engagement_id and session_id are not foreign keys, so the fence never required a policy and nothing breaks — but lifecycle rows then outlive their sessions as dangling references.
  2. They are foreign keys and the fence should have required a policy, in which case the fence has a gap and a discard of a candidate that carried a session raises an integrity error.
  3. A policy exists somewhere this note did not find, and it should be read rather than guessed at.

Whichever it is, there is a doctrinal collision underneath. The two-records note says the operational record is append-only on the same terms as Memory. A discard that deletes lifecycle rows would erase from the operational record, via a path ruled before that record existed. The pre-commitment doctrine does not obviously extend: a grant refused to an agent on a candidate engagement is still a governance act that happened.

Do not resolve this by adding a policy entry. The header's own warning applies exactly — a forced policy passes the fence and is worse than an undeclared FK.

5. Two interactions worth recording

5a — a contribution credential can be issued and destroyed with no trace. The policy declares contribution_credentialsdelete ("a credential against an unmade thing must die with it"), and the credential lifecycle writes via raw SQL into its own table, reaching neither record and no FORAY reserved location. So issuance leaves no trace and destruction leaves no trace. Filed separately as the bypass finding; recorded here because the two combine into a complete absence rather than a partial one.

5b — an anchor outlives the local record, deliberately. From the discard module's own docstring: if a future operation FORAY-anchors the candidate prior to commit, the on-chain anchor remains after local deletion — the anchor proves the candidate existed; the deletion is the operator's choice.

This is the reverse of the N4 case. There, Loomworks references something FORAY does not hold. Here, FORAY holds evidence Loomworks has erased, and Loomworks retains nothing to reconcile it against. Deliberate, defensible, and worth stating as a property rather than leaving in a docstring — a buyer who anchors candidates will ask.

6. What this note does not do

It does not reopen the pre-commitment carve-out, which is ruled and built. It does not answer whether the discard act should be recorded. It does not decide what such a row would carry. It does not resolve §4 or add any discard policy. It does not close the credential bypass. And it does not treat §5b as a defect — it is a stated consequence of a ruled choice.

7. Correction from v0.1

Preserved rather than smoothed.

v0.1 posed two readings — that candidate material sits outside Memory, or that deleting it contradicts Memory does not forget… this holds at every scope. It presented both as open.

The first was already ruled, at Phase 25 §7.2, with a static-check allow-list, a policy table, and a fence test. v0.1 read the policy file's comments and the seed, and did not read the discard module's docstring, where the carve-out and its reasoning are stated plainly.

Two lessons, both earned before. Read the implementation, not only the file that references it — the answer was one file away. And: a documented exception in a docstring is invisible to anyone reading the documents. The seed says Memory does not forget at every scope; the carve-out lives in a Python module. Neither document is wrong; a reader of the record could not have found the ruling. That is the same shape as the arc's other findings — a fact living only where two correct artifacts meet.

What v0.1 got right and this version keeps: it refused to answer, it named the registry entry it gates, and it said the question reaches wider than that entry. All three hold, and §2 is wider still.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — open design question: candidate discard and the record — v0.2 — 2026-08-21