app.services.decisions

Formal accept/reject decisions (POST /api/v1/records/{id}/decisions).

A decision binds to the record’s logical identity and the digest of its current content at request time; the server sets the event id and timestamp. Checks, in this order (the same rules give the detail page’s can_decide through decision_blocker()):

  1. the record exists -> else 404 record_not_found;

  2. the approval policy is configured -> else 409 policy_missing;

  3. the policy requires approval -> else 409 approval_not_required;

  4. the caller is a policy reviewer (issuer|subject or a role) -> else 403 not_eligible;

  5. the content is approvable (valid UUID, valid UTF-8) -> else 409 not_approvable;

  6. not a self-approval (when the policy disallows it) -> else 403 self_approval. Self-approval means the reviewer’s verified email equals the record author (case-insensitive), or the author equals the reviewer’s issuer|subject exactly. Records without an author (polled filesystem/GitLab rows) cannot be detected;

  7. expected_digest, when sent, equals the current digest -> else 409 digest_changed (the reviewer never decides content they did not see);

  8. no decision exists for (identity, digest) -> else 409 already_decided (also on a concurrent race, through the unique constraint).

Events are append-only (database trigger); nothing here updates or deletes one.

Public API: Reviewer, DecisionBlocker, Approvable, approval_blocker(), decision_blocker(), record_decision().

class app.services.decisions.Approvable(*args, **kwargs)

Bases: Protocol

The fields of a stored row that decide whether it is approvable.

property issues: tuple[Issue, ...]

Stored validation issues.

property adr_uuid: UUID | None

Authored UUID, if valid.

class app.services.decisions.DecisionBlocker(*, code, message, hint, forbidden=False)

Bases: object

Why the caller cannot decide a record now.

Parameters:
  • code (str) – Stable machine-readable code.

  • message (str) – Reader-facing reason.

  • hint (str) – What can be done about it.

  • forbidden (bool) – True for caller-specific refusals (HTTP 403), False for state conflicts (HTTP 409).

code: str
message: str
hint: str
forbidden: bool
to_error()

Convert to the domain error raised by the decision endpoint.

Return type:

AdrianoError

Returns:

A ForbiddenError or ConflictError.

class app.services.decisions.Reviewer(*, issuer, subject, display, email, roles)

Bases: object

The caller of a decision, from verified token claims.

Parameters:
  • issuer (str) – Token issuer.

  • subject (str) – Token subject.

  • display (str | None) – Display name.

  • email (str | None) – Verified email, if known.

  • roles (tuple[str, ...]) – IdP roles.

issuer: str
subject: str
display: str | None
email: str | None
roles: tuple[str, ...]
app.services.decisions.approval_blocker(record)

Explain why a stored row’s content cannot be decided at all.

Mirrors app.domain.records.ParsedRecord.approval_blocker.

Parameters:

record (Approvable) – The row.

Return type:

str | None

Returns:

A reader-facing reason, or None if approvable.

app.services.decisions.decision_blocker(record, events, policy_config, reviewer, *, expected_digest=None)

Tell why reviewer cannot decide record now, if they cannot.

Parameters:
Return type:

DecisionBlocker | None

Returns:

The first failing check (module docstring), or None.

app.services.decisions.record_decision(provider, record_id, *, action, comment, expected_digest, reviewer, policy_config)

Append an accept/reject decision for a record’s current content.

Parameters:
Return type:

DecisionEvent

Returns:

The stored event.

Raises: