ADR-0111: The Imported-Person Model, and Project Portability's First Doctrines¶
- Status: accepted (2026-08-09 — "I confirm ADR 111")
- Date: 2026-08-09
- Deciders: Nicolas Burri (register #31 — his goal "in a couple of days I would like to be able to upload pulsemed to lqms.ch", his constraint "I would rather not have invites sent to all my colleagues who are mentioned in our company's qms", his corner case "what if the same email is registered in the target system as well?"; his answers matched the recommended options 3-of-4 blind, route reconciled)
- Relates to: ADR-0108 (person ≠ login; unbound persons; verified-email consent binding), ADR-0110 (trail is evidence, not a read model — the attribution split below is its doctrine applied to imports), ADR-0075 §2 (documented data corrections at import — the backdate precedent), ADR-0063 (person keyed on the login subject — for BOUND persons), V088 (the release stamp imported attribution writes), ADR-0057 (frozen released content), register 2026-08-06 #31, the supplier-vs-manufacturer lane vision (portability yields lane templates).
Context¶
Moving a project between instances (first: PULSEMED from the demo corpus onto lqms.ch) forces the question every future import will face: the people in the history. A regulated corpus is the work of named humans; the target instance must show who brought the system to its state — document history included — without turning every name in a source QMS into an account, an email, or an invitation. And the same email may already exist as a live user on the target.
The substrate for the answer already exists: ADR-0108 separated person from login and made unbound persons (invited, not yet bound) a first-class state. What is missing is a person that is deliberately never bound — present as history, absent as an actor.
Decision¶
- External persons. An import mints each historical actor as an
app_userrecord that is unbound, uninvitable by default, and never mailed: no login subject, no invitation, a provenance marker naming the origin ("imported from MERIDIAN-QMS, 2026-08-…"). They appear in history, version stamps (V088released_by), and directories with their real names and the provenance badge. Making one invitable is a separate, explicit, audited act — never a side effect of import. - Attribution splits per ADR-0110: trail = this instance's acts, state = historical truth.
The target's append-only trail records what happened here: imported, by the operating
account, when. The domain state (authors,
released_by/released_at, effective dates) carries the source-historical facts, written at import as documented data corrections — the ADR-0075 §2 pattern that already governs backdated effectivity. The trail never claims acts this instance did not witness; the state never pretends the import operator wrote a corpus they didn't. - Never auto-merge on email equality. Identity is per-instance. An email match against a live target user is a claim, not an identity: acting on it would let a dataset attach history to a real account without that person's participation. The import always creates a distinct external person and warns on collision. Two sanctioned paths unify identities later:
- Bulk-but-explicit link (the migration case): after import, the operator is shown the matched list ("N imported persons match live accounts by email") and confirms it as one audited act — reviewed, never silent.
- Consent binding (the onboarding case): if an external person is ever genuinely invited, ADR-0108's verified-email machinery unifies with the person's own participation.
- The source audit trail travels as an evidence artifact. The exporting instance's trail
arrives as a readable, attached artifact of the imported scope — inspectable history in full.
It is never merged into the target's
audit_event: foreign rows would make the trail claim witness to acts it never saw (ADR-0110 / ADR-0029). The native trail records the import acts themselves. - Route: the importer is step one of the generic transfer arc, not instead of it. The decisions above — the person model, the collision rule, the evidence shape — are designed as the generic portability model. The existing import pipeline pointed at lqms.ch is merely the first transport to exercise them; the future project export/import format inherits the model unchanged. (Nicolas's tendency was generic-first; reconciled so nothing built for PULSEMED is throwaway.)
Non-decisions, made explicit¶
- The generic transfer format itself (serialization, blob transport, scope re-keying, cross-version compatibility) is NOT decided here — it is the follow-on arc this ADR's model feeds.
- No change to bound-person identity (ADR-0063): a person with a login subject keys on it, exactly as today.
- Display disambiguation details (how a collision-adjacent external person is visually told apart from the live user of the same name) are an implementation concern; the requirement is only that the provenance marker is always visible where the person is named.
Realization note §1 (2026-08-09, V089) — and one email-path clarification¶
§1 is realized: EXTERNAL as a fourth app_user status value (a kind column was rejected —
either possible status underneath it re-opens exactly the §3 auto-merge or live-person-predicate
hazards; V089's header argues it in full). Provenance survives promotion by one-way CHECK; the
natural key is (origin, lower(email)) predicated on origin. The mint authority is decided per
the Consequences clause: MANAGE_USERS at GLOBAL (creation and ending of a person's existence
stay one authority — ADR-0108 §7's symmetric half).
Clarification the build surfaced: §1's "uninvitable by default" holds on the by-id path (named, audited refusal) but is deliberately STRUCTURAL on the email-keyed paths — the imported record is simply absent from the email match key, so inviting that address mints the live person beside the record, exactly as for an address this instance has never seen. A named refusal there would be an enumeration oracle (ADR-0108 §3) and would make every imported colleague permanently uninvitable — the #31 goal inverted. §3's doctrine cuts both ways: equality never merges, and equality never decides.
Consequences¶
- A person kind/status for external persons (migration + model), the provenance marker, the import-time collision warning, and the bulk link act (audited, MANAGE_USERS-shaped authority — the implementation picks the exact permission with the register).
- The importer gains: external-person minting from source history, attribution-of-record onto them (V088 stamps included), remote-target operation driven by one real authenticated operator account, and the source-trail evidence artifact.
- The user manual's history/directory topics explain the provenance badge; the ops guide (#26) gains the import runbook.
- PULSEMED onto lqms.ch is the first exercise; the generic export/import format is registered as the follow-on arc (lane-template future rides on it).