Version. 0.1
Date. 2026-06-18
Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (scoping) on DUNIN7-M4.
Status. Scoping note for CR-C — make principals the real identity table, turn off the mirror trigger, rename PersonRow to PrincipalRow, and move HostAccountRow out of the Stele module. Grounded against engine main at bee2b4c (CC read-only inspection, 2026-06-18). Scoping note precedes the change request itself, per convention. Markdown primary (technical consumer).
There are two tables holding the same people: persons and principals. Right now persons is the real one — the app reads and writes there. principals is a copy. A database trigger keeps the copy in sync: every write to persons is photocopied into principals.
The problem is that everything important already points at the copy, not the original. The login credentials, the host account, and the one place that reads a display name all look at principals. But the app still writes to persons, and the trigger does the copying. The app and the database disagree about which table is real, and the trigger hides the disagreement.
CR-C ends the disagreement. It makes the app write directly to principals, then turns off the trigger. After CR-C, principals is the real identity table and there is no copying.
The persons table stays standing for now, empty of its identity job. Three small references and twelve host foreign keys still point at it; clearing those and removing the table is the next change request, CR-B. CR-C does not touch them.
There is no live data here. playground_dev can be rebuilt at any time. So the danger in CR-C is not data loss — it is corrupting the codebase with a careless rename. The rename touches 58 code files and must be done as a token-level sweep, never a blanket find-and-replace.
bee2b4c)CC ran a read-only inspection. Four findings shape the scope.
1. The rename surface is 219 lines / 58 code files — not the 257/65 the handoff carried. The raw count is 298 lines / 69 files, but 79 of those lines are historical CR records in docs/. Those are history and must not be rewritten — corrections are preserved, not smoothed. The real target is src/ (43 lines / 14 files), tests/ (175 lines / 43 files), and one comment in migration 0088.
The 14 src/ files: stele/registry.py (7), persons/login.py (6), persons/presence.py (5), stele/person_totp.py (4), stele/models.py (4), persons/signup.py (3), persons/migration.py (3), credit/lifecycle.py (3), persons/onboard.py (2), credentials/migrate.py (2), and one each in persons/models.py, identity_base.py, credit/near_exhaustion.py, authority/founder.py.
2. The 3 _PersonRow stubs are distinct tokens and are NOT renamed in CR-C. They live in files/models.py:38, notifications/models.py:41, orchestration/models.py:32. Each is a local stub class mapping __tablename__ = "persons" — an FK target pointing at the persons table. Because persons stays standing in CR-C, these stubs stay too. CR-B retargets them. The word-boundary token PersonRow correctly excludes _PersonRow, so a NAME-token sweep leaves them alone automatically.
3. The trigger and the rename are one task, not two. The handoff listed "retire the trigger" and "rename PersonRow" as separate carry-forwards. Grounding shows they are the same work. Today identity reads and writes flow through persons/PersonRow; principals is a near-write-only copy. Turning off the trigger means the app must write to principals directly — and making the app use principals is the rename. You cannot do one without the other.
4. The file inversion is worse than one stray import, and the handoff's path was wrong. stele/models.py is a mixed-Base file: PersonRow and HostAccountRow sit on the host Base, while PrincipalRow and the two credential rows sit on SteleBase. So a host model lives inside the Stele module. The correct path is src/loomworks/persons/host_account_view.py (the handoff said stele/persons/..., which does not exist). That file imports HostAccountRow back across the boundary.
principals becomes the real identity table. persons is kept standing but loses its identity job. The trigger is retired.
This is the only resolution that actually turns off the trigger, and it is the seed-true one. The seed commits to the UUID as identity, held in the principal record; every foreign key in the schema already points at principals. CR-C brings the application code into line with what the plumbing already believes.
The persons table is not dropped in CR-C. The Operator's standing view is that persons should go — and it will, in CR-B. The reason to leave it standing in CR-C is scope cleanliness, not danger: three _PersonRow stubs and twelve host foreign keys still reference persons.id. Dropping the table would force CR-C to do CR-B's foreign-key retargeting to keep the build green, collapsing the two change requests into one. CR-C makes principals canonical and retires the trigger; CR-B clears the remaining references and removes persons.
Recorded alongside, per Discovery discipline: the rejected alternative was "leave persons the write-source, leave the trigger live" — which is simply not retiring the trigger, and the handoff lists trigger retirement as in-scope, so it fails the brief.
The Operator noted the no-live-data reality weakens the danger argument for keeping persons. That is correct and recorded. The argument for keeping it is scope-boundary, not safety. Errors that surface from leaving it in place are cheap to fix here and will be fixed as they come.
Task 1 — the collapse (the spine). Point every identity read and write at principals/PrincipalRow instead of persons/PersonRow. Then retire the mirror trigger in a migration. Concretely, the reads and writes that move:
get_principal_by_id (registry.py:155) reads select(PersonRow) today — repoint to PrincipalRow.create_principal / mint_principal) write PersonRow today — repoint to PrincipalRow.PersonRow.This is where the central design care lives. The build CR (next document) needs a fresh grounding pass to list every read and write site exactly, before any code moves.
Task 2 — the rename (the code-surface of the collapse). PersonRow → PrincipalRow across the 58 code files, as a token-level NAME-token sweep. This is mechanically the bulk of the diff but follows from Task 1 — it is not a separate decision. The _PersonRow stubs are excluded automatically.
Task 3 — relocate HostAccountRow (independent). Move HostAccountRow out of stele/models.py to a host-side module, and repoint the import in host_account_view.py. This can be a clean separate step. Note: the collapse also removes PersonRow from stele/models.py (it is renamed and the class moves with the identity work), so after CR-C, stele/models.py holds only SteleBase rows — the Phase 7 precondition. That falls out of Tasks 1–3 together; it is not extra work.
One more grounding pass from CC: the exact identity read and write call sites to repoint in Task 1 — every select(PersonRow), every PersonRow(...) construction, every identity write — listed against bee2b4c. The scoping note settles the decision; the build CR needs the precise site list so the steps move real code, not a mental model of it. Signature and placement derive from the real body, not the design.
PersonRow only. A blanket \bPerson\b regex would corrupt title="Person" in OpenAPI, "Test Person" string literals, and docstrings. Proven in 6b's 3.7 sweep and re-proven three times in CR-A. _PersonRow stubs are distinct tokens, excluded by the word boundary.docs/. The 79 historical-record lines are preserved history.bee2b4c, not from this note's summary.playground_dev, not the suite alone. The safety model is CR-A's, not 6b's "revert pure Python."git add <paths>, never -A).persons now" position and the reason it was set aside are recorded above.principals.id vs host_account.id) — orthogonal to CR-C, settled before CR-B is scoped, not here.__tablename__ moves — principals already exists as its own table with PrincipalRow mapping it; PersonRow's __tablename__ = "persons" is part of what the collapse retires. The build CR confirms the mapping end-state against the real models.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Stele Extraction CR-C Scoping Note — v0.1 — 2026-06-18