Date. 2026-06-19
By. Claude Code (CC) on DUNIN7-M4 — read-only inspection per loomworks-stele-cr-b-step-0-inspection-brief-v0_1.md.
Against. engine origin/main = d0f302e (verified), playground_dev at alembic head 0090, working tree clean. Nothing changed.
Headline: the inherited lists hold — 11 host FKs (exact, zero drift), 4 id-only _PersonRow stubs (exact), trigger present. Every FK column is already uuid, so all 11 re-targets are pure constraint re-points — zero column-type migrations. Two nuances surfaced for the drafter: 5 of 11 FK columns are NOT named person_id, and 3 carry ON DELETE CASCADE.
| Check | Expected | Found |
|---|---|---|
| origin/main | d0f302e | d0f302e ✓ |
| local main | — | d0f302e, tree clean ✓ |
| alembic head (playground_dev) | 0090 | 0090 ✓ |
No drift — orientation is current. Proceeded.
| Column | Type | NULL | DB default |
|---|---|---|---|
| principals.id (target) | uuid | NOT NULL | none (app-side uuid.uuid4) |
| persons.id (current target) | uuid | NOT NULL | gen_random_uuid() |
All 11 host FK columns and their types (full detail in B). Every one is uuid:
| FK column | Type | Matches principals.id? |
|---|---|---|
| credit.credit_grant.claimed_by_person_id | uuid | yes |
| companion_notifications.person_id | uuid | yes |
| conversation_turns.person_id | uuid | yes |
| engagement_tags.added_by_person_id | uuid | yes |
| engagements.created_by_person_id | uuid | yes |
| memberships.person_id | uuid | yes |
| organization_memberships.person_id | uuid | yes |
| person_settings.person_id | uuid | yes |
| saved_filters.owner_person_id | uuid | yes |
| uploaded_files.uploaded_by_person_id | uuid | yes |
| workspaces.owner_person_id | uuid | yes |
→ 11/11 match. No "no". Zero column-type migrations — principals.id, persons.id, and all 11 FK columns are the same native uuid type. The re-target is a constraint swap throughout.
Count = exactly 11. Zero drift — every table in the inherited list is present; no 12th FK references persons; none of the 11 is gone. (pg_constraint query, contype='f', confrelid = persons.)
| # | Schema.table | FK column | Constraint name | NULL | ON DELETE | ON UPDATE |
|---|---|---|---|---|---|---|
| 1 | credit.credit_grant | claimed_by_person_id | fk_credit_grant_claimed_by_person | nullable | NO ACTION | NO ACTION |
| 2 | public.companion_notifications | person_id | companion_notifications_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 3 | public.conversation_turns | person_id | conversation_turns_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 4 | public.engagement_tags | added_by_person_id | engagement_tags_added_by_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 5 | public.engagements | created_by_person_id | engagements_created_by_person_id_fkey | nullable | NO ACTION | NO ACTION |
| 6 | public.memberships | person_id | memberships_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 7 | public.organization_memberships | person_id | organization_memberships_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 8 | public.person_settings | person_id | person_settings_person_id_fkey | NOT NULL | CASCADE | NO ACTION |
| 9 | public.saved_filters | owner_person_id | saved_filters_owner_person_id_fkey | NOT NULL | CASCADE | NO ACTION |
| 10 | public.uploaded_files | uploaded_by_person_id | uploaded_files_uploaded_by_person_id_fkey | NOT NULL | NO ACTION | NO ACTION |
| 11 | public.workspaces | owner_person_id | workspaces_owner_person_id_fkey | NOT NULL | CASCADE | NO ACTION |
All 11 reference public.persons(id).
Two facts the inherited "11 host FKs / person_id" shorthand hides:
person_id everywhere — 5 of 11 differ:
credit_grant.claimed_by_person_id, engagement_tags.added_by_person_id, engagements.created_by_person_id, saved_filters.owner_person_id, uploaded_files.uploaded_by_person_id, workspaces.owner_person_id — that's 6 distinct non-person_id names across 5 tables besides person_id. The migration must name each column individually; a blanket person_id assumption would miss 6 columns.
(person_id exactly: companion_notifications, conversation_turns, memberships, organization_memberships, person_settings — 5 tables.)
ON DELETE CASCADE (person_settings, saved_filters, workspaces); the other 8 are ON DELETE NO ACTION. All 11 are ON UPDATE NO ACTION. The re-targeted constraints must preserve each one's delete behavior — the CASCADE semantics shift from "deleting a persons row cascades" to "deleting a principals row cascades," which is the intended end-state but should be stated explicitly in the CR._PersonRow stubs
Count = exactly 4. Zero drift. Each is an id-only FK-target stub (single mapped_column, the PK id), __tablename__ = "persons", on its module's own Base/MetaData. None is an app-columns model.
| # | File | Class | Maps | Purpose (from docstring) |
|---|---|---|---|---|
| 1 | src/loomworks/files/models.py | _PersonRow(Base) | id only | FK target for uploaded_by_person_id |
| 2 | src/loomworks/notifications/models.py | _PersonRow(Base) | id only | so CompanionNotificationRow.person_id resolves in the notifications metadata |
| 3 | src/loomworks/orchestration/models.py | _PersonRow(Base) | id only | so ConverseTurnRow.person_id resolves in the orchestration metadata |
| 4 | src/loomworks/persons/models.py | _PersonRow(Base) | id only | persons-table registration on shared identity_base.Base for all 11 host FKs (the CR-C stub) |
Each is id: Mapped[UUID] = mapped_column(PG_UUID(as_uuid=True), primary_key=True) (the persons/models.py one adds default=uuid.uuid4). grep -rl '^class _PersonRow' src/loomworks/ returns these 4 and no others.
Note for the build: these are 4 separate MetaData registrations of the persons table (files, notifications, orchestration each have their own Base; persons/models.py is the shared identity_base.Base). All 4 must be removed when persons is dropped — not just the persons/models.py one.
Migration 0090 is installed and the reverse mirror is live. Confirmed in the DB:
stele_principals_mirror_to_persons — AFTER INSERT OR UPDATE ON public.principals FOR EACH ROWstele_mirror_principals_to_persons() — plpgsql, body:```sql INSERT INTO persons (id, display_name, totp_secret, first_login_at, last_presence_proof_at, created_at, updated_at) VALUES (NEW.id, NEW.display_name, NEW.totp_secret, NEW.first_login_at, NEW.last_presence_proof_at, NEW.created_at, NEW.updated_at) ON CONFLICT (id) DO UPDATE SET display_name = EXCLUDED.display_name, totp_secret = EXCLUDED.totp_secret, first_login_at = EXCLUDED.first_login_at, last_presence_proof_at = EXCLUDED.last_presence_proof_at, updated_at = EXCLUDED.updated_at; -- NB: created_at is NOT updated on conflict (kept immutable) RETURN NEW; ```
stele_persons_mirror_to_principals trigger = 0 rows; stele_mirror_persons_to_principals function = 0 rows.For the CR-B down-path: retiring this trigger means the down-migration must restore both the trigger and the function verbatim (names + body above). This is the inverse of CR-C's 0090, whose own downgrade already restores the forward (0085) trigger — so CR-B's migration will need its own up (drop reverse) / down (re-create reverse) pair around the above definition.
uuid, matching principals.id. Zero column-type migrations. Each is: drop the REFERENCES persons(id) constraint, add REFERENCES principals(id) with the same delete/update behavior.NOT NULL (only credit_grant.claimed_by_person_id and engagements.created_by_person_id are nullable). For a constraint re-point the ordering concern is data validity, not column population: every existing non-null FK value must exist in principals.id before the new constraint is added. This holds today — persons is the reverse mirror of principals (every identity write since CR-C originates in principals and mirrors into persons), so persons.id ⊆ principals.id; any FK value currently resolving against a persons.id also resolves against the same principals.id. The CR should still assert this with a validation query at build time, but no backfill is implied.public — credit.credit_grant (schema credit). No audit-schema FK references persons (the inherited "audit/credit" phrasing over-counts; only credit applies). The migration must schema-qualify that one constraint. The other 10 are all public.person_id; it must address each of the 6 non-person_id columns by name (claimed_by_person_id, added_by_person_id, created_by_person_id, owner_person_id ×2, uploaded_by_person_id).persons requires removing all 4 _PersonRow stubs across 4 modules/MetaDatas, not only the persons/models.py one — and is gated behind all 11 FKs re-targeting first.uuid, so no column-type migration (this removes anticipated work).person_id; 3 FKs are ON DELETE CASCADE; only credit.credit_grant is non-public; no audit FK exists.Nothing changed. No migration, no code, no branch.
DUNIN7 — Done In Seven LLC — Miami, Florida Stele Extraction CR-B Step 0 Inspection Report — v0.1 — 2026-06-19