NOT a stable public API. Engine-tier identity vocabulary — published to crates.io only because the SDK closure requires it on the registry; 3rd parties never name this crate. They meet these types through an SDK product facade or a wire contract, never here.
Principal identity — the scaccounts.ppnums vocabularies
Two value-sets, both owned by the scaccounts.ppnums table and both read by
more than one organ, so neither can live in either organ:
| Type | Column | CHECK |
|---|---|---|
[EntityType] |
entity_type |
ck_ppnums_entity_type_enum |
[LifecycleState] |
lifecycle_state (+ 2 audit columns) |
ck_ppnums_lifecycle_state_enum, ck_ppnum_lifecycle_events_{from,to}_state_enum |
They are here for one reason, applied twice: a value-set consumed across
the organ boundary collapses into one vocabulary rather than being mirrored
and then bound. EntityType arrived by RFC_202607252223, LifecycleState
by RFC_202607251658 P1 — and the second move is also what finally makes the
cross-fact between them stateable exactly once
([LifecycleState::can_transition_for_entity], the
ck_ppnums_expired_only_mask rule).
EntityType — the one entity vocabulary
What kind of entity a ppnum is. One type, one name, one value-set,
shared by the token engine and both services. Before this crate the same
fact was reified four times — accounts_core::EntityType (6 variants),
chat_core::port::EntityClass (5, a hand-maintained mirror),
ppoppo_token::EntityType (3), and the ppnum.EntityType proto enum —
and two of those disagreed about whether delegated was a member.
The axis this crate is NOT
EntityType answers what the principal is. It does not answer who
is currently acting for it — that is the RFC 8693 §4.1 act claim, and
keeping the two apart is the entire point.
There is deliberately no Delegated variant. A human identity driven
by an agent is Human plus act — two facts, two fields. Compressing
them into one string field is the mistake
[STANDARDS_AUTH_PPOPPO] §4.2.1 already rejected for the retired role
claim; that rule was never applied to its two siblings
(EntityType::Delegated, SenderBadge::Delegated), and this crate is
where it finally is. Recovering "is this delegated?" from a single value
is impossible by construction here.
Why an engine-tier crate with zero dependencies
The type must be reachable from three places at once, and the crate lattice leaves exactly one option:
ppoppo-tokenneeds it for theentity_typeclaim — andengine → sharedis forbidden (xtask::policy::rules::taxonomy), so acrates/shared/*home is illegal. Engine tier it is.chat-coreneeds it, and bans IO/transport crates (Constitution Principle I). Purestdis the only shape it can accept — the same stance that already lets it depend onppoppo-clock.
Sharing one type also removes a value-set mirror: EntityClass
existed only to restate scaccounts.ppnums.entity_type inside PCS, and
[DIRECTION_COUPLING_PASPCS] §4 K5 records that mirror as a gap
(PCS's drift test scopes nspname='scchat' and structurally cannot see
scaccounts). With one type there is nothing left to drift.
Table, not scattered predicates
[TABLE] is the SSOT of per-variant attributes. Each fact previously
lived somewhere else — the wire string in an as_str match,
credential-eligibility in a PAS use-case, the AI-disclosure obligation in
a doc comment. The enum is retained because exhaustive match is
load-bearing: a sixth variant must fail to compile rather than default
into a claim.
Attributes that belong to one owner stay with that owner and are
deliberately absent here — notably number_class (people/infra/
ephemeral), a GENERATED column in scaccounts that PCS never reads.
A shared table is not a dumping ground.
LifecycleState — the one lifecycle vocabulary
What state a ppnum is in, plus the legal transitions between states.
Arrived by RFC_202607251658 P1 for two reasons, and it carries the
transition lattice as well as the value-set because of the second:
- PCS reads the value-set.
chat-coreclassifiesscaccounts.ppnums.lifecycle_stateon its liveness path, and did so through a hand-maintained 8-variant copy. Same unguardable shape asEntityClassabove — PCS's drift test is schema-scoped toscchatand structurally cannot see ascaccountsCHECK([DIRECTION_COUPLING_PASPCS] §4 K5 / §6 I7). can_transition_for_entityis a cross-fact. It layersck_ppnums_expired_only_mask— only [EntityType::Mask] may reach [LifecycleState::Expired] — which is a statement about both value-sets. Rust's orphan rule means whichever crate does not own the type cannot say it as an inherent method, so leaving the state machine inaccounts-corewould have required an extension trait: the invariant expressible in two places again.
This is not the dumping ground the paragraph above rules out. PAS keeps what only PAS reads — the column, the business triggers that drive transitions, and the audit trail. What moved is the fact neither organ could own alone.