DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-104-stele-extraction-phase-1-v0_1.md

Loomworks change request — Stele extraction Phase 1 (move the identity records + principal view; carry the credential row models and credentials.py; settle the persons.id FK / Base decision) — CR-2026-104 — v0.1

Version. 0.1 Date. 2026-06-14 Status. Change request — the second executable phase of the Stele extraction, drafted against the post-Phase-0 tree (engine origin/main e4656b4; suite baseline 2920 passed / 46 skipped / 0 failed). Phase 1 moves the identity records (PersonRow), the principal view (the registry: Person, get_person_by_id, …), and carries the two credential row models (WebauthnCredentialRow, RecoveryCodeRow) and persons/credentials.py into stele/ — the pieces Phase 0 (CR-2026-103 v0.3) reassigned here because they are entangled in the shared persons/models.py Base. Phase 1's load-bearing element is not the moves — it is the persons.id foreign-key / Base decision (brief v0.4 §4G/§5B), which this CR surfaces explicitly and which the Operator must lock before Step execution. Markdown primary (technical consumer: Claude Code). Halt-before-push; per-step commits; verify against engine-database truth, not the suite alone.

Decisions this CR rests on (settled): (1) Stele's interface is a host-agnostic, in-process-first / service-ready API contract. (2) The contract is complete (passkey-revoke, TOTP-rotate arrive Phase 5). (3) Sign-up split is approach B (Phase 4, not here). (4) Move, not duplicate — one implementation throughout; safety from small reversible slices. (5) White-label / multi-language hooks thread into user-facing ops in later phases. (6) Phase 0 is complete and pushed (engine e4656b4): recovery.py, webauthn.py, session.py live in stele/; the isolation harness passes; sign-in verified against playground_dev.

Source grounding. The Stele body-of-work brief v0.4 (§4G, §5B the FK/Base decision; §5 the re-sequenced phase plan; Phase 1 row = "the packaging foundation"); the integration design v0.2 (§5.2: "on day one Stele owns the whole persons table physically with lifecycle columns nullable-and-host-owned; the physical identity/account table split is deferred to the service extraction" — brief §5B proposes pulling this forward); the Phase 0 completion record v0.1; and a tree-truth verification at e4656b4 (importer counts, FK map, Base structure) recorded in §3 below.


1. The gating decision — persons.id foreign key / shared Base (must be locked before Step 1.1)

Brief v0.4 §5B states this decision and Phase 1's Base/FK decision are the same decision, and that the CR "should state the decision explicitly, present both branches, and record which was chosen and why." It has two linked parts.

Tree-truth that frames it (verified at e4656b4)

persons/models.py defines one DeclarativeBase (Base, line 35). Eight tables register on it. Four carry a ForeignKey("persons.id"):

| Table (ORM row) | FK → persons.id? | Phase 1 disposition | |---|---|---| | webauthn_credentials (WebauthnCredentialRow) | yes | moves to Stele | | recovery_codes (RecoveryCodeRow) | yes | moves to Stele | | memberships (MembershipRow) | yes | stays (Loomworks governance) | | organization_memberships (OrganizationMembershipRow) | yes | stays |

Plus persons.personal_engagement_id → engagements.id (line 91): persons moves, engagements (_EngagementRow) stays.

So if PersonRow is given a separate metadata, foreign keys cross the persons/Stele boundary in both directions: governance rows that stay still point into the moved persons table, and the moved persons table still points out to the staying engagements table. That two-way crossing is the whole of the difficulty.

D1 — Direction / end-state (own-table vs host-contract)

> Does standalone Stele ship its own principal table (FKs point inward), or declare persons.id as a contract the host must satisfy (FKs point at a host-supplied table)?

Recommendation (brief §5B, surfaced not pre-decided): own-its-own-table — the Miami bar is "a stranger reaches a passing run and a working sign-in by cloning," which host-supplied-contract defeats.

D2 — Timing / mechanism for Phase 1 (the tension this CR must surface)

The brief (§5B) proposes pulling the physical identity/account table split forward to Phase 1; the integration design (§5.2) currently defers that physical split to the service extraction; and the brief's own Phase-1 row calls the moves "low-mechanical." These pull in different directions. Two branches:

