DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path inspection-briefs/loomworks-b105-sweep-step-0-inspection-v0_1.md

B-105 sweep — Step 0 inspection — v0.1

Date. 2026-08-15 · Item. [B-105] orphaned ADMIN findings · Ruling. Build list v0.88: findings are per-engagement; the sweep is interim hygiene only; the schema move waits behind B-69; the sweep CR must self-describe as interim. Mode. READ-ONLY. No writes, no migrations, no CR. Live tree (loomworks-engine at a7ae23c deployed) and production database playground_dev (read queries only).

1. STORAGE — where finding rows live

Findings are memory_events rows, not a table of their own. Every finding row in production sits on the ADMINISTRATIVE engagement — SELECT DISTINCT engagement_id FROM memory_events WHERE object_type='finding' returns exactly one value: 00000000-0000-0000-0000-000000000001 (display identifier E0001, state active).

Columns: event_id, engagement_id, engagement_version, object_id, object_version, object_type ('finding'), event_kind, payload (JSONB), provenance, timestamp, actor_id, actor_kind. The finding's substance is in payload: seed_id (lineage key), state (open/addressed/superseded), requirement_identifier, location_in_seed, suggestion, seed_version_examined, addressed_in_cycle_ref (Finding, engagement/types.py:215).

The FK is memory_events.engagement_id → engagements(id) — one of CR-2026-215's 27 declared policies. It points at the ADMIN engagement, never at the candidate. That is the whole mechanism of the orphaning: discard deletes the candidate's own rows and the seed's ADMIN events, but the FK on a finding row references an engagement (ADMIN) that never gets deleted, so nothing ever pulls the finding along.

2. ARE FINDINGS ASSERTIONS? — No. Delete is clean, with one carve-out obligation.

Finding (types.py:215) and Assertion (types.py:280) are sibling MemoryObject subclasses — separate classes, separate object_type literals. The assertions read model selects object_type = 'assertion' only (engagement/assertions.py:132); no finding can ever enter it. Corrections-preserved is an assertions discipline and does not bind findings. Findings carry their own intra-lineage vocabulary (open/addressed/superseded) — that is per-finding state, not the assertion retraction discipline.

The one obligation delete does carry: findings ARE memory_events rows, so a sweep is a DELETE FROM memory_events — governed by the no-delete static check (tests/test_no_delete_statements.py), where candidate_discard.py is today's sole allow-listed executor. The sweep module must be explicitly allow-listed (or live inside the existing executor), which is the right shape anyway: the sweep is an extension of the same Phase-25 carve-out ("nothing to preserve before commit") — these rows only ever described discarded candidates' seeds.

3. ORPHAN IDENTIFICATION — the predicate, and why it is safe

payload->>'seed_id' exists on every post-Fault-A finding (seed_id is Optional on the model solely for pre-Fault-A back-compat; 22 legacy latest-version objects in production carry NULL — see §4).

Is the seed deleted at discard? Yes. candidate_discard.py retains the ADMIN seed cleanup: DELETE FROM memory_events WHERE engagement_id = :admin AND object_id = :sid. A discarded candidate's seed rows are gone.

Is absence a safe predicate? Yes, with the predicate stated globally, not ADMIN-scoped. Two facts close it:

The predicate: latest-version finding objects where payload->>'seed_id' IS NOT NULL AND no memory_events row exists with object_type='seed' and that object_id — then delete all event rows of those finding objects. NULL-seed_id legacy findings are excluded by the NOT-NULL clause and are outside this ruling (they are pre-Fault-A pollution, already invisible to every lineage reader; the sweep CR should name them as untouched, per the WRITING/self-describe discipline).

4. ORPHAN COUNT — production numbers (read 2026-08-15)

5 orphaned lineages · 16 latest-version finding objects · 22 event rows total. The seed_ids:

| seed_id | finding objects | |---|---| | 7e7c0989-cef7-4336-8bc2-ecafb0ac65d3 (E0129 — the CR-215 specimen; includes R-A9 d544824f…) | 5 | | d9a6610b-05c9-454f-b6aa-eca4b5593870 | 1 | | dc418ef1-d6a1-41c7-98e4-c3b626acac6c | 5 | | ddeb1594-75f4-4d4b-8aac-b93b62c0d9c1 | 3 | | e4ee052d-9b3c-48cc-ac21-2dc5e29537c9 | 2 |

These correspond to the walk arc's discarded-after-induction candidates (E0129/E0130/E0132/E0133 plus one earlier discard). By state: 6 addressed, 10 open.

Full ledger for context: 51 latest-version finding objects on ADMIN = 16 orphaned + 13 living (7 lineages: 6 current candidates' seeds on ADMIN + E0128's rehomed seed) + 22 NULL-seed_id legacy (13 addressed / 9 open).

5. READERS — five sites, four safe, one that the sweep WILL change

| # | Reader | Filter | Sweep effect | |---|---|---|---| | 1 | _load_induction_stateengagement/creation.py:646 (query at :675) | ADMIN + payload->>'seed_id' = :sid | None. seed_id comes from a living candidate's row; orphan lineages are unreachable. | | 2 | GET /engagements/{id}/seed/findingsapi/routers/engagements.py:492 (query at :530) | seed lineage from the engagement's own row | None, same reason. | | 3 | _load_open_findingsorchestration/prompt.py:~260 (CR-212 candidate context) | payload->>'seed_id' = :sid | None. | | 4 | _candidate_markerorchestration/prompt.py:~298 (CR-212 staleness marker) | payload->>'seed_id' = :sid | None. | | 5 | list_my_candidate_engagementsapi/routers/engagements.py:1400 (candidates dashboard, open_finding_count) | ADMIN + state='open' — NO seed_id filter | Yes — the value changes. |

Reader 5 is this inspection's finding. The candidates-dashboard route computes open_finding_count per candidate card as a count of every open finding on the ADMIN log — today 31 (12 living + 10 orphaned + 9 NULL-legacy), the same inflated number on every card, regardless of whose lineage the findings belong to. The sweep drops it to 21 — a change toward correctness, not a regression: the count is wrong today because of the orphans (and independently because of lineage-blending and NULL-legacy inflation, defects the sweep does not fix). Surface exposure is nil in practice: open_finding_count is typed (loomworks/src/lib/api/door3.ts:145) but no component renders it — the change is API-visible only. The missing lineage filter on this reader is its own candidate item (same one-line fix shape as the cycle count directly above it at :1385, which DOES filter by seed_id), to be filed at the gate, not folded into the sweep.

6. LIVE-FINDING SAFETY — the predicate provably cannot touch a living finding

A living finding's seed exists in memory_events by construction, so the §3 predicate excludes it:

The census breakdown in §4 is itself the proof run: the 13 living objects all group under seed_exists = true, the 16 orphans all under false, with no overlap — the predicate partitions production cleanly today. The sweep CR should carry this same census as its own pre/post acceptance evidence, and (per the ruling) state in its own text that it is interim hygiene under the per-engagement ruling, with the schema move waiting behind B-69.


DUNIN7 — Done In Seven LLC — Miami, Florida — B-105 sweep Step 0 inspection — v0.1 — 2026-08-15 Five lineages, sixteen objects, twenty-two rows; four readers that cannot see them and one that counts them on every card.