Version. 0.1
Date. 2026-06-18
Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (scoping/CR) on DUNIN7-M4; CC executing on DUNIN7-M4.
Status. Completion record. CR-2026-112 (Stele Extraction CR-C) — the identity-table collapse — is built, verified on playground_dev, merged to engine main at d0f302e, and pushed. Markdown primary (technical consumer).
Governing CR. change-requests/cr-2026-112-stele-extraction-cr-c-v0_2.md (as-built, record 9193c17); v0.1 preserved alongside as the planned trajectory.
What CR-C did. Made principals the real identity table. Before CR-C, the app read and wrote identity data to persons, and a database trigger copied each write into principals. Everything structural — login credentials, host account, the display-name read — already pointed at principals, but the app didn't. CR-C flipped the app to use principals directly and reversed the trigger so it now copies the other way (principals → persons), keeping persons populated for the 11 host foreign keys that still point at it.
What CR-C did NOT do. It did not drop persons — that table still stands as a reversed mirror, kept in sync solely to feed the 11 host FKs. It did not retire the trigger — the trigger was reversed, not removed. It did not retarget the host FKs or remove the _PersonRow registration stubs. All four are CR-B's job.
The end-state. principals is the canonical identity table — sole read/write source. persons is a mirror. stele/models.py holds only Stele-Base rows (PrincipalRow + the two credential rows) — the structural precondition for the standalone repo (Phase 7), met cleanly, not approximated.
The build was four commits, not the planned five. Execution found four cracks the plan didn't anticipate, each caught at a halt point before damage. They are recorded in the CR's v0.2 §0 and summarized below. The richest correction was the trigger: the plan said "retire it," but the 11 host FKs made that impossible in CR-C, so it was reversed instead — persons becomes the mirror, and the trigger retires in CR-B once the FKs move.
principals is now the canonical identity table. The application reads and writes identity data (id, display_name, totp_secret, first_login_at, last_presence_proof_at, created_at, updated_at) directly to principals via PrincipalRow. The PersonRow identity model is gone; a minimal id-only _PersonRow stub holds the persons-table registration so the 11 host FKs resolve.
The mirror trigger is reversed: migration 0090 dropped the persons → principals trigger from 0085 and installed a principals → persons trigger. persons is now the mirror, kept populated for the host FKs. stele/models.py is Stele-Base-only.
d0f302e)
| # | Commit | What |
|---|---|---|
| 1 | 271dcf2 | 5 pure identity reads PersonRow→PrincipalRow (sites #2,#3,#4,#5,#7) |
| 2 | ee2c7c0 | principals = write-source; reverse mirror trigger (migration 0090); 6 writes + 2 coupled loads (#1,#6) + follow-on test reads |
| 3 | a8fb53b | PersonRow → id-only persons-table FK-target stub; test reads repointed (WHY-checked) |
| 4 | 73a1e94 | relocate HostAccountRow + _PersonRow stub → persons/models.py; stele/models.py now SteleBase-only |
Merge commit d0f302e (--no-ff), 82 files changed (+510/-390), migration 0090 added. Suite 2928 / 0 / 46 skipped on merged main. Phase-4 caution passed — origin/main confirmed bee2b4c before merge, no drift.
This phase had a rich correction path, every change caught at a halt before damage — the same inspect-first-and-halt discipline that ran through CR-A.
3a — the reads/writes split does not hold for load-then-write pairs (Commit 1). The plan put all 7 reads in Commit 1. Two — #1 (delete-scrub load) and #6 (TOTP-confirm load) — are the load half of a load-then-mutate on the same object; repointing the load silently moves the write. Two TOTP tests went red. Resolution: Commit 1 took only the 5 pure reads; #1/#6 moved to Commit 2 with their writes. Established alongside: a test reading a column from persons after its write moved to principals must be repointed in the same commit as the write, not deferred.
3b — the 11 host FKs force the trigger to reverse, not retire; Commits 2+3 fold (Commit 2). The plan retired the trigger and treated persons as idle afterward. It is not idle — 11 host FKs reference persons.id, and onboarding writes to those tables on every signup. Writing identity only to principals would leave no persons row and break every signup. The write-source and trigger direction are one decision. Resolution: principals becomes the write-source and the trigger is reversed (0090) so persons stays mirror-populated. The trigger retires in CR-B. Old Commits 2 and 3 folded into one.
3c — PersonRow cannot be fully deleted; it reduces to an FK-target stub (Commit 3). The plan removed the PersonRow class entirely. But PersonRow also registered the persons table on the shared identity_base.Base where the 11 host-FK tables live; deleting it broke configure_mappers (~414 tests red). The 3 existing _PersonRow stubs are on independent per-module Bases and don't cover the shared Base. Resolution: PersonRow reduced to a minimal id-only _PersonRow(Base) stub — the FK target, no app columns. The 4th such stub; all four removed in CR-B.
3d — "SteleBase-only" required moving two Base classes, not one (Commit 4). Commit 3 added the _PersonRow stub to stele/models.py, also on identity_base.Base. Moving only HostAccountRow would leave the stub and the module would stay mixed-Base. Resolution: both HostAccountRow and the _PersonRow stub moved to persons/models.py. stele/models.py ends with exactly 3 Stele-Base classes, identity_base.Base import gone. Phase-7 precondition met, not approximated.
On playground_dev (migration 0090), rolled back after: a principals write mirror-populates persons (display_name carried); host_account.id → principals.id and person_settings.person_id → persons.id (the onboarding host-FK class) both resolve. Migration 0090 down-path restores the 0085 forward trigger verbatim. Full suite green on merged main (2928/0/46).
principals is the canonical identity table — sole identity read/write source.persons is a reversed mirror, kept populated by trigger 0090 to feed the 11 host FKs. Standing, not dropped.PersonRow identity model is gone; an id-only _PersonRow stub (in persons/models.py) holds the persons registration.stele/models.py is Stele-Base-only (PrincipalRow + WebauthnCredentialRow + RecoveryCodeRow) — the Phase-7 precondition.main at d0f302e, alembic 0090, suite 2928/0/46.
CR-B is the next arc. CR-C's reversal sets it up: persons is now a pure mirror with no role except feeding FKs, which is exactly the state that makes CR-B's removal clean.
0090). Once the 11 host FKs no longer reference persons, the principals → persons mirror has no job. Retires with the table drop.persons.id. The open identity-target decision (principals.id vs host_account.id) is settled before CR-B is scoped — Claude.ai's lean, CC's schema-semantics read, and the strategic-direction record all point at principals.id; the CR-A handoff's literal wording said host_account.id. Settle first._PersonRow stubs (files/, notifications/, orchestration/models.py, and the new one in persons/models.py) — they exist only to register persons for the FKs; they go with the table.persons table. The Operator's standing instinct from CR-C scoping ("persons should go") lands here.main (d0f302e) — done at time of this record, or being pushed.loomworks-record at phases/stele-extraction-cr-c/.main at d0f302e, alembic 0090, the reversed trigger, the as-built deltas, and CR-B as the next arc with its four carry-forwards.cr-2026-112-stele-cr-c (local + origin) after the record and manifest land.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Extraction CR-C Completion Record — v0.1 — 2026-06-18