docs(adr-0026): promote to accepted + resolve open questions #216

Closed
julien wants to merge 1 commits from docs/adr-0026-accept-and-tighten-lifecycle into main
Owner

Summary

ADR-0026 shipped as proposed in #213 with two open questions explicitly flagged for the acceptance pass. This PR resolves both and promotes the ADR proposed → accepted.

No code changes — same shape as #205 (ADR-0025 acceptance).

What lands

File Change
docs/decisions/0026-person-user-portal-data-model.md Frontmatter status: proposed → accepted; the Lifecycle section rewritten to drop email-based dedup; new "Seeding the org hierarchy" subsection picking inline-migration over a separate seed script; Consequences section reworked to match the new dedup choice.
docs/decisions/README.md 0026 row: proposed → accepted.
CLAUDE.md Roll-up bumped to 0001 → 0026 accepted; new Architecture bullet for the portal-side data model; the @RequireScope Prisma-resolver roadmap entry now references ADR-0026 as accepted with the two-PR implementation phasing.

Open questions resolved

Q1 — Person dedup at first sign-in

Decision: v1 looks up by entraOid only; no email-based merging.

The original ADR-0026 specified an email-based dedup at sign-in: if a Person row with matching email existed, the new User attached to it; otherwise a fresh Person was created. That path is fragile in two ways:

  • Two distinct people can genuinely share an email — shared family alias, generic info@ mailbox at a small partner organisation, error in an upstream feed. Auto-merge silently corrupts the golden record without surfacing a conflict.
  • The "match by email" UX optimisation only helps when a v1 admin pre-created a Person row with the right email and the same human later signs in. In v1 there is no Pléiades sync producing those pre-rows, so the optimisation has no live consumer to validate it.

The accepted shape: the provisioner trusts entraOid as the only natural key. If User.entraOid = X exists, refresh lastSignInAt and return. Otherwise, create a fresh Person + linked User in one transaction (Person.source = 'self-signin', names from the Entra displayName split, email from the Entra claim).

ADR-0027 inherits the reconciliation problem: when Pléiades / Acteurs+ syncs run after v1 sessions exist, two Person rows may describe the same human (one self-signin, one pleiades). The merge becomes an operator-confirmed flow surfaced in the admin UI, not an automated email-based join.

Q2 — Seed placement for Region / Delegation / Etablissement

Decision: inline in the Prisma migration as reference data.

Two candidates:

  • Inline in the migration — schema + INSERT block in the same SQL file; every environment that runs prisma migrate deploy gets the same starting set.
  • Separate prisma/seed.ts — schema ships empty tables; a seed script is run on demand.

The org hierarchy is reference data — slow-moving, every environment needs it, none of the rows are environment-specific. Inline in the migration is the conservative posture: one atomic deploy ships the schema and the rows that make the schema usable. The v1 seed is intentionally small (only the codes the test tenant exercises — a handful of établissements + Délégation 33 + Région 75 Nouvelle-Aquitaine + siege). ADR-0027's Pléiades sync becomes the authoritative source once it ships.

Test-tenant user_scopes rows for the 19 personas are not in this inline seed — they are environment-specific and ride in prisma/seed.ts (implementation PR 2).

Notes for the reviewer

  • CLAUDE.md Architecture bullet for ADR-0026. Added at the same level of detail as the ADR-0025 bullet that precedes it (decision summary + sees-link). Implementation hasn't shipped yet — when PR 1 lands, the bullet won't need re-editing, only the "Still on the roadmap" entry will move into "Shipped on main".

  • The Consequences section's Bad-because was rewritten, not just trimmed. The original wording said Person.email as the v1 dedup key was fragile, and pointed at ADR-0027 for the fix. With the new dedup choice the fragility is gone — but a new failure mode appears: a Person created via self-signin and later imported from Pléiades look like duplicates until reconciled. The new Bad-because makes that explicit so a future reader understands the v1 cost. ADR-0027 owns the merge flow.

  • No code changes, so no pnpm ci:check impact. pnpm exec prettier --check clean on the three touched files.

Test plan

  • pnpm exec prettier --check — clean on the three touched files.
  • ADR cross-references still resolve (the inline backlinks to ADR-0025 §"Sources of truth", ADR-0027 future, ADR-0013, ADR-0016, ADR-0024 unchanged in their target).
  • Review focus — Q1 / Q2 resolution rationales, the rewritten Consequences entry, the CLAUDE.md Architecture bullet phrasing.

