anagrapheus.identity.decisions¶
anagrapheus.identity.decisions
¶
Building the identity decisions, ready to append.
These helpers return a PendingEvent rather than appending one. An identity
decision almost always accompanies the domain event that occasioned it — you
establish an entity because something happened — and the caller owns that batch and
its transaction. Handing back an event keeps identity out of the write path.
They serve the default vocabulary. An application that binds its own event types
already owns those names and constructs its own payloads; what it should still reach
for is :func:mint_entity_id, because the replay hazard it addresses is not a
property of the names.
mint_entity_id
¶
mint_entity_id(
material: Mapping[str, Json],
*,
resolver: PinResolver | None = None,
kind: str = ENTITY_PIN_KIND,
) -> UUID
Mint an id for a newly established entity, pinned when a resolver is given.
Minting consumes entropy, which makes it exactly the kind of effect a pin keys on. Under generation-replay an unpinned mint draws a fresh id, and every belief, pin and reference keyed on the original silently orphans — the same failure mode as pin-key drift, reached from the other direction.
Pass a resolver and the id is drawn through the pin ledger under material,
so a replayed run re-establishes the same entity. Without one this is a plain
uuid7 and a re-run will not agree with the original: correct for a live run
that will never be replayed, and quietly wrong for one that will.
material is the application's canonicalization policy for entity identity, and
the same discipline applies as to any pin key — structured data, never prose.
Source code in src/anagrapheus/identity/decisions.py
establish
¶
establish(
registry: EventTypeRegistry,
surface_form: str,
*,
provenance: str,
entity_id: UUID | None = None,
stance: str = UNGRADED,
material: Mapping[str, Json] | None = None,
resolver: PinResolver | None = None,
correlation_id: UUID | None = None,
) -> tuple[UUID, PendingEvent]
Declare a new entity into existence; return its id and the event to append.
Establishing is not resolution and does not consult the registry: there is no
external referent to match, so there is nothing to match against. Supply
entity_id to use an id you already hold, or resolver to have it pinned —
see :func:mint_entity_id.
With a resolver, material is load-bearing. It defaults to
{"surface_form": ..., "provenance": ...}, and a pin key is reused for identical
material — so two genuinely distinct records that share a name and a source would
be minted as one entity, erasing the very ambiguity this layer exists to preserve.
Whenever that is possible, pass material carrying a discriminator that identifies
the record: the source system's own id, say. It must be stable across runs like
any pin material — a counter or a timestamp would replay to a different entity and
defeat the pinning.
Source code in src/anagrapheus/identity/decisions.py
alias
¶
alias(
registry: EventTypeRegistry,
entity_id: UUID,
surface_form: str,
stance: str = UNGRADED,
*,
correlation_id: UUID | None = None,
) -> PendingEvent
Record another surface form for an entity, at a stance from your vocabulary.
Source code in src/anagrapheus/identity/decisions.py
merge
¶
merge(
registry: EventTypeRegistry,
*,
kept: UUID,
absorbed: UUID,
source_event: UUID | None = None,
correlation_id: UUID | None = None,
identity: IdentityRegistry | None = None,
) -> PendingEvent
Record that two ids name one referent.
The correction appends: nothing is erased from the log, and undoing this is another
event, :func:unmerge, rather than a deletion. In the projection the absorbed
entity yields its surface forms to the survivor — it names nothing once it has
yielded — and history() keeps the record of which entity originally recorded
each of them.
Pass identity, the registry as it currently stands, to have the merge checked
before it exists. If absorbed is already merged, into kept or anywhere
else, this raises :class:~anagrapheus.identity.errors.AlreadyMerged: an entity
yields to one survivor at a time, so withdraw the standing merge with
:func:unmerge first. If kept already yields, directly or through a chain, to
absorbed, it raises :class:~anagrapheus.identity.errors.MergeCycle. If
kept has itself been merged into another entity, it raises
:class:~anagrapheus.identity.errors.AlreadyMerged too: only an entity that has
not yielded can survive a merge, so merge into the entity kept resolves to, or
unmerge kept first. The checks are only as current as the registry, so catch
it up first. Nor can they see a merge built earlier in the same batch and not yet
appended. They do not ask whether both entities were established: one established
earlier in the same batch is not in the registry yet.
Whatever reaches the log is folded without raising. A merge that names an entity nothing established, takes an entity already merged, or gives one to a survivor that has itself been merged is a recorded no-op: nothing moves, no re-key is emitted, and the history of each entity named that exists says it was ignored. That covers a merge that would close a cycle, whose survivor already yields.
Source code in src/anagrapheus/identity/decisions.py
unmerge
¶
unmerge(
registry: EventTypeRegistry,
identity: IdentityRegistry,
*,
kept: UUID,
absorbed: UUID,
source_event: UUID | None = None,
correlation_id: UUID | None = None,
) -> PendingEvent
Withdraw the merge of absorbed into kept.
absorbed becomes canonical again and takes back the surface forms the merge
moved onto kept; the merge itself stays in the log and in history(). The
belief layer hears about it through rekeys_since as a choice, not a move: a
belief keyed on kept may have been about either entity.
Checked before append against identity, the registry as it currently stands:
if absorbed is not merged directly into kept, this raises
:class:~anagrapheus.identity.errors.NotMerged rather than returning an event that
would do nothing. Catch the registry up first; an event that slips past the check
anyway is folded as a recorded no-op.
Source code in src/anagrapheus/identity/decisions.py
split
¶
split(
registry: EventTypeRegistry,
original: UUID,
into: Sequence[tuple[UUID, Sequence[str]]],
*,
source_event: UUID | None = None,
correlation_id: UUID | None = None,
) -> PendingEvent
Record that one id named several referents, carving out the new ones.
into pairs each new entity id with the surface forms that move to it. Forms
left unnamed stay with original, which survives.