DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path change-requests/cr-2026-110-stele-extraction-phase-6b-v0_6.md

CR-2026-110 — Stele Extraction Phase 6b — Person-view split — v0.6

Version. 0.6 Date. 2026-06-16 Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (scoping) on DUNIN7-M4. Status. Steps 1–3.3 built, pushed, green on branch stele-phase-6b-person-view-split (b83deea). This version corrects a propagated error in v0.5 (the false "field-equal to Principal" claim) and finalizes the Step 3.7 plan against the 3.7 inspect. Markdown primary (technical consumer: CC executes). Against. branch stele-phase-6b-person-view-split b83deea. Suite 2933 / 46 / 0.


Plain-language summary

What changed from v0.5. Two things. (1) Correction: v0.5 stated the Person view is "field-equal to Principal." That is false — confirmed by the 3.7 inspect. Person is {id, display_name, created_at, updated_at}; Principal adds first_login_at and last_presence_proof_at. So Person ⊂ Principal, a strict subset. The Person → Principal swap is still safe, but for a different reason than v0.5 gave: it is superset-safe (Principal adds two Optional fields, and zero code reads them off any view var), not field-equal. (2) 3.7 finalized: the inspect resolved 3.7's one care-point — the host-vs-identity split of the shim consumers came back 0 host-needing / 68 identity-only, so the sweep is uniform.

Decision needed. One small grouping call: fold the ~5 function renames into 3.7, or split them into a 3.7b follow-on. CR lean: fold in (so Person is genuinely retired in one commit). Confirmable in a word.

Out of scope (contract phase). Drop persons host columns · re-target the 12 host FKs · retire the mirror triggers · rename PersonRow.


Correction trajectory (preserved, not smoothed)

Carries v0.1–v0.5 (recorded there). This version adds:

  1. The false "field-equal" claim (a propagated error, corrected here). CC's Step 3.3 report described the now-stripped Person view as "field-equal to Principal." Claude.ai took that at face value and propagated it into v0.5 (build-state table and 3.7 scope) and into the framing of 3.7 as a "clean rename, no reconciliation." The 3.7 inspect showed it is false: Person = {id, display_name, created_at, updated_at} is a strict subset of Principal = {id, display_name, first_login_at, last_presence_proof_at, created_at, updated_at}. The swap remains safe — superset-safe, because Principal's two extra fields are Optional and an audit found zero reads of .first_login_at / .last_presence_proof_at off any view var — but the reason in v0.5 ("field-equal, clean rename") was wrong. This is a drift-surface failure: a fact from an earlier tool output was treated as authoritative without re-verification and written into a CR. The source Person view docstring carries the same false "equals Principal" claim and is fixed in 3.7. Recorded rather than silently corrected.

Build state (as of b83deea) — corrected

| Step | Commit | State | |---|---|---| | 1 | (in branch) | Principal + HostAccount views authored | | 2 | 23d4bb5 | stele host-reads re-homed | | 3.0 | 73846d4 | CurrentUser delivery path (additive) | | 3.1 | 1396552 | companion_name stripped; HostAccount cascade thread established | | 3.2 | f49c63f | personal_engagement_id stripped; shared test helper proven; host classification confirmed | | 3.3 | b83deea | email/mobile/exhaustion_preference/spend_pause stripped — strip phase complete |

Person view now carries: {id, display_name, created_at, updated_at} — a strict subset of Principal (which also has first_login_at, last_presence_proof_at). Not field-equal (correcting v0.5). Person survives solely as the get_current_person shim.

Twin state (on the branch, never to reach main): Person (subset) and Principal (superset). 3.7 collapses PersonPrincipal; the swap is superset-safe (the two extra Principal fields are Optional and unread off any view var).


Settled Step 3 method (M1–M7)

Unchanged from v0.4. Summary: M1 combined get_current_user delivery (two cached loads, host-assembled in deps.py); M2 reader re-home by site class; M3 the HostAccount cascade thread; M4 shared test helper; M5 builder↔view anti-drift; M6 commit discipline relaxes once machinery exists; M7 API shape constant.


Step 3.7 — finalized plan (against the 3.7 inspect)

The last view-layer step. The inspect resolved the one care-point: 0 host-needing, 68 identity-only — the host-vs-identity split has already collapsed to one bucket (3.1–3.3 did the dangerous work). Proven type-level, not just observed: Person is frozen=True with host fields gone, so any surviving host read would AttributeError, and the suite is green. So the sweep is uniform — every get_current_person site goes to the identity resolver, no per-site judgment.

The genuinely new work (not a rename):

  1. Author get_current_principalPrincipal (resolve_session → resolved.principal, no host load). ~15-line analogue of get_current_person. ResolvedSession already carries both person and principal, so the body is a one-field swap — no new DB load.
  2. Author the optional variantget_current_person_optional re-points (does not fully retire; its nullable behavior is still needed by assert_engagement_creator and Phase-25 draft creation) to return Principal | None (get_current_principal_optional).

The mechanical sweep (uniform, superset-safe):

  1. Swap 68 Depends(get_current_person) targetsget_current_principal.
  2. Swap 96 annotations (: Person / -> Person) and 33 imports (from ...registry import Person) → Principal.
  3. The one non-mechanical site: deps.py:204 person: "Optional[Person]" = None (the inline twin of get_current_person_optional) — hand-edit to the optional principal form.
  4. Bare unannotated Depends sites (require_founder, engagements.py ×4, seed_conversation.py ×5) — swap the dependency target only, no annotation.

Retirement + collapse:

  1. Retire get_current_person and ResolvedSession.person (re-point the shim reads to .principal).
  2. Collapse the Step-1 Principal view and the now-subset Person view into one (Principal). No twin reaches main.
  3. Fix the false docstring — the Person view's "equals Principal" claim is wrong (it's a subset); correct or remove it as the view collapses.

Function renames (~5 sites) — [GROUPING: fold into 3.7 per CR lean; confirmable]:

  1. get_person_by_idget_principal_by_id (caller auth_dev.py:82); get_person_by_email (already returns Principal, just misnamed) → get_principal_by_email; create_person, _row_to_person → principal-named; get_principal_and_person_by_id collapses to get_principal_by_id (sole caller session.py:161).

Out of scope in 3.7: get_current_contributor — resolves a Contributor (engagement-scoped, not person-host), does not depend on get_current_person. Leave alone.

PersonRow (ORM row) keeps its name — contract-phase. Verify against playground_dev. Suite green. Commit. Halt.

Step 3.8 — boundary + invariant verification. stele/ imports nothing from persons/; no migration; triggers/columns untouched; full suite green; commit; halt; then merge to main, record, retire branch.


Invariants

No migration · triggers untouched (0085, 0086) · persons keeps all 23 columns · stele/ imports nothing from persons/ · API shape constant (M7; Person is never a response_model — confirmed tree-wide, zero OpenAPI drift) · suite green at every commit, halt before push, playground_dev per touched route · corrections preserved.


Definition of done


Carry-forwards to contract phase


DUNIN7 — Done In Seven LLC — Miami, Florida CR-2026-110 — Stele Extraction Phase 6b — Person-view split — v0.6 — 2026-06-16