DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-103-stele-extraction-phase-0-v0_3.md

Loomworks change request — Stele extraction Phase 0 (stand up the module, move the zero-caller pieces) — CR-2026-103 — v0.3

Version. 0.3 Date. 2026-06-14 Status. Change request, twice amended. v0.3 corrects a latent contradiction in v0.2: it described Phase 0 as "no edits to load-bearing code," but the three files it keeps in Phase 0 (recovery.py, webauthn.py, session.py) are imported by ~113 sites (≈20 production, 87 test, 6 script), including persons/login.py, persons/signup.py, and api/deps.py where get_current_person lives. Moving a file under "move, not duplicate" requires repointing every importer — so v0.2 as written had no executable path (repoint → forbidden edits to load-bearing modules; don't repoint → broken suite). v0.3 resolves this by distinguishing two senses of "touch load-bearing code": a logic/behaviour edit (forbidden in Phase 0 — that is what Phases 3 and 4 do) versus an import-path repoint (a mechanical, behaviour-preserving rewrite of an import line, the unavoidable completion of any file move, which IS in scope). Phase 0 does the second across all importers of a moved symbol; it does not do the first anywhere. The coupling map's "zero external callers" is now understood precisely: zero callers outside the identity set (no non-identity subsystem imports these), which is what made them safe to move — but they are heavily imported within the identity surface, and those imports get repointed. The v0.2 row-model/credentials.py→Phase 1 and TOTP→Phases 3/4 reassignments are unchanged and correct. Markdown primary (technical consumer: Claude Code). Halt-before-push; per-step commits; verify against engine-database truth. Decisions this CR rests on (all settled): (1) Stele's interface is a host-agnostic API contract, in-process first, service-ready. (2) The contract is complete (defines passkey-revoke and TOTP-rotate, which arrive in a later phase). (3) Sign-up split is approach B — mint-then-onboard with a heal path (Phase 4, not this CR). (4) Move, not duplicate — relocate code in small reversible slices; one implementation throughout; safety from small reversible steps, not a parallel fallback. (5) White-label/multi-language hooks (tenant_context, locale) thread into user-facing operations (later phases, not this CR). Source grounding. The Stele extraction scoping note v0.1 (Phase 0 definition), the integration design v0.2 (the contract), and the Claude Code coupling map (engine c210715). The coupling map established that the pieces below have zero external callers — that is what makes Phase 0 the safe first slice. v0.2 adds the tree-truth verification at 60e7cfa (the coupling-map anchor plus the Step 0.1 skeleton), which confirmed the zero-caller claim and corrected the file-separability assumption.

> Amendment note (v0.1 → v0.2), per corrections-preserved discipline. v0.1 §1 listed five pieces as moving into stele/ in Phase 0: the WebAuthn credential storage + WebauthnCredentialRow, the recovery logic + RecoveryCodeRow, the WebAuthn ceremony, the person-scoped TOTP internals, and the session encode/decode. That list was built from the coupling map, which read each piece as a separable file. The tree-truth check before Step 0.2 found otherwise: (i) WebauthnCredentialRow and RecoveryCodeRow are defined in the shared persons/models.py alongside PersonRow (Phase 1, explicitly not moving) under a single shared Base with ForeignKey("persons.id") — moving them is a metadata/Base-split decision, not a file move; (ii) the person-scoped TOTP internals are not a file (persons/totp.py does not exist) but inline logic inside signup.py (Phase 4) and login.py (Phase 3) — both load-bearing and out of scope for Phase 0; (iii) persons/credentials.py's functions import WebauthnCredentialRow, so they cannot cleanly precede the row model's move. The v0.1 boundary is not wrong as an eventual destination — every piece still moves into stele/ — but its phasing assumed file-separability the tree does not have. v0.2 reassigns the two interwoven pieces (and credentials.py) to the phases that already own the modules they live in, and holds Phase 0 to the three clean file-moves. The v1 list is preserved here as the superseded phasing.

> Amendment note (v0.2 → v0.3), per corrections-preserved discipline. v0.2 kept three files in Phase 0 — recovery.py, webauthn.py, session.py — and described the phase as "pure relocation of standalone modules … no edits to load-bearing code," with §2 authorizing import updates only among the moved files and into still-persons/ symbols. The tree-truth importer count run before Step 0.2 showed those three files are imported by ~113 sites (recovery.py: 3 production + 5 test; webauthn.py: 10 production + 10 test; session.py: 7 production + 72 test + 6 script), and the production importers include persons/login.py, persons/signup.py, and api/deps.py (get_current_person). Under "move, not duplicate," relocating a file means repointing all its importers — so v0.2 had no executable path: repointing edits the modules §3 says not to touch, and not repointing breaks the suite §2 requires green. v0.2's framing was right that Phase 0 changes no behaviour; it was wrong to equate "no behaviour change" with "no edits to load-bearing files," because an import-line rewrite is an edit to the file but not to its behaviour. v0.3 draws that line explicitly (§2, §3): logic edits to load-bearing modules are forbidden and deferred to Phases 3/4; import-path repoints are the mechanical completion of a move and are in scope. The destination is unchanged; v0.3 only makes the move executable.


Plain-language summary

This is the first, safest step of giving Stele a clean boundary. It creates the Stele module and moves into it three self-contained identity files — the recovery-code logic, the WebAuthn ceremony, and the session encode/decode. No subsystem outside identity imports these, which is what makes them safe to move: no feature outside the auth surface is affected. They are imported within the identity surface (by login, sign-up, and the dependency that resolves the current person), so moving each file means rewriting those import lines to point at the new location — a mechanical, behaviour-preserving change. No behaviour changes anywhere; the logic of login, sign-up, and get_current_person is untouched — only where they import from changes. This is the ideal slice to prove the module structure and to stand up the test harness that exercises Stele's moved logic in isolation. If anything looks off at the halt, each move reverts as a single commit. The identity records, the credential row models, the person-TOTP internals, and the actor constructors do not move in this phase — those are later CRs.

1. Scope of Phase 0

**In scope — move into the new stele module (clean standalone files; zero callers outside the identity set, confirmed at 60e7cfa; importers within the identity surface are repointed per §2):**

For each of the three: move the file into stele/, then repoint every importer of its symbols to the new stele/ path — this includes the ~113 import sites across production, test, and script code (see Step 0.2 for the per-file counts). Repointing is an import-line rewrite only; no importer's logic changes. Imports into still-persons/ symbols (e.g. WebauthnCredentialRow, RecoveryCodeRow, PersonRow) stay pointing at persons/ — those rows do not move this phase. Run the suite per move; it must stay green.

Reassigned out of Phase 0 by the v0.2 boundary correction (these still move into stele/ eventually — at the phase that owns the module they currently live in):

Explicitly NOT in scope this phase (later CRs — unchanged from v0.1):

> Superseded v0.1 in-scope list, preserved per corrections-not-smoothed. v0.1 listed in Phase 0: the WebAuthn credential storage + WebauthnCredentialRow (credentials.py), the recovery logic + RecoveryCodeRow (recovery.py), the WebAuthn ceremony (webauthn.py), the person-scoped TOTP internals, and the session logic (session.py). v0.2 keeps recovery.py (functions only), webauthn.py, and session.py in Phase 0; moves the row models and credentials.py to Phase 1; moves the TOTP internals to Phases 3/4. The destination is unchanged; only the phasing is corrected.

2. Steps (each its own commit; halt before push)

Step 0.1 — Create the module skeleton. Create the stele module/package inside the engine (confirm the engine's module layout and place it consistently — e.g. src/loomworks/stele/). Empty package with its __init__. Commit. No behaviour change.

Step 0.2 — Move the three files, repointing all importers, one file at a time. Move, in order: persons/recovery.py, then persons/webauthn.py, then persons/session.py into stele/. The importers were counted at 60e7cfa (the verification that produced this v0.3): recovery.py — 3 production (api/routers/me_security.py, persons/login.py, persons/signup.py) + 5 test; webauthn.py — 10 production (api/deps.py, api/app.py, routers claim/me_security/login/signup, engagement/commit_attestation.py, persons/credentials.py, persons/login.py, persons/signup.py) + 10 test; session.py — 7 production (api/deps.py where get_current_person lives, routers login/signup/me_reactivate/auth_dev, persons/login.py, persons/signup.py) + 72 test + 6 script.

For EACH file:

Re-scoped halt condition (v0.3): the original "unexpected external importer → halt" no longer applies — external importers are now expected and repointed. Halt instead if repointing an importer would require more than an import-line change — i.e. if any call site cannot be made to work by rewriting its import alone and would need a logic edit. That would mean the symbol's surface changed in the move (it should not — this is relocation, not refactor) and needs a decision. Also halt if the suite cannot be made green by import repointing alone.

Do NOT move credentials.py, the row models, or the TOTP internals — reassigned to later phases per §1.

Step 0.3 — Wire the KEK reference. The session and credential logic encrypt under the shared key-encryption key, which stays in credentials/ (it is also used by api-keys, system-config, and the credential store — it is shared infra, not Stele's). Confirm the moved Stele code references the KEK from its existing location and does not copy or move it. Commit if any import path adjustment is needed.

Step 0.4 — Stand up the isolation test harness. Write a test that exercises the moved Stele pieces without booting the rest of the engine — encode and decode a session (fully self-contained in stele/session.py); run a WebAuthn registration ceremony against a test config (stele/webauthn.py); and exercise the recovery functions (stele/recovery.py) — generate, store, verify-and-consume one. Boundary note (v0.2): RecoveryCodeRow and WebauthnCredentialRow do NOT move this phase — they remain defined in persons/models.py under the shared Base. So the harness creates the tables from that shared metadata (the rows the stele/ functions operate on still live in persons/ until Phase 1), provides a test KEK and a test config, and does not import engagements, memory, or credit. This is still the proof that Stele's moved logic exercises in isolation (the Operator's "minor ecosystem" requirement); the row models join stele/ in Phase 1, at which point the harness can create Stele-owned tables. Commit.

Step 0.5 — Full-suite regression. Run the complete engine test suite and confirm nothing broke (nothing should — no external caller was touched). Verify against the engine database that sign-in still works end to end (the login path still imports these symbols, now from stele/ — confirm the import-path updates are complete and correct). This is the database-truth check, not suite-only. Halt and report results.

3. What must not happen in Phase 0

4. Halt and report

After Step 0.5, halt before any push. Report: the module location, the files moved (with the importer-repoint count for each and confirmation no importer's logic changed), the KEK-reference handling, the isolation test result (proof Stele's moved logic exercises in isolation), and the full-suite + database sign-in regression result. On the Operator's review, push Phase 0; then Phase 1 (the identity records and registry, now also carrying the credential row models and credentials.py) is the next CR.

5. Reversibility

Every step is its own commit. If the halt review finds a problem, the offending step reverts as a single commit with no effect on the others. There is no parallel copy to reconcile — a revert simply puts the moved file back at its prior path. This is the safety model: small, reversible slices, not a fallback implementation.


DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks change request — Stele extraction Phase 0 (stand up the module, move the zero-caller pieces) — CR-2026-103 — v0.3 — 2026-06-14