Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

- Freeze the `cfr-review-state/v1` human-review state machine. Independent
approve/reject decisions now compose in either order, approvals bind to their
immutable audit-trace prefix, exact retries are idempotent, and concurrent
mutations use fail-closed revision compare-and-set semantics. Admin-only JSON
review endpoints derive reviewer identity from the verified authorization
context; the public resolve surface and all retrieval behavior remain unchanged.

- Freeze the prospective `cfr-unit-fields/v1` contract: application quantities remain aligned to
the factor denominator, while `resolved_quantity_kg` now means kilograms only and remains null
for non-mass activity. Preview totals normalize `gCO2e`/`tCO2e` numerators to `kgCO2e`, and lock
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ keeps both fields outside the production response allowlist.
- [Contributing](CONTRIBUTING.md)
- [Bring Your Own Catalog](docs/BRING_YOUR_OWN_CATALOG.md)
- [Catalog-to-lock integrity contract](docs/CFR_CATALOG_LOCK_INTEGRITY_V3.md)
- [Human-review workflow state machine](docs/CFR_REVIEW_WORKFLOW_STATE_MACHINE_V1.md)
- [Public API safety contract](docs/CFR_API_SAFETY_CONTRACT.md)
- [Release notes](docs/RELEASE_NOTES_V0.14.3.md)
- [Technical implementation reference](docs/CFR_TECHNICAL_DOCUMENT.md)
Expand Down
86 changes: 86 additions & 0 deletions docs/CFR_REVIEW_WORKFLOW_STATE_MACHINE_V1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CFR Review Workflow State Machine v1

Status: **FROZEN** for PR-C (`codex/review-workflow-state-machine-v1`).

This contract governs only human review decisions and immutable locking. It does
not change retrieval, ranking, semantic matching, candidate identity, catalog
content, factor values, qualification, or the public resolve API.

## States

A resolution run has one of three review states:

- `OPEN`: no candidate has been approved and the resolution is not locked.
- `SELECTED`: exactly one candidate has an `APPROVED` decision. Other candidates
may independently have `REJECTED` decisions.
- `LOCKED`: the selected candidate and its approval have been committed to an
immutable `LockedResolution`.

Each candidate has at most one terminal human decision in a resolution run:
`APPROVED` or `REJECTED`. A request can have at most one approved candidate and
at most one lock.

## Legal transitions

- `OPEN -> OPEN`: reject any undecided candidate.
- `OPEN -> SELECTED`: approve one undecided candidate.
- `SELECTED -> SELECTED`: reject any other undecided candidate.
- `SELECTED -> LOCKED`: lock the approved candidate.
- An exact replay of a committed approve or reject operation returns the
previously committed object without appending another trace event. A lock
retry for the already locked candidate returns the canonical immutable lock;
its original reviewer identity is never replaced by the retrying caller.

The order of independent decisions does not change lockability. In particular,
`approve A -> reject B -> lock A` and `reject B -> approve A -> lock A` are both
valid. Their append-only audit traces may differ in event order, while the same
candidate and decision bindings govern the final lock.

## Forbidden transitions

- Approving a rejected candidate or rejecting an approved candidate.
- Approving a second candidate in the same resolution run.
- Locking an unapproved candidate or a candidate different from the approved
candidate.
- Any new or conflicting decision after lock.
- Reusing the same candidate decision with a different status, reviewer, note,
or approval mode.
- Committing against an explicitly supplied stale trace revision.

All conflicts fail closed with stable domain errors. The administration API maps
state conflicts and stale revisions to stable HTTP 409 reason codes and never
returns internal exception text.

## Integrity bindings

Every decision remains bound to the exact candidate digest, recommendation
digest and revision, database anchor, semantic registry anchor, policy anchor,
verified reviewer identity, and the append-only trace prefix created by that
decision.

A later legal decision may extend the trace but cannot invalidate an earlier
approval. Locking verifies the approval's recorded trace revision and chain hash
against the corresponding immutable prefix of the current trace. Rewritten,
missing, or non-prefix history fails closed.

## Concurrency and retry

The store is the atomic authority for decision and lock commits.

- Compare-and-set checks cover the recommendation digest and trace revision.
- An explicit expected revision is strict: a mismatch raises
`STALE_REVIEW_REVISION` and is not retried.
- Without an explicit revision, the engine may retry a bounded number of times
after a concurrent legal trace append.
- Concurrent exact duplicate operations converge on one stored decision or lock.
- Concurrent conflicting approvals have one winner; all other attempts fail
closed. Double lock is impossible.
- Reconstructing the engine over the same store preserves state and permits the
next legal transition.

## Administration API identity

Review mutations exist only on the isolated administration application. Request
bodies do not accept a reviewer identity. The reviewer is always the
`actor_id` supplied by the verified authorization context. The public
`create_app` surface remains read/resolve-only and unchanged.
9 changes: 8 additions & 1 deletion src/a1_factor_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
interpret_process_emission_observation,
)
from .engine import A1FactorResolutionEngine
from .integrity import CatalogIntegrityError, PersistenceIntegrityError
from .integrity import (
CatalogIntegrityError,
PersistenceIntegrityError,
ReviewStateConflictError,
StaleReviewRevisionError,
)
from .material_registry import (
DEFAULT_MATERIAL_REGISTRY,
FormRule,
Expand Down Expand Up @@ -180,6 +185,8 @@
"LockedResolutionEvidenceSnapshot",
"CatalogIntegrityError",
"PersistenceIntegrityError",
"ReviewStateConflictError",
"StaleReviewRevisionError",
"LinkAttempt",
"LinkOutcome",
"LinkStrategy",
Expand Down
Loading