DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path phases/stele-extraction-cr-c/loomworks-stele-cr-c-completion-record-v0_1.md

Loomworks — Stele Extraction CR-C Completion Record — v0.1

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.


Plain-language summary

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.


1. What was built

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.

2. The build (merged at d0f302e)

| # | Commit | What | |---|---|---| | 1 | 271dcf2 | 5 pure identity reads PersonRowPrincipalRow (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.

3. The four as-built corrections (caught at halt points, each on the merits)

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.

4. Verification — proven on the real database

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).

5. End-state

6. Carry-forwards to CR-B (recorded, landing where they can land cleanly)

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.

  1. Retire the reversed trigger (0090). Once the 11 host FKs no longer reference persons, the principals → persons mirror has no job. Retires with the table drop.
  2. Retarget the 11 host FKs off 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.
  3. Remove the 4 _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.
  4. Drop the persons table. The Operator's standing instinct from CR-C scoping ("persons should go") lands here.

7. What closing CR-C needs (the remaining sequence)

  1. Merge pushed to engine main (d0f302e) — done at time of this record, or being pushed.
  2. This completion record filed to loomworks-record at phases/stele-extraction-cr-c/.
  3. Manifest fold-in — record CR-C complete, main at d0f302e, alembic 0090, the reversed trigger, the as-built deltas, and CR-B as the next arc with its four carry-forwards.
  4. Branch retire — delete cr-2026-112-stele-cr-c (local + origin) after the record and manifest land.

8. Methodology lessons banked from CR-C (for the v0.21 consolidation, not for CR-B execution)

  1. A "reads then writes" commit split breaks on load-then-mutate pairs — the load and its write are one site, not two.
  2. A test that observes a column must be repointed in the same commit as the write it observes, not deferred to a later structural commit.
  3. "Retire the trigger" assumed the mirrored table was idle; a table feeding live FKs cannot go idle until the FKs move — the write-source flip and the trigger direction are one decision, not two.
  4. An ORM row class can do two jobs — identity model and table registration; removing the first does not remove the second, and FK-target registration must outlive the identity model when dependents remain.
  5. A "make the module clean" end-state must account for every class added to that module during the same CR, not only the classes present at scoping.

DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Extraction CR-C Completion Record — v0.1 — 2026-06-18