diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/ACTION_CUSTODY.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/ACTION_CUSTODY.md index 086a14a2a..9ed43c9dd 100644 --- a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/ACTION_CUSTODY.md +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/ACTION_CUSTODY.md @@ -32,7 +32,7 @@ This table is the planning source of truth for the v0.1 review and human-revisio | `review.artifact_reference.reconcile` | `operations.reconcile.run` | fixed artifact-reference reconciler only | bounded review artifact reference batch | REV | registered planned | `WS-XINT-003-08B` | | `review.projection.rebuild` | `operations.projection.rebuild` | fixed projection rebuilder only | derived review projection batch | REV | registered planned | `WS-XINT-003-08B` | -The 19 registered `review.*` rows move from historical `AUTH_REV_*` planning labels to these exact XINT-003 waves only as planning custody. Their runtime `ActionOwner` values change with each later activation, never in 01. +The 19 historically transferred `review.*` rows move from `AUTH_REV_*` planning labels to these exact XINT-003 waves only as planning custody. Their runtime `ActionOwner` values change with each later activation, never in 01. ## Fixed-service closure @@ -78,6 +78,18 @@ Activation chunks may not add ActionIds, PermissionIds, service identities, resource-context classes, adapter protocols, lifecycle rules, or product routes. Any such discovery returns to 02C/02D planning. +`WS-XINT-003-02D` publishes the code-level closed manifest in +`backend/app/modules/authorization/review_contracts.py`. Its strict frozen +models are inert integration contracts: scalar canonical IDs, closed action and +mode discriminators, digests, bounded reasons, and timezone-aware timestamps. +They are not runtime evaluators and do not make a planned action executable. +REV owns row locking and final-fact composition; later XINT activation adapters +must consume these exact families without adding another context family. The +two fixed identities sharing `review.reconcile.run` are bound to disjoint +server-derived modes. The two future evidence-ingest actions have explicit +unsupported manifest rows with no resource model. XINT-002 artifact/submission +actions remain reference-only external handoffs. + | ActionId | Exact prerequisite behavior/manifest | |---|---| | `project.review_policy.update` | 02A immutable identity/lineage plus refreshed REV-03P/AUTH-12D2; activated only by `WS-XINT-003-02B` | diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/REVIEW_LOG.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/REVIEW_LOG.md index e95d6c7b0..26fff55dd 100644 --- a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/REVIEW_LOG.md +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/REVIEW_LOG.md @@ -152,3 +152,21 @@ The first hosted semantic lanes exposed the expected post-0049 public-schema fingerprint change before running product tests. The chunk contract now permits only that exact `tests/conftest.py` fingerprint update; no reset allow-list or schema-integrity behavior changed. + +## WS-XINT-003-02D AUTH PREP integration readiness + +Architecture, security/auth, product/operations, QA, senior engineering, CI +integrity, reuse/dedup, test-delta, and docs reviewers examined the inert typed +contract manifest. Valid findings split concealed no-work results from queue +lineage, made initial and revised decisions mutually exclusive, bound exact +revision predecessor/response facts, required lifecycle adjacency proof, and +closed lease, preference, and revision-preparation state vocabularies. + +The manifest covers all 23 `review.*` actions while every row remains planned. +The two future evidence-ingest actions remain unsupported, XINT-002 actions are +reference-only, and no evaluator, route, background execution code, migration, or REV lifecycle +behavior is added. Fifteen contract tests and three existing PREP regression +tests pass; changed-module coverage is 100.00 percent. CodeRabbit's four valid +findings corrected operator-read documentation, guide-repair fact wording, +the true-only no-self-review proof, fixed-service execution-mode closure, and +annotation-based inertness proof. Corrective exact-head CI remains required. diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/STATUS.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/STATUS.md index 08b01e250..4d177e5fe 100644 --- a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/STATUS.md +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/STATUS.md @@ -2,9 +2,10 @@ ## Current status -WS-XINT-003-01, WS-XINT-003-02A, and WS-XINT-003-02B are merged. PR #248 merged -02B as `25fc27c4` on 2026-08-03 after Backend, Agent Gates, and CodeRabbit passed -on the final PR head. Exactly `project.review_policy.update` and +WS-XINT-003-01, WS-XINT-003-02A, WS-XINT-003-02B, and WS-XINT-003-02C are +merged. PR #255 merged 02C as `745d9c3f` on 2026-08-03 after Backend, Agent +Gates, and CodeRabbit passed on the final PR head. Exactly +`project.review_policy.update` and `project.revision_policy.update` are active; review/revision lifecycle actions remain planned or unavailable. @@ -29,8 +30,9 @@ REV-owned semantics with AUTH-owned mutation authorization. materialization. Evidence binding remains planned/unavailable and 07B is reserved pending separate REV-owned intent. - All 23 registered review/revision lifecycle actions remain planned; 02C adds - the four recovery/lifecycle rows and six fixed-service identities without - provisioning or authority. The superseded 08R placeholder is never executable. The + the four recovery/lifecycle rows and provisions/admits six fixed-service + identities without execution authority because every associated lifecycle + action remains unavailable. The superseded 08R placeholder is never executable. The two policy mutation actions activated by 02B are setup authority, not lifecycle activation. @@ -49,9 +51,10 @@ REV-owned semantics with AUTH-owned mutation authorization. ## Next step -Complete WS-XINT-003-02C evidence and review, then merge it before 02D. 02C and -02D front-load the complete unavailable catalogue, fixed-service matrix, and -fail-closed PREP contracts that REV needs before it begins its full lifecycle -implementation. Do not infer review lifecycle +WS-XINT-003-02D implements the complete typed, fail-closed PREP/read contract +handoff on top of merged 02C. Its merge permits REV to begin hidden lifecycle +implementation against that stable surface. AUTH then returns to its independent +agenda; later XINT activation waves reconnect only to exact merged REV behavior. +Do not infer review lifecycle activation from readiness: later action activation still requires exact merged REV behavior and integrated proof. diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/chunks/WS-XINT-003-02D-auth-prep-integration-readiness.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/chunks/WS-XINT-003-02D-auth-prep-integration-readiness.md index 0017ba777..00dd4dfcf 100644 --- a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/chunks/WS-XINT-003-02D-auth-prep-integration-readiness.md +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/chunks/WS-XINT-003-02D-auth-prep-integration-readiness.md @@ -2,8 +2,7 @@ ## Status -Proposed planning contract after 02C. Refresh exact files and verification -commands from current `main` before implementation. +Implementation-ready contract refreshed from merged 02C at `745d9c3f`. ## Parent initiative @@ -29,17 +28,21 @@ SLA. ## Allowed files -Refresh to exact current-main paths within: +Only these exact paths may change: ```text -backend/app/modules/authorization/runtime.py -backend/app/modules/authorization/prepared.py -backend/app/modules/authorization/kernel.py -backend/app/modules/authorization/ -backend/tests/ +backend/app/modules/authorization/review_contracts.py +backend/scripts/run_test_lanes.py +backend/tests/test_review_authorization_contracts.py docs/spec_authorization_service.md docs/spec_review_lifecycle.md -.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/** +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/ACTION_CUSTODY.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/STATUS.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/REVIEW_LOG.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/chunks/WS-XINT-003-02D-auth-prep-integration-readiness.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-internal-review.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-pr-trust-bundle.md +.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-external-review-response.md ``` ## Not allowed changes @@ -51,6 +54,68 @@ docs/spec_review_lifecycle.md serialized prepared handle. - No omnibus nullable resource context, generic dictionary/service locator, local REV policy engine, fallback authority, or role-only shortcut. +- No edit to `runtime.py`, `prepared.py`, `kernel.py`, `catalogue.py`, service + identity registration, migrations, routes, background execution code, or REV + product code. +- No XINT-002 contract implementation. Shared ART/submission rows are manifest + references only and retain their existing owners, types, and activation gates. + +## Closed contract families + +`review_contracts.py` publishes strict frozen Pydantic models containing only +scalar identifiers, closed enums, digests, bounded reasons/timestamps and +server-composed booleans/counts. It defines no repository, loader, evaluator, +callback, ORM value, byte-bearing field, or authorization handle. The manifest +maps every XINT-003 action below to exactly one family and execution mode. + +| Contract family | Exact XINT-003 actions | Required final facts | +|---|---|---| +| concealed queue | `review.queue.read` | a minimal `none` shape contains project, reviewer/grant, policy, phase and queue-state digest only; offer/active-lease shapes additionally bind queue entry/lease when present, task, assignment, Submission, CheckerRun admission, no-self-review actors and exact lineage | +| claim | `review.claim` | concealed-queue facts plus claim operation, idempotency, queue generation, reviewer global active-lease count, reviewer contribution-policy identity/generation/digest, packet-manifest digest | +| lease mutation | `review.release`, `review.lease_expiry.run`, `review.lease.force_release` | project, queue entry, lease/generation, reviewer, task, Submission, lease status/expiry, lifecycle phase, reason or due boundary, lease-state digest | +| preference mutation | `review.decline_preference`, `review.preference_expiry.run` | project, queue entry, preference/generation, preferred reviewer, source Review, source Submission, status/expiry, reason or due boundary, preference-state digest | +| reviewer reads | `review.context.read`, `review.chain.read` | project, task, assignment, exact active lease/reviewer, packet manifest, current Submission and binding, chain boundary/digest, lifecycle phase; chain read also binds requested subject actor and bounded cursor | +| decision | `review.decision` | mutually exclusive `initial` and `revision` shapes both bind project, task, assignment, current Submission, CheckerRun admission, queue entry, active lease/reviewer, packet manifest, Review operation, decision, findings/resolution digest, ReviewPolicy and reviewer ContributionPolicy freezes, artifact hash and lifecycle digest; the revision shape additionally requires a distinct predecessor Review/Submission, revision episode, exact preparation head generation/digest, and finding-response lineage/count | +| operator queue read | `review.queue.inspect` | bounded project/shard, filter digest, bounded cursor, redaction state, lifecycle phase/digest | +| operator queue mutation | `review.queue.routing.override`, `review.queue.routing.correct`, `review.queue.close` | project, queue entry/generation, task, Submission, current routing/lease facts, requested mode, canonical reason, lifecycle phase/digest | +| reconciliation | `review.reconcile.run` | fixed execution mode, project/shard, trigger, bounded cursor, finding IDs digest, observed watermark/time, lifecycle phase/digest | +| artifact-reference reconciliation | `review.artifact_reference.reconcile` | project/shard, exact review/reference set digest, bounded cursor, reason, observed watermark, lifecycle phase/digest | +| projection rebuild | `review.projection.rebuild` | named projection, project/shard, source watermark, bounded cursor, source-event digest, lifecycle phase/digest | +| revision repair | `review.revision_context.repair` | project, task, source/current assignment, prior Submission, originating needs-revision Review, episode, exact head ID/digest/generation, canonical `kept`/`rebased`/`blocked` outcome and forward/backward direction, server-proven repairability, current guide ID/activation sequence and ReviewPolicy/RevisionPolicy identity triples, replacement assignment when any, canonical reason, lifecycle digest | +| revision obligation close | `review.revision_obligation.close` | project, task, assignment, originating needs-revision Review, episode/head, frozen revision-policy identity/generation/digest, approved limit/deadline facts, exact reached cause, lifecycle digest | +| legacy close | `review.revision_context.legacy_close` | reconciliation finding, project, task, assignment, optional queue, absence-of-recoverable-root proof digest, CheckerRun-remediation exclusion, canonical reason, lifecycle digest | +| lifecycle activation | `review.lifecycle.activation.manage` | singleton, operation, expected generation/current phase, adjacent target phase, reviewed manifest digest, drain observations digest, bounded batch/deadline, canonical reason | + +The two future evidence-upload actions are manifest entries with execution mode +`unsupported_future_intent`, no resource model, and no prepare/consume support: +`review.finding_evidence.ingest` and +`review.finding_response_evidence.ingest`. + +Every shared contract family carries its exact closed `action_id`; family-local +mode/reason fields are action-specific and validated so sibling actions cannot +substitute for one another. Every fixed-service model also carries the exact +`service_identity` and a closed server-derived `execution_mode`. In particular, +the two identities admitted for `review.reconcile.run` have disjoint modes and +tests must reject either identity using the other's mode. + +Externally owned `artifact.review_packet.materialize`, +`artifact.review_evidence.binding.create`, `artifact.submission_bundle.prepare`, +and `submission.create` appear only in a closed external-handoff reference map. +02D must not define replacement contexts or adapters for them. + +The already-active `project.review_policy.update` and +`project.revision_policy.update` rows are closed references to their existing +`ProjectReviewPolicyMutationResourceContext` and +`ProjectRevisionPolicyMutationResourceContext`; 02D neither replaces nor +changes those proven 02B contracts. + +Every mutation/service contract includes exact operation/idempotency/request +binding through the existing `PreparedAuthorizationInput` and opaque +`PreparedAuthorizationHandle`; those protocol values are deliberately not +fields of the resource models. Read contracts use request-scoped evaluation, +not PREP consumption. Later activation adapters must revalidate actor and exact +identity link plus matched grant, or the exact fixed-service identity, before +evaluating these final server-composed facts. ## Acceptance criteria @@ -71,9 +136,11 @@ docs/spec_review_lifecycle.md single-use. - Unavailable actions fail closed at prepare and consume. Publishing a contract does not grant runtime authority. -- Contract tests prove copied, reconstructed, serialized, replayed, - wrong-session, wrong-transaction, wrong-action, wrong-principal, - cross-project/resource, stale-digest, revoked, and unavailable denial. +- Existing PREP regression tests continue to prove copied, reconstructed, + serialized, replayed, wrong-session, wrong-transaction, wrong-action, + wrong-principal, revoked, and unavailable denial. New contract tests prove + strict construction, action/mode/identity parity, cross-resource and stale- + digest distinction, handle exclusion, and serialization-safe scalar shapes. - Static scans prove Celery payloads cannot carry handles and AUTH does not import REV product repositories or implement lifecycle rules. - The interface includes enough exact fields for REV to implement every later @@ -82,10 +149,23 @@ docs/spec_review_lifecycle.md ## Verification commands -Refresh exact paths at implementation start, then include Ruff, focused PREP, -kernel, serialization, parity and denial tests, changed-subsystem coverage at -or above 90 percent, and hosted Backend coverage preserving the repository-wide -78 percent floor. +```bash +cd backend +ruff check app/modules/authorization/review_contracts.py tests/test_review_authorization_contracts.py +mypy app/modules/authorization/review_contracts.py +.venv/bin/pytest -q tests/test_review_authorization_contracts.py +.venv/bin/coverage erase +.venv/bin/coverage run -m pytest -q tests/test_review_authorization_contracts.py +.venv/bin/coverage report --include='app/modules/authorization/review_contracts.py' --precision=2 --fail-under=90 +cd .. +git diff --check +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_review_contracts.py +``` + +The PR exact head must also pass GitHub `Backend` including all five PostgreSQL +lanes and the aggregate repository-wide `coverage --fail-under=78`, plus Agent +Gates. No local full-suite execution is required on the user machine. ## Required reviewers diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-external-review-response.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-external-review-response.md new file mode 100644 index 000000000..842faf3b5 --- /dev/null +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-external-review-response.md @@ -0,0 +1,48 @@ +# External Review Response: WS-XINT-003-02D + +## Current status + +CodeRabbit completed its review with four valid in-scope findings. This record +captures their resolution and the exact-head checks. Internal review does not +substitute for required human approval. + +The first PR head failed Agent Gates and the shared backend pre-test gate because +two new planning records used the ambiguous human/product term `worker` for +background execution code. The wording is corrected without changing a test, +workflow, or threshold, and the exact local stale-authorization scan passes. + +The next hosted Backend run reached the unchanged docstring gate and reported +that the new contract module documented only 26 of its 54 class/callable +surfaces, reducing repository docstring coverage to 79.9 percent. Every missing +new contract class now has a specific docstring. The unchanged local gate passes +at 80.9 percent; no unrelated file, configuration, or threshold changed. + +The following run reached the canonical semantic-lane inventory and correctly +rejected the new test module because it had not yet been assigned to a lane. +`test_review_authorization_contracts.py` is now assigned exactly once to +`shared_foundations`. The canonical collect-only runner and its focused CI +contract tests pass locally; no lane validation or evidence rule was weakened. + +CodeRabbit then found four contract-quality mismatches. The operator queue +documentation now separates the bounded `REQUEST_READ` inspection shape from +the three `PREPARED_OPERATOR` mutations, and revision-repair documentation now +states the implemented guide ID/activation-sequence facts. `no_self_review` is +now a true-only server proof with a distinct validation error from actor +identity equality. Every fixed-service `execution_mode` uses one importable +closed enum. Finally, inertness tests recursively admit only scalar, enum, +literal, or optional annotations, rejecting prepared handles, byte-bearing +types, callbacks, and unbounded containers by type rather than field spelling. + +Comments addressed: four. + +Comments deferred: none. + +Human decisions needed: normal approval of PR #257 only. + +Commands rerun: Ruff, focused mypy, focused contract/PREP tests, changed-module +coverage, docstring coverage, semantic-lane inventory, stale wording scans, +Markdown links, and diff whitespace checks. Hosted exact-head checks must pass +again after the corrective commit. + +Remaining risks: the contracts remain intentionally inert; later REV-owned +composition must still prove transaction-bound runtime enforcement. diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-internal-review.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-internal-review.md new file mode 100644 index 000000000..ea2d5004a --- /dev/null +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-internal-review.md @@ -0,0 +1,44 @@ +# Internal Review: WS-XINT-003-02D + +## Scope + +Final review of the inert typed REV authorization contract manifest and its +AUTH/REV handoff documentation. No evaluator, lifecycle behavior, or action +activation is included. + +## Results + +- Architecture: PASS; the manifest is inert and imports no REV or XINT-002 implementation. +- Security/auth: PASS after the concealed `none` queue shape was separated from + offer/lease lineage and lifecycle adjacency received an explicit server proof. +- Product/operations: PASS after revised decisions gained a distinct exact + predecessor, preparation-head, and finding-response lineage shape. +- QA: PASS WITH LOW RISKS after initial and revision decision shapes became + mutually exclusive and self-predecessor revisions were rejected. +- Senior engineering: PASS WITH LOW RISKS after lease/preference states and + revision preparation outcome/direction became closed canonical enums. +- CI integrity: PASS WITH LOW RISKS; no workflow, threshold, dependency, skip, + or package-script change exists. +- Reuse/dedup: PASS WITH LOW RISKS; existing PREP/runtime contracts are + referenced rather than forked, with parity tested against the runtime map. +- Test delta: PASS WITH LOW RISKS; tests are additive and union/manifest parity + is now locked. +- Docs: PASS after status and historical action-count wording were corrected. + +No blocking finding remains. All reviewer sessions completed. + +## Deterministic evidence + +- Ruff and focused mypy pass. +- Repository docstring coverage passes at 80.9 percent after every new contract + class was documented. +- 15 focused contract tests pass. +- The canonical semantic-lane collect-only runner passes; the new module is + assigned exactly once to `shared_foundations`. +- Three existing PREP construction, forgery, copy, serialization, nested-root, + and planned-action denial regression tests pass. +- `review_contracts.py` coverage is 100.00 percent. +- Markdown links, stale review contracts, and diff whitespace checks pass. + +The exact PR head still requires GitHub Backend, Agent Gates, and external +CodeRabbit review before merge readiness. diff --git a/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-pr-trust-bundle.md b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-pr-trust-bundle.md new file mode 100644 index 000000000..ce89e725a --- /dev/null +++ b/.agent-loop/initiatives/WS-XINT-003-rev-auth-end-to-end/reviews/WS-XINT-003-02D-pr-trust-bundle.md @@ -0,0 +1,54 @@ +# PR Trust Bundle: WS-XINT-003-02D + +## Intent and scope + +Publish the complete inert typed authorization surface REV can implement +against without returning to AUTH for a new action, principal, protocol, or +resource-context family. Every lifecycle action remains planned/unavailable. + +## Design and safety + +- A closed manifest covers all 23 registered `review.*` actions. +- Twenty-one approved action rows map to strict frozen scalar resource models; + the two future evidence-ingest rows are explicitly unsupported with no model. +- Concealed current-work `none` results cannot carry queue, task, Submission, + CheckerRun, contributor, or lease lineage. +- Initial and revision decisions are mutually exclusive. Revision decisions + bind a distinct predecessor Submission, predecessor Review, revision episode, + exact preparation head, and response-lineage digest/count. +- Fixed-service models bind exact service identity and server-derived mode; the + two reconciliation services cannot exchange modes, and all fixed-service + modes share one closed importable enum. +- Queue lineage requires a true-only server proof of no self-review and a + separately validated reviewer/contributor identity distinction. +- Existing 02B policy contexts and XINT-002 actions are references only. + +## Exclusions + +No PREP/kernel/runtime evaluator, catalogue, service registry, migration, +route, background execution code, REV repository/model, lifecycle mutation, provider I/O, or action +availability changes. + +## Evidence + +- Ruff: pass. +- Mypy: pass. +- Docstring coverage: 80.9 percent, unchanged gate. +- Contract tests: 15 passed. +- Semantic-lane inventory: pass; exact `shared_foundations` assignment. +- Existing PREP regression tests: 3 passed. +- Changed-module coverage: 100.00 percent. +- Internal architecture, security, product, QA, senior, CI, reuse, test-delta, + and docs review: pass; valid findings resolved. +- CodeRabbit: four valid comments addressed; no finding deferred. +- Markdown links, stale review contracts, and diff whitespace: pass. + +Hosted GitHub Actions must provide the five PostgreSQL/backend lanes, aggregate +repository coverage at or above 78 percent, authorization coverage at or above +90 percent, and final exact-head evidence. + +## Human review focus + +Confirm the manifest is complete enough for REV composers, that no model leaks +concealed lineage or weakens revision ancestry, and that publishing these types +does not create runtime authority. diff --git a/backend/app/modules/authorization/review_contracts.py b/backend/app/modules/authorization/review_contracts.py new file mode 100644 index 000000000..e59726caf --- /dev/null +++ b/backend/app/modules/authorization/review_contracts.py @@ -0,0 +1,703 @@ +"""Inert typed authorization contracts for the planned REV lifecycle boundary.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum +from types import MappingProxyType +from typing import Literal +from uuid import UUID + +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, field_validator, model_validator + +from app.modules.actors.service_identities import ServiceIdentity +from app.modules.authorization.catalogue import ActionId + +_STRICT_FROZEN = ConfigDict(extra="forbid", frozen=True, strict=True) +_DIGEST = r"^sha256:[0-9a-f]{64}$" + + +class ReviewContractExecution(StrEnum): + """Closed integration modes; none makes a planned action executable.""" + + REQUEST_READ = "request_read" + PREPARED_HUMAN = "prepared_human" + PREPARED_OPERATOR = "prepared_operator" + PREPARED_SERVICE = "prepared_service" + UNSUPPORTED_FUTURE_INTENT = "unsupported_future_intent" + + +class ReviewDecisionValue(StrEnum): + """Canonical persisted review decisions.""" + + ACCEPT = "accept" + NEEDS_REVISION = "needs_revision" + REJECT = "reject" + + +class ReviewLifecyclePhase(StrEnum): + """Closed phase labels carried as locked REV facts, not AUTH policy.""" + + DISABLED = "disabled" + SHADOW = "shadow" + DRAINING = "draining" + LIVE = "live" + + +class QueueSelectionMode(StrEnum): + """Concealed current-work result shape.""" + + ACTIVE_LEASE = "active_lease" + OFFER = "offer" + NONE = "none" + + +class ServiceExecutionMode(StrEnum): + """Closed server-derived modes for fixed-service review contracts.""" + + AUTHORITY_INVALIDATION = "authority_invalidation" + GENERAL = "general" + DUE_LEASE = "due_lease" + DUE_PREFERENCE = "due_preference" + ARTIFACT_REFERENCE = "artifact_reference" + PROJECTION_REBUILD = "projection_rebuild" + + +class ReviewLeaseStatus(StrEnum): + """Closed lease states exposed to later authorization adapters.""" + + ACTIVE = "active" + RELEASED = "released" + EXPIRED = "expired" + CONSUMED = "consumed" + REVOKED = "revoked" + + +class ReviewPreferenceStatus(StrEnum): + """Closed preference states exposed to later authorization adapters.""" + + ACTIVE = "active" + DECLINED = "declined" + EXPIRED = "expired" + CONSUMED = "consumed" + REVOKED = "revoked" + + +class RevisionPreparationOutcome(StrEnum): + """Canonical immutable revision-context outcomes.""" + + KEPT = "kept" + REBASED = "rebased" + BLOCKED = "blocked" + + +class RevisionPreparationDirection(StrEnum): + """Canonical direction for a rebased revision context.""" + + FORWARD = "forward" + BACKWARD = "backward" + + +class RevisionClosureCause(StrEnum): + """Only approved reasons for closing an exhausted revision obligation.""" + + LIMIT_REACHED = "revision_limit_reached" + DEADLINE_EXPIRED = "revision_deadline_expired" + + +class RoutingMode(StrEnum): + """Action-specific operator routing modes.""" + + OVERRIDE = "override" + CORRECT = "correct" + CLOSE = "close" + + +class _ReviewContract(BaseModel): + """Facts common to every inert REV authorization resource contract.""" + + model_config = _STRICT_FROZEN + + action_id: ActionId + lifecycle_phase: ReviewLifecyclePhase + lifecycle_digest: str = Field(pattern=_DIGEST) + + +class _ProjectContract(_ReviewContract): + """Common exact-project scope for REV authorization resource contracts.""" + + project_id: UUID + + +class _QueueLineage(_ProjectContract): + """Canonical queue, work, actor, grant, and policy lineage.""" + + queue_entry_id: UUID + queue_generation: int = Field(ge=1) + task_id: UUID + task_assignment_id: UUID + submission_id: UUID + checker_run_id: UUID + reviewer_actor_profile_id: UUID + contributor_actor_profile_id: UUID + reviewer_grant_id: UUID + review_policy_id: UUID + review_policy_generation: int = Field(ge=1) + review_policy_digest: str = Field(pattern=_DIGEST) + queue_state_digest: str = Field(pattern=_DIGEST) + no_self_review: Literal[True] + + @field_validator("no_self_review", mode="before") + @classmethod + def require_no_self_review_proof(cls, value): + """Require REV's explicit server-owned no-self-review proof.""" + if value is not True: + raise ValueError("no-self-review proof must be true") + return value + + @model_validator(mode="after") + def require_distinct_reviewer(self): + """Reject a self-review context even before a later evaluator exists.""" + if self.reviewer_actor_profile_id == self.contributor_actor_profile_id: + raise ValueError("reviewer and contributor must be distinct") + return self + + +class ReviewQueueReadContract(_QueueLineage): + """Concealed reviewer current-work facts.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_READ] + selection_mode: Literal[QueueSelectionMode.ACTIVE_LEASE, QueueSelectionMode.OFFER] + review_lease_id: UUID | None = None + + @model_validator(mode="after") + def bind_selection(self): + """An active-lease result alone carries a lease selector.""" + if (self.selection_mode is QueueSelectionMode.ACTIVE_LEASE) != ( + self.review_lease_id is not None + ): + raise ValueError("current-work selection and lease are inconsistent") + return self + + +class ReviewQueueNoneContract(_ProjectContract): + """Minimal concealed result when the reviewer has no current work.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_READ] + selection_mode: Literal[QueueSelectionMode.NONE] + reviewer_actor_profile_id: UUID + reviewer_grant_id: UUID + review_policy_id: UUID + review_policy_generation: int = Field(ge=1) + review_policy_digest: str = Field(pattern=_DIGEST) + queue_state_digest: str = Field(pattern=_DIGEST) + + +class ReviewClaimContract(_QueueLineage): + """Final claim facts after REV locks its canonical lineage.""" + + action_id: Literal[ActionId.REVIEW_CLAIM] + claim_operation_id: UUID + active_reviewer_lease_count: int = Field(ge=0, le=1) + reviewer_contribution_policy_id: UUID + reviewer_contribution_policy_generation: int = Field(ge=1) + reviewer_contribution_policy_digest: str = Field(pattern=_DIGEST) + packet_manifest_id: UUID + packet_manifest_digest: str = Field(pattern=_DIGEST) + + +class _LeaseContract(_ProjectContract): + """Canonical final facts shared by exact lease transitions.""" + + queue_entry_id: UUID + review_lease_id: UUID + lease_generation: int = Field(ge=1) + reviewer_actor_profile_id: UUID + task_id: UUID + submission_id: UUID + lease_status: ReviewLeaseStatus + expires_at: AwareDatetime + lease_state_digest: str = Field(pattern=_DIGEST) + + +class ReviewReleaseContract(_LeaseContract): + """Owning-reviewer release facts for one exact active lease.""" + + action_id: Literal[ActionId.REVIEW_RELEASE] + reason: str = Field(min_length=1, max_length=512) + + +class ReviewLeaseExpiryContract(_LeaseContract): + """Fixed-service expiry facts for one exact due lease.""" + + action_id: Literal[ActionId.REVIEW_LEASE_EXPIRY_RUN] + service_identity: Literal[ServiceIdentity.REVIEW_LEASE_EXPIRY] + execution_mode: Literal[ServiceExecutionMode.DUE_LEASE] + due_boundary: AwareDatetime + claimed_ids_digest: str = Field(pattern=_DIGEST) + cursor: str | None = Field(default=None, max_length=512) + + +class ReviewLeaseForceReleaseContract(_LeaseContract): + """Reason-bound Operator force-release facts for one exact lease.""" + + action_id: Literal[ActionId.REVIEW_LEASE_FORCE_RELEASE] + reason: str = Field(min_length=1, max_length=512) + + +class _PreferenceContract(_ProjectContract): + """Canonical final facts shared by exact reviewer preferences.""" + + queue_entry_id: UUID + preference_id: UUID + preference_generation: int = Field(ge=1) + preferred_reviewer_actor_profile_id: UUID + source_review_id: UUID + source_submission_id: UUID + preference_status: ReviewPreferenceStatus + expires_at: AwareDatetime + preference_state_digest: str = Field(pattern=_DIGEST) + + +class ReviewDeclinePreferenceContract(_PreferenceContract): + """Offered-reviewer decline facts for one exact preference.""" + + action_id: Literal[ActionId.REVIEW_DECLINE_PREFERENCE] + reason: str = Field(min_length=1, max_length=512) + + +class ReviewPreferenceExpiryContract(_PreferenceContract): + """Fixed-service expiry facts for one exact due preference.""" + + action_id: Literal[ActionId.REVIEW_PREFERENCE_EXPIRY_RUN] + service_identity: Literal[ServiceIdentity.REVIEW_PREFERENCE_EXPIRY] + execution_mode: Literal[ServiceExecutionMode.DUE_PREFERENCE] + due_boundary: AwareDatetime + claimed_ids_digest: str = Field(pattern=_DIGEST) + cursor: str | None = Field(default=None, max_length=512) + + +class _ReviewerPacketContract(_ProjectContract): + """Exact active-lease and immutable packet lineage for reviewer reads.""" + + task_id: UUID + task_assignment_id: UUID + submission_id: UUID + checker_run_id: UUID + queue_entry_id: UUID + review_lease_id: UUID + reviewer_actor_profile_id: UUID + packet_manifest_id: UUID + packet_manifest_generation: int = Field(ge=1) + packet_manifest_digest: str = Field(pattern=_DIGEST) + artifact_binding_id: UUID + chain_digest: str = Field(pattern=_DIGEST) + + +class ReviewContextReadContract(_ReviewerPacketContract): + """Lease-bounded context-read facts for one immutable review packet.""" + + action_id: Literal[ActionId.REVIEW_CONTEXT_READ] + + +class ReviewChainReadContract(_ReviewerPacketContract): + """Metadata-only chain-read facts for one authorized subject and cursor.""" + + action_id: Literal[ActionId.REVIEW_CHAIN_READ] + requested_subject_actor_profile_id: UUID + chain_head_submission_id: UUID + cursor: str | None = Field(default=None, max_length=512) + metadata_only: Literal[True] + + +class _ReviewDecisionContract(_ReviewerPacketContract): + """Facts shared by mutually exclusive initial and revision decisions.""" + + action_id: Literal[ActionId.REVIEW_DECISION] + review_operation_id: UUID + decision: ReviewDecisionValue + finding_count: int = Field(ge=0) + blocking_finding_count: int = Field(ge=0) + findings_resolutions_digest: str = Field(pattern=_DIGEST) + review_policy_id: UUID + review_policy_generation: int = Field(ge=1) + review_policy_digest: str = Field(pattern=_DIGEST) + reviewer_contribution_policy_id: UUID + reviewer_contribution_policy_generation: int = Field(ge=1) + reviewer_contribution_policy_digest: str = Field(pattern=_DIGEST) + artifact_hash: str = Field(pattern=_DIGEST) + + @model_validator(mode="after") + def require_decision_shape(self): + """Keep the approved needs-revision blocking-finding invariant explicit.""" + if self.blocking_finding_count > self.finding_count: + raise ValueError("blocking finding count exceeds finding count") + if self.decision is ReviewDecisionValue.NEEDS_REVISION and self.blocking_finding_count < 1: + raise ValueError("needs_revision requires a blocking finding") + return self + + +class ReviewDecisionContract(_ReviewDecisionContract): + """Exact immutable lineage presented for an initial review decision.""" + + decision_shape: Literal["initial"] + predecessor_review_id: Literal[None] = None + + +class ReviewRevisionDecisionContract(_ReviewDecisionContract): + """Decision facts for a revised Submission with exact response lineage.""" + + decision_shape: Literal["revision"] + predecessor_review_id: UUID + predecessor_submission_id: UUID + revision_episode_id: UUID + preparation_head_id: UUID + preparation_head_generation: int = Field(ge=1) + preparation_head_digest: str = Field(pattern=_DIGEST) + finding_response_count: int = Field(ge=0) + finding_response_lineage_digest: str = Field(pattern=_DIGEST) + + @model_validator(mode="after") + def require_distinct_predecessor(self): + """Reject a Submission that claims itself as its own predecessor.""" + if self.predecessor_submission_id == self.submission_id: + raise ValueError("revision predecessor must differ from current submission") + return self + + +class ReviewQueueInspectContract(_ProjectContract): + """Bounded redacted Operator queue-inspection facts.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_INSPECT] + shard: str = Field(min_length=1, max_length=128) + filter_digest: str = Field(pattern=_DIGEST) + cursor: str | None = Field(default=None, max_length=512) + redacted: Literal[True] + + +class _QueueOperatorContract(_ProjectContract): + """Canonical queue lineage shared by reason-bound Operator mutations.""" + + queue_entry_id: UUID + queue_generation: int = Field(ge=1) + task_id: UUID + submission_id: UUID + current_routing_digest: str = Field(pattern=_DIGEST) + current_lease_id: UUID | None = None + reason: str = Field(min_length=1, max_length=512) + + +class ReviewQueueRoutingOverrideContract(_QueueOperatorContract): + """Operator override facts for one exact queue routing generation.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_ROUTING_OVERRIDE] + routing_mode: Literal[RoutingMode.OVERRIDE] + requested_reviewer_actor_profile_id: UUID + + +class ReviewQueueRoutingCorrectContract(_QueueOperatorContract): + """Operator correction facts for one exact invalid routing state.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_ROUTING_CORRECT] + routing_mode: Literal[RoutingMode.CORRECT] + corrected_routing_digest: str = Field(pattern=_DIGEST) + + +class ReviewQueueCloseContract(_QueueOperatorContract): + """Operator closure facts for one exact stale queue entry.""" + + action_id: Literal[ActionId.REVIEW_QUEUE_CLOSE] + routing_mode: Literal[RoutingMode.CLOSE] + terminal_reason: str = Field(min_length=1, max_length=128) + + +class _ReconcileContract(_ProjectContract): + """Bounded shard, trigger, finding, time, and cursor reconciliation facts.""" + + action_id: Literal[ActionId.REVIEW_RECONCILE_RUN] + shard: str = Field(min_length=1, max_length=128) + trigger: str = Field(min_length=1, max_length=128) + finding_ids_digest: str = Field(pattern=_DIGEST) + observed_at: AwareDatetime + watermark: str = Field(min_length=1, max_length=512) + cursor: str | None = Field(default=None, max_length=512) + + +class ReviewAuthorityInvalidationReconcileContract(_ReconcileContract): + """Authority-invalidation mode bound to its exact fixed service.""" + + service_identity: Literal[ServiceIdentity.REVIEW_AUTHORITY_INVALIDATION_RECONCILIATION] + execution_mode: Literal[ServiceExecutionMode.AUTHORITY_INVALIDATION] + + +class ReviewGeneralReconcileContract(_ReconcileContract): + """General reconciliation mode bound to its exact fixed service.""" + + service_identity: Literal[ServiceIdentity.REVIEW_RECONCILIATION] + execution_mode: Literal[ServiceExecutionMode.GENERAL] + reason: str = Field(min_length=1, max_length=512) + + +class ReviewArtifactReferenceReconcileContract(_ProjectContract): + """Exact artifact-reference set facts for the fixed reconciler.""" + + action_id: Literal[ActionId.REVIEW_ARTIFACT_REFERENCE_RECONCILE] + service_identity: Literal[ServiceIdentity.REVIEW_ARTIFACT_REFERENCE_RECONCILIATION] + execution_mode: Literal[ServiceExecutionMode.ARTIFACT_REFERENCE] + shard: str = Field(min_length=1, max_length=128) + review_reference_set_digest: str = Field(pattern=_DIGEST) + observed_at: AwareDatetime + watermark: str = Field(min_length=1, max_length=512) + cursor: str | None = Field(default=None, max_length=512) + reason: str = Field(min_length=1, max_length=512) + + +class ReviewProjectionRebuildContract(_ProjectContract): + """Bounded source-event and watermark facts for projection rebuild.""" + + action_id: Literal[ActionId.REVIEW_PROJECTION_REBUILD] + service_identity: Literal[ServiceIdentity.REVIEW_PROJECTION] + execution_mode: Literal[ServiceExecutionMode.PROJECTION_REBUILD] + projection_name: str = Field(min_length=1, max_length=128) + shard: str = Field(min_length=1, max_length=128) + source_watermark: str = Field(min_length=1, max_length=512) + source_event_digest: str = Field(pattern=_DIGEST) + cursor: str | None = Field(default=None, max_length=512) + + +class _RevisionEpisodeContract(_ProjectContract): + """Exact Review-rooted preparation episode and current-head lineage.""" + + task_id: UUID + task_assignment_id: UUID + source_task_assignment_id: UUID + prior_submission_id: UUID + needs_revision_review_id: UUID + revision_episode_id: UUID + preparation_head_id: UUID + preparation_head_generation: int = Field(ge=1) + preparation_head_digest: str = Field(pattern=_DIGEST) + + +class ReviewRevisionContextRepairContract(_RevisionEpisodeContract): + """Covered-project repair facts for one exact repairable preparation head.""" + + action_id: Literal[ActionId.REVIEW_REVISION_CONTEXT_REPAIR] + preparation_head_outcome: RevisionPreparationOutcome + preparation_head_direction: RevisionPreparationDirection | None = None + preparation_head_repairable: Literal[True] + guide_id: UUID + guide_activation_sequence: int = Field(ge=1) + review_policy_id: UUID + review_policy_generation: int = Field(ge=1) + review_policy_digest: str = Field(pattern=_DIGEST) + revision_policy_id: UUID + revision_policy_generation: int = Field(ge=1) + revision_policy_digest: str = Field(pattern=_DIGEST) + replacement_task_assignment_id: UUID | None = None + reason: str = Field(min_length=1, max_length=512) + + @model_validator(mode="after") + def require_canonical_direction(self): + """Only rebased preparations carry a forward/backward direction.""" + if (self.preparation_head_outcome is RevisionPreparationOutcome.REBASED) != ( + self.preparation_head_direction is not None + ): + raise ValueError("revision preparation outcome and direction are inconsistent") + return self + + +class ReviewRevisionObligationCloseContract(_RevisionEpisodeContract): + """Covered-project closure facts for one proven exhausted obligation.""" + + action_id: Literal[ActionId.REVIEW_REVISION_OBLIGATION_CLOSE] + revision_policy_id: UUID + revision_policy_generation: int = Field(ge=1) + revision_policy_digest: str = Field(pattern=_DIGEST) + revision_round: int = Field(ge=1) + revision_limit: int | None = Field(default=None, ge=1) + revision_deadline: AwareDatetime | None = None + observed_at: AwareDatetime + reached_cause: RevisionClosureCause + + @model_validator(mode="after") + def require_reached_boundary(self): + """Bind the selected closure cause to its frozen server-owned boundary.""" + if self.reached_cause is RevisionClosureCause.LIMIT_REACHED: + if self.revision_limit is None or self.revision_round < self.revision_limit: + raise ValueError("revision limit is not reached") + elif self.revision_deadline is None or self.observed_at < self.revision_deadline: + raise ValueError("revision deadline is not reached") + return self + + +class ReviewRevisionContextLegacyCloseContract(_ProjectContract): + """Evidence-linked Operator closure facts for unrecoverable legacy context.""" + + action_id: Literal[ActionId.REVIEW_REVISION_CONTEXT_LEGACY_CLOSE] + reconciliation_finding_id: UUID + task_id: UUID + task_assignment_id: UUID + queue_entry_id: UUID | None = None + recoverable_root_absence_digest: str = Field(pattern=_DIGEST) + checker_remediation_excluded: Literal[True] + reason: Literal["legacy_revision_context_unrecoverable"] + + +class ReviewLifecycleActivationContract(_ReviewContract): + """Generation-bound adjacent lifecycle-control transition facts.""" + + action_id: Literal[ActionId.REVIEW_LIFECYCLE_ACTIVATION_MANAGE] + singleton_id: UUID + operation_id: UUID + expected_generation: int = Field(ge=1) + current_phase: ReviewLifecyclePhase + target_phase: ReviewLifecyclePhase + adjacent_transition_confirmed: Literal[True] + reviewed_manifest_digest: str = Field(pattern=_DIGEST) + drain_observations_digest: str = Field(pattern=_DIGEST) + batch_limit: int = Field(ge=1, le=10_000) + deadline: AwareDatetime + reason: str = Field(min_length=1, max_length=512) + + @model_validator(mode="after") + def require_phase_change(self): + """Reject same-phase requests; legal adjacency remains REV-owned.""" + if self.current_phase is self.target_phase: + raise ValueError("lifecycle activation must change phase") + return self + + +ReviewAuthorizationResourceContract = ( + ReviewQueueReadContract + | ReviewQueueNoneContract + | ReviewClaimContract + | ReviewReleaseContract + | ReviewLeaseExpiryContract + | ReviewLeaseForceReleaseContract + | ReviewDeclinePreferenceContract + | ReviewPreferenceExpiryContract + | ReviewContextReadContract + | ReviewChainReadContract + | ReviewDecisionContract + | ReviewRevisionDecisionContract + | ReviewQueueInspectContract + | ReviewQueueRoutingOverrideContract + | ReviewQueueRoutingCorrectContract + | ReviewQueueCloseContract + | ReviewAuthorityInvalidationReconcileContract + | ReviewGeneralReconcileContract + | ReviewArtifactReferenceReconcileContract + | ReviewProjectionRebuildContract + | ReviewRevisionContextRepairContract + | ReviewRevisionObligationCloseContract + | ReviewRevisionContextLegacyCloseContract + | ReviewLifecycleActivationContract +) + + +@dataclass(frozen=True, slots=True) +class ReviewAuthorizationContractSpec: + """One closed manifest row; it is metadata, not runtime authority.""" + + execution: ReviewContractExecution + resource_models: tuple[type[BaseModel], ...] + + +REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION = MappingProxyType( + { + ActionId.REVIEW_QUEUE_READ: ReviewAuthorizationContractSpec( + ReviewContractExecution.REQUEST_READ, + (ReviewQueueReadContract, ReviewQueueNoneContract), + ), + ActionId.REVIEW_CLAIM: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, (ReviewClaimContract,) + ), + ActionId.REVIEW_RELEASE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, (ReviewReleaseContract,) + ), + ActionId.REVIEW_DECLINE_PREFERENCE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, (ReviewDeclinePreferenceContract,) + ), + ActionId.REVIEW_PREFERENCE_EXPIRY_RUN: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_SERVICE, (ReviewPreferenceExpiryContract,) + ), + ActionId.REVIEW_LEASE_EXPIRY_RUN: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_SERVICE, (ReviewLeaseExpiryContract,) + ), + ActionId.REVIEW_CONTEXT_READ: ReviewAuthorizationContractSpec( + ReviewContractExecution.REQUEST_READ, (ReviewContextReadContract,) + ), + ActionId.REVIEW_CHAIN_READ: ReviewAuthorizationContractSpec( + ReviewContractExecution.REQUEST_READ, (ReviewChainReadContract,) + ), + ActionId.REVIEW_FINDING_EVIDENCE_INGEST: ReviewAuthorizationContractSpec( + ReviewContractExecution.UNSUPPORTED_FUTURE_INTENT, () + ), + ActionId.REVIEW_DECISION: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, + (ReviewDecisionContract, ReviewRevisionDecisionContract), + ), + ActionId.REVIEW_FINDING_RESPONSE_EVIDENCE_INGEST: ReviewAuthorizationContractSpec( + ReviewContractExecution.UNSUPPORTED_FUTURE_INTENT, () + ), + ActionId.REVIEW_QUEUE_INSPECT: ReviewAuthorizationContractSpec( + ReviewContractExecution.REQUEST_READ, (ReviewQueueInspectContract,) + ), + ActionId.REVIEW_LEASE_FORCE_RELEASE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, (ReviewLeaseForceReleaseContract,) + ), + ActionId.REVIEW_QUEUE_ROUTING_OVERRIDE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, (ReviewQueueRoutingOverrideContract,) + ), + ActionId.REVIEW_QUEUE_ROUTING_CORRECT: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, (ReviewQueueRoutingCorrectContract,) + ), + ActionId.REVIEW_QUEUE_CLOSE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, (ReviewQueueCloseContract,) + ), + ActionId.REVIEW_RECONCILE_RUN: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_SERVICE, + (ReviewAuthorityInvalidationReconcileContract, ReviewGeneralReconcileContract), + ), + ActionId.REVIEW_ARTIFACT_REFERENCE_RECONCILE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_SERVICE, + (ReviewArtifactReferenceReconcileContract,), + ), + ActionId.REVIEW_PROJECTION_REBUILD: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_SERVICE, (ReviewProjectionRebuildContract,) + ), + ActionId.REVIEW_REVISION_CONTEXT_REPAIR: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, (ReviewRevisionContextRepairContract,) + ), + ActionId.REVIEW_REVISION_OBLIGATION_CLOSE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_HUMAN, + (ReviewRevisionObligationCloseContract,), + ), + ActionId.REVIEW_REVISION_CONTEXT_LEGACY_CLOSE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, + (ReviewRevisionContextLegacyCloseContract,), + ), + ActionId.REVIEW_LIFECYCLE_ACTIVATION_MANAGE: ReviewAuthorizationContractSpec( + ReviewContractExecution.PREPARED_OPERATOR, (ReviewLifecycleActivationContract,) + ), + } +) + + +EXTERNAL_REVIEW_AUTHORIZATION_HANDOFFS = MappingProxyType( + { + ActionId.ARTIFACT_REVIEW_PACKET_MATERIALIZE: "WS-XINT-002-07A", + ActionId.ARTIFACT_REVIEW_EVIDENCE_BINDING_CREATE: "future REV-owned intent", + ActionId.ARTIFACT_SUBMISSION_BUNDLE_PREPARE: "WS-XINT-002-05D", + ActionId.SUBMISSION_CREATE: "WS-XINT-002-05D", + } +) + + +EXISTING_REVIEW_SETUP_CONTRACTS = MappingProxyType( + { + ActionId.PROJECT_REVIEW_POLICY_UPDATE: "ProjectReviewPolicyMutationResourceContext", + ActionId.PROJECT_REVISION_POLICY_UPDATE: "ProjectRevisionPolicyMutationResourceContext", + } +) diff --git a/backend/scripts/run_test_lanes.py b/backend/scripts/run_test_lanes.py index e949196e0..c34dc4fa3 100644 --- a/backend/scripts/run_test_lanes.py +++ b/backend/scripts/run_test_lanes.py @@ -131,6 +131,7 @@ class TestLane: "tests/test_outbox.py", "tests/test_policy_identity_lineage.py", "tests/test_project_policy_mutations.py", + "tests/test_review_authorization_contracts.py", ), ), TestLane( diff --git a/backend/tests/test_review_authorization_contracts.py b/backend/tests/test_review_authorization_contracts.py new file mode 100644 index 000000000..26075f224 --- /dev/null +++ b/backend/tests/test_review_authorization_contracts.py @@ -0,0 +1,547 @@ +"""Closed-contract proof for the inert REV authorization integration surface.""" + +from __future__ import annotations + +from datetime import UTC, datetime +from enum import Enum +from pathlib import Path +from types import NoneType, UnionType +from typing import Literal, Union, get_args, get_origin +from uuid import UUID, uuid4 + +import pytest +from pydantic import AwareDatetime, ValidationError + +from app.modules.actors.service_identities import ServiceIdentity +from app.modules.authorization.catalogue import ACTION_BY_ID, ActionAvailability, ActionId +from app.modules.authorization.prepared import PreparedAuthorizationHandle +from app.modules.authorization.runtime import PROJECT_MUTATION_RESOURCE_BY_ACTION +from app.modules.authorization.review_contracts import ( + EXTERNAL_REVIEW_AUTHORIZATION_HANDOFFS, + EXISTING_REVIEW_SETUP_CONTRACTS, + REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION, + QueueSelectionMode, + ReviewAuthorityInvalidationReconcileContract, + ReviewAuthorizationResourceContract, + ReviewContractExecution, + ReviewDecisionContract, + ReviewDecisionValue, + ReviewGeneralReconcileContract, + ReviewLifecycleActivationContract, + ReviewLifecyclePhase, + ReviewLeaseStatus, + ReviewPreferenceStatus, + ReviewQueueNoneContract, + ReviewQueueReadContract, + ReviewReleaseContract, + ReviewDeclinePreferenceContract, + ReviewRevisionContextRepairContract, + ReviewRevisionDecisionContract, + ReviewRevisionObligationCloseContract, + ServiceExecutionMode, + RevisionPreparationDirection, + RevisionPreparationOutcome, + RevisionClosureCause, +) + +SHA = "sha256:" + "a" * 64 +NOW = datetime(2026, 8, 3, tzinfo=UTC) + + +def _queue_values() -> dict[str, object]: + return { + "action_id": ActionId.REVIEW_QUEUE_READ, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "queue_entry_id": uuid4(), + "queue_generation": 1, + "task_id": uuid4(), + "task_assignment_id": uuid4(), + "submission_id": uuid4(), + "checker_run_id": uuid4(), + "reviewer_actor_profile_id": uuid4(), + "contributor_actor_profile_id": uuid4(), + "reviewer_grant_id": uuid4(), + "review_policy_id": uuid4(), + "review_policy_generation": 1, + "review_policy_digest": SHA, + "queue_state_digest": SHA, + "no_self_review": True, + "selection_mode": QueueSelectionMode.OFFER, + } + + +def _reconcile_values() -> dict[str, object]: + return { + "action_id": ActionId.REVIEW_RECONCILE_RUN, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "shard": "project:0", + "trigger": "grant_revoked", + "finding_ids_digest": SHA, + "observed_at": NOW, + "watermark": "42", + } + + +def _decision_values() -> dict[str, object]: + return { + "action_id": ActionId.REVIEW_DECISION, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "task_id": uuid4(), + "task_assignment_id": uuid4(), + "submission_id": uuid4(), + "checker_run_id": uuid4(), + "queue_entry_id": uuid4(), + "review_lease_id": uuid4(), + "reviewer_actor_profile_id": uuid4(), + "packet_manifest_id": uuid4(), + "packet_manifest_generation": 1, + "packet_manifest_digest": SHA, + "artifact_binding_id": uuid4(), + "chain_digest": SHA, + "review_operation_id": uuid4(), + "decision_shape": "initial", + "decision": ReviewDecisionValue.ACCEPT, + "finding_count": 0, + "blocking_finding_count": 0, + "findings_resolutions_digest": SHA, + "review_policy_id": uuid4(), + "review_policy_generation": 1, + "review_policy_digest": SHA, + "reviewer_contribution_policy_id": uuid4(), + "reviewer_contribution_policy_generation": 1, + "reviewer_contribution_policy_digest": SHA, + "artifact_hash": SHA, + } + + +def _obligation_values() -> dict[str, object]: + return { + "action_id": ActionId.REVIEW_REVISION_OBLIGATION_CLOSE, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "task_id": uuid4(), + "task_assignment_id": uuid4(), + "source_task_assignment_id": uuid4(), + "prior_submission_id": uuid4(), + "needs_revision_review_id": uuid4(), + "revision_episode_id": uuid4(), + "preparation_head_id": uuid4(), + "preparation_head_generation": 1, + "preparation_head_digest": SHA, + "revision_policy_id": uuid4(), + "revision_policy_generation": 1, + "revision_policy_digest": SHA, + "revision_round": 3, + "revision_limit": 3, + "observed_at": NOW, + "reached_cause": RevisionClosureCause.LIMIT_REACHED, + } + + +def test_manifest_exactly_covers_registered_review_actions_and_stays_unavailable(): + expected = frozenset(action for action in ActionId if action.value.startswith("review.")) + + assert frozenset(REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION) == expected + assert all( + ACTION_BY_ID[action].availability is ActionAvailability.PLANNED for action in expected + ) + assert { + action + for action, spec in REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION.items() + if spec.execution is ReviewContractExecution.UNSUPPORTED_FUTURE_INTENT + } == { + ActionId.REVIEW_FINDING_EVIDENCE_INGEST, + ActionId.REVIEW_FINDING_RESPONSE_EVIDENCE_INGEST, + } + + +def test_every_executable_manifest_model_has_one_exact_action_discriminator(): + for action, spec in REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION.items(): + if spec.execution is ReviewContractExecution.UNSUPPORTED_FUTURE_INTENT: + assert spec.resource_models == () + continue + assert spec.resource_models + for model in spec.resource_models: + assert get_args(model.model_fields["action_id"].annotation) == (action,) + assert model.model_config["extra"] == "forbid" + assert model.model_config["frozen"] is True + assert model.model_config["strict"] is True + + assert set(get_args(ReviewAuthorizationResourceContract)) == { + model + for spec in REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION.values() + for model in spec.resource_models + } + + +def test_contract_models_exclude_handles_callbacks_bytes_and_unbounded_maps(): + for spec in REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION.values(): + for model in spec.resource_models: + _assert_inert_contract_model(model) + + +def _assert_inert_contract_model(model): + """Allow only bounded scalar, enum, literal, and optional field annotations.""" + + def assert_allowed(annotation, field_name): + origin = get_origin(annotation) + if origin is Literal: + return + if origin in (Union, UnionType): + for member in get_args(annotation): + assert_allowed(member, field_name) + return + assert annotation is not PreparedAuthorizationHandle, field_name + assert annotation not in (bytes, bytearray), field_name + assert annotation in (str, int, bool, datetime, AwareDatetime, UUID, NoneType) or ( + isinstance(annotation, type) and issubclass(annotation, Enum) + ), field_name + + for name, field in model.model_fields.items(): + assert_allowed(field.annotation, name) + + +def test_external_handoffs_are_closed_references_not_review_contracts(): + assert dict(EXTERNAL_REVIEW_AUTHORIZATION_HANDOFFS) == { + ActionId.ARTIFACT_REVIEW_PACKET_MATERIALIZE: "WS-XINT-002-07A", + ActionId.ARTIFACT_REVIEW_EVIDENCE_BINDING_CREATE: "future REV-owned intent", + ActionId.ARTIFACT_SUBMISSION_BUNDLE_PREPARE: "WS-XINT-002-05D", + ActionId.SUBMISSION_CREATE: "WS-XINT-002-05D", + } + assert set(EXTERNAL_REVIEW_AUTHORIZATION_HANDOFFS).isdisjoint( + REVIEW_AUTHORIZATION_CONTRACT_BY_ACTION + ) + + +def test_already_active_policy_setup_reuses_existing_exact_runtime_contracts(): + assert set(EXISTING_REVIEW_SETUP_CONTRACTS) == { + ActionId.PROJECT_REVIEW_POLICY_UPDATE, + ActionId.PROJECT_REVISION_POLICY_UPDATE, + } + assert set(EXISTING_REVIEW_SETUP_CONTRACTS.values()) == { + "ProjectReviewPolicyMutationResourceContext", + "ProjectRevisionPolicyMutationResourceContext", + } + assert { + action: PROJECT_MUTATION_RESOURCE_BY_ACTION[action].__name__ + for action in EXISTING_REVIEW_SETUP_CONTRACTS + } == dict(EXISTING_REVIEW_SETUP_CONTRACTS) + + +def test_queue_contract_rejects_self_review_wrong_action_extra_and_inconsistent_lease(): + values = _queue_values() + assert ReviewQueueReadContract.model_validate(values).model_dump(mode="json") + + with pytest.raises(ValidationError): + ReviewQueueReadContract.model_validate(values | {"unexpected": "authority"}) + with pytest.raises(ValidationError): + ReviewQueueReadContract.model_validate(values | {"action_id": ActionId.REVIEW_CLAIM}) + with pytest.raises(ValidationError): + ReviewQueueReadContract.model_validate( + values | {"contributor_actor_profile_id": values["reviewer_actor_profile_id"]} + ) + with pytest.raises(ValidationError, match="no-self-review proof must be true"): + ReviewQueueReadContract.model_validate(values | {"no_self_review": False}) + with pytest.raises(ValidationError): + ReviewQueueReadContract.model_validate( + values | {"selection_mode": QueueSelectionMode.ACTIVE_LEASE} + ) + + changed = ReviewQueueReadContract.model_validate( + values | {"queue_state_digest": "sha256:" + "b" * 64} + ) + assert changed != ReviewQueueReadContract.model_validate(values) + + +def test_queue_none_contract_is_minimal_and_rejects_lineage_disclosure(): + values = { + "action_id": ActionId.REVIEW_QUEUE_READ, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "selection_mode": QueueSelectionMode.NONE, + "reviewer_actor_profile_id": uuid4(), + "reviewer_grant_id": uuid4(), + "review_policy_id": uuid4(), + "review_policy_generation": 1, + "review_policy_digest": SHA, + "queue_state_digest": SHA, + } + result = ReviewQueueNoneContract.model_validate(values) + assert "submission_id" not in type(result).model_fields + with pytest.raises(ValidationError): + ReviewQueueNoneContract.model_validate(values | {"submission_id": uuid4()}) + with pytest.raises(ValidationError): + ReviewQueueReadContract.model_validate( + _queue_values() | {"selection_mode": QueueSelectionMode.NONE} + ) + + +def test_reconciliation_identity_and_mode_cannot_be_swapped(): + values = _reconcile_values() + authority = ReviewAuthorityInvalidationReconcileContract.model_validate( + values + | { + "service_identity": ServiceIdentity.REVIEW_AUTHORITY_INVALIDATION_RECONCILIATION, + "execution_mode": ServiceExecutionMode.AUTHORITY_INVALIDATION, + } + ) + assert authority.execution_mode is ServiceExecutionMode.AUTHORITY_INVALIDATION + + general = ReviewGeneralReconcileContract.model_validate( + values + | { + "service_identity": ServiceIdentity.REVIEW_RECONCILIATION, + "execution_mode": ServiceExecutionMode.GENERAL, + "reason": "scheduled bounded reconciliation", + } + ) + assert general.execution_mode is ServiceExecutionMode.GENERAL + + with pytest.raises(ValidationError): + ReviewAuthorityInvalidationReconcileContract.model_validate( + values + | { + "service_identity": ServiceIdentity.REVIEW_RECONCILIATION, + "execution_mode": ServiceExecutionMode.AUTHORITY_INVALIDATION, + } + ) + with pytest.raises(ValidationError): + ReviewGeneralReconcileContract.model_validate( + values + | { + "service_identity": ServiceIdentity.REVIEW_RECONCILIATION, + "execution_mode": ServiceExecutionMode.AUTHORITY_INVALIDATION, + "reason": "wrong mode", + } + ) + + +def test_lease_and_preference_statuses_are_closed(): + lease_values = { + "action_id": ActionId.REVIEW_RELEASE, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "queue_entry_id": uuid4(), + "review_lease_id": uuid4(), + "lease_generation": 1, + "reviewer_actor_profile_id": uuid4(), + "task_id": uuid4(), + "submission_id": uuid4(), + "lease_status": ReviewLeaseStatus.ACTIVE, + "expires_at": NOW, + "lease_state_digest": SHA, + "reason": "reviewer release", + } + assert ( + ReviewReleaseContract.model_validate(lease_values).lease_status is ReviewLeaseStatus.ACTIVE + ) + with pytest.raises(ValidationError): + ReviewReleaseContract.model_validate(lease_values | {"lease_status": "unknown"}) + + preference_values = { + "action_id": ActionId.REVIEW_DECLINE_PREFERENCE, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "project_id": uuid4(), + "queue_entry_id": uuid4(), + "preference_id": uuid4(), + "preference_generation": 1, + "preferred_reviewer_actor_profile_id": uuid4(), + "source_review_id": uuid4(), + "source_submission_id": uuid4(), + "preference_status": ReviewPreferenceStatus.ACTIVE, + "expires_at": NOW, + "preference_state_digest": SHA, + "reason": "reviewer decline", + } + assert ( + ReviewDeclinePreferenceContract.model_validate(preference_values).preference_status + is ReviewPreferenceStatus.ACTIVE + ) + with pytest.raises(ValidationError): + ReviewDeclinePreferenceContract.model_validate( + preference_values | {"preference_status": "unknown"} + ) + + +def test_decision_requires_consistent_counts_and_blocking_finding_for_revision(): + values = _decision_values() + assert ReviewDecisionContract.model_validate(values).decision is ReviewDecisionValue.ACCEPT + with pytest.raises(ValidationError): + ReviewDecisionContract.model_validate( + values | {"finding_count": 0, "blocking_finding_count": 1} + ) + with pytest.raises(ValidationError): + ReviewDecisionContract.model_validate( + values | {"decision": ReviewDecisionValue.NEEDS_REVISION} + ) + revised = ReviewDecisionContract.model_validate( + values + | { + "decision": ReviewDecisionValue.NEEDS_REVISION, + "finding_count": 1, + "blocking_finding_count": 1, + } + ) + assert revised.blocking_finding_count == 1 + + +def test_revised_submission_decision_requires_exact_predecessor_and_response_lineage(): + values = _decision_values() | { + "decision_shape": "revision", + "predecessor_review_id": uuid4(), + "predecessor_submission_id": uuid4(), + "revision_episode_id": uuid4(), + "preparation_head_id": uuid4(), + "preparation_head_generation": 2, + "preparation_head_digest": SHA, + "finding_response_count": 1, + "finding_response_lineage_digest": SHA, + } + contract = ReviewRevisionDecisionContract.model_validate(values) + assert contract.predecessor_submission_id != contract.submission_id + for field in ( + "predecessor_submission_id", + "revision_episode_id", + "preparation_head_id", + "finding_response_lineage_digest", + ): + with pytest.raises(ValidationError): + ReviewRevisionDecisionContract.model_validate( + {key: value for key, value in values.items() if key != field} + ) + with pytest.raises(ValidationError): + ReviewDecisionContract.model_validate( + _decision_values() | {"predecessor_review_id": uuid4()} + ) + with pytest.raises(ValidationError): + ReviewRevisionDecisionContract.model_validate( + values | {"predecessor_submission_id": values["submission_id"]} + ) + with pytest.raises(ValidationError): + ReviewRevisionDecisionContract.model_validate(values | {"decision_shape": "initial"}) + + +def test_obligation_close_requires_the_selected_frozen_boundary_to_be_reached(): + values = _obligation_values() + assert ReviewRevisionObligationCloseContract.model_validate(values).revision_round == 3 + with pytest.raises(ValidationError): + ReviewRevisionObligationCloseContract.model_validate(values | {"revision_round": 2}) + with pytest.raises(ValidationError): + ReviewRevisionObligationCloseContract.model_validate( + values + | { + "reached_cause": RevisionClosureCause.DEADLINE_EXPIRED, + "revision_deadline": None, + } + ) + expired = ReviewRevisionObligationCloseContract.model_validate( + values + | { + "reached_cause": RevisionClosureCause.DEADLINE_EXPIRED, + "revision_deadline": NOW, + } + ) + assert expired.reached_cause is RevisionClosureCause.DEADLINE_EXPIRED + + +def test_revision_repair_uses_canonical_outcome_direction_and_repairability(): + values = { + key: value + for key, value in _obligation_values().items() + if key + not in { + "action_id", + "revision_policy_id", + "revision_policy_generation", + "revision_policy_digest", + "revision_round", + "revision_limit", + "observed_at", + "reached_cause", + } + } | { + "action_id": ActionId.REVIEW_REVISION_CONTEXT_REPAIR, + "preparation_head_outcome": RevisionPreparationOutcome.BLOCKED, + "preparation_head_repairable": True, + "guide_id": uuid4(), + "guide_activation_sequence": 2, + "review_policy_id": uuid4(), + "review_policy_generation": 1, + "review_policy_digest": SHA, + "revision_policy_id": uuid4(), + "revision_policy_generation": 1, + "revision_policy_digest": SHA, + "reason": "repair blocked current context", + } + assert ( + ReviewRevisionContextRepairContract.model_validate(values).preparation_head_direction + is None + ) + with pytest.raises(ValidationError): + ReviewRevisionContextRepairContract.model_validate( + values | {"preparation_head_outcome": "unknown"} + ) + with pytest.raises(ValidationError): + ReviewRevisionContextRepairContract.model_validate( + values | {"preparation_head_direction": RevisionPreparationDirection.FORWARD} + ) + rebased = ReviewRevisionContextRepairContract.model_validate( + values + | { + "preparation_head_outcome": RevisionPreparationOutcome.REBASED, + "preparation_head_direction": RevisionPreparationDirection.BACKWARD, + } + ) + assert rebased.preparation_head_direction is RevisionPreparationDirection.BACKWARD + + +def test_lifecycle_activation_rejects_same_phase_and_stays_scalar_serializable(): + values = { + "action_id": ActionId.REVIEW_LIFECYCLE_ACTIVATION_MANAGE, + "lifecycle_phase": ReviewLifecyclePhase.SHADOW, + "lifecycle_digest": SHA, + "singleton_id": uuid4(), + "operation_id": uuid4(), + "expected_generation": 1, + "current_phase": ReviewLifecyclePhase.SHADOW, + "target_phase": ReviewLifecyclePhase.DRAINING, + "adjacent_transition_confirmed": True, + "reviewed_manifest_digest": SHA, + "drain_observations_digest": SHA, + "batch_limit": 100, + "deadline": NOW, + "reason": "reviewed activation transition", + } + ReviewLifecycleActivationContract.model_validate(values) + _assert_inert_contract_model(ReviewLifecycleActivationContract) + with pytest.raises(ValidationError): + ReviewLifecycleActivationContract.model_validate( + values | {"target_phase": ReviewLifecyclePhase.SHADOW} + ) + + +def test_contract_module_has_no_rev_import_and_workers_carry_no_prepared_handle(): + app_root = Path(__file__).parents[1] / "app" + contract_source = (app_root / "modules" / "authorization" / "review_contracts.py").read_text( + encoding="utf-8" + ) + assert "app.modules.review" not in contract_source + assert "PreparedAuthorizationHandle" not in contract_source + + worker_sources = [ + path.read_text(encoding="utf-8") + for path in app_root.rglob("*.py") + if "worker" in path.name or "tasks" in path.parts + ] + assert worker_sources + assert all("PreparedAuthorizationHandle" not in source for source in worker_sources) diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index ccff57611..c9ad7e2dd 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -362,6 +362,25 @@ uses the canonical custody map and exact merged REV proof. | `review.revision_context.legacy_close` | `operations.reconcile.run` | `WS-XINT-003-08A` | | `review.lifecycle.activation.manage` | `operations.reconcile.run` | `WS-XINT-003-08B` | +### REV integration contracts + +`WS-XINT-003-02D` publishes the complete inert REV authorization manifest in +`app.modules.authorization.review_contracts`. Every registered `review.*` +ActionId maps to one strict typed resource family or, for the two unapproved +evidence-upload actions, to explicit `unsupported_future_intent`. Shared +families retain exact action discriminators; fixed-service contracts bind the +exact service identity and server-derived execution mode. In particular, the +two services sharing `review.reconcile.run` cannot exchange modes. + +These frozen scalar models carry no ORM rows, bytes, provider values, callback, +or prepared handle. Publishing them changes no action availability and adds no +evaluator. Reads continue through request-scoped authorization. Mutations and +service commands later use the existing opaque, process-local, transaction- +bound `PreparedAuthorizationHandle`; REV locks and composes canonical facts, +while the exact activation wave installs the corresponding AUTH evaluator. +XINT-002 packet, evidence-binding, and revision-submission actions are external +handoff references only and are not redefined by this manifest. + Initial and revision submission use the same `submission.create` action, permission, and route. Revision preparation is an internal participant and lifecycle guard of that command; no `submission.revise` or revision-prepare diff --git a/docs/spec_review_lifecycle.md b/docs/spec_review_lifecycle.md index 6b2146e04..e4a564a60 100644 --- a/docs/spec_review_lifecycle.md +++ b/docs/spec_review_lifecycle.md @@ -162,6 +162,16 @@ route or service command commits that evidence once. No REV, task, ART, CON, shared-audit, or shared-outbox effect survives. If restaging fails, nothing commits. +Before REV lifecycle implementation begins, `WS-XINT-003-02D` publishes the +closed typed action/resource manifest in +`app.modules.authorization.review_contracts`. REV composers target those exact +strict frozen scalar contracts after locking canonical rows. The manifest is +not an evaluator and activates nothing; it contains neither REV repositories +nor product rules. Later XINT waves connect the already-published family to the +existing PREP/kernel path only after matching hidden REV behavior exists. +Adding a new action, principal, protocol, or resource-context family requires a +planning amendment rather than an ad hoc REV implementation change. + ## Canonical Records The existing `Submission` is the versioned submission identity. Domain prose