DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-216-orphaned-findings-sweep-v0_1.md

CR-2026-216 — Orphaned ADMIN findings sweep (B-105 interim hygiene) — v0.1

Version. 0.1 Date. 2026-08-15 Author. Claude.ai, drafted against inspection-briefs/loomworks-b105-sweep-step-0-inspection-v0_1.md. Repository. loomworks-engine. Build-list item. B-105 (partial — interim hygiene only).


Plain-language summary

When a draft project is discarded, its review findings survive in the administrative engagement's log, pointing at a seed that no longer exists. This change request removes those orphaned finding rows. It is deliberately the small fix: the Operator has ruled (2026-08-15, build list v0.88) that findings are per-engagement objects, and the full correction — a real database link from finding to engagement, a migration, and a 28th discard policy — waits behind B-69 (seed mutability). This sweep is interim hygiene under a made ruling. Its execution must not be read as the design answer.

What changes: 22 event rows across 5 discarded-candidate lineages are deleted, and future discards stop leaving orphans behind. What does not change: living findings (active candidates and committed engagements), the 22 legacy NULL-seed_id findings, and every reader's behavior for any living engagement — except one API-visible count that becomes more correct (§5).

Decision needed from the Operator: none within this CR. One adjacent defect (§5) files as B-106 unless the Operator redirects.


0. Ruling context (verbatim obligations)

  1. B-105 ruled per-engagement; schema move behind B-69. This CR's text states its interim-hygiene status (done, above).
  2. Findings are memory_events rows but not assertions (Finding at types.py:215 and Assertion at types.py:280 are sibling MemoryObject subclasses; the assertions read model selects object_type='assertion' only). Corrections-preserved does not bind findings. Delete is clean.
  3. The sweep is a DELETE against memory_events and must be allow-listed in the no-delete static check (candidate_discard.py is today's sole allowed executor).

1. Scope

In scope.

Out of scope, named so nobody takes the easy read.

2. The orphan predicate (exact, from the inspection)

A finding row is an orphan when all hold:

Safety of the predicate: candidate_discard is the only DELETE path against memory_events in the tree, so absence of a seed lineage ⇔ that candidate was discarded. No other cause exists. The production census partitions cleanly: 13 living objects all under seed-exists, 16 orphaned objects (22 event rows, 5 lineages) all under seed-absent, no overlap.

3. Steps

Step 0 — branch and number check. Create branch cr-2026-216-orphaned-findings-sweep. Verify no CR-2026-216 exists anywhere in loomworks-record; if the number is claimed, HALT and report — do not renumber silently.

Step 1 — forward fix. Extend candidate_discard so discarding a candidate also deletes the candidate's finding rows from ADMIN, identified by the discarded seed's seed_id (available at discard time — it is the same seed the retained ADMIN cleanup already deletes). This is a 28th action inside the existing executor, not a 28th FK policy — the FK census's declared-policy table is unchanged because these rows do not FK the candidate. Record the deletion table-by-table in the discard's existing evidence pattern.

Step 2 — one-time sweep. A management script (not a migration) implementing §2's predicate exactly. Dry-run mode first: print the lineages, object counts, and event-row counts it would delete. Expected: 5 lineages (7e7c0989…, d9a6610b…, dc418ef1…, ddeb1594…, e4ee052d…), 16 latest-version finding objects, 22 event rows. If the dry run's numbers differ from the census, HALT and report before deleting.

Step 3 — static check. Allow-list the new DELETE site(s) in the no-delete check. The allow-list entry names this CR.

Step 4 — tests.

Step 5 — production run. Dry run in production; compare against §2's census; on match, execute; re-run the census as post-evidence (expected: 0 orphans, 13 living objects intact, 22 legacy rows intact). The pre/post census pair is the CR's acceptance evidence per the inspection's §6.

Checkpoint A — after Step 4's green run and Step 5's dry-run match, before tag and push. Gate string: GATE A APPROVED — PROCEED for CR-2026-216.

4. Acceptance

  1. Production census post-sweep: 0 rows match the orphan predicate.
  2. The 13 living finding objects and 22 legacy NULL-seed_id rows are byte-identically intact (count and object_id set match pre-census).
  3. Discarding a test candidate (synthetic engagement only — never shared test engagements, never the personal engagement) leaves no findings behind.
  4. open_finding_count in list_my_candidate_engagements drops from 31 to 21 — the API-visible change toward correctness, verified and recorded; the remaining wrongness (no lineage filter) is B-106's.
  5. ruff zero-tolerance and mypy message-level non-regression hold.

5. Filing instruction — B-106

Add to the build list (v0.89, new file alongside v0.88): B-106. Candidate cards' open_finding_count has no lineage filter — every card shows the global count. list_my_candidate_engagements (engagements.py:1400) computes open_finding_count with no seed_id filter while the cycle count directly above it (:1385) filters correctly; every candidate card carries the same number (31 pre-sweep, 21 post). API-visible only today — the surface types the field (door3.ts:145) but no component renders it. Fix shape: apply the same lineage filter the cycle count uses. Filed from CR-2026-216's Step 0 inspection, per the Operator.

6. Discipline

Stage by explicit path, never git add -A. Re-verify HEAD before each write. Halt before every push. Sweep executes against production only after the dry-run/census match and Checkpoint A. One session, one repository.


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-216 — orphaned findings sweep — v0.1 — 2026-08-15