docs(adr): split ADR-0026 + propose ADR-0027 (Structure hierarchy) #217
Reference in New Issue
Block a user
Delete Branch "docs/adr-0026-narrow-adr-0027-structure"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Splits ADR-0026 into two sibling ADRs after a cascade / acteurs_plus source-of-truth audit caught a design break: the first draft pinned
Etablissement.finessas primary key, but ≥ 30 % of APF's real structure inventory has no FINESS (antennes, dispositifs, entreprises adaptées, mouvement, administratif, siège).proposedPerson+User+UserScopeonly (identity model)proposedRegion+Delegation+Structure(cascade-aligned:kinddiscriminator + nullable FINESS / SIRET /codePaie)ADR-0028(future)ADR-0027placeholder)No code changes — all three artefacts moving in this PR are markdown.
What lands
docs/decisions/0026-person-user-portal-data-model.md+ organisational hierarchy);Region/Delegation/Etablissementschema removed;Person.emailunique constraint dropped (two distinct humans can share an email — see Lifecycle); Lifecycle rewritten without email-based dedup;UserScope.valuedocumented as opaque string referencing ADR-0027 codes; Confirmation dropsEtablissement.kindbullet; ADR-0027 sync references renumbered to ADR-0028; "What ADR-0026 ships vs adjacent ADRs" rewritten to three columns.docs/decisions/0027-portal-side-organisational-hierarchy.mdStructurewithkinddiscriminator + nullable FINESS / SIRET /codePaie, internalcodePK that doubles as FINESS for medico-social structures). Considered options A (FINESS-only — original ADR-0026 draft), B (chosen), C (full cascade replication), D (remote read against cascade). Inline-migration seed for the test tenant (Region 75 + Delegation 33 + a handful of medico-social structures +siege); full inventory deferred to ADR-0028's cascade sync.Structure.kindenum-as-string drift-gated.docs/decisions/README.mdproposed. New ADR-0027 row:proposed, tagsdata, backend.CLAUDE.md0001 → 0025 accepted;ADR-0026 + ADR-0027 proposed.@RequireScopePrisma-resolver roadmap entry references both ADRs + the ADR-0028 follow-up. No new Architecture bullet (entries land when their ADRs ship).Why split
The cascade audit was the trigger. Cascade — APF's medico-social structure registry + Pléiades/Talentia HR integration — models
Structurewith a seven-value type discriminator:medico_social,antenne,dispositif,entreprise_adaptee,mouvement,administratif,sanitaire. Three of those (antenne,dispositif, part ofentreprise_adaptee) do not have a FINESS by construction. Cascade carries FINESS / SIRET / SIREN / PléiadescodePaie/ TalentiacodeComptaon separate per-source enrichment rows (StructureSourceFiness,StructureSourceSirene,StructureSourcePleiades,StructureSourceTalentia), nullable and many-to-one againstStructure.The acteurs_plus audit confirmed: acteurs_plus does not store FINESS / SIRET / SIREN on its hierarchy entities at all — it uses a portal-internal
code(unique string) + anexternalIdpointer.The first ADR-0026 draft's
Etablissement.finessPK excluded all non-medico-social structures by construction. The fix is not to make FINESS nullable onEtablissement(that smuggles the discriminator into absence-of-value semantics) — it is to adopt cascade'sStructure+kinddiscriminator directly. Doing that inside ADR-0026 would have ballooned its scope; splitting is the cleaner shape:Person+User+UserScope). The Person model is unchanged from the first draft except for the email-dedup rewrite (already discussed before the audit landed).Pole,Service, arbitrary nesting, per-source enrichment) called out explicitly as ADR-0028's territory.ADR-0027 schema highlights
The vocabulary mismatch — ADR-0025's scope kind name is
etablissementbut the value is now aStructure.codeof any kind — is documented as a known wart, with a possible ADR-0025 amendment as the rename path if a maintainer trips over it.Notes for the reviewer
Person.emailunique constraint dropped. Two distinct humans genuinely can share an email (shared family alias, genericinfo@mailbox, error in an upstream feed). The first draft hademail String? @uniquecarried over from a "let's use it as a v1 dedup key" line of thinking that the audit reshaped. The lifecycle now treatsentraOidas the only natural key the v1 provisioner trusts; email is an attribute, indexed for operator-driven lookup (admin UI search, ADR-0028 reconciliation flow), not a constraint.UserScope.valuehas no FK to ADR-0027 tables. Deliberate: a scope can outlive its target (a structure decommissioned mid-quarter still has historical UserScope rows pointing at its code, which the audit log needs to read). Admin UI write path validates; runtime guard tolerates stale codes (they fail the resource match, not the sign-in).docs/adr-0026-accept-and-tighten-lifecycleis superseded by this one. That branch promoted ADR-0026 (full first draft) toaccepted. The Q1 / Q2 resolutions from that PR are preserved here — Q1 (no email-dedup) is the new ADR-0026 Lifecycle section; Q2 (inline-migration seed) moves to ADR-0027's "Seeding posture" section since it is org-hierarchy-specific. The old PR can be closed without merging.pnpm ci:checkimpact.pnpm exec prettier --checkclean on the four touched files.Test plan
pnpm exec prettier --check— clean on the four touched files.ADR-NNNNlink in the two ADRs round-trips; the new ADR-0028 reference is a known dangling marker for the future sync ADR).codePK); thePerson.emailnon-unique change in ADR-0026; the scope-kind vocabulary mismatch documented in ADR-0027.What's next (post-merge)
Per ADR-0026 §"Phasing" and ADR-0027 §"Phasing" — the two ADR PRs ship in parallel once accepted:
Region/Delegation/StructurePrisma schema + inline reference-data migration +Structure.kindcatalogue + drift-gate extension.Person/User/UserScopePrisma schema +PersonAndUserProvisionercalled fromSessionEstablisher+Person.sourcecatalogue + drift-gate extension + updatedPrincipalBuilder. Independent of (1) at the schema level — can ship in parallel.PrismaScopeResolverreplacingStubScopeResolver+/admin/users/:id/scopesadmin screen +prisma/seed.tspopulating the 19 test personas'user_scopespernotes/test-tenant-role-assignments.md. Depends on both (1) and (2) — the seed referencesStructure.codevalues from (1) and writesUserScoperows from (2).Pole,Service, per-source enrichment) the sync needs.