app.domain.approval

Approval policy, approval-state evaluation and reviewer eligibility.

Approval evidence is scoped by the logical identity (source_id, project, record_key) and the content digest. The caller passes only the decision events of one identity; evaluate() then derives the state of the record’s current digest. Rules, applied in this order (storage queries that filter by state must mirror them):

  1. no valid policy -> policy_missing (needs approval);

  2. policy required: false -> not_required (no approval needed);

  3. the latest event for the current digest is accepted -> accepted (no approval needed);

  4. the latest event for the current digest is rejected -> rejected (needs approval);

  5. an accepted event exists for another digest -> changed_since_acceptance (needs approval);

  6. otherwise -> pending (needs approval).

approvable (see app.domain.records.ParsedRecord.approvable) does not change the state; it only controls ApprovalState.can_decide. A record without a valid UUID or with bad encoding therefore shows pending and cannot be decided.

Public API: PolicyStatus, Policy, DecisionAction, DecisionEventView, ApprovalStateCode, ApprovalState, evaluate(), last_decided(), is_eligible(), principal(), is_self_approval().

class app.domain.approval.PolicyStatus(*values)

Bases: StrEnum

Configuration status of the global approval policy.

CONFIGURED = 'configured'
MISSING = 'missing'
INVALID = 'invalid'
class app.domain.approval.Policy(id, version, required=True, reviewer_users=frozenset({}), reviewer_roles=frozenset({}), allow_self_approval=False)

Bases: object

The single global approval policy (Phase 1).

Parameters:
  • id (str) – Policy identifier recorded with every decision.

  • version (int) – Policy version recorded with every decision.

  • required (bool) – Whether approval is required at all.

  • reviewer_users (frozenset[str]) – Eligible principals as issuer|subject.

  • reviewer_roles (frozenset[str]) – Eligible IdP role names.

  • allow_self_approval (bool) – Whether authors may decide their own records.

id: str
version: int
required: bool
reviewer_users: frozenset[str]
reviewer_roles: frozenset[str]
allow_self_approval: bool
class app.domain.approval.DecisionAction(*values)

Bases: StrEnum

Formal decision recorded by a reviewer.

ACCEPTED = 'accepted'
REJECTED = 'rejected'
class app.domain.approval.DecisionEventView(id, digest, action, created_at)

Bases: object

The fields of a decision event needed for state evaluation.

Parameters:
  • id (str) – Stable event id.

  • digest (str) – Content digest string the decision applies to.

  • action (DecisionAction) – Accepted or rejected.

  • created_at (datetime) – Server timestamp of the decision.

id: str
digest: str
action: DecisionAction
created_at: datetime
class app.domain.approval.ApprovalStateCode(*values)

Bases: StrEnum

The six approval states of a record’s current content.

POLICY_MISSING = 'policy_missing'
NOT_REQUIRED = 'not_required'
ACCEPTED = 'accepted'
REJECTED = 'rejected'
CHANGED_SINCE_ACCEPTANCE = 'changed_since_acceptance'
PENDING = 'pending'
class app.domain.approval.ApprovalState(state, needs_approval, current_event=None, last_accepted=None, can_decide=False)

Bases: object

Evaluated approval state of a record’s current digest.

Parameters:
  • state (ApprovalStateCode) – The state code.

  • needs_approval (bool) – Whether the record belongs in the “Needs approval” filter.

  • current_event (DecisionEventView | None) – Latest decision for the current digest, if any.

  • last_accepted (DecisionEventView | None) – Latest acceptance for another digest, set for changed_since_acceptance.

  • can_decide (bool) – Whether a reviewer may record a decision now.

state: ApprovalStateCode
needs_approval: bool
current_event: DecisionEventView | None
last_accepted: DecisionEventView | None
can_decide: bool
app.domain.approval.evaluate(policy, identity_events, current_digest, approvable)

Evaluate the approval state of a record’s current digest.

Parameters:
  • policy (Policy | None) – The configured policy, or None when it is missing or invalid.

  • identity_events (Iterable[DecisionEventView]) – All decision events of the record’s logical identity (any digest).

  • current_digest (str) – Digest string of the record’s current content.

  • approvable (bool) – Whether the content may be decided at all.

Return type:

ApprovalState

Returns:

The state; see the module docstring for the rules.

app.domain.approval.last_decided(identity_events, *, exclude_digest=None)

Pick the default “from” revision for a diff.

Parameters:
  • identity_events (Iterable[DecisionEventView]) – All decision events of one identity.

  • exclude_digest (str | None) – Digest to ignore, typically the current one.

Return type:

DecisionEventView | None

Returns:

The latest accepted event, else the latest rejected event, else None.

app.domain.approval.principal(issuer, subject)

Build the stable principal string used in reviewer allowlists.

Parameters:
  • issuer (str) – Token issuer.

  • subject (str) – Token subject.

Return type:

str

Returns:

issuer|subject.

app.domain.approval.is_eligible(policy, issuer, subject, roles)

Tell whether a caller may record decisions under the policy.

Read permission never implies review: only the policy’s reviewer users and roles are eligible.

Parameters:
  • policy (Policy | None) – The configured policy, or None if missing.

  • issuer (str) – Caller’s token issuer.

  • subject (str) – Caller’s token subject.

  • roles (Iterable[str]) – Caller’s IdP roles.

Return type:

bool

Returns:

True if the caller is a listed user or holds a listed role; always False without a policy.

app.domain.approval.is_self_approval(policy, reviewer_email, author)

Tell whether a decision would be a forbidden self-approval.

Parameters:
  • policy (Policy) – The configured policy.

  • reviewer_email (str | None) – The reviewer’s verified email, if known.

  • author (str | None) – The record author (push request or commit author), if known.

Return type:

bool

Returns:

True when self-approval is disallowed and the verified email equals the author (case-insensitive).