Recommendation: lock D1 = own-table, and execute D2 = Branch 1 — separate the direction (decide now) from the physical split (do it at the de-engine phase, where the host-account table and standalone metadata are built together). Per corrections-not-smoothed, both branches and the §4G/§5.2/§5B tension are recorded; the Operator locks D1 and D2 before Step 1.1. The step plan below assumes Branch 1; §2's note states how the steps change under Branch 2.


2. Scope of Phase 1

In scope — move into stele/ (Branch 1: definitions move, shared Base, FKs unchanged):

Stays in persons/ (NOT moving this phase): OrganizationRow, OrganizationMembershipRow, MembershipRow, MembershipDesignationRow, MigrationEventRow, _EngagementRow; signup.py, login.py, migration.py, memberships.py, organizations.py, presence.py, personal_engagement.py; the person-scoped TOTP internals; and the account-lifecycle columns on the persons table (host-owned, nullable — integration design §5.2).

Explicitly NOT in scope (later phases, unchanged from the brief): actor constructors (Phase 2); get_current_person pointing at Stele (Phase 3); the sign-up split (Phase 4); recovery completion + passkey-revoke + TOTP-rotate (Phase 5); de-engine/packaging/SDK + the physical persons→principals+account split (Phase 6); docs-with-runnable-examples + reference UI + the WebAuthn full-ceremony close (Phase 7); agent identity (later). The Principal-view narrowing (integration design §5.2 — excluding exhaustion_preference/spend_pause from Person) is a behaviour-touching change and is not part of this move; registry.Person moves unchanged.

If the Operator locks Branch 2 instead: Steps 1.1–1.4 are preceded by a schema step (the principals/host-account split migration + separate MetaData), the FK repoints become cross-metadata wiring rather than name resolution, and Step 1.5's DB-truth check adds a migration round-trip + data-integrity verification. That is a materially larger, higher-risk CR and would warrant its own re-scoped version.

3. Tree-truth verification (at e4656b4) — blast radius

Move-not-duplicate means every importer of a moved symbol gets an import-line repoint (the Phase-0 mechanic, proven across ~113 sites). Counts:

Halt condition (carried from Phase 0): halt if any importer needs more than an import-line change (a logic edit), or if the suite cannot be made green by repointing alone.

4. Steps (each its own commit; halt before push) — Branch 1

Step 1.1 — Relocate the shared Base. Introduce a shared DeclarativeBase both persons/models.py and stele/models.py import (one MetaData across both modules); repoint Base importers. No rows moved yet; zero schema change. Full suite. Commit.

Step 1.2 — Move the row models. Move PersonRow, WebauthnCredentialRow, RecoveryCodeRow into stele/models.py; selectively repoint the 61 importers per-symbol (splitting mixed lines so staying classes keep pointing at persons/models); repoint stele/recovery.py's RecoveryCodeRow import. Confirm FK resolution unchanged (shared MetaData). Full suite + DB-truth (a row insert/read exercising the moved FKs against playground_dev). Commit.

Step 1.3 — Move the registry. Move persons/registry.pystele/registry.py; repoint all 121 importers; Person view moved unchanged (no narrowing). Full suite. Commit.

Step 1.4 — Move the credential store. Move persons/credentials.pystele/credentials.py; repoint the ~11 importers; its WebauthnCredentialRow import is now intra-Stele. Full suite. Commit.

Step 1.5 — Full-suite regression + database-truth. Run the complete suite. Verify against playground_dev that the identity-record path now resolves PersonRow/registry from stele/: sign-in end to end (the Phase-0 check, now also exercising the relocated get_person_by_id), plus a credential-row read for a known person through the moved stele.credentials. Confirm no schema/migration change occurred (Branch 1 invariant: alembic heads unchanged, table definitions byte-equivalent). Halt and report.

5. What must not happen in Phase 1

6. Halt and report

After Step 1.5, halt before any push. Report: which D1/D2 branch the Operator locked; the symbols moved with per-symbol importer-repoint counts (production/test/script) and confirmation no importer's logic changed; the shared-Base/FK handling and proof FK resolution is unchanged; the no-schema-change confirmation (Branch 1); and the full-suite + database sign-in/credential-read regression result. On review, push Phase 1; Phase 2 (the actor constructors) is next.

7. Reversibility

Every step is its own commit; the offending step reverts as a single commit with no effect on the others. Under Branch 1 there is no schema change to unwind — a revert simply puts the moved definitions back at their prior path.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks change request — Stele extraction Phase 1 — CR-2026-104 — v0.1 — 2026-06-14