What's next

Per ADR-0026's §More Information phasing — now unblocked:

  1. #213 — ADR-0026 proposed.
  2. This PR — ADR-0026 accepted, open questions closed.
  3. Implementation PR 1 — Prisma schema + Person.source / Etablissement.kind catalogue constants + drift-gate extension + PersonAndUserProvisioner called from SessionEstablisher + updated PrincipalBuilder populating Principal.user.{id, personId} from the real rows.
  4. Implementation PR 2PrismaScopeResolver replacing StubScopeResolver + /admin/users/:id/scopes admin-app screen + prisma/seed.ts populating the 19 test personas' user_scopes per notes/test-tenant-role-assignments.md.
  5. ADR-0027 (proposed) — Pléiades + Acteurs+ syncs + facet schemas (Salarié / Élu / Adhérent / Bénéficiaire) + operator-confirmed reconciliation flow.
## Summary [ADR-0026](docs/decisions/0026-person-user-portal-data-model.md) shipped as `proposed` in #213 with two open questions explicitly flagged for the acceptance pass. This PR resolves both and promotes the ADR `proposed → accepted`. No code changes — same shape as [#205](https://git.unespace.com/julien/apf_portal/pulls/205) (ADR-0025 acceptance). ## What lands | File | Change | | --- | --- | | `docs/decisions/0026-person-user-portal-data-model.md` | Frontmatter `status: proposed → accepted`; the Lifecycle section rewritten to drop email-based dedup; new "Seeding the org hierarchy" subsection picking inline-migration over a separate seed script; Consequences section reworked to match the new dedup choice. | | `docs/decisions/README.md` | 0026 row: `proposed → accepted`. | | `CLAUDE.md` | Roll-up bumped to `0001 → 0026 accepted`; new Architecture bullet for the portal-side data model; the `@RequireScope` Prisma-resolver roadmap entry now references ADR-0026 as accepted with the two-PR implementation phasing. | ## Open questions resolved ### Q1 — `Person` dedup at first sign-in **Decision: v1 looks up by `entraOid` only; no email-based merging.** The original ADR-0026 specified an email-based dedup at sign-in: if a Person row with matching email existed, the new User attached to it; otherwise a fresh Person was created. That path is fragile in two ways: - Two distinct people can genuinely share an email — shared family alias, generic `info@` mailbox at a small partner organisation, error in an upstream feed. Auto-merge silently corrupts the golden record without surfacing a conflict. - The "match by email" UX optimisation only helps when a v1 admin pre-created a Person row with the right email and the same human later signs in. In v1 there is no Pléiades sync producing those pre-rows, so the optimisation has no live consumer to validate it. The accepted shape: the provisioner trusts `entraOid` as the only natural key. If `User.entraOid = X` exists, refresh `lastSignInAt` and return. Otherwise, create a fresh `Person` + linked `User` in one transaction (`Person.source = 'self-signin'`, names from the Entra `displayName` split, email from the Entra claim). ADR-0027 inherits the reconciliation problem: when Pléiades / Acteurs+ syncs run after v1 sessions exist, two Person rows may describe the same human (one `self-signin`, one `pleiades`). The merge becomes an operator-confirmed flow surfaced in the admin UI, not an automated email-based join. ### Q2 — Seed placement for `Region` / `Delegation` / `Etablissement` **Decision: inline in the Prisma migration as reference data.** Two candidates: - **Inline in the migration** — schema + INSERT block in the same SQL file; every environment that runs `prisma migrate deploy` gets the same starting set. - **Separate `prisma/seed.ts`** — schema ships empty tables; a seed script is run on demand. The org hierarchy is _reference data_ — slow-moving, every environment needs it, none of the rows are environment-specific. Inline in the migration is the conservative posture: one atomic deploy ships the schema and the rows that make the schema usable. The v1 seed is intentionally small (only the codes the test tenant exercises — a handful of établissements + Délégation 33 + Région 75 Nouvelle-Aquitaine + `siege`). ADR-0027's Pléiades sync becomes the authoritative source once it ships. Test-tenant `user_scopes` rows for the 19 personas are **not** in this inline seed — they are environment-specific and ride in `prisma/seed.ts` (implementation PR 2). ## Notes for the reviewer - **CLAUDE.md Architecture bullet for ADR-0026.** Added at the same level of detail as the ADR-0025 bullet that precedes it (decision summary + sees-link). Implementation hasn't shipped yet — when PR 1 lands, the bullet won't need re-editing, only the "Still on the roadmap" entry will move into "Shipped on `main`". - **The Consequences section's Bad-because was rewritten, not just trimmed.** The original wording said `Person.email` as the v1 dedup key was fragile, and pointed at ADR-0027 for the fix. With the new dedup choice the fragility is gone — but a new failure mode appears: a Person created via `self-signin` and later imported from Pléiades look like duplicates until reconciled. The new Bad-because makes that explicit so a future reader understands the v1 cost. ADR-0027 owns the merge flow. - **No code changes**, so no `pnpm ci:check` impact. `pnpm exec prettier --check` clean on the three touched files. ## Test plan - [x] `pnpm exec prettier --check` — clean on the three touched files. - [x] ADR cross-references still resolve (the inline backlinks to ADR-0025 §"Sources of truth", ADR-0027 future, ADR-0013, ADR-0016, ADR-0024 unchanged in their target). - [ ] **Review focus** — Q1 / Q2 resolution rationales, the rewritten Consequences entry, the CLAUDE.md Architecture bullet phrasing. ## What's next Per ADR-0026's `§More Information` phasing — now unblocked: 1. ✅ #213 — ADR-0026 proposed. 2. ✅ **This PR** — ADR-0026 accepted, open questions closed. 3. **Implementation PR 1** — Prisma schema + `Person.source` / `Etablissement.kind` catalogue constants + drift-gate extension + `PersonAndUserProvisioner` called from `SessionEstablisher` + updated `PrincipalBuilder` populating `Principal.user.{id, personId}` from the real rows. 4. **Implementation PR 2** — `PrismaScopeResolver` replacing `StubScopeResolver` + `/admin/users/:id/scopes` admin-app screen + `prisma/seed.ts` populating the 19 test personas' `user_scopes` per `notes/test-tenant-role-assignments.md`. 5. **ADR-0027 (proposed)** — Pléiades + Acteurs+ syncs + facet schemas (Salarié / Élu / Adhérent / Bénéficiaire) + operator-confirmed reconciliation flow.
julien added 1 commit 2026-05-24 06:04:28 +02:00
docs(adr-0026): promote to accepted + resolve open questions
CI / commits (pull_request) Successful in 2m49s
CI / check (pull_request) Successful in 3m25s
CI / scan (pull_request) Successful in 3m37s
CI / a11y (pull_request) Successful in 3m25s
Docs site / build (pull_request) Successful in 3m32s
CI / perf (pull_request) Successful in 5m33s
07cb466d51
ADR-0026 was reviewed; status flips proposed -> accepted with the
two open questions closed:

