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

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

Version. 0.2 Date. 2026-06-16 Author. Marvin Percival (DUNIN7 Operator) with Claude.ai (scoping) on DUNIN7-M4. Status. Steps 1–2 built, pushed, green on branch stele-phase-6b-person-view-split (23d4bb5). This version resolves Step 3's open decisions from grounding (2026-06-16) and is the worklist for Step 3 execution. Markdown primary (technical consumer: CC executes). Against. branch stele-phase-6b-person-view-split 23d4bb5 (Steps 1–2). Base was engine main def8b04.


Plain-language summary

What this does. Splits Stele's fused Person read view into a pure Principal view (identity/auth) and a host-side HostAccount view (host account state), re-homes every reader onto the right view, and retires the Person name at the view and function layer (replaced by Principal).

What changed from v0.1. v0.1 carried the handoff's framing of Step 3. Grounding (2026-06-16) resolved four things v0.1 left open, all now settled below: (1) the delivery mechanism — how a handler gets host data after the strip; (2) the Person retirement scope — path 1, name retired, including a class rename; (3) the strip order — per-field, hottest first; (4) the dependency rename to avoid the "person" label surviving on the combined object. The blast radius also tightened: the real host-field reader surface is ~26 sites, not the 66 raw upper bound.

The decisive grounding fact. The per-request resolution already loads the entire persons row (all 23 columns) once and caches it for the request (Depends default). The host data is already in hand. So exposing both a Principal and a HostAccount off that one loaded row costs zero extra query during expand, and becomes one cached shared-id join at contract. This retired an earlier hybrid proposal (treating personal_engagement_id's hot-path reads as a per-read query cost) — there is no per-read cost; it is one load per request, read many times off the cached object.

Decision needed. None outstanding — Step 3's decisions are settled (recorded below). This version is for the record and to drive CC execution.

Out of scope (contract phase, separate CR). Dropping persons host columns · re-targeting the 12 host FKs · retiring the mirror triggers · renaming PersonRow the ORM row (the row name survives 6b; its rename/relocation is contract-phase) · re-examining whether personal_engagement_id is correctly classified as a host column given its hot-path identity-adjacent use (flagged, contract-phase).


Correction trajectory (preserved, not smoothed)

Step 3's shape moved through several positions during scoping. Recorded so the path is walkable:

  1. Delivery mechanism. Leaned option 2 (separate get_current_host_account dependency, "handlers ask for host data explicitly") on boundary-purity grounds. Then personal_engagement_id's ~16 hot-path reads surfaced, and leaned hybrid (cheap principal-side personal_engagement_id, host-dependency for the rest), accepting a boundary blur. Then the Operator asked why the values can't be resolved once and reused. Grounding confirmed the row is already loaded once and cached per request — so the hot-path cost never existed. The hybrid and the boundary blur were both retired. Settled: option 1 / "resolve-once, expose-both" — one cached resolution, two typed projections, no blur, no tax.
  2. Person retirement scope. v0.1 left "reduced or retired" open. Operator chose path 1Person name retired, replaced by Principal. Inspection then showed the class-name rename is a ~177-site annotation sweep (separate from, and larger than, the function renames, which are ~4 sites). Operator confirmed Option 1 — full collapse inside 6b, so main never carries a Person/Principal twin.
  3. Dependency name. Leaned get_current_actor. Caught: "actor" is already load-bearing (ActorRef, actor_from_companion/actor_from_person from Step 2) — a collision, the same duplicate-meaning confusion the rename is meant to avoid. Settled: get_current_user returning a CurrentUser wrapper with .principal and .host_account. "User" = the authenticated requester; distinct from "actor" = who acts in a conversation.

Settled Step 3 decisions

| # | Decision | Settled | |---|---|---| | D1 | Delivery mechanism | Resolve-once, expose-both. The per-request resolution (already loading the full row, already Depends-cached) returns two typed objectsPrincipal and HostAccount — off the one loaded row. Handlers read …principal.<field> / …host_account.<field>. Zero added query in expand; one cached shared-id join at contract. | | D2 | Two typed objects, not re-fused | The combined return must expose .principal and .host_account as separate typed projections. A single object carrying all fields would be the Person shim renamed — forbidden. | | D3 | Person name retired (path 1, full collapse in 6b) | Class PersonPrincipal; Person view + its functions removed; no Person/Principal twin reaches main. | | D4 | Dependency rename | get_current_personget_current_user, returns CurrentUser (.principal, .host_account). Folds into the same ~177-site annotation sweep as the class rename — no extra spread. | | D5 | Strip order | Per-field, hottest first: personal_engagement_id (~16) → companion_name (~6) → trailing four (email, mobile, exhaustion_preference, spend_pause; ~5 total, mostly the memberships.py response dict). Each: strip → rename-to-find → re-point → suite green → commit. | | D6 | API shape held constant | The internal split changes no API response or request schema. Person is never a response_model; no /person routes exist; the self-surface is /me/*. The one response-dict consumer (memberships.py:349–356) keeps its exact JSON keys, sourced from the host projection. The class/dependency renames are internal-only, zero API-contract impact. |


The real Step 3 surface (grounding, 2026-06-16)

Resolution chain: get_current_person (deps.py) → resolve_session (session.py) → get_person_by_idselect(PersonRow).where(id==).scalar_one_or_none()_row_to_personPerson, carried on ResolvedSession(person, payload). One SELECT of the full 23-column row per request, Depends-cached.

Host-field reader surface — ~26 real person.<host> reads (not the 66 raw upper bound):

| Field | Real reads | Notable locations | |---|---|---| | personal_engagement_id | ~16 (hot) | address_resolver.py, me_create_engagement.py, orchestration/router.py, converse.py, notifications.py | | companion_name | ~6 | orchestration/router.py, converse.py, notifications.py (+ Step-2 callers already reading caller-side) | | email | 2 | me_security.py:179, memberships.py:349 | | mobile / exhaustion_preference / spend_pause | 1 each | all memberships.py:350/354/356 |

The one response consumer: memberships.py:349–356 serializes 5 host fields into a person-detail response dict. Re-points to the host projection; keeps identical response keys (D6).

Write-then-read-stale: no new surface. Host-field writers (onboard.py, credit/lifecycle.py) target their own PersonRow, never the frozen view; writes flow persons → mirror triggers → host_account. The view has no write path. The combined return is still resolved-once-per-request, same as today.

Class-name surface: Person type name ~177 references, overwhelmingly person: Person = Depends(get_current_person) handler annotations. Never a response_model. The rename (D3/D4) is a pure-annotation sweep, zero API impact.


Build steps (Step 3)

Each step: build, suite to green, halt before push. Per-field commits keep each rename-to-find break-wall readable. Inspect-and-report first on the combined-return wiring before mutating.

Step 3.0 — Author the combined return (get_current_user / CurrentUser).

Step 3.1–3.6 — Strip host fields one at a time (D5 order). Per field, in order personal_engagement_idcompanion_nameemailmobileexhaustion_preferencespend_pause:

Step 3.7 — Retire the Person name (D3/D4 class + dependency rename).

Step 3.8 — Boundary + invariant verification.


Invariants (hold throughout)


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.2 — 2026-06-16