ppoppo-identity 0.19.0

Principal-identity vocabulary for the ppoppo ecosystem — the single EntityType value-set shared by the token engine and both services
Documentation

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-token needs it for the entity_type claim — and engine → shared is forbidden (xtask::policy::rules::taxonomy), so a crates/shared/* home is illegal. Engine tier it is.
  • chat-core needs it, and bans IO/transport crates (Constitution Principle I). Pure std is the only shape it can accept — the same stance that already lets it depend on ppoppo-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-core classifies scaccounts.ppnums.lifecycle_state on its liveness path, and did so through a hand-maintained 8-variant copy. Same unguardable shape as EntityClass above — PCS's drift test is schema-scoped to scchat and structurally cannot see a scaccounts CHECK ([DIRECTION_COUPLING_PASPCS] §4 K5 / §6 I7).
  • can_transition_for_entity is a cross-fact. It layers ck_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 in accounts-core would 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.