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

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

Version. 0.2 Date. 2026-06-14 Status. Change request, amended. v0.2 corrects the Phase 0 scope boundary after a tree-truth verification (engine 60e7cfa = coupling-map anchor c210715 + the empty stele/ skeleton) found that the coupling map's "zero external callers" claim holds, but its implicit assumption — that each §1 piece is a separable, movable file — does not. Three pieces are clean file-moves and stay in Phase 0; two pieces (the ORM row models, and the person-scoped TOTP internals) are interwoven into shared or load-bearing modules and are reassigned to the phases that already own those modules. persons/credentials.py's functions are reassigned with them, because they depend on a row model that is not moving in Phase 0. This keeps Phase 0 the genuinely safe slice it was designed to be: pure relocation of standalone modules, no schema decision, no edits to load-bearing code. 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.


Plain-language summary

This is the first, safest step of giving Stele a clean boundary. It creates the Stele module and moves into it only the pieces of identity code that nothing else in the engine imports — the credential storage, recovery codes, TOTP logic, the WebAuthn ceremony, and the session encode/decode. Because nothing outside imports these, moving them cannot break any caller — which makes this the ideal slice to prove the module structure and to stand up the test harness that exercises Stele in isolation. No behaviour changes; this is pure relocation plus a standalone test. If anything looks off at the halt, it reverts as a single commit. Nothing load-bearing (the "who is signed in?" path, the identity records, sign-up) moves in this phase — those are later CRs.

1. Scope of Phase 0

In scope — move into the new stele module (clean standalone files, zero external callers, confirmed file-separable at 60e7cfa):

For each of the three: before moving, grep the whole engine for external importers of its symbols; confirm zero (the coupling map says zero — verify against the live tree). Move the file into stele/, update imports among the moved files to the new paths, leave imports into still-persons/ symbols (e.g. WebauthnCredentialRow, RecoveryCodeRow, PersonRow) pointing at persons/. Run the suite per move.

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 clean-internal files, one at a time. The zero-caller claim and the file-separability of the three in-scope files were verified at 60e7cfa before this step (the tree-truth check that produced the v0.2 boundary correction). Move, in order: persons/recovery.py, persons/webauthn.py, persons/session.py into stele/. For EACH, before moving: re-confirm zero external importers of its symbols against the live tree; then move the file, update imports among the moved files to the new stele/ paths, leave imports into still-persons/ symbols (WebauthnCredentialRow, RecoveryCodeRow, PersonRow) pointing at persons/, and run the test suite. One commit per file moved. If any file turns out to have an unexpected external importer not seen at verification, halt and report rather than proceeding. (The row models, credentials.py, and the TOTP internals are NOT moved this step — they are reassigned to later phases per the v0.2 §1 correction.)

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 confirmation of zero external callers for each), the KEK-reference handling, the isolation test result (proof Stele 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) 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.2 — 2026-06-14