1. Person dedup at first sign-in. v1 looks up by entraOid only;
   no email-based merging. Two distinct people genuinely can
   share an email, and silent auto-merge would corrupt the
   golden record. ADR-0027 introduces operator-confirmed
   reconciliation; v1 takes the safe-by-default posture.

2. Org-hierarchy seed placement. Region / Delegation /
   Etablissement codes ship inline in the Prisma migration as
   reference data — slow-moving, environment-agnostic, single
   atomic deploy. ADR-0027's Pléiades sync supersedes the
   inline seed when it ships. user_scopes for the 19 test
   personas stay in prisma/seed.ts (implementation PR 2) since
   those are test-tenant-specific data.

Consequences section reworked to reflect the new dedup choice:
the email-fragility Bad-because is removed; a new Bad-because
documents the v1 cost (a self-signed-in Person + a later
Pléiades-imported Person look like duplicates until ADR-0027's
operator merge flow resolves them — observable, not silent).

CLAUDE.md updated:
- Roll-up bumped 0001 -> 0025 -> 0001 -> 0026.
- New Architecture bullet for the portal-side data model.
- Roadmap entry on @RequireScope Prisma resolver updated to
  reflect ADR-0026 (accepted) and the two-PR implementation
  phasing.

Index row flipped to accepted.
julien closed this pull request 2026-05-24 06:53:17 +02:00
Some checks are pending
CI / commits (pull_request) Successful in 2m49s
CI / check (pull_request) Successful in 3m25s
CI / scan (pull_request) Successful in 3m37s
CI / a11y (pull_request) Successful in 3m25s
Docs site / build (pull_request) Successful in 3m32s
CI / perf (pull_request) Successful in 5m33s
check, scan, commits, perf, a11y
Required

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#216