diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index 6d3e16510..b890819f9 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -1354,7 +1354,15 @@ }, { "group": "lifecycle", - "target": "backend/app/modules/reviews/acceptance/schemas.py" + "target": "backend/app/modules/reviews/acceptance/participant.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/reviews/acceptance/repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/reviews/api/acceptance.py" }, { "group": "lifecycle", @@ -1400,6 +1408,10 @@ "group": "lifecycle", "target": "backend/app/modules/reviews/schemas.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/accepted_effects.py" + }, { "group": "lifecycle", "target": "backend/app/modules/tasks/api/accepted_effects.py" @@ -1649,7 +1661,7 @@ "target": "backend/scripts/validate_test_lane_evidence.py" } ], - "authority_digest": "fdbbf9eb7ded754eb10e532c3a3b811f4af227136fcd2870f7f638405656345b", + "authority_digest": "aaa433056bbc7cdd2c85c6ff62c4fff12cfa7c5f4e90afc40487d702328495c3", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/.commitrail/INDEX.md b/.commitrail/INDEX.md index ea9573b3e..0e2c018cf 100644 --- a/.commitrail/INDEX.md +++ b/.commitrail/INDEX.md @@ -9,13 +9,13 @@ for current product capability. | [WS-DB-002](initiatives/WS-DB-002/OVERVIEW.md) | Complete | Shared UUIDv7 record generation, native-UUID relationships and fresh v0.1 baseline; natural-owner retry custody and aligned CI/local setup | | [WS-MCP-002](initiatives/WS-MCP-002/OVERVIEW.md) | Planned | Nine tools through WS-MCP-002-03: self-service and administrative reads; 18 tools remain and WS-MCP-002-04 administrative grant mutations are next | | [WS-CLI-001](initiatives/WS-CLI-001/OVERVIEW.md) | Planned | Public Go CLI reads and human profile editing delivered through WS-CLI-001-02; role-specific public journeys and binary distribution remain | -| [WS-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | Source storage, inert AUTH contracts, TASK request reservation, hidden exact AUTH preparation and CON-07 hidden participation are delivered; shared acceptance/TASK effects are next for the selected automated path, followed by hidden handlers and atomic routing activation. True admission does not depend on CON/shared acceptance. | +| [WS-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | Source storage, inert AUTH contracts, TASK request reservation, hidden exact AUTH preparation and the REV-04C hidden FinalAcceptance/TASK/CON participant are delivered; hidden routing handlers are next. True admission remains independent of shared acceptance; false activation still requires exact AUTH receipt custody, database complete-set enforcement, currentness race proof and atomic routing activation. | | [WS-ART-001](initiatives/WS-ART-001/OVERVIEW.md) | Planned | Exact checker input/output custody and packet foundations are delivered; routing integration, remediation and public intake remain. | -| [WS-AUTH-001](initiatives/WS-AUTH-001/OVERVIEW.md) | Planned | AUTH-19A commitments, TASK request reservation, ARCH-04E2-A hidden strict PREP matching and CON-07 hidden participation are delivered; the action remains unavailable, with shared acceptance/TASK effects, atomic receipt custody and scoped activation still required for the automated path. | -| [WS-CON-001](initiatives/WS-CON-001/OVERVIEW.md) | Planned | Contribution/award storage and CON-07 hidden source-neutral submitter participation with complete frozen award sets are delivered; production consumption requires shared acceptance/TASK effects, genuine authority, retained source receipts, fulfillment-root ordinals and lifecycle activation. | +| [WS-AUTH-001](initiatives/WS-AUTH-001/OVERVIEW.md) | Planned | AUTH-19A commitments, TASK request reservation, ARCH-04E2-A hidden strict PREP matching and the REV-04C hidden FinalAcceptance/TASK/CON participant are delivered; the action remains unavailable, with mandatory exact AUTH receipt input, database closure, audit/outbox and scoped activation still required for the automated path. | +| [WS-CON-001](initiatives/WS-CON-001/OVERVIEW.md) | Planned | Contribution/award storage, CON-07 participation and REV-04C hidden FinalAcceptance/TASK/CON composition are delivered; production consumption still requires genuine authority, database complete-set enforcement, currentness race proof, shared audit/outbox, fulfillment-root ordinals and lifecycle activation. | | [WS-AUTH-003](initiatives/WS-AUTH-003/OVERVIEW.md) | Planned | Canonical source facts and disabled lifecycle fencing are delivered; exact lifecycle activation remains tied to the governed consequence. | -| [WS-POL-003](initiatives/WS-POL-003/OVERVIEW.md) | Planned | Locked policy configuration and inert source commitments are delivered; false-policy activation remains gated by shared acceptance, exact authority, routing and remediation. | -| [WS-REV-001](initiatives/WS-REV-001/OVERVIEW.md) | Planned | Packet, Review and FinalAcceptance storage plus CON-07 hidden submitter participation are delivered; source receipt custody and shared acceptance/TASK effects remain, with human review/revision separately dependency-gated. | +| [WS-POL-003](initiatives/WS-POL-003/OVERVIEW.md) | Planned | Locked policy configuration, inert source commitments and the REV-04C hidden false/pass acceptance participant are delivered; false-policy activation remains gated by exact AUTH receipt custody, database closure, currentness proof, remediation and scoped lifecycle authority. | +| [WS-REV-001](initiatives/WS-REV-001/OVERVIEW.md) | Planned | Packet and Review storage plus REV-04C hidden source-neutral FinalAcceptance/TASK/CON composition are delivered; hidden routing handlers are next, while authority/evidence closure and human review/revision remain separately gated. | | [WS-QUAL-002](initiatives/WS-QUAL-002/OVERVIEW.md) | Planned | Populate subsystem ownership before changed-line mutation work | | [WS-QUAL-003](initiatives/WS-QUAL-003/OVERVIEW.md) | Planned | Audit and prune test proof, add missing safety cases, decompose oversized test modules | | [WS-XINT-002](initiatives/WS-XINT-002/OVERVIEW.md) | Planned | Remaining ART/AUTH activation edges only | diff --git a/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md b/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md index 094b78264..bfbfd1b18 100644 --- a/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md @@ -7,8 +7,10 @@ resource/preparation matching and a nominal fixed-router adapter through canonic PREP. The action remains planned and unavailable, so it issues no handle, allow or receipt and writes no source or effect. The selected automated path now has CON-07 hidden submitter participation and complete frozen award-set staging/replay. -Shared acceptance and TASK terminal effects are next. Mandatory source-receipt -custody, fulfillment-root ordinal custody and scoped lifecycle activation remain +[REV-04C](../WS-REV-001/WS-REV-001-04C.md) now composes FinalAcceptance, TASK terminal effects and CON participation +inside one hidden caller-owned transaction. Mandatory exact AUTH receipt input, +database complete-set enforcement, currentness race proof, shared audit/outbox, +fulfillment-root ordinal custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -25,7 +27,8 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies metadata-only packet types. REV-03B packets, REV-04A Review storage, REV-04B FinalAcceptance storage, CON-03C contribution/award storage, REV-12A1 disabled - fencing, hidden AUTH preparation and CON-07 hidden submitter participation are + fencing, hidden AUTH preparation, CON-07 hidden submitter participation and + REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. No packet resolver or human runtime is live. @@ -40,8 +43,8 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered storage boundary: hidden [ARCH-04B2 checker-output custody](WS-ARCH-001-04B2.md), following hidden input materialization (ARCH-04B). Typed store, byte-free recovery and flush-only verified binding exist; the CHECKERS zero-slot reservation reader is implemented; output write/bind authority remains unavailable. - Delivered source boundary: [ARCH-04E1A routing-source facts](WS-ARCH-001-04E1A.md) follow [ARCH-04D2](WS-ARCH-001-04D2.md) exact input, execute and finalize - authority. The source table has no writer, reader, handler, current pointer, - routing authority or acceptance effect implementation. False is proven only as + authority. The source table has no routing handler, current pointer or routing + authority. REV-04C supplies the hidden acceptance-effects participant. False is proven only as a scalar DTO value because activation still rejects it. - Delivered preparation boundary: [ARCH-04E2-A](WS-ARCH-001-04E2A.md) binds the reserved request, exact source and branch consequence through canonical @@ -53,9 +56,10 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), [REV-04A](../WS-REV-001/WS-REV-001-04A.md) adds complete immutable Review, findings, resolutions and completed request storage; no decision runtime. [REV-04B](../WS-REV-001/WS-REV-001-04B.md) adds shared FinalAcceptance source storage, without AUTH receipt custody or a production writer. -- Next usable boundary: shared acceptance/TASK effects using the delivered - REV-12A1 fence and CON-07 participant, plus mandatory same-table acceptance authority hardening before shared - acceptance composition, then ARCH-04E1B/04E2/04E3 and ARCH-04F. Output-file +- Next usable boundary: ARCH-04E1B-B hidden handlers using the delivered + REV-04C participant, then mandatory exact AUTH receipt input, database closure, + audit/outbox, TASK-before-CHECKERS currentness race proof and activation at + ARCH-04E2-B before 04E3 and ARCH-04F. Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers public manager activation context and exact guide activation using the existing CP07 operation. @@ -84,7 +88,7 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), Delivered [REV-12A1](../WS-REV-001/WS-REV-001-12A1.md) supplies only disabled generation-zero controller storage and transaction locking. Acceptance-source -AUTH custody, shared acceptance/TASK effects and authorized activation remain +AUTH custody, database closure, currentness proof and authorized activation remain separate work. This is delivery priority, not a prerequisite of true human admission. Both diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md index 34b0d506c..320f92230 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -53,8 +55,8 @@ public intake remains deferred to ARCH-02I. | [WS-ARCH-001-04C](chunks/WS-ARCH-001-04C-checker-current-result.md) | CHECKER hidden durable current output and supersession behavior | L1 | Complete hidden request/lease/result custody with exact empty-output support and atomic completion; exact service authority delivered by 04D2 | | [WS-ARCH-001-04D1](../WS-ARCH-001-04D1.md) | Canonical terminal ART material custody | L1 | Complete; valid retained history preserved; invalid upgrades refused | | [WS-ARCH-001-04D2](../WS-ARCH-001-04D2.md) | AUTH exact fixed-service post-submit activation (replaces XINT-06B) | L1 | Complete: exact input, execute and finalize authority; output write/bind remains unavailable | -| [WS-ARCH-001-04E1A](../WS-ARCH-001-04E1A.md) | Route-neutral immutable source schema and shared accepted-effects types | L1 | Complete; no runtime writer/reader, routing authority, current pointer or effects implementation; false proof is scalar transport only | -| [WS-ARCH-001-04E](chunks/WS-ARCH-001-04E-canonical-allow-review.md) | TASK current routing: true to canonical `allow_review`, false/pass to shared acceptance | L1 | Both branches: 04E1A/04D2/OUTBOX-02 -> delivered request reservation 04E1B-A -> delivered AUTH preparation 04E2-A -> hidden handlers 04E1B-B -> activation 04E2-B -> live 04E3. False additionally needs shared acceptance/TASK effects using delivered CON-07 participation, mandatory source-receipt custody, fulfillment-root ordinal custody and scoped lifecycle activation; true does not depend on CON. False guide activation also requires 04F remediation | +| [WS-ARCH-001-04E1A](../WS-ARCH-001-04E1A.md) | Route-neutral immutable source schema and shared accepted-effects types | L1 | Complete; REV-04C uses a bounded exact-source verifier, with no general routing publication writer/reader, routing authority or current pointer; its hidden effects participant does not make false routing available | +| [WS-ARCH-001-04E](chunks/WS-ARCH-001-04E-canonical-allow-review.md) | TASK current routing: true to canonical `allow_review`, false/pass to shared acceptance | L1 | Both branches: delivered 04E1A/04D2/OUTBOX-02, request reservation 04E1B-A and AUTH preparation 04E2-A -> hidden handlers/currentness proof 04E1B-B -> authority/evidence closure and activation 04E2-B -> live 04E3. False uses delivered REV-04C participation and additionally needs mandatory exact receipt/database/audit closure, fulfillment-root ordinal custody and scoped lifecycle activation; true does not depend on CON. False guide activation also requires 04F remediation | | [WS-ARCH-001-03D](../WS-ARCH-001-03D.md) | Exact activated historical guide through hidden durable intake; obsolete lookup removed | L1 | Complete; hidden exact post-submit materialization, ARCH-04B2 output custody, ARCH-04C execution, ARCH-04D1/04D2 custody/authority and ARCH-04E1A source-only facts/types delivered; public cutover remains deferred | | [WS-ARCH-001-04F](chunks/WS-ARCH-001-04F-checker-remediation.md) | Contributor-correctable checker failures and same-lineage admission-backed replacement Submission | L1 | Planned after 04E; replaces XINT-05C, required before public 02I, not before REV begins from `allow_review` | diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md b/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md index 72d7d6af8..1f4fdc3c1 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md @@ -6,9 +6,11 @@ future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP. The action remains planned/unavailable, so no handle, allow, receipt, source write or product effect is reachable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -70,12 +72,12 @@ checker-remediation boundary before public Submission cutover. | AUTH-OUTBOX-01 | Merged shared outbox persistence and AUTH service/PREP foundations | Complete: planned dispatcher identity/action/matrix and unavailable typed authority contract | | CON-02B | AUTH-OUTBOX-01 | Complete: shared hidden dispatcher/claim fencing, typed handlers and recovery | | AUTH-OUTBOX-02 | CON-02B exact hidden manifest | Exact dispatcher mechanics only; no feature authority | -| [ARCH-04E1A](../WS-ARCH-001-04E1A.md) | ARCH-04C/04D2 | Complete: route-neutral immutable TASK source schema/detached facts and source-neutral accepted-effects types; no runtime writer/reader, handlers, current pointer, routing authority, acceptance implementation or REV dependency | +| [ARCH-04E1A](../WS-ARCH-001-04E1A.md) | ARCH-04C/04D2 | Complete: route-neutral immutable TASK source schema/detached facts and source-neutral accepted-effects types; REV-04C uses a bounded exact-source verifier, with no general routing publication writer/reader, handlers, current pointer or routing authority | | ARCH-04E1B-A | ARCH-04E1A, AUTH-19A | Complete: TASK caller-session request/source-identity reservation; no source publication, handler or commit | | [ARCH-04E2-A](../WS-ARCH-001-04E2A.md) | ARCH-04E1B-A | Complete: strict AUTH-private resource/request/consequence matcher and nominal fixed-router adapter through canonical PREP; action stays planned/unavailable, with no handle, allow, receipt, source write or effect | -| ARCH-04E1B-B | ARCH-04E2-A and CON-02B; false additionally shared acceptance/TASK effects using delivered CON-07 participation and fencing | Hidden TASK handlers; durable receipt proof follows at exact activation | -| Scoped XINT-003-08B controller activation | Early existing REV-12A foundation and hidden shared acceptance/writer/observation proof | Existing Operator lifecycle-control action for the bounded shared manifest, not human runtime | -| ARCH-04E2-B | ARCH-04E1B-B; scoped XINT-003-08B controller activation for false | AUTH exact consequence activation and mandatory receipt custody; genuine allow commits with all governed effects | +| ARCH-04E1B-B | ARCH-04E2-A and CON-02B; false additionally uses delivered REV-04C shared acceptance participation | Hidden TASK handlers plus TASK-before-CHECKERS currentness and both successor-generation race orders; durable receipt proof follows at exact activation | +| Scoped XINT-003-08B controller activation | Existing REV-12A foundation, delivered REV-04C hidden participant and remaining real-writer observation proof | Existing Operator lifecycle-control action for the bounded shared manifest, not human runtime | +| ARCH-04E2-B | ARCH-04E1B-B; scoped XINT-003-08B controller activation for false | Require the exact AUTH receipt on the same strict input; add database complete-set and audit/outbox closure; genuine allow commits with all governed effects | | ARCH-04E3 | ARCH-04E2, ARCH-04D2, AUTH-OUTBOX-02; shared acceptance proof for false | TASK live dispatch/routing composition: true to allow_review, false/pass to shared acceptance when proven | | ARCH-04E | ARCH-04E3 | Completed coordination boundary consumed by downstream REV | | ARCH-04F (later public-cutover prerequisite) | ARCH-04E | CHECKER failure facts and TASK/ART remediation resubmission, not REV | @@ -114,9 +116,10 @@ new permission requirement. Delivered supporting foundations are [ARCH-04B2](../WS-ARCH-001-04B2.md), [AUTH-OUTBOX-01/02](../../WS-AUTH-001/planning/PLAN.md#ws-auth-001-outbox-01--unavailable-dispatcher-contract), and [CON-02B](../../WS-CON-001/OVERVIEW.md#con-02b-current-dispatcher-contract). -REV-12A1 delivers disabled controller/fence mechanics, and CON-07 delivers the -hidden source-neutral submitter participant and complete frozen award-set owner. -The current false-branch priority is shared acceptance and TASK terminal effects. +REV-12A1 delivers disabled controller/fence mechanics, CON-07 delivers the +hidden source-neutral submitter participant and complete frozen award-set owner, +and REV-04C composes hidden FinalAcceptance/TASK/CON effects. The current +false-branch priority is hidden routing handlers. Both branches have request reservation 04E1B-A and hidden AUTH preparation 04E2-A delivered before handlers 04E1B-B, activation 04E2-B and live 04E3; true routing can proceed after its own prerequisites without CON or shared acceptance. False adds those participants and scoped lifecycle activation. diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md b/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md index 2e72bc149..03931d935 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md @@ -35,16 +35,19 @@ Split preparation from consequences within the existing TASK and AUTH owners: positive SQL proof follow at 04E2-B, not before CON-07. Keep standalone invocation, live workers and consequence activation unavailable. This phase requires no acceptance-consuming handler and cannot commit an allow independently. -3. **CON-07 and shared acceptance composition** consume the exact prepared - contracts. CON-07 is a caller-owned flush-only participant; its isolated - storage controls prove contribution/award behavior, not acceptance authority. - Do not require a fabricated AUTH allow to make those controls reachable. - At 04E2-B, install mandatory receipt custody and prove the first genuine allow - atomically with source publication, FinalAcceptance, TASK and contribution/ - award effects, plus required audit/outbox records. Refuse retained - pre-authority sources before any production composition or consumer. Pre-CON - proof covers exact preparation, unavailable execution and rollback, not a - durable allowed operation. +3. **CON-07 and REV-04C shared acceptance composition** consume the exact + prepared contracts. CON-07 is a caller-owned flush-only participant; REV-04C + now composes it with FinalAcceptance and TASK accepted/completed effects in + one hidden caller-owned transaction for either source. Its isolated proof is + mechanical transaction proof, not acceptance authority or a complete + `SharedFinalAcceptanceOperation`. Do not require a fabricated AUTH allow to + make those controls reachable. At 04E2-B, evolve the same strict participant + input to require the exact AUTH decision-event receipt with no optional or + default path, then prove the first genuine allow atomically with source + publication, FinalAcceptance, TASK and contribution/award effects, plus the + required audit/outbox records. That phase also adds database-enforced + FinalAcceptance/TASK/CON complete-set closure and refuses retained + pre-authority sources before any production composition or consumer. 4. **ARCH-04E1B-B / 04E2-B / 04E3** complete hidden handlers, exact consequence activation and live composition as detailed below. True admission uses the same preparation/issuer phases but does not require CON/shared acceptance; @@ -67,11 +70,12 @@ enqueue human review or treat `allow_review` as acceptance authority. The current TASK children do not implement REV/CON internals: consume the [canonical shared participants and authority contract](../../../../../docs/spec_review_lifecycle.md#finalacceptance). 04E1A's TASK manifest schema/detached facts and narrow accepted-effects Protocol -are delivered after 04C, without REV dependency, runtime participant or handlers. +are delivered after 04C. REV-04C now supplies the hidden runtime participant, +without a routing handler or authorized/live composition. REV-04B now references that schema after its source prerequisites: [ART-07A1 packet types](../../../WS-ART-001/WS-ART-001-07A1.md) are delivered; REV-03B normalized packets and REV-04A immutable Review source storage are delivered; REV-04B shared FinalAcceptance storage is delivered; CON-03C contribution/award storage and REV-12A1 disabled controller/fence are delivered; hidden AUTH preparation is delivered before CON-07; mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. -CON-07/shared acceptance prerequisites, using the delivered exact AUTH preparation and +CON-07/REV-04C shared acceptance prerequisites, using the delivered exact AUTH preparation and REV-12A1 disabled controller/fence, are hard dependencies of false handler composition, not of this early schema or true admission. Both branches use the delivered preparation phase above before hidden 04E1B-B, canonical activation 04E2-B and live 04E3. True proceeds with its own prerequisites; @@ -82,14 +86,16 @@ making automated acceptance depend on live human queues or leases. The existing success-manifest, `review_pending` transition and corresponding tests below describe only `human_review_required=true`. They must not run for -false. Before its acceptance participant is implemented and activated, false +false. Before its delivered acceptance participant is authorized and activated, false has no live success route; an unexpected false attempt fails closed without creating human admission, acceptance or contribution effects. 1. **[ARCH-04E1A — TASK source foundation](../../WS-ARCH-001-04E1A.md) — Complete.** One route-neutral immutable source schema and detached internal facts, plus - source-neutral accepted-effects types, follow 04C/04D2. There is no runtime - writer/reader/composition, current pointer, routing allow or accepted effects. + source-neutral accepted-effects types, follow 04C/04D2. REV-04C supplies the + hidden FinalAcceptance/TASK/CON participant and uses the bounded exact-source + verifier. There is no general routing publication writer/reader, current + pointer, routing allow or authorized accepted effect. Source evidence alone never admits a human or authorizes acceptance. The current true-policy graph supplies real SQL proof; false is value-shape transport only because guide activation still rejects it. Before publication, @@ -97,8 +103,8 @@ creating human admission, acceptance or contribution effects. owner-receipt custody and refuse every retained pre-authority source row, without backfill, mutation, deletion or a parallel table. 2. **ARCH-04E1B-B — hidden TASK handlers.** After 04E1B-A/04E2-A and - CON-02B's handler/claim contract (plus shared acceptance foundations for - false), TASK implements unavailable request/event + CON-02B's handler/claim contract (plus delivered REV-04C acceptance foundations + for false), TASK implements unavailable request/event production for its own evaluation-request event and the TASK consumer of 04C's already-defined final-result notification, exact public facts, currentness protocol and transaction proof described below. No live worker or action activation. @@ -116,8 +122,11 @@ creating human admission, acceptance or contribution effects. Bind exact Task/Assignment, equal assignment/Submission contribution-policy version, stabilized artifact hash, manifest identity, allocated acceptance identity, actor, project, request/idempotency and transaction as well. - False/pass includes the shared acceptance consequence after its hidden - proof; true permits only human admission. Allow only AUTH adapters/catalogue/parity/composition + False/pass includes the delivered hidden shared acceptance consequence; true + permits only human admission. The same strict false input must first gain the + mandatory exact AUTH decision-event receipt, and 04E1B-B must prove the + TASK-before-CHECKERS currentness protocol and both acceptance/successor- + generation race orders. Allow only AUTH adapters/catalogue/parity/composition and focused proof; no TASK state-machine implementation. It cannot execute checker, ART, dispatcher, human review or generic contribution actions. Derived submitter/award writes occur only through the shared participant, @@ -127,9 +136,9 @@ creating human admission, acceptance or contribution effects. the existing shared dispatcher. TASK owns this narrow live integration and legacy-call reachability cutover, not another implementation of 04E1B. -If the shared acceptance foundation lands later than true routing, keep false -activation unavailable and integrate it into this same handler after the named -predecessors; do not add another dispatcher, success event or acceptance engine. +True routing remains independent of shared acceptance. Keep false activation +unavailable until the named authority, database, race and remediation +predecessors land; do not add another dispatcher, success event or acceptance engine. If true routing is already active, false requires exact successor AUTH resource, evaluator and parity/activation proof under the same ActionId; a handler-only change cannot silently widen its permitted effects. diff --git a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md index f77df9f39..32f12279c 100644 --- a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md @@ -7,8 +7,10 @@ resource/preparation matching and a nominal fixed-router adapter through canonic PREP. The action remains planned and unavailable, so it issues no handle, allow or receipt and writes no source or effect. The selected automated path now has CON-07 hidden submitter participation and complete frozen award-set staging/replay. -Shared acceptance and TASK terminal effects are next. Mandatory source-receipt -custody, fulfillment-root ordinal custody and scoped lifecycle activation remain +[REV-04C](../WS-REV-001/WS-REV-001-04C.md) now composes FinalAcceptance, TASK terminal effects and CON participation +inside one hidden caller-owned transaction. Mandatory exact AUTH receipt input, +database complete-set enforcement, currentness race proof, shared audit/outbox, +fulfillment-root ordinal custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -27,7 +29,8 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies metadata-only packet types. REV-03B packets, REV-04A Review storage, REV-04B FinalAcceptance storage, CON-03C contribution/award storage, REV-12A1 disabled - fencing, hidden AUTH preparation and CON-07 hidden submitter participation are + fencing, hidden AUTH preparation, CON-07 hidden submitter participation and + REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. No packet resolver or human runtime is live. @@ -45,10 +48,10 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), - Completed activation boundary: AUTH-12H exact-project manager authority for CP07 complete-guide activation/binding, with live-authority replay. - Delivered dependent boundary: ARCH-04E1A immutable route-neutral source facts - and type-only accepted-effects contracts follow + and accepted-effects contracts follow [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and - finalize authority. They add no routing action, handler or deployable effects - participant. Output-file authority remains unavailable for the zero-output + finalize authority. REV-04C supplies the hidden effects participant, but no + routing action, handler or deployable authorized operation. Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers public manager activation context and exact guide activation. [CP05A](../WS-ARCH-001/WS-ARCH-001-CP05A.md) delivers public Finance @@ -73,8 +76,9 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered storage: [REV-03B](../WS-REV-001/WS-REV-001-03B.md) freezes exact lease packets with normalized live guide ingests; no resolver or byte authority. [REV-04A](../WS-REV-001/WS-REV-001-04A.md) adds complete immutable Review, findings, resolutions and completed request storage; no decision runtime. -- Next usable boundary: shared acceptance/TASK effects before hidden ARCH-04E1B-B; - ARCH-04E2 then owns exact routing activation before 04E3 live composition. +- Next usable boundary: hidden ARCH-04E1B-B routing handlers using the REV-04C + participant; ARCH-04E2-B then owns mandatory same-input receipt custody, + database closure, audit/outbox and exact routing activation before 04E3 live composition. - Governing source: `docs/spec_authorization_service.md`, authorization code, migrations, and tests. - Preserve: Flow token verification only, no Workstream login/session system, @@ -83,7 +87,7 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), Delivered [REV-12A1](../WS-REV-001/WS-REV-001-12A1.md) supplies only disabled generation-zero controller storage and transaction locking. Acceptance-source -AUTH custody, shared acceptance/TASK effects and authorized activation remain +AUTH custody, database closure, currentness proof and authorized activation remain separate work. This is delivery priority, not a prerequisite of true human admission. Both diff --git a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md index 803b87ec7..c0d7b60cf 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -38,7 +40,7 @@ work and historical proposals. | [ARCH-04D2](../../WS-ARCH-001/WS-ARCH-001-04D2.md) | Complete: exact materialization and execute/finalize authority; output write/bind unavailable; replaces AUTH-14/XINT-06B | | [AUTH-OUTBOX-01](PLAN.md#ws-auth-001-outbox-01--unavailable-dispatcher-contract) | Complete: unavailable exact dispatcher identity/action/phase contract; CON-02B and AUTH-OUTBOX-02 mechanics complete; feature authority/registration remain separate | | [AUTH-OUTBOX-02](PLAN.md#ws-auth-001-outbox-02--exact-dispatcher-activation) | Complete: exact dispatcher mechanics activation, phase audit custody and bounded prefork delivery; ARCH-03C2 subsequently registers assignment invalidation, while future handlers require their own exact authority | -| [ARCH-04E2](../../WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md#current-bounded-sequence) | Both branches: delivered source-only 04E1A -> delivered request reservation 04E1B-A -> delivered hidden AUTH preparation 04E2-A -> handlers 04E1B-B -> consequence activation 04E2-B -> live 04E3. True uses its own prerequisites and does not depend on CON; false additionally requires shared acceptance/TASK effects using delivered CON-07 participation, acceptance-source AUTH custody, fulfillment-root ordinal custody and scoped lifecycle activation | +| [ARCH-04E2](../../WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md#current-bounded-sequence) | Both branches: delivered source-only 04E1A -> request reservation 04E1B-A -> hidden AUTH preparation 04E2-A -> handlers/currentness proof 04E1B-B -> authority/evidence closure and consequence activation 04E2-B -> live 04E3. True uses its own prerequisites and does not depend on CON; false uses delivered REV-04C participation and additionally requires exact AUTH receipt/database/audit closure, fulfillment-root ordinal custody and scoped lifecycle activation | Guide activation needs CP05 -> CP06 -> hidden CP07 and POL-07, which also requires independent ARCH-04A registered-capability proof. It does not need diff --git a/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md b/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md index efc835ac1..708314339 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -46,7 +48,7 @@ AUTH-13/14 cutovers are not additional implementation work. public TASK activation is complete through ARCH-03C7. - [AUTH-18](../WS-AUTH-001-18.md) delivers public manager activation and exact selection discovery over CP07/AUTH-12H. ARCH-03D completes hidden approved-guide - intake; ARCH-04B hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 exact input/execution/finalization authority, ARCH-04E1A source-only facts/types and ARCH-04E2-A hidden strict preparation are delivered. REV-12A1 disabled fencing and CON-07 hidden participation are delivered. Shared acceptance/TASK effects, hidden handlers 04E1B-B, activation 04E2-B and live 04E3 remain. True routing proceeds on its own prerequisites without CON. False additionally requires acceptance-source AUTH custody and scoped lifecycle activation; it is the current delivery priority. + intake; ARCH-04B hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 exact input/execution/finalization authority, ARCH-04E1A source-only facts/types and ARCH-04E2-A hidden strict preparation are delivered. REV-12A1 fencing, CON-07 participation and REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Hidden handlers 04E1B-B, authority/evidence closure and activation 04E2-B, and live 04E3 remain. True routing proceeds on its own prerequisites without CON. False additionally requires acceptance-source AUTH custody, database/currentness/audit closure and scoped lifecycle activation; it is the current delivery priority. - CP05 owns exact ContributionPolicy-action activation after merged CP04B. - CP08 delivered the minimal lineage writers. ARCH-03B8 hidden task audit evidence and 03B9 hidden assignment invalidation are complete. ARCH-03C delivered diff --git a/.commitrail/initiatives/WS-CON-001/OVERVIEW.md b/.commitrail/initiatives/WS-CON-001/OVERVIEW.md index 59921c653..aa784d897 100644 --- a/.commitrail/initiatives/WS-CON-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-CON-001/OVERVIEW.md @@ -5,9 +5,11 @@ planned router identity. TASK request and future source-ID reservation (ARCH-04E1B-A) and strict hidden AUTH preparation (ARCH-04E2-A) are delivered. The nominal fixed-router adapter still cannot obtain a handle, allow or receipt, and writes no source or effect while the action remains planned/unavailable. -CON-07 now delivers hidden source-neutral submitter participation and complete -frozen award-set staging/replay. Shared acceptance and TASK terminal effects are -next. Mandatory source-receipt custody, fulfillment-root ordinal custody and +CON-07 delivers hidden source-neutral submitter participation and complete +frozen award-set staging/replay. [REV-04C](../WS-REV-001/WS-REV-001-04C.md) now composes it with FinalAcceptance and +TASK terminal effects in one hidden caller-owned transaction. Mandatory exact +AUTH receipt input, database complete-set enforcement, shared audit/outbox, +currentness race proof, fulfillment-root ordinal custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -20,8 +22,8 @@ and the [capability ledger](../../../docs/roadmap_status.md). - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies metadata-only packet types. REV-03B packets, REV-04A Review source storage, REV-04B FinalAcceptance storage, CON-03C contribution/award storage, REV-12A1 - disabled fencing, hidden AUTH preparation and CON-07 hidden submitter - participation are delivered. Mandatory persisted custody accompanies the + disabled fencing, hidden AUTH preparation, CON-07 hidden submitter + participation and REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. No packet resolver or human runtime is live. @@ -63,9 +65,10 @@ and the [capability ledger](../../../docs/roadmap_status.md). [CON-07](WS-CON-001-07.md) adds hidden source-neutral submitter participation and complete frozen award-set staging/replay; no recognition route or fulfillment operation is live. -- Next usable boundary: shared acceptance and TASK accepted/completed effects, - plus mandatory same-table acceptance authority hardening before one shared - operation serves both human and automatic triggers. +- Next usable boundary: ARCH-04E1B-B hidden routing handlers using the shared + participant. The complete authorized operation still requires a mandatory + same-input AUTH receipt, database complete-set enforcement, audit/outbox and + currentness proof before either trigger can consume it in production. - Governing sources: `docs/spec_contribution_compensation.md`, [`CONFORMANCE.md`](CONFORMANCE.md), code, migrations, and tests. - Preserve: exact policy-version lineage, no claim-time drift, decimal-string @@ -74,8 +77,9 @@ and the [capability ledger](../../../docs/roadmap_status.md). Delivered [REV-12A1](../WS-REV-001/WS-REV-001-12A1.md) supplies only disabled generation-zero controller storage and transaction locking. CON-07 consumes its -canonical fence through a CON-owned Protocol, but acceptance-source AUTH custody, -shared acceptance/TASK effects and authorized activation remain separate work. +canonical fence through a CON-owned Protocol, and REV-04C composes the hidden +mechanical participant. Acceptance-source AUTH custody, database closure, +currentness proof and authorized activation remain separate work. This is delivery priority, not a prerequisite of true human admission. Both routing branches have delivered request reservation 04E1B-A → delivered AUTH preparation 04E2-A → hidden handlers 04E1B-B → activation 04E2-B → live 04E3; true routing diff --git a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md index 4e9aeb31f..1cc6e4fe2 100644 --- a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md +++ b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md @@ -7,8 +7,10 @@ resource/preparation matching and a nominal fixed-router adapter through canonic PREP while the action remains planned/unavailable. It issues no handle, allow or receipt and writes no source or effect. The selected automated path now has CON-07 hidden submitter participation and complete frozen award-set staging/replay. -Shared acceptance and TASK terminal effects are next. Mandatory source-receipt -custody, fulfillment-root ordinal custody and scoped lifecycle activation remain +[REV-04C](../WS-REV-001/WS-REV-001-04C.md) now composes FinalAcceptance, TASK terminal effects and CON participation +inside one hidden caller-owned transaction. Mandatory exact AUTH receipt input, +database complete-set enforcement, currentness race proof, shared audit/outbox, +fulfillment-root ordinal custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, @@ -33,7 +35,8 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies metadata-only packet types. REV-03B packets, REV-04A Review storage, REV-04B FinalAcceptance storage, CON-03C contribution/award storage, REV-12A1 disabled - fencing, hidden AUTH preparation and CON-07 hidden submitter participation are + fencing, hidden AUTH preparation, CON-07 hidden submitter participation and + REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. No packet resolver or human runtime is live. @@ -50,9 +53,10 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Intent: compile one locked guide and its policies into authoritative, versioned project behavior without circular subsystem authority. - Delivered dependent boundary: ARCH-04E1A route-neutral source facts and - type-only accepted-effects contracts follow + accepted-effects contracts follow [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and - finalize authority. They add no route, acceptance path or activation support. + finalize authority. REV-04C supplies the hidden acceptance-effects participant, + but no route, authorized operation or activation support. Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers public manager activation context and exact guide activation. @@ -81,8 +85,9 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Delivered storage: [REV-03B](../WS-REV-001/WS-REV-001-03B.md) freezes exact lease packets with normalized live guide ingests; no resolver or byte authority. [REV-04A](../WS-REV-001/WS-REV-001-04A.md) adds complete immutable Review, findings, resolutions and completed request storage; no decision runtime. -- Next usable boundary: shared acceptance/TASK effects using the delivered - REV-12A1 fence and CON-07 participant, then ARCH-04E1B-B/04E2-B/04E3. +- Next usable boundary: ARCH-04E1B-B hidden handlers using the delivered + REV-04C participant, then mandatory AUTH receipt/database closure and + activation at 04E2-B before 04E3. ARCH-04F remediation still precedes enabling false. - Governing sources: project-guide specifications, authorization and contribution-policy specifications, code, migrations, and tests. @@ -92,7 +97,7 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), Delivered [REV-12A1](../WS-REV-001/WS-REV-001-12A1.md) supplies only disabled generation-zero controller storage and transaction locking. Acceptance-source -AUTH custody, shared acceptance/TASK effects and authorized activation remain +AUTH custody, database closure, currentness proof and authorized activation remain separate work. This is delivery priority, not a prerequisite of true human admission. Both diff --git a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md index b61d92e5d..06f358ea9 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, diff --git a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md index 7716031d7..ddf8643cc 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md +++ b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. CON-07 hidden submitter -participation and complete frozen award-set staging/replay are delivered. Shared -acceptance and TASK terminal effects are next. Mandatory source-receipt custody, -fulfillment-root ordinal custody and scoped lifecycle activation remain required +participation and complete frozen award-set staging/replay are delivered. REV-04C +adds the hidden FinalAcceptance/TASK/CON participant. Hidden handlers 04E1B-B are +next. Mandatory exact same-input AUTH receipt, database complete-set enforcement, +TASK-before-CHECKERS race proof, shared audit/outbox, fulfillment-root ordinal +custody and scoped lifecycle activation remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, diff --git a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md index 0f5a2a8c9..bb06cd0f7 100644 --- a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md @@ -6,9 +6,12 @@ planned router identity. TASK request and future source-ID reservation The nominal fixed-router adapter still cannot obtain a handle, allow or receipt, and writes no source or effect while the action remains planned/unavailable. CON-07 hidden submitter participation and complete frozen award-set staging/replay -are delivered. Shared acceptance and TASK terminal effects are next. Mandatory -source-receipt custody, fulfillment-root ordinal custody and scoped lifecycle -activation remain required before production consumption. Neither phase may +are delivered. [REV-04C](WS-REV-001-04C.md) now composes FinalAcceptance, TASK terminal effects and +that CON participant in one hidden caller-owned transaction for either source. +It is not the complete authorized operation. Mandatory exact AUTH receipt input, +database complete-set enforcement across FinalAcceptance/TASK/CON, shared +audit/outbox, fulfillment-root ordinal custody and scoped lifecycle activation +remain required before production consumption. Neither phase may commit a standalone allow. The first durable receipt must commit with its full governed consequence. Hidden handlers and activation follow as 04E1B-B/04E2-B, then live 04E3; true admission does not depend on CON/shared acceptance. @@ -21,7 +24,8 @@ of review/revision behavior. The downstream owner contracts remain separate. - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies metadata-only packet types. REV-03B packets, REV-04A Review storage, REV-04B FinalAcceptance storage, CON-03C contribution/award storage, REV-12A1 disabled - fencing, hidden AUTH preparation and CON-07 hidden submitter participation are + fencing, hidden AUTH preparation, CON-07 hidden submitter participation and + REV-04C hidden FinalAcceptance/TASK/CON composition are delivered. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. No packet resolver or human runtime is live. @@ -31,14 +35,16 @@ of review/revision behavior. The downstream owner contracts remain separate. - Intent: ensure the authorized reviewer evaluates the exact verified artifact under the locked policy version and produces attributable outcomes. - Delivered upstream boundary: ARCH-04E1A provides immutable route-neutral TASK - source storage, detached source facts and the type-only accepted-effects - Protocol. It provides no writer, reader, handler, routing authority, current - pointer or acceptance implementation. + source storage, detached source facts and accepted-effects contracts. REV-04C + supplies their hidden FinalAcceptance/TASK/CON participant and bounded stored- + source verifier; no general routing publication writer/reader, handler, + routing authority or current pointer exists. - Delivered storage: [REV-03B](../WS-REV-001/WS-REV-001-03B.md) freezes exact lease packets with normalized live guide ingests; no resolver or byte authority. [REV-04A](../WS-REV-001/WS-REV-001-04A.md) adds complete immutable Review, findings, resolutions and completed request storage; no decision runtime. -- Next usable boundary: shared acceptance/TASK effects using the delivered - REV-12A1 fence and CON-07 participant; human hidden behavior may continue independently behind exact AUTH, +- Next usable boundary: ARCH-04E1B-B hidden routing handlers using the delivered + REV-04C participant. They must prove TASK-before-CHECKERS currentness and both + successor-generation race orders; human hidden behavior may continue independently behind exact AUTH, ART and CON prerequisites. - Governing sources: `docs/spec_review_lifecycle.md`, `docs/engineering/review_authorization_action_custody.md`, code, migrations, @@ -48,7 +54,7 @@ of review/revision behavior. The downstream owner contracts remain separate. Delivered [REV-12A1](../WS-REV-001/WS-REV-001-12A1.md) supplies only disabled generation-zero controller storage and transaction locking. Acceptance-source -AUTH custody, shared acceptance/TASK effects and authorized activation remain +AUTH custody, database closure, currentness proof and authorized activation remain separate work. This is delivery priority, not a prerequisite of true human admission. Both @@ -86,8 +92,10 @@ REV owns shared final-acceptance semantics for both paths. The automated path must have explicit AUTH service authority and exact TASK/CHECKERS evidence, without a fabricated Review, ReviewLease, human actor, or reviewer contribution. CON validates stored source ancestry through the delivered source-neutral -submitter participant and complete frozen award-set owner. Shared acceptance, -TASK effects and runtime composition remain planned. +submitter participant and complete frozen award-set owner. REV-04C supplies the +hidden source-neutral FinalAcceptance/TASK/CON participant, but no routing +handler, exact AUTH decision-event receipt, audit/outbox consequence or live +runtime composition. The human branch below continues to use `allow_review`; it is not an automatic acceptance signal. @@ -104,7 +112,14 @@ proof. No adjudication setting or behavior is included. 2. Continue hidden claim/revision behavior against canonical `allow_review`, copying the Submission policy version without a current-policy lookup. 3. TASK's early 04E1A source schema/detached facts and source-neutral accepted- - effects types are delivered. After delivered REV-03B and REV-04A storage, REV-04B shared FinalAcceptance storage is also delivered. REV-12A1 disabled fencing, ARCH-04E2-A exact source preparation and CON-07 hidden participation are delivered; enforce mandatory receipt custody with the first genuinely authorized atomic composition before production consumption. This foundation + effects types are delivered. After delivered REV-03B and REV-04A storage, + REV-04B shared FinalAcceptance storage, REV-12A1 disabled fencing, + ARCH-04E2-A exact source preparation, CON-07 hidden participation and REV-04C + hidden FinalAcceptance/TASK/CON composition are delivered. Evolve the same + strict participant input to require the exact AUTH decision-event receipt, + with no optional/default path, before production consumption. 04E2-B also + owns database-enforced complete-set closure, shared audit/outbox, + fulfillment-root ordinal custody and scoped activation. This foundation can precede human runtime: ARCH-04E uses it for false/pass acceptance without live queues, leases or decisions. The delivered REV-12A1 disabled fence serves both triggers; actual root ordinal custody and authorized drain/operator work extend the same controller before diff --git a/.commitrail/initiatives/WS-REV-001/WS-REV-001-04C.md b/.commitrail/initiatives/WS-REV-001/WS-REV-001-04C.md new file mode 100644 index 000000000..65f1ce844 --- /dev/null +++ b/.commitrail/initiatives/WS-REV-001/WS-REV-001-04C.md @@ -0,0 +1,183 @@ +# REV-04C — Hidden shared acceptance and TASK terminal participation + +- Initiative: `WS-REV-001` +- Durable disposition: `Complete` +- Intended merge outcome: one hidden REV participant stages exact FinalAcceptance, TASK accepted/completed effects and complete submitter economic facts in a caller-owned transaction; live acceptance remains unavailable. + +## Intent + +Connect the delivered acceptance storage, lifecycle fence and CON-07 participant. +Both eventual acceptance triggers must use this same transactional core. Keep +actual AUTH/source receipt custody and shared audit/outbox composition at the +already-planned activation boundary; this core is not the complete authorized +`SharedFinalAcceptanceOperation` and introduces no alternative business path. + +## Current behavior + +Main `0ba68eaa` includes merged CON-07. REV owns immutable FinalAcceptance +storage and the disabled generation-zero root-transaction fence. TASK exposes +only `TaskAcceptedEffectsPort`; it has no terminal participant. CON creates or +replays contributions and complete frozen awards, but currently derives its own +new/replay disposition. An enclosing acceptance replay could consequently fill +in a missing contribution rather than reject partial prior effects. + +The current source schemas predate required receipt hardening. Neither +`review.decision` nor `task.post_submit.route` can issue a genuine allow. False +guide activation remains unavailable. Existing true-policy stored Review +fixtures are valid storage controls, not authority or a live human workflow. + +## Bounded change + +### Allowed + +- Move the existing FinalAcceptanceInput from private `reviews/acceptance/schemas.py` into the public contract, updating every caller and removing the superseded file without an alias. +- `backend/app/modules/reviews/api/{acceptance,__init__}.py` and `reviews/acceptance/{__init__,schemas,repository,participant}.py`: closed values, exact source identity recovery and one hidden flush-only shared participant. +- `backend/app/modules/tasks/api/accepted_effects.py`, `tasks/accepted_effects.py`, `tasks/api/__init__.py` exports and narrowly necessary owner repository methods: exact TASK lineage/prestate locking, terminal mutation and read-only replay. +- `backend/app/modules/contributions/api/participation.py` and `contributions/records/{participant,repository}.py`: explicit enclosing acceptance disposition; reject missing replay records before insertion, without a compatibility default. +- Focused tests in `backend/tests/reviews/acceptance/`, `tests/tasks/accepted_effects/`, `tests/contributions/participation/` and affected canonical source fixtures; retain distinct existing guard proof. +- Exact ownership/lane registrations in `.ci/behavior-ownership/partition.v1.json`, `backend/scripts/{behavior_ownership,test_lane_catalogue}.py`, and their focused expectation tests; no changed gates, caps or partition algorithm. +- This record, current REV/CON/ARCH/AUTH/POL navigation, `.commitrail/INDEX.md`, README and affected canonical lifecycle, contribution, authorization, artifact, data-model, glossary and roadmap claims. Reconcile the linked current ARCH-04E parent contract in the same PR. Local roadmap exports only if present. + +### Not allowed + +- Production composition/registration, routes, handlers, workers, AUTH activation or fabricated allow/receipt, false guide activation, source publication/current pointer, live human decisions, reviewer contributions, fulfillment roots/delivery, payment or reputation. +- REV imports of private TASK/CON/CHECKERS owners, TASK imports of REV, provider/ART I/O, an additional source table/acceptance engine, compatibility aliases, no-op production participants, or silently repairing retained partial effects. +- No change to the deferred contributor lease/skip scope. + +## Design and decisions + +Acquire the existing canonical REV fence first, in the caller's root transaction; +managed and raw SQL savepoints remain rejected. Owner-local TASK locking must +precede acceptance/source writes that can take foreign-key locks. The TASK +participant validates the exact project, task, assignment, latest Submission, +contributor, frozen contribution policy and artifact identity. Its terminal +mutation changes only Task status to accepted and the exact assignment status +to completed; no generic user-command transition is enabled. + +REV stores or recovers one exact source-bound acceptance. UUIDv7 allocation and +exclusive source replay use existing owner storage, not IDs derived from a +request token or a new reservation store. Caller-preallocated identity remains +available for the future exact AUTH consequence. The request carries a caller-preallocated UUIDv7 acceptance identity; committed +replay must supply that stored identity. A future PREP identity resolver remains +outside this participant. No extra reservation store is introduced. + +TASK first locks and validates Task -> Assignment -> latest Submission and +reports the observed new or terminal replay state. REV then inserts only for +new work or selects and exactly compares only for replay, using every unique +acceptance axis (ID, task, submission, exclusive source) to reject conflicting +identity. A new insertion that loses cannot become replay. TASK applies effects +only after this decision. REV validates the human source digest against the +stored Review, and automated source facts through the TASK public contract; +TASK owns the content ID, not artifact bytes. + +The strict shared request contains FinalAcceptanceInput, TaskAcceptedEffectsRequest, +expected generation and correlation. Common IDs must match; human uses +review_pending, automated uses evaluation_pending. REV reloads the actual Review +and compares source, policy, assignment, version, reviewer and artifact hash. +The TASK public accepted-effects port also exposes require_routing_source(request, +manifest_id): its owner reads the exact immutable stored routing manifest and +compares project/task/submission/version/assignment/contributor/frozen policy, +content hash and false human-review setting to the already-locked TASK facts. +It never treats caller detached values as stored proof. Exact aggregate AUTH +commitments/currentness remain mandatory at activation. Unpaid participation +retains no correlation and makes no replay-correlation claim. + +The acceptance owner derives `new|replay` once. TASK and CON receive it as +operation provenance, not caller authority or a new business policy. New work +requires its valid prestate and absent effects. Replay requires already-complete, +exact TASK/CON/award facts and returns original IDs without mutation. Any partial +prior set conflicts; catching the exception must not leave newly repaired rows. +The existing compensation complete-set owner remains canonical. + +The participant returns exact immutable acceptance/TASK/contribution/award facts. +It neither commits nor authorizes. Future complete operation composition evolves this same participant input and +schema to require the verified AUTH event, without a compatibility default, and +adds mandatory shared audit/outbox staging; +it may not replace it with a second sequence or a no-op participant. Future TASK +routing consumes a TASK-owned structural interface supplied by composition so +REV -> TASK/CON remains acyclic; no reverse TASK -> REV dependency is introduced. + +No migration is included. This participant guarantees its own call sequence in +a caller-owned transaction, not database enforcement of every direct-SQL +complete effect set. Mandatory source AUTH receipt hardening and deferred +FinalAcceptance/TASK/CON complete-set guards belong together at 04E2-B, before +activation; retained pre-authority rows must be refused, not repaired or deleted. +CHECKERS can currently reserve a later evaluation without the TASK lock. The +04E1B-B/activation work must introduce the existing-direction consumer seam +that locks TASK before CHECKERS and rejects terminal tasks, and the source +handler must revalidate currentness under those locks. This chunk does not +claim acceptance-versus-evaluation-supersession safety or live acceptance. +Before activation, prove both race orders: a successor generation first makes +acceptance stale; acceptance first prevents a successor reservation after TASK +locking. Never enable the generic ALLOWED_TASK_TRANSITIONS accepted transition. + +## Acceptance criteria + +- Valid human-source storage controls stage one exact acceptance, accepted task, + completed assignment, submitter contribution and zero/one/two frozen awards. +- Replay preserves every stored identity and scalar, performs no repair, and + rejects independent valid foreign source/owner substitutions and changed + correlation where retained. +- Named partial cases: FinalAcceptance only with pending TASK; terminal TASK and + assignment without acceptance; accepted/active and pending/completed mixed + states; exact acceptance plus terminal TASK without CON; exact contribution + with partial awards staged before the existing deferred completeness check. + Each rejects before any missing effect is inserted; catch and commit where the + existing database permits the partial fixture, otherwise inspect then roll back. A guard-removal probe reaches the intended replay + assertion rather than a prior invalid-source guard. +- Late real SQL failure and caller rollback discard every newly staged effect. + No participant commits, creates reviewer work, or accesses artifact bytes. +- Independent PostgreSQL sessions prove exact-source convergence, conflicting + source rejection, and the required lock retention/order. Missing root and + both savepoint forms fail before product writes. +- TASK owner checks reject foreign projects before foreign row locks and reject + stale Submission/assignment/policy/content facts. Terminal and mixed partial + states cannot be treated as a new acceptance. +- False-source metadata and true-policy rejection remain covered without + fabricating positive false activation. Automated positive authority/currentness + and whole audit/outbox atomicity remain explicit activation obligations. +- A named negative-structure test scans production imports, constructions, + routes, workers and registrations to prove the hidden participant is unreachable; + both review.decision and task.post_submit.route remain planned. No no-op + authority adapter or fallback is permitted. +- Replace the direct-CON concurrent insert-or-replay proof with shared-owner + concurrent commit/rollback winner proof, retaining stored winner-ID assertions. +- Full hosted suite completes with zero skips/deselections; no required proof, + ownership guard or CI limit is weakened. + +## Risk and review routing + +- Risk class: L1 — bounded terminal workflow, economic facts and transaction custody. +- Required reviews: architecture/reuse, security, QA/test-delta, CI integrity for registration changes, documentation/product-operations. +- Human focus: hidden mechanical composition is not live acceptance authority; complete source/evidence custody and automated positive runtime proof remain required before activation. + +## Evidence + +Plan review settled source identity, the two-phase lock order, explicit replay +provenance and reachable storage controls. Database complete-set hardening stays +paired with mandatory AUTH custody before activation, as specified above. Focused real PostgreSQL tests exercise the owner ports +and caller transaction; pure tests cover strict closed values. Run Ruff, +module/authority/ownership checks, stale wording, links, Commitrail and the full +unchanged hosted completeness gate. Exact target/results belong in the PR. + +## Review findings + +Discovery identified a partial-replay repair gap in the enclosing use of CON-07. +Explicit acceptance-derived new/replay provenance and no-insert replay address +that requirement without adding a second contribution implementation. The +existing REV queue/lease files are active owners and are not obsolete scaffolding. +Plan review required executable production-unreachability proof, individually +reachable partial-state cases, stored-source verification and both future +CHECKERS race orders. The public API boundary check required moving the existing +source contract into REV's public API; the private schema and its imports were +removed rather than retained as aliases. +The no-insert mutation proof uses an unpaid source so an independent award +replay guard cannot mask the removed contribution guard; paid partial-award +proof remains separate. TASK tests retain moved public-contract assertions and +exercise both mechanical prestates without claiming automated authority. + +## Reconciliation + +- Current source: CON-07 is merged; preserve current MCP/CLI and observability work. +- Next usable boundary: hidden routing handlers, complete authority/evidence composition and scoped lifecycle activation, then live integration and remediation before public intake/false activation. +- Remaining risks: exact real AUTH/source receipts, complete shared audit/outbox custody, fulfillment-root ordinals, both production trigger paths and human runtime remain outside this hidden participant. diff --git a/README.md b/README.md index 459b15e24..f753e9c5d 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ submission packets, immutable artifact storage, pre-submit intake checks, and authorized retained submission/checker history. ARCH-04C implements hidden durable post-submit execution and unfinished-attempt recovery. ARCH-04D2 supplies exact service authority. ARCH-04E1A adds immutable route-neutral TASK source -storage, detached source facts and a type-only accepted-effects Protocol; +storage, detached source facts and accepted-effects contracts; automatic dispatch, routing and acceptance remain unavailable. Project-guide ingestion stores original documents, records immutable metadata and provides authorized exact-file reads to the unified setup agent. Guide metadata in PostgreSQL also holds at least one required @@ -178,14 +178,18 @@ metadata-only reviewer packet types, not a resolver or byte-access capability. REV-03B persists immutable normalized packets using live guide ingest identities. REV-04A Review and REV-04B shared FinalAcceptance storage foundations are delivered; CON-03C contribution/award storage, REV-12A1's disabled controller/transaction fence, exact hidden AUTH preparation and CON-07 hidden source-neutral submitter -participation with complete frozen award sets are delivered. Acceptance storage -has no production writer or AUTH receipt; mandatory exact -authority hardening precedes any runtime consumer. These prerequisites do not require live +participation with complete frozen award sets are delivered. REV-04C adds one +hidden source-neutral participant that stages FinalAcceptance, TASK +accepted/completed effects and the complete CON submitter outcome in the caller's +transaction. It is not a complete authorized acceptance operation: its input has +no exact AUTH decision-event receipt, and database complete-set enforcement, +currentness race proof, shared audit/outbox and lifecycle activation remain. +These prerequisites do not require live human review before the first automated acceptance path. Active work is connecting those foundations into the remaining production -lifecycle: the remaining artifact custody chain, review and revision, -shared acceptance/TASK effects, reviewer participation and conditional fulfillment. +lifecycle: hidden routing handlers, acceptance authority/evidence closure, +review and revision, reviewer participation and conditional fulfillment. Contribution evidence remains the input for a separately implemented future reputation projection. Frontend product work follows stable and tested backend contracts for the surface it consumes. @@ -600,8 +604,10 @@ pre-submit execution and completed replay use that service, with ART retaining canonical evidence ownership. The obsolete checker worker, manual execution and submission-finalize repair routes are removed. `evaluate_post_submission` now uses hidden durable execution with exact ARCH-04D2 service authority. ARCH-04E1A -retains route-neutral source evidence without a writer, reader, handler or -current pointer; automatic dispatch and routing remain ARCH-04E work. +retains route-neutral source evidence. REV-04C uses its bounded exact-source +verifier and hidden FinalAcceptance/TASK/CON participant, while no general +routing publication writer/reader, handler or current pointer exists; automatic +dispatch and routing remain ARCH-04E work. Submission and checker history use live exact-project Submitter authority for the original contributor. Separate `/projects/{project_id}` reads require a covering Project Manager grant and expose fixed management fields. Token roles confer no @@ -706,11 +712,15 @@ nominal fixed-router adapter through canonical PREP. Because allow, receipt, source write, publication or effect is reachable. The true branch binds only the future TASK `evaluation_pending -> review_pending` manifest effect, without creating a REV queue dependency. The false branch binds exact -`TaskAcceptedEffectsRequest` values for the future shared FinalAcceptance path, +`TaskAcceptedEffectsRequest` values for the shared FinalAcceptance path, without fabricating a Review. CON-07 now supplies the hidden source-neutral -submitter participant and complete frozen award-set staging/replay. Shared -acceptance and TASK terminal effects remain next; actual receipt custody must -commit with the governed consequence before production consumption. +submitter participant and complete frozen award-set staging/replay. REV-04C now +composes FinalAcceptance, TASK terminal effects and that CON participant in one +hidden caller-owned transaction for either source. Hidden handlers are next. +The mandatory exact AUTH receipt must become required on the same strict input, +with no optional/default path, before production consumption; database +FinalAcceptance/TASK/CON closure, TASK-before-CHECKERS race proof, shared +audit/outbox, fulfillment-root ordinals and activation remain later gates. ## v0.1 Success Standard diff --git a/backend/app/modules/contributions/api/participation.py b/backend/app/modules/contributions/api/participation.py index b1253a15b..4523ea3fe 100644 --- a/backend/app/modules/contributions/api/participation.py +++ b/backend/app/modules/contributions/api/participation.py @@ -1,15 +1,13 @@ """Hidden submitter participation contract; construction grants no authority.""" -from typing import Annotated, Protocol +from typing import Annotated, Literal, Protocol from uuid import UUID from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, StrictInt, StrictStr from app.modules.compensation.api.awards import CompensationAwardFacts -_STRICT_FROZEN = ConfigDict( - extra="forbid", frozen=True, strict=True, revalidate_instances="always" -) +_STRICT_FROZEN = ConfigDict(extra="forbid", frozen=True, strict=True, revalidate_instances="always") _Digest = Annotated[StrictStr, Field(pattern=r"^sha256:[0-9a-f]{64}$")] _Generation = Annotated[StrictInt, Field(ge=0, le=9_223_372_036_854_775_807)] @@ -35,6 +33,7 @@ class SubmitterParticipationRequest(BaseModel): model_config = _STRICT_FROZEN + acceptance_disposition: Literal["new", "replay"] project_id: UUID task_id: UUID submission_id: UUID diff --git a/backend/app/modules/contributions/records/participant.py b/backend/app/modules/contributions/records/participant.py index cb5f81b7f..ca64b44fd 100644 --- a/backend/app/modules/contributions/records/participant.py +++ b/backend/app/modules/contributions/records/participant.py @@ -54,17 +54,15 @@ async def participate_submitter( rule = await self._repository.get_frozen_rule( checked.project_id, checked.contribution_policy_version_id ) - contribution, disposition = await self._repository.insert_or_get(checked) + contribution = await self._repository.apply_acceptance_disposition(checked) award_request = CompleteAwardSetRequest( - disposition=disposition, + disposition=checked.acceptance_disposition, compensation_mode=rule.compensation_mode, contribution=AwardContributionFacts( id=contribution.id, project_id=contribution.project_id, contributor_id=contribution.contributor_id, - contribution_policy_version_id=( - contribution.contribution_policy_version_id - ), + contribution_policy_version_id=(contribution.contribution_policy_version_id), contribution_type="accepted_submission", ), definitions=rule.definitions, @@ -75,9 +73,7 @@ async def participate_submitter( await self._awards.complete_award_set(award_request) ) except (CompensationAwardConflict, TypeError, ValueError, ValidationError) as exc: - raise ContributionParticipationConflict( - "contribution_participation_conflict" - ) from exc + raise ContributionParticipationConflict("contribution_participation_conflict") from exc self._require_exact_awards(checked, contribution, rule, award_result) return SubmitterParticipationResult( contribution=contribution, @@ -109,8 +105,7 @@ def _award_matches(award, definition, contribution, correlation_id) -> bool: award.project_id == contribution.project_id and award.contribution_record_id == contribution.id and award.contributor_id == contribution.contributor_id - and award.contribution_policy_version_id - == contribution.contribution_policy_version_id + and award.contribution_policy_version_id == contribution.contribution_policy_version_id and award.award_definition_id == definition.id and award.adapter_binding_id == definition.adapter_binding_id and award.instrument_type == definition.instrument_type diff --git a/backend/app/modules/contributions/records/repository.py b/backend/app/modules/contributions/records/repository.py index 6a8336248..c2f82fd40 100644 --- a/backend/app/modules/contributions/records/repository.py +++ b/backend/app/modules/contributions/records/repository.py @@ -48,8 +48,7 @@ async def get_frozen_rule( select(ContributionRule.id, ContributionRule.compensation_mode) .join( ContributionPolicyVersion, - ContributionPolicyVersion.id - == ContributionRule.contribution_policy_version_id, + ContributionPolicyVersion.id == ContributionRule.contribution_policy_version_id, ) .where( ContributionPolicyVersion.id == contribution_policy_version_id, @@ -105,9 +104,7 @@ async def get_frozen_rule( ) from exc instruments = {item.instrument_type for item in definitions} - complete = ( - rule.compensation_mode == "unpaid" and not definitions - ) or ( + complete = (rule.compensation_mode == "unpaid" and not definitions) or ( rule.compensation_mode == "compensated" and 1 <= len(definitions) <= 2 and len(instruments) == len(definitions) @@ -119,10 +116,16 @@ async def get_frozen_rule( definitions=definitions, ) - async def insert_or_get( + async def apply_acceptance_disposition( self, request: SubmitterParticipationRequest - ) -> tuple[SubmitterContributionFacts, Literal["new", "replay"]]: - """Insert once by FinalAcceptance source, then compare the exact winner.""" + ) -> SubmitterContributionFacts: + """Insert new work or read an exact replay without repairing either mode.""" + if request.acceptance_disposition == "replay": + winner = await self._select_exact_source(request) + if winner is None or not self._matches(winner, request): + raise ContributionParticipationConflict("contribution_participation_conflict") + return self._facts(winner) + candidate_id = new_record_id() inserted_id = await self._session.scalar( insert(ContributionRecord) @@ -140,24 +143,41 @@ async def insert_or_get( artifact_hash=request.artifact_hash, contribution_policy_version_id=request.contribution_policy_version_id, ) - .on_conflict_do_nothing( - index_elements=[ContributionRecord.source_final_acceptance_id] - ) + .on_conflict_do_nothing(index_elements=[ContributionRecord.source_final_acceptance_id]) .returning(ContributionRecord.id) ) + if inserted_id != candidate_id: + raise ContributionParticipationConflict("contribution_participation_conflict") winner = await self._session.scalar( select(ContributionRecord) .where( - ContributionRecord.source_final_acceptance_id - == request.final_acceptance_id, + ContributionRecord.id == candidate_id, ContributionRecord.project_id == str(request.project_id), ) .execution_options(populate_existing=True) ) if winner is None or not self._matches(winner, request): raise ContributionParticipationConflict("contribution_participation_conflict") + return self._facts(winner) + + async def _select_exact_source( + self, request: SubmitterParticipationRequest + ) -> ContributionRecord | None: + """Read the source-unique replay candidate without staging a row.""" + return await self._session.scalar( + select(ContributionRecord) + .where( + ContributionRecord.source_final_acceptance_id == request.final_acceptance_id, + ContributionRecord.project_id == str(request.project_id), + ) + .execution_options(populate_existing=True) + ) + + @staticmethod + def _facts(winner: ContributionRecord) -> SubmitterContributionFacts: + """Validate stored scalar types before returning public immutable facts.""" try: - facts = SubmitterContributionFacts( + return SubmitterContributionFacts( id=winner.id, project_id=UUID(winner.project_id), task_id=UUID(winner.task_id), @@ -170,15 +190,10 @@ async def insert_or_get( created_at=winner.created_at, ) except (TypeError, ValueError, ValidationError) as exc: - raise ContributionParticipationConflict( - "contribution_participation_conflict" - ) from exc - return facts, "new" if inserted_id == candidate_id else "replay" + raise ContributionParticipationConflict("contribution_participation_conflict") from exc @staticmethod - def _matches( - winner: ContributionRecord, request: SubmitterParticipationRequest - ) -> bool: + def _matches(winner: ContributionRecord, request: SubmitterParticipationRequest) -> bool: """Compare every caller-supplied immutable contribution source fact.""" return ( winner.project_id == str(request.project_id) @@ -191,6 +206,5 @@ def _matches( and winner.source_final_acceptance_id == request.final_acceptance_id and winner.source_task_assignment_id == str(request.task_assignment_id) and winner.artifact_hash == request.artifact_hash - and winner.contribution_policy_version_id - == request.contribution_policy_version_id + and winner.contribution_policy_version_id == request.contribution_policy_version_id ) diff --git a/backend/app/modules/reviews/acceptance/__init__.py b/backend/app/modules/reviews/acceptance/__init__.py index ce4d1deca..84d06aaf3 100644 --- a/backend/app/modules/reviews/acceptance/__init__.py +++ b/backend/app/modules/reviews/acceptance/__init__.py @@ -1 +1 @@ -"""Shared acceptance storage foundation; no authorized acceptance operation.""" +"""Hidden shared acceptance storage and participation, without runtime authority.""" diff --git a/backend/app/modules/reviews/acceptance/participant.py b/backend/app/modules/reviews/acceptance/participant.py new file mode 100644 index 000000000..271273638 --- /dev/null +++ b/backend/app/modules/reviews/acceptance/participant.py @@ -0,0 +1,99 @@ +"""Hidden shared acceptance core; no production entry or AUTH capability.""" + +from pydantic import ValidationError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.contributions.api import ( + ContributionParticipationConflict, + ContributionParticipationUnavailable, + SubmitterParticipationPort, + SubmitterParticipationRequest, + SubmitterParticipationResult, +) +from app.modules.reviews.acceptance.repository import FinalAcceptanceRepository +from app.modules.reviews.api.acceptance import ( + FinalAcceptanceConflict, FinalAcceptanceRequest, FinalAcceptanceResult, +) +from app.modules.reviews.api.lifecycle import JointLifecycleMutationFence +from app.modules.tasks.api.accepted_effects import ( + TaskAcceptedEffectsPort, TaskAcceptedEffectsResult, TaskAcceptedEffectsUnavailable, + TaskAcceptedPreparation, +) + + +class FinalAcceptanceParticipant: + """Compose existing owners in one caller transaction; never commit or authorize.""" + + def __init__( + self, session: AsyncSession, *, fence: JointLifecycleMutationFence, + tasks: TaskAcceptedEffectsPort, contributions: SubmitterParticipationPort, + ) -> None: + self._fence = fence + self._tasks = tasks + self._contributions = contributions + self._repository = FinalAcceptanceRepository(session) + + async def participate(self, request: FinalAcceptanceRequest) -> FinalAcceptanceResult: + """Fence, lock TASK, bind source, persist acceptance, then stage owner effects.""" + try: + checked = FinalAcceptanceRequest.model_validate(request) + except (TypeError, ValueError, ValidationError) as exc: + raise FinalAcceptanceConflict("final_acceptance_conflict") from exc + await self._fence.acquire(checked.expected_generation) + try: + return await self._participate(checked) + except ( + TaskAcceptedEffectsUnavailable, ContributionParticipationConflict, + ContributionParticipationUnavailable, + TypeError, ValueError, ValidationError, + ) as exc: + raise FinalAcceptanceConflict("final_acceptance_conflict") from exc + + async def _participate(self, request: FinalAcceptanceRequest) -> FinalAcceptanceResult: + source, task = request.acceptance, request.task_effects + prepared = TaskAcceptedPreparation.model_validate( + await self._tasks.lock_accepted_effects( + task, expected_generation=request.expected_generation, + ) + ) + if prepared.locked_review_policy_id != source.policy_context_ref: + raise FinalAcceptanceConflict("final_acceptance_conflict") + if source.acceptance_source == "human_review": + await self._repository.require_human_source(request) + else: + await self._tasks.require_routing_source(task, source.source_routing_manifest_id) + acceptance = await self._repository.persist(source, disposition=prepared.disposition) + effects = TaskAcceptedEffectsResult.model_validate( + await self._tasks.apply_accepted_effects( + task, disposition=prepared.disposition, + expected_generation=request.expected_generation, + ) + ) + if effects.request != task: + raise FinalAcceptanceConflict("final_acceptance_conflict") + participation_request = SubmitterParticipationRequest( + acceptance_disposition=prepared.disposition, + project_id=source.project_id, task_id=source.task_id, + submission_id=source.submission_id, final_acceptance_id=source.id, + task_assignment_id=task.assignment_id, contributor_id=task.contributor_id, + contribution_policy_version_id=task.contribution_policy_version_id, + artifact_hash=task.content_sha256, correlation_id=request.correlation_id, + expected_generation=request.expected_generation, + ) + participation = SubmitterParticipationResult.model_validate( + await self._contributions.participate_submitter(participation_request) + ) + contribution = participation.contribution + expected = { + "project_id": source.project_id, "task_id": source.task_id, + "submission_id": source.submission_id, "contributor_id": task.contributor_id, + "source_final_acceptance_id": source.id, + "source_task_assignment_id": task.assignment_id, + "artifact_hash": task.content_sha256, + "contribution_policy_version_id": task.contribution_policy_version_id, + } + if any(getattr(contribution, key) != value for key, value in expected.items()): + raise FinalAcceptanceConflict("final_acceptance_conflict") + return FinalAcceptanceResult( + acceptance=acceptance, task_effects=effects, participation=participation, + ) diff --git a/backend/app/modules/reviews/acceptance/repository.py b/backend/app/modules/reviews/acceptance/repository.py new file mode 100644 index 000000000..745094616 --- /dev/null +++ b/backend/app/modules/reviews/acceptance/repository.py @@ -0,0 +1,92 @@ +"""REV-owned source validation and mode-specific acceptance persistence.""" + +from typing import Literal +from uuid import UUID + +from sqlalchemy import or_, select +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.reviews.acceptance.models import FinalAcceptance +from app.modules.reviews.api.acceptance import FinalAcceptanceInput +from app.modules.reviews.api.acceptance import ( + FinalAcceptanceConflict, FinalAcceptanceFacts, FinalAcceptanceRequest, +) +from app.modules.reviews.decision.models import Review + +_STRING_IDS = frozenset({ + "project_id", "task_id", "submission_id", "accepted_submitter_id", + "recorded_by", "policy_context_ref", +}) + + +class FinalAcceptanceRepository: + """The caller holds TASK locks before any acceptance foreign-key write.""" + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def require_human_source(self, request: FinalAcceptanceRequest) -> None: + """Bind every relevant effect to the actual immutable accepting Review.""" + source, task = request.acceptance, request.task_effects + review = await self._session.scalar( + select(Review).where( + Review.id == source.source_review_id, + Review.project_id == str(source.project_id), + Review.task_id == str(source.task_id), + Review.submission_id == str(source.submission_id), + ).execution_options(populate_existing=True) + ) + if review is None or ( + review.decision != "accept" + or review.task_assignment_id != str(task.assignment_id) + or review.submission_version != task.submission_version + or review.reviewer_id != str(source.recorded_by) + or review.locked_review_policy_id != str(source.policy_context_ref) + or review.artifact_hash != task.content_sha256 + ): + raise FinalAcceptanceConflict("final_acceptance_conflict") + + async def persist( + self, source: FinalAcceptanceInput, *, disposition: Literal["new", "replay"] + ) -> FinalAcceptanceFacts: + """New must win its insert; replay only reads an exact complete identity.""" + values = { + name: str(value) if name in _STRING_IDS else value + for name, value in source.model_dump().items() + } + if disposition == "new": + inserted = await self._session.scalar( + insert(FinalAcceptance).values(**values) + .on_conflict_do_nothing().returning(FinalAcceptance.id) + ) + if inserted != source.id: + raise FinalAcceptanceConflict("final_acceptance_conflict") + elif disposition != "replay": + raise FinalAcceptanceConflict("final_acceptance_conflict") + + source_predicate = ( + FinalAcceptance.source_review_id == source.source_review_id + if source.source_review_id is not None + else FinalAcceptance.source_routing_manifest_id == source.source_routing_manifest_id + ) + candidates = (await self._session.scalars( + select(FinalAcceptance).where(or_( + FinalAcceptance.id == source.id, + FinalAcceptance.task_id == str(source.task_id), + FinalAcceptance.submission_id == str(source.submission_id), + source_predicate, + )).execution_options(populate_existing=True) + )).all() + if len(candidates) != 1 or any( + getattr(candidates[0], name) != value for name, value in values.items() + ): + raise FinalAcceptanceConflict("final_acceptance_conflict") + row = candidates[0] + return FinalAcceptanceFacts( + **{ + name: UUID(getattr(row, name)) if name in _STRING_IDS else getattr(row, name) + for name in FinalAcceptanceInput.model_fields + }, + accepted_at=row.accepted_at, + ) diff --git a/backend/app/modules/reviews/acceptance/schemas.py b/backend/app/modules/reviews/acceptance/schemas.py deleted file mode 100644 index 4832c1b56..000000000 --- a/backend/app/modules/reviews/acceptance/schemas.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Closed source metadata, without authority, effects or a creation operation.""" - -from typing import Literal, Self -from uuid import UUID - -from pydantic import BaseModel, ConfigDict, model_validator - - -class FinalAcceptanceInput(BaseModel): - """One exclusive source; owner validation and future AUTH custody remain required.""" - - model_config = ConfigDict(extra="forbid", frozen=True, strict=True) - - id: UUID - project_id: UUID - task_id: UUID - submission_id: UUID - acceptance_source: Literal["human_review", "task_post_submit_route"] - source_review_id: UUID | None - source_routing_manifest_id: UUID | None - accepted_submitter_id: UUID - recorded_by: UUID - policy_context_ref: UUID - - @model_validator(mode="after") - def exclusive_source(self) -> Self: - human = self.acceptance_source == "human_review" - if (self.source_review_id is not None) != human or ( - self.source_routing_manifest_id is not None - ) == human: - raise ValueError("acceptance source identity is not exclusive") - return self diff --git a/backend/app/modules/reviews/api/acceptance.py b/backend/app/modules/reviews/api/acceptance.py new file mode 100644 index 000000000..ca41fec53 --- /dev/null +++ b/backend/app/modules/reviews/api/acceptance.py @@ -0,0 +1,94 @@ +"""Hidden shared acceptance values; neither construction nor storage grants authority.""" + +from typing import Annotated, Literal, Protocol, Self +from uuid import UUID + +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, StrictInt, model_validator + +from app.modules.contributions.api import SubmitterParticipationResult +from app.modules.tasks.api.accepted_effects import TaskAcceptedEffectsRequest, TaskAcceptedEffectsResult + +_STRICT = ConfigDict(extra="forbid", frozen=True, strict=True, revalidate_instances="always") + + +class FinalAcceptanceInput(BaseModel): + """One exclusive source; owner validation and future AUTH custody remain required.""" + + model_config = _STRICT + + id: UUID + project_id: UUID + task_id: UUID + submission_id: UUID + acceptance_source: Literal["human_review", "task_post_submit_route"] + source_review_id: UUID | None + source_routing_manifest_id: UUID | None + accepted_submitter_id: UUID + recorded_by: UUID + policy_context_ref: UUID + + @model_validator(mode="after") + def exclusive_source(self) -> Self: + human = self.acceptance_source == "human_review" + if (self.source_review_id is not None) != human or ( + self.source_routing_manifest_id is not None + ) == human: + raise ValueError("acceptance source identity is not exclusive") + return self + + +class FinalAcceptanceConflict(RuntimeError): + """Exact source, lineage, or complete prior effects are unavailable.""" + + +class FinalAcceptanceRequest(BaseModel): + """Exact proposed effects; verified source AUTH custody is still unavailable.""" + + model_config = _STRICT + + acceptance: FinalAcceptanceInput + task_effects: TaskAcceptedEffectsRequest + correlation_id: UUID + expected_generation: Annotated[StrictInt, Field(ge=0, le=9_223_372_036_854_775_807)] + + @model_validator(mode="after") + def exact_effects(self) -> Self: + acceptance, task = self.acceptance, self.task_effects + expected_status = ( + "review_pending" if acceptance.acceptance_source == "human_review" + else "evaluation_pending" + ) + if ( + acceptance.id != task.final_acceptance_id + or acceptance.project_id != task.project_id + or acceptance.task_id != task.task_id + or acceptance.submission_id != task.submission_id + or acceptance.accepted_submitter_id != task.contributor_id + or task.expected_task_status != expected_status + ): + raise ValueError("acceptance effect lineage differs") + return self + + +class FinalAcceptanceFacts(FinalAcceptanceInput): + """Immutable acceptance identity and database-owned timestamp.""" + + accepted_at: AwareDatetime + + +class FinalAcceptanceResult(BaseModel): + """Exact staged or replayed facts, without a commit or authority claim.""" + + model_config = _STRICT + + acceptance: FinalAcceptanceFacts + task_effects: TaskAcceptedEffectsResult + participation: SubmitterParticipationResult + + +class FinalAcceptancePort(Protocol): + """Hidden flush-only core; production activation must harden this same input.""" + + async def participate(self, request: FinalAcceptanceRequest) -> FinalAcceptanceResult: + """Stage exact effects in the caller's fenced root transaction.""" + ... diff --git a/backend/app/modules/tasks/accepted_effects.py b/backend/app/modules/tasks/accepted_effects.py new file mode 100644 index 000000000..718fa91fc --- /dev/null +++ b/backend/app/modules/tasks/accepted_effects.py @@ -0,0 +1,190 @@ +"""Hidden two-phase TASK terminal participant for shared final acceptance.""" + +from typing import Literal +from uuid import UUID + +from pydantic import ValidationError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.tasks.api.accepted_effects import ( + TaskAcceptedEffectsFence, + TaskAcceptedEffectsRequest, + TaskAcceptedEffectsResult, + TaskAcceptedEffectsUnavailable, + TaskAcceptedPreparation, +) +from app.modules.tasks.repository import TaskRepository + + +class TaskAcceptedEffectsParticipant: + """Lock, validate, and apply TASK-owned final-acceptance effects.""" + + def __init__( + self, + session: AsyncSession, + *, + fence: TaskAcceptedEffectsFence, + ) -> None: + """Bind TASK effects to the caller's session and injected canonical fence.""" + self._session = session + self._repository = TaskRepository(session) + self._fence = fence + + async def lock_accepted_effects( + self, + request: TaskAcceptedEffectsRequest, + *, + expected_generation: int, + ) -> TaskAcceptedPreparation: + """Acquire the fence first, then lock and validate exact TASK lineage.""" + checked = self._validate_request(request) + return await self._lock_and_validate( + checked, expected_generation=expected_generation + ) + + async def apply_accepted_effects( + self, + request: TaskAcceptedEffectsRequest, + *, + disposition: Literal["new", "replay"], + expected_generation: int, + ) -> TaskAcceptedEffectsResult: + """Recheck the observed disposition before mutating only terminal statuses.""" + checked = self._validate_request(request) + if type(disposition) is not str or disposition not in ("new", "replay"): + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + preparation, task, assignment = await self._lock_and_validate_rows( + checked, expected_generation=expected_generation + ) + if preparation.disposition != disposition: + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + if disposition == "new": + task.status = "accepted" + assignment.status = "completed" + await self._session.flush() + return TaskAcceptedEffectsResult( + request=checked, + task_status="accepted", + assignment_status="completed", + ) + + async def require_routing_source( + self, + request: TaskAcceptedEffectsRequest, + manifest_id: UUID, + ) -> None: + """Reload and compare the actual immutable false-policy routing manifest.""" + checked = self._validate_request(request) + if type(manifest_id) is not UUID: + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + source = await self._repository.read_routing_manifest( + manifest_id=manifest_id, + project_id=checked.project_id, + task_id=checked.task_id, + submission_id=checked.submission_id, + ) + if source is None or not ( + UUID(source.project_id) == checked.project_id + and UUID(source.task_id) == checked.task_id + and UUID(source.submission_id) == checked.submission_id + and source.submission_version == checked.submission_version + and UUID(source.assignment_id) == checked.assignment_id + and UUID(source.contributor_id) == checked.contributor_id + and source.contribution_policy_version_id + == checked.contribution_policy_version_id + and source.content_sha256 == checked.content_sha256 + and source.human_review_required is False + ): + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + + async def _lock_and_validate( + self, + request: TaskAcceptedEffectsRequest, + *, + expected_generation: int, + ) -> TaskAcceptedPreparation: + preparation, _, _ = await self._lock_and_validate_rows( + request, expected_generation=expected_generation + ) + return preparation + + async def _lock_and_validate_rows( + self, + request: TaskAcceptedEffectsRequest, + *, + expected_generation: int, + ): + """Retain the canonical fence and Task -> Assignment -> Submission locks.""" + await self._fence.acquire(expected_generation) + task = await self._repository.lock_project_task( + request.project_id, request.task_id + ) + if task is None: + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + assignment = await self._repository.lock_accepted_assignment( + project_id=request.project_id, + task_id=request.task_id, + assignment_id=request.assignment_id, + ) + latest_submission = await self._repository.get_latest_submission_for_task( + str(request.task_id), for_update=True, populate_existing=True + ) + if assignment is None or latest_submission is None: + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + + if ( + task.assigned_to != str(request.contributor_id) + or task.locked_contribution_policy_version_id + != request.contribution_policy_version_id + or task.locked_review_policy_id is None + or assignment.contributor_id != str(request.contributor_id) + or assignment.submitter_contribution_policy_version_id + != request.contribution_policy_version_id + or assignment.accepted_at is None + or assignment.released_at is not None + or latest_submission.id != str(request.submission_id) + or latest_submission.version != request.submission_version + or latest_submission.task_assignment_id != str(request.assignment_id) + or latest_submission.contributor_id != str(request.contributor_id) + or latest_submission.contribution_policy_version_id + != request.contribution_policy_version_id + or latest_submission.artifact_content_id != str(request.content_id) + or latest_submission.status != "submitted" + or latest_submission.locked_review_policy_id + != task.locked_review_policy_id + ): + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + + if ( + task.status == request.expected_task_status + and assignment.status == "active" + ): + disposition: Literal["new", "replay"] = "new" + elif task.status == "accepted" and assignment.status == "completed": + disposition = "replay" + else: + raise TaskAcceptedEffectsUnavailable("task_accepted_effects_unavailable") + try: + preparation = TaskAcceptedPreparation( + disposition=disposition, + locked_review_policy_id=UUID(task.locked_review_policy_id), + ) + except (TypeError, ValueError, ValidationError) as exc: + raise TaskAcceptedEffectsUnavailable( + "task_accepted_effects_unavailable" + ) from exc + return preparation, task, assignment + + @staticmethod + def _validate_request( + request: TaskAcceptedEffectsRequest, + ) -> TaskAcceptedEffectsRequest: + try: + return TaskAcceptedEffectsRequest.model_validate(request) + except (TypeError, ValueError, ValidationError) as exc: + raise TaskAcceptedEffectsUnavailable( + "task_accepted_effects_unavailable" + ) from exc + + +__all__ = ("TaskAcceptedEffectsParticipant",) diff --git a/backend/app/modules/tasks/api/__init__.py b/backend/app/modules/tasks/api/__init__.py index 56f8d53b1..bd9770311 100644 --- a/backend/app/modules/tasks/api/__init__.py +++ b/backend/app/modules/tasks/api/__init__.py @@ -1,7 +1,9 @@ """Dependency-safe public API for the TASKS business module.""" from app.modules.tasks.api.accepted_effects import ( + TaskAcceptedEffectsFence, TaskAcceptedEffectsPort, + TaskAcceptedPreparation, TaskAcceptedEffectsRequest, TaskAcceptedEffectsResult, TaskAcceptedEffectsUnavailable, @@ -103,6 +105,8 @@ "OperationalTaskQueuePort", "OperationalTaskSummary", "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsFence", + "TaskAcceptedPreparation", "TaskAcceptedEffectsRequest", "TaskAcceptedEffectsResult", "TaskAcceptedEffectsUnavailable", diff --git a/backend/app/modules/tasks/api/accepted_effects.py b/backend/app/modules/tasks/api/accepted_effects.py index 2ce78bce1..cb809e425 100644 --- a/backend/app/modules/tasks/api/accepted_effects.py +++ b/backend/app/modules/tasks/api/accepted_effects.py @@ -7,6 +7,10 @@ _Sha256 = Annotated[StrictStr, Field(pattern=r"^sha256:[0-9a-f]{64}$")] _PositiveVersion = Annotated[StrictInt, Field(ge=1, le=2_147_483_647)] +_Generation = Annotated[StrictInt, Field(ge=0, le=9_223_372_036_854_775_807)] +_STRICT_FROZEN = ConfigDict( + extra="forbid", frozen=True, strict=True, revalidate_instances="always" +) class TaskAcceptedEffectsUnavailable(RuntimeError): @@ -16,7 +20,7 @@ class TaskAcceptedEffectsUnavailable(RuntimeError): class TaskAcceptedEffectsRequest(BaseModel): """Exact TASK identity and lineage selected by the acceptance owner.""" - model_config = ConfigDict(extra="forbid", frozen=True, strict=True) + model_config = _STRICT_FROZEN project_id: UUID task_id: UUID @@ -31,32 +35,72 @@ class TaskAcceptedEffectsRequest(BaseModel): expected_task_status: Literal["evaluation_pending", "review_pending"] +class TaskAcceptedPreparation(BaseModel): + """Observed TASK disposition and exact locked ReviewPolicy identity.""" + + model_config = _STRICT_FROZEN + + disposition: Literal["new", "replay"] + locked_review_policy_id: UUID + + class TaskAcceptedEffectsResult(BaseModel): """Exact accepted request identity and the two TASK-owned terminal states.""" - model_config = ConfigDict(extra="forbid", frozen=True, strict=True) + model_config = _STRICT_FROZEN request: TaskAcceptedEffectsRequest task_status: Literal["accepted"] assignment_status: Literal["completed"] +class TaskAcceptedEffectsFence(Protocol): + """Acquire the canonical externally owned lifecycle fence.""" + + async def acquire(self, expected_generation: _Generation) -> object: + """Retain the expected generation through the caller's transaction.""" + ... + + class TaskAcceptedEffectsPort(Protocol): - """Apply flush-only TASK effects inside the caller's one root transaction. + """Prepare and apply flush-only TASK effects in one caller transaction. The participant never commits, authorizes, creates REV or CON facts, or calls back into routing. """ + async def lock_accepted_effects( + self, + request: TaskAcceptedEffectsRequest, + *, + expected_generation: _Generation, + ) -> TaskAcceptedPreparation: + """Fence, lock, and report exact new or terminal-replay TASK state.""" + ... + async def apply_accepted_effects( - self, request: TaskAcceptedEffectsRequest + self, + request: TaskAcceptedEffectsRequest, + *, + disposition: Literal["new", "replay"], + expected_generation: _Generation, ) -> TaskAcceptedEffectsResult: - """Validate exact TASK state and lineage, then flush accepted effects.""" + """Revalidate the observed disposition, then flush terminal effects.""" + ... + + async def require_routing_source( + self, + request: TaskAcceptedEffectsRequest, + manifest_id: UUID, + ) -> None: + """Require an exact stored false-policy TASK routing manifest.""" ... __all__ = ( "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsFence", + "TaskAcceptedPreparation", "TaskAcceptedEffectsRequest", "TaskAcceptedEffectsResult", "TaskAcceptedEffectsUnavailable", diff --git a/backend/app/modules/tasks/repository.py b/backend/app/modules/tasks/repository.py index eb109f839..d73456553 100644 --- a/backend/app/modules/tasks/repository.py +++ b/backend/app/modules/tasks/repository.py @@ -38,6 +38,7 @@ TaskAssignment, WorkstreamTask, ) +from app.modules.tasks.post_submit_routing.models import TaskPostSubmitRoutingManifest def _unassigned_ready_task(): @@ -317,6 +318,44 @@ async def lock_project_task(self, project_id: UUID, task_id: UUID) -> Workstream ).with_for_update().execution_options(populate_existing=True) ) + async def lock_accepted_assignment( + self, + *, + project_id: UUID, + task_id: UUID, + assignment_id: UUID, + ) -> TaskAssignment | None: + """Lock only the requested project/task assignment after its Task lock.""" + with self._session.no_autoflush: + return await self._session.scalar( + select(TaskAssignment).where( + TaskAssignment.id == str(assignment_id), + TaskAssignment.task_id == str(task_id), + TaskAssignment.project_id == str(project_id), + ).with_for_update().execution_options(populate_existing=True) + ) + + async def read_routing_manifest( + self, + *, + manifest_id: UUID, + project_id: UUID, + task_id: UUID, + submission_id: UUID, + ) -> TaskPostSubmitRoutingManifest | None: + """Reload one immutable TASK routing source through exact owner scope.""" + with self._session.no_autoflush: + return await self._session.scalar( + select(TaskPostSubmitRoutingManifest) + .where( + TaskPostSubmitRoutingManifest.id == manifest_id, + TaskPostSubmitRoutingManifest.project_id == str(project_id), + TaskPostSubmitRoutingManifest.task_id == str(task_id), + TaskPostSubmitRoutingManifest.submission_id == str(submission_id), + ) + .execution_options(populate_existing=True) + ) + async def add_assignment(self, assignment: TaskAssignment) -> TaskAssignment: """Persist an assignment and refresh generated database fields. diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index ce288975f..25956b45d 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -164,6 +164,13 @@ "backend/app/modules/authorization/acceptance_source_contracts.py", }) REV_12A1_FENCE_TARGETS = frozenset({"backend/app/modules/reviews/api/lifecycle.py", "backend/app/modules/reviews/lifecycle/models.py", "backend/app/modules/reviews/lifecycle/fence.py"}) +REV_04C_REMOVED_TARGETS = frozenset({"backend/app/modules/reviews/acceptance/schemas.py"}) +REV_04C_PARTICIPATION_TARGETS = frozenset({ + "backend/app/modules/reviews/api/acceptance.py", + "backend/app/modules/reviews/acceptance/repository.py", + "backend/app/modules/reviews/acceptance/participant.py", + "backend/app/modules/tasks/accepted_effects.py", +}) REV_04B_ACCEPTANCE_TARGETS = frozenset({"backend/app/modules/reviews/acceptance/models.py", "backend/app/modules/reviews/acceptance/schemas.py"}) REV_04A_SOURCE_TARGETS = frozenset({"backend/app/modules/reviews/decision/models.py", "backend/app/modules/reviews/decision/schemas.py"}) REV_03B_PACKET_TARGETS = frozenset({'backend/app/modules/reviews/packet/models.py', 'backend/app/modules/reviews/packet/repository.py', 'backend/app/modules/reviews/packet/schemas.py'}) @@ -718,12 +725,16 @@ def _validate_additive_partition_transition( current_by_target = {item["target"]: item for item in current_assignments} _validate_policy_lineage_relocation(set(trusted_targets), set(current_by_target)) removed = set(trusted_targets) - set(current_by_target) + if removed & REV_04C_REMOVED_TARGETS and ( + "backend/app/modules/reviews/api/acceptance.py" not in current_by_target + ): + raise BehaviorOwnershipError("untrusted_partition_change") retained_trusted = [ item for item in trusted_assignments if item["target"] not in removed ] if ( trusted_targets != sorted(trusted_targets) - or removed - (ARCH_04D2_REMOVED_TARGETS | ARCH_03D_REMOVED_TARGETS | TASK_CHECKER_CLEANUP_REMOVED_TARGETS | ARCH_03C2_REMOVED_TARGETS | OUTBOX_IDENTITY_REMOVED_TARGETS | V01_BASELINE_REMOVED_TARGETS | POL_03B_REMOVED_TARGETS | POL_04B_REMOVED_TARGETS | POL_05A_REMOVED_TARGETS | ARCH_03A_GUIDE_CONTEXT_REMOVED_TARGETS | ARCH_04B_SEAM_REMOVED_TARGETS) + or removed - (REV_04C_REMOVED_TARGETS | ARCH_04D2_REMOVED_TARGETS | ARCH_03D_REMOVED_TARGETS | TASK_CHECKER_CLEANUP_REMOVED_TARGETS | ARCH_03C2_REMOVED_TARGETS | OUTBOX_IDENTITY_REMOVED_TARGETS | V01_BASELINE_REMOVED_TARGETS | POL_03B_REMOVED_TARGETS | POL_04B_REMOVED_TARGETS | POL_05A_REMOVED_TARGETS | ARCH_03A_GUIDE_CONTEXT_REMOVED_TARGETS | ARCH_04B_SEAM_REMOVED_TARGETS) or [current_by_target[item["target"]] for item in retained_trusted] != retained_trusted ): @@ -773,6 +784,7 @@ def _validate_additive_partition_transition( | AUTH_19A_SOURCE_CONTRACT_TARGETS | ARCH_04E2A_ROUTING_AUTH_TARGETS | REV_12A1_FENCE_TARGETS + | REV_04C_PARTICIPATION_TARGETS | REV_04B_ACCEPTANCE_TARGETS | REV_04A_SOURCE_TARGETS | REV_03B_PACKET_TARGETS diff --git a/backend/scripts/test_lane_catalogue.py b/backend/scripts/test_lane_catalogue.py index 3573fb19b..6d5b18e8a 100644 --- a/backend/scripts/test_lane_catalogue.py +++ b/backend/scripts/test_lane_catalogue.py @@ -251,6 +251,11 @@ class TestLane: ) PROJECT_MODULES = ( + "tests/reviews/acceptance/test_participant_contracts.py", + "tests/reviews/acceptance/test_participation.py", + "tests/reviews/acceptance/test_participation_transactions.py", + "tests/tasks/accepted_effects/test_contracts.py", + "tests/tasks/accepted_effects/test_postgresql.py", # Frozen policy participation shares measured project capacity, not the full schema lane. "tests/contributions/records/test_contracts.py", "tests/contributions/records/test_storage.py", diff --git a/backend/tests/contributions/participation/support.py b/backend/tests/contributions/participation/support.py index b9922881e..7cd38e89c 100644 --- a/backend/tests/contributions/participation/support.py +++ b/backend/tests/contributions/participation/support.py @@ -6,17 +6,16 @@ from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence -def request_for(h, **changes): +def request_for(h, *, acceptance_disposition, **changes): values = { + "acceptance_disposition": acceptance_disposition, "project_id": h.submitter_record.project_id, "task_id": h.submitter_record.task_id, "submission_id": h.submitter_record.submission_id, "final_acceptance_id": h.submitter_record.source_final_acceptance_id, "task_assignment_id": h.submitter_record.source_task_assignment_id, "contributor_id": h.submitter_record.contributor_id, - "contribution_policy_version_id": ( - h.submitter_record.contribution_policy_version_id - ), + "contribution_policy_version_id": (h.submitter_record.contribution_policy_version_id), "artifact_hash": h.submitter_record.artifact_hash, "correlation_id": h.submitter_record.id, "expected_generation": 0, diff --git a/backend/tests/contributions/participation/test_contracts.py b/backend/tests/contributions/participation/test_contracts.py index bd7ff3028..1e81d9bc6 100644 --- a/backend/tests/contributions/participation/test_contracts.py +++ b/backend/tests/contributions/participation/test_contracts.py @@ -8,29 +8,45 @@ from app.core.identifiers import new_record_id from app.modules.compensation.api import ( - AwardContributionFacts, AwardDefinitionFacts, CompleteAwardSetRequest, + AwardContributionFacts, + AwardDefinitionFacts, + CompleteAwardSetRequest, CompensationInstrumentType, ) from app.modules.contributions.api import ( - ContributionParticipationUnavailable, SubmitterParticipationRequest, + ContributionParticipationUnavailable, + SubmitterParticipationRequest, ) from app.modules.contributions.records.participant import SubmitterContributionParticipant def request(): return SubmitterParticipationRequest( - project_id=new_record_id(), task_id=new_record_id(), submission_id=new_record_id(), - final_acceptance_id=new_record_id(), task_assignment_id=new_record_id(), - contributor_id=new_record_id(), contribution_policy_version_id=new_record_id(), - artifact_hash="sha256:" + "a" * 64, correlation_id=new_record_id(), expected_generation=0, + acceptance_disposition="new", + project_id=new_record_id(), + task_id=new_record_id(), + submission_id=new_record_id(), + final_acceptance_id=new_record_id(), + task_assignment_id=new_record_id(), + contributor_id=new_record_id(), + contribution_policy_version_id=new_record_id(), + artifact_hash="sha256:" + "a" * 64, + correlation_id=new_record_id(), + expected_generation=0, ) -@pytest.mark.parametrize("change", [ - {"project_id": "01900000-0000-7000-8000-000000000001"}, - {"expected_generation": True}, {"expected_generation": -1}, - {"artifact_hash": "unverified"}, {"source_review_id": new_record_id()}, -]) +@pytest.mark.parametrize( + "change", + [ + {"acceptance_disposition": "repair"}, + {"project_id": "01900000-0000-7000-8000-000000000001"}, + {"expected_generation": True}, + {"expected_generation": -1}, + {"artifact_hash": "unverified"}, + {"source_review_id": new_record_id()}, + ], +) async def test_invalid_constructed_input_rejects_before_fence_or_owner_work(change): original = request() with pytest.raises(ValidationError): @@ -45,20 +61,37 @@ async def test_invalid_constructed_input_rejects_before_fence_or_owner_work(chan assert awards.mock_calls == [] +def test_acceptance_disposition_is_required_without_a_compatibility_default(): + values = request().model_dump() + values.pop("acceptance_disposition") + with pytest.raises(ValidationError): + SubmitterParticipationRequest(**values) + + def award_request(): contribution = AwardContributionFacts( - id=new_record_id(), project_id=new_record_id(), contributor_id=new_record_id(), - contribution_policy_version_id=new_record_id(), contribution_type="accepted_submission", + id=new_record_id(), + project_id=new_record_id(), + contributor_id=new_record_id(), + contribution_policy_version_id=new_record_id(), + contribution_type="accepted_submission", ) definition = AwardDefinitionFacts( - id=new_record_id(), project_id=contribution.project_id, + id=new_record_id(), + project_id=contribution.project_id, contribution_policy_version_id=contribution.contribution_policy_version_id, - contribution_type="accepted_submission", instrument_type=CompensationInstrumentType.MONEY, - unit_code="USD", quantity=Decimal("2.125000000000000001"), adapter_binding_id=new_record_id(), + contribution_type="accepted_submission", + instrument_type=CompensationInstrumentType.MONEY, + unit_code="USD", + quantity=Decimal("2.125000000000000001"), + adapter_binding_id=new_record_id(), ) return CompleteAwardSetRequest( - disposition="new", compensation_mode="compensated", contribution=contribution, - definitions=(definition,), correlation_id=new_record_id(), + disposition="new", + compensation_mode="compensated", + contribution=contribution, + definitions=(definition,), + correlation_id=new_record_id(), ) @@ -66,21 +99,26 @@ def test_award_contract_rejects_incomplete_duplicate_and_foreign_definitions(): valid = award_request() assert valid.definitions[0].quantity == Decimal("2.125000000000000001") changes = [ - {"definitions": ()}, {"compensation_mode": "unpaid"}, + {"definitions": ()}, + {"compensation_mode": "unpaid"}, {"definitions": valid.definitions * 2}, ] for field in ("project_id", "contribution_policy_version_id"): - changes.append({"definitions": ( - valid.definitions[0].model_copy(update={field: new_record_id()}), - )}) + changes.append( + {"definitions": (valid.definitions[0].model_copy(update={field: new_record_id()}),)} + ) for change in changes: with pytest.raises(ValidationError): CompleteAwardSetRequest.model_validate(valid.model_copy(update=change)) - unpaid = CompleteAwardSetRequest.model_validate(valid.model_copy( - update={"compensation_mode": "unpaid", "definitions": ()}, - )) + unpaid = CompleteAwardSetRequest.model_validate( + valid.model_copy( + update={"compensation_mode": "unpaid", "definitions": ()}, + ) + ) assert unpaid.definitions == () with pytest.raises(ValidationError): - AwardDefinitionFacts.model_validate(valid.definitions[0].model_copy( - update={"quantity": 2.125}, - )) + AwardDefinitionFacts.model_validate( + valid.definitions[0].model_copy( + update={"quantity": 2.125}, + ) + ) diff --git a/backend/tests/contributions/participation/test_postgresql.py b/backend/tests/contributions/participation/test_postgresql.py index 9ea48f075..b73165114 100644 --- a/backend/tests/contributions/participation/test_postgresql.py +++ b/backend/tests/contributions/participation/test_postgresql.py @@ -2,7 +2,6 @@ import pytest from sqlalchemy import text -from sqlalchemy.exc import DBAPIError from app.core.identifiers import new_record_id from app.modules.compensation.api import ( @@ -30,6 +29,7 @@ from .support import participant, request_for + @pytest.mark.parametrize( "instruments", [(), ("money",), ("project_points",), ("money", "project_points")], @@ -41,22 +41,36 @@ async def test_create_and_exact_replay_all_frozen_award_shapes( async with contribution_source( tmp_path, isolated_database_env, contribution_awards=instruments ) as h: - request = request_for(h, correlation_id=new_record_id()) + request = request_for(h, acceptance_disposition="new", correlation_id=new_record_id()) async with h.factory() as session, session.begin(): created = await participant(session).participate_submitter(request) async with h.factory() as session, session.begin(): - replayed = await participant(session).participate_submitter(request) + replayed = await participant(session).participate_submitter( + request.model_copy(update={"acceptance_disposition": "replay"}) + ) assert replayed == created assert created.contribution.source_final_acceptance_id == h.acceptance.id - assert created.contribution.source_task_assignment_id == h.submitter_record.source_task_assignment_id + assert ( + created.contribution.source_task_assignment_id + == h.submitter_record.source_task_assignment_id + ) assert created.contribution.artifact_hash == h.submitter_record.artifact_hash assert {award.instrument_type.value for award in created.awards} == set(instruments) async with h.factory() as session: - record = (await session.execute(text(""" + record = ( + ( + await session.execute( + text(""" SELECT * FROM public.contribution_records WHERE id=:record_id - """), {"record_id": created.contribution.id})).mappings().one() + """), + {"record_id": created.contribution.id}, + ) + ) + .mappings() + .one() + ) stored_contribution = SubmitterContributionFacts( id=record["id"], project_id=record["project_id"], @@ -73,7 +87,10 @@ async def test_create_and_exact_replay_all_frozen_award_shapes( assert record["contribution_type"] == "accepted_submission" assert record["source_review_id"] is None assert record["source_review_lease_id"] is None - stored = (await session.execute(text(""" + stored = ( + ( + await session.execute( + text(""" SELECT a.*, d.quantity AS definition_quantity, d.unit_code AS definition_unit, d.adapter_binding_id AS definition_binding, @@ -85,7 +102,13 @@ async def test_create_and_exact_replay_all_frozen_award_shapes( JOIN public.contribution_award_definitions d ON d.id=a.award_definition_id WHERE a.contribution_record_id=:record_id ORDER BY a.instrument_type - """), {"record_id": created.contribution.id})).mappings().all() + """), + {"record_id": created.contribution.id}, + ) + ) + .mappings() + .all() + ) assert len(stored) == len(instruments) stored_awards = tuple( CompensationAwardFacts( @@ -93,14 +116,10 @@ async def test_create_and_exact_replay_all_frozen_award_shapes( project_id=award["project_id"], contribution_record_id=award["contribution_record_id"], contributor_id=award["contributor_id"], - contribution_policy_version_id=award[ - "contribution_policy_version_id" - ], + contribution_policy_version_id=award["contribution_policy_version_id"], award_definition_id=award["award_definition_id"], adapter_binding_id=award["adapter_binding_id"], - instrument_type=CompensationInstrumentType( - award["instrument_type"] - ), + instrument_type=CompensationInstrumentType(award["instrument_type"]), unit_code=award["unit_code"], quantity=award["quantity"], created_at=award["created_at"], @@ -118,10 +137,50 @@ async def test_create_and_exact_replay_all_frozen_award_shapes( assert award["instrument_type"] == award["definition_instrument"] assert award["definition_type"] == "accepted_submission" assert award["correlation_id"] == request.correlation_id - assert await session.scalar(text( - "SELECT count(*) FROM public.contribution_records " - "WHERE contribution_type='completed_review'" - )) == 0 + assert ( + await session.scalar( + text( + "SELECT count(*) FROM public.contribution_records " + "WHERE contribution_type='completed_review'" + ) + ) + == 0 + ) + + +async def test_missing_replay_conflicts_and_outer_commit_leaves_no_economic_facts( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + request = request_for(h, acceptance_disposition="replay", correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + with pytest.raises(ContributionParticipationConflict): + await participant(session).participate_submitter(request) + + async with h.factory() as session: + assert await rows(session, "contribution_records") == [] + assert await rows(session, "compensation_awards") == [] + + +async def test_new_against_existing_exact_facts_conflicts_without_replay( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + request = request_for(h, acceptance_disposition="new", correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + created = await participant(session).participate_submitter(request) + + async with h.factory() as session, session.begin(): + with pytest.raises(ContributionParticipationConflict): + await participant(session).participate_submitter(request) + + async with h.factory() as session: + stored_records = await rows(session, "contribution_records") + stored_awards = await rows(session, "compensation_awards") + assert [row["id"] for row in stored_records] == [str(created.contribution.id)] + assert {row["id"] for row in stored_awards} == { + str(award.id) for award in created.awards + } async def test_retired_policy_and_suspended_bindings_keep_frozen_awards_due( @@ -131,7 +190,11 @@ async def test_retired_policy_and_suspended_bindings_keep_frozen_awards_due( await retire_policy_and_suspend_bindings(h) async with h.factory() as session, session.begin(): result = await participant(session).participate_submitter( - request_for(h, correlation_id=new_record_id()) + request_for( + h, + acceptance_disposition="new", + correlation_id=new_record_id(), + ) ) assert {award.instrument_type.value for award in result.awards} == { "money", @@ -150,10 +213,15 @@ async def test_exact_replay_rejects_each_changed_source_fact_and_real_foreign_so provision_services=False, storage_settings=h.settings, ) as foreign: - original = request_for(h, correlation_id=new_record_id()) - foreign_request = request_for(foreign, correlation_id=original.correlation_id) + original = request_for(h, acceptance_disposition="new", correlation_id=new_record_id()) + foreign_request = request_for( + foreign, + acceptance_disposition="replay", + correlation_id=original.correlation_id, + ) async with h.factory() as session, session.begin(): created = await participant(session).participate_submitter(original) + replay = original.model_copy(update={"acceptance_disposition": "replay"}) conflict_fields = ( "task_id", @@ -162,42 +230,34 @@ async def test_exact_replay_rejects_each_changed_source_fact_and_real_foreign_so "contributor_id", ) for field in conflict_fields: - async with h.factory() as session: - with pytest.raises(DBAPIError, match="contribution .* mismatch"): - async with session.begin(): - await participant(session).participate_submitter( - original.model_copy( - update={field: getattr(foreign_request, field)} - ) - ) - await session.rollback() + async with h.factory() as session, session.begin(): + with pytest.raises(ContributionParticipationConflict): + await participant(session).participate_submitter( + replay.model_copy(update={field: getattr(foreign_request, field)}) + ) changed_digest = "sha256:" + ( "0" * 64 if original.artifact_hash != "sha256:" + "0" * 64 else "1" * 64 ) - async with h.factory() as session: - with pytest.raises(DBAPIError, match="contribution .* mismatch"): - async with session.begin(): - await participant(session).participate_submitter( - original.model_copy(update={"artifact_hash": changed_digest}) - ) - await session.rollback() + async with h.factory() as session, session.begin(): + with pytest.raises(ContributionParticipationConflict): + await participant(session).participate_submitter( + replay.model_copy(update={"artifact_hash": changed_digest}) + ) for field in ("project_id", "contribution_policy_version_id"): async with h.factory() as session, session.begin(): - with pytest.raises(RuntimeError, match="contribution_participation_unavailable"): + with pytest.raises( + RuntimeError, match="contribution_participation_unavailable" + ): await participant(session).participate_submitter( - original.model_copy(update={field: getattr(foreign_request, field)}) + replay.model_copy(update={field: getattr(foreign_request, field)}) ) - async with h.factory() as session: - with pytest.raises(DBAPIError, match="contribution submitter source mismatch"): - async with session.begin(): - await participant(session).participate_submitter( - original.model_copy( - update={ - "final_acceptance_id": foreign_request.final_acceptance_id - } - ) + async with h.factory() as session, session.begin(): + with pytest.raises(ContributionParticipationConflict): + await participant(session).participate_submitter( + replay.model_copy( + update={"final_acceptance_id": foreign_request.final_acceptance_id} ) - await session.rollback() + ) assert len(await rows(session, "contribution_records")) == 1 assert len(await rows(session, "compensation_awards")) == 2 assert created.contribution.id is not None @@ -207,13 +267,20 @@ async def test_paid_correlation_conflicts_but_unpaid_replay_is_honest( tmp_path, isolated_database_env ): async with contribution_source(tmp_path / "paid", isolated_database_env, paid=True) as paid: - paid_request = request_for(paid, correlation_id=new_record_id()) + paid_request = request_for( + paid, acceptance_disposition="new", correlation_id=new_record_id() + ) async with paid.factory() as session, session.begin(): created = await participant(session).participate_submitter(paid_request) async with paid.factory() as session, session.begin(): with pytest.raises(ContributionParticipationConflict): await participant(session).participate_submitter( - paid_request.model_copy(update={"correlation_id": new_record_id()}) + paid_request.model_copy( + update={ + "acceptance_disposition": "replay", + "correlation_id": new_record_id(), + } + ) ) async with paid.factory() as session: assert len(await rows(session, "compensation_awards")) == len(created.awards) == 2 @@ -224,12 +291,19 @@ async def test_paid_correlation_conflicts_but_unpaid_replay_is_honest( provision_services=False, storage_settings=paid.settings, ) as unpaid: - first_request = request_for(unpaid, correlation_id=new_record_id()) + first_request = request_for( + unpaid, acceptance_disposition="new", correlation_id=new_record_id() + ) async with unpaid.factory() as session, session.begin(): first = await participant(session).participate_submitter(first_request) async with unpaid.factory() as session, session.begin(): second = await participant(session).participate_submitter( - first_request.model_copy(update={"correlation_id": new_record_id()}) + first_request.model_copy( + update={ + "acceptance_disposition": "replay", + "correlation_id": new_record_id(), + } + ) ) assert second == first assert second.awards == () @@ -239,7 +313,7 @@ async def test_partial_same_transaction_replay_rejects_without_backfill( tmp_path, isolated_database_env, monkeypatch ): async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: - request = request_for(h, correlation_id=new_record_id()) + request = request_for(h, acceptance_disposition="replay", correlation_id=new_record_id()) async with h.factory() as session: await session.begin() await PostgresJointLifecycleMutationFence(session).acquire(0) @@ -263,9 +337,7 @@ async def test_partial_same_transaction_replay_rejects_without_backfill( h.submitter_record.contribution_policy_version_id ), contribution_type="accepted_submission", - instrument_type=CompensationInstrumentType( - award["instrument_type"] - ), + instrument_type=CompensationInstrumentType(award["instrument_type"]), unit_code=award["unit_code"], quantity=award["quantity"], adapter_binding_id=award["adapter_binding_id"], diff --git a/backend/tests/contributions/participation/test_transactions.py b/backend/tests/contributions/participation/test_transactions.py index 0b2d0519f..a2abc145e 100644 --- a/backend/tests/contributions/participation/test_transactions.py +++ b/backend/tests/contributions/participation/test_transactions.py @@ -1,7 +1,4 @@ -"""Caller transaction, rollback, fence, and PostgreSQL winner proof.""" - -import asyncio -from contextlib import suppress +"""Caller transaction, rollback, and canonical fence proof.""" import pytest from sqlalchemy import text @@ -12,17 +9,17 @@ from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence from tests.contributions.records.support import contribution_source, rows from tests.reviews.acceptance.support import insert_acceptance -from tests.reviews.packet.test_repository import wait_for_blocker from .support import participant, request_for + async def test_later_sql_failure_rolls_back_acceptance_contribution_and_awards( tmp_path, isolated_database_env ): async with contribution_source( tmp_path, isolated_database_env, paid=True, persist_acceptance=False ) as h: - request = request_for(h, correlation_id=new_record_id()) + request = request_for(h, acceptance_disposition="new", correlation_id=new_record_id()) async with h.factory() as session: with pytest.raises(DBAPIError, match="division by zero"): async with session.begin(): @@ -38,65 +35,11 @@ async def test_later_sql_failure_rolls_back_acceptance_contribution_and_awards( assert await rows(session, "compensation_awards") == [] -@pytest.mark.parametrize("commit_first", [True, False], ids=("commit-wins", "rollback-loses")) -async def test_concurrent_participants_converge_on_committed_winner_ids( - tmp_path, isolated_database_env, commit_first -): - async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: - request = request_for(h, correlation_id=new_record_id()) - ready = asyncio.Future() - - async def competitor(): - async with h.factory() as session: - await session.begin() - ready.set_result(await session.scalar(text("SELECT pg_backend_pid()"))) - try: - result = await participant(session).participate_submitter(request) - await session.commit() - return result - except BaseException: - await session.rollback() - raise - - async with h.factory() as first_session: - await first_session.begin() - first = await participant(first_session).participate_submitter(request) - pending = asyncio.create_task(competitor()) - try: - await wait_for_blocker(h.factory, await ready) - await ( - first_session.commit() if commit_first else first_session.rollback() - ) - winner = await asyncio.wait_for(pending, 10) - finally: - if not pending.done(): - pending.cancel() - with suppress(asyncio.CancelledError): - await pending - - if commit_first: - assert winner == first - else: - assert winner.contribution.id != first.contribution.id - assert {award.id for award in winner.awards}.isdisjoint( - {award.id for award in first.awards} - ) - async with h.factory() as session: - stored_records = await rows(session, "contribution_records") - stored_awards = await rows(session, "compensation_awards") - assert [row["id"] for row in stored_records] == [ - str(winner.contribution.id) - ] - assert {row["id"] for row in stored_awards} == { - str(award.id) for award in winner.awards - } - - async def test_canonical_fence_rejects_missing_and_savepoint_transactions( tmp_path, isolated_database_env ): async with contribution_source(tmp_path, isolated_database_env) as h: - request = request_for(h, correlation_id=new_record_id()) + request = request_for(h, acceptance_disposition="new", correlation_id=new_record_id()) async with h.factory() as session: with pytest.raises(JointLifecycleUnavailable, match="root transaction"): await participant(session).participate_submitter(request) @@ -110,9 +53,7 @@ async def test_canonical_fence_rejects_missing_and_savepoint_transactions( async with h.factory() as session: await session.begin() await session.execute(text("SAVEPOINT caller_raw_savepoint")) - with pytest.raises( - JointLifecycleUnavailable, match="root database transaction" - ): + with pytest.raises(JointLifecycleUnavailable, match="root database transaction"): await participant(session).participate_submitter(request) await session.rollback() diff --git a/backend/tests/reviews/acceptance/participation_support.py b/backend/tests/reviews/acceptance/participation_support.py new file mode 100644 index 000000000..3aa2a3e38 --- /dev/null +++ b/backend/tests/reviews/acceptance/participation_support.py @@ -0,0 +1,139 @@ +"""Real human-source composition for hidden shared-acceptance integration proof.""" + +from uuid import UUID + +from sqlalchemy import text + +from app.core.identifiers import new_record_id +from app.modules.compensation.awards.participant import CompensationAwardParticipant +from app.modules.contributions.records.participant import SubmitterContributionParticipant +from app.modules.reviews.acceptance.participant import FinalAcceptanceParticipant +from app.modules.reviews.api.acceptance import FinalAcceptanceRequest +from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence +from app.modules.tasks.accepted_effects import TaskAcceptedEffectsParticipant +from app.modules.tasks.api.accepted_effects import TaskAcceptedEffectsRequest + + +async def prepare_review_pending(h) -> None: + """Arrange the retained human-review prestate without claiming runtime authority.""" + async with h.factory() as session, session.begin(): + result = await session.execute( + text( + "UPDATE public.workstream_tasks SET status='review_pending' " + "WHERE id=:task_id AND project_id=:project_id" + ), + {"task_id": h.acceptance.task_id, "project_id": h.acceptance.project_id}, + ) + assert result.rowcount == 1 + result = await session.execute( + text( + "UPDATE public.task_assignments " + "SET accepted_at=coalesce(accepted_at, clock_timestamp()) " + "WHERE id=:assignment_id AND status='active' AND released_at IS NULL" + ), + {"assignment_id": h.review.task_assignment_id}, + ) + assert result.rowcount == 1 + state = ( + await session.execute( + text( + "SELECT task.status AS task_status, assignment.status AS assignment_status, " + "assignment.accepted_at, assignment.released_at " + "FROM public.workstream_tasks task " + "JOIN public.task_assignments assignment ON assignment.id=:assignment_id " + "WHERE task.id=:task_id" + ), + { + "task_id": h.acceptance.task_id, + "assignment_id": h.review.task_assignment_id, + }, + ) + ).mappings().one() + assert state["task_status"] == "review_pending" + assert state["assignment_status"] == "active" + assert state["accepted_at"] is not None + assert state["released_at"] is None + + +async def request_for(h, **changes) -> FinalAcceptanceRequest: + async with h.factory() as session: + source = ( + await session.execute( + text( + "SELECT s.version, s.task_assignment_id, s.contributor_id, " + "s.contribution_policy_version_id, s.artifact_content_id, content.sha256 " + "FROM public.submissions s " + "JOIN public.artifact_contents content ON content.id=s.artifact_content_id " + "WHERE s.id=:submission_id" + ), + {"submission_id": h.acceptance.submission_id}, + ) + ).mappings().one() + values = { + "acceptance": h.acceptance, + "task_effects": TaskAcceptedEffectsRequest( + project_id=h.acceptance.project_id, + task_id=h.acceptance.task_id, + assignment_id=UUID(str(source["task_assignment_id"])), + submission_id=h.acceptance.submission_id, + submission_version=source["version"], + contributor_id=UUID(str(source["contributor_id"])), + contribution_policy_version_id=source["contribution_policy_version_id"], + content_id=UUID(str(source["artifact_content_id"])), + content_sha256=source["sha256"], + final_acceptance_id=h.acceptance.id, + expected_task_status="review_pending", + ), + "correlation_id": new_record_id(), + "expected_generation": 0, + } + values.update(changes) + return FinalAcceptanceRequest(**values) + + +def participant(session) -> FinalAcceptanceParticipant: + fence = PostgresJointLifecycleMutationFence(session) + tasks = TaskAcceptedEffectsParticipant(session, fence=fence) + contributions = SubmitterContributionParticipant( + session, + fence=fence, + awards=CompensationAwardParticipant(session), + ) + return FinalAcceptanceParticipant( + session, + fence=fence, + tasks=tasks, + contributions=contributions, + ) + + +async def stored_effects(session, task_id) -> dict[str, object]: + row = ( + await session.execute( + text( + "SELECT task.status AS task_status, assignment.status AS assignment_status, " + "(SELECT count(*) FROM public.final_acceptances) AS acceptances, " + "(SELECT count(*) FROM public.contribution_records " + " WHERE contribution_type='accepted_submission') AS contributions, " + "(SELECT count(*) FROM public.contribution_records " + " WHERE contribution_type='completed_review') AS reviewer_contributions, " + "(SELECT count(*) FROM public.compensation_awards) AS awards " + "FROM public.workstream_tasks task " + "JOIN public.task_assignments assignment ON assignment.task_id=task.id " + "WHERE task.id=:task_id" + ), + {"task_id": task_id}, + ) + ).mappings().one() + return dict(row) + + +async def stage_terminal_task(session, request: FinalAcceptanceRequest) -> None: + await session.execute( + text("UPDATE public.workstream_tasks SET status='accepted' WHERE id=:id"), + {"id": request.task_effects.task_id}, + ) + await session.execute( + text("UPDATE public.task_assignments SET status='completed' WHERE id=:id"), + {"id": request.task_effects.assignment_id}, + ) diff --git a/backend/tests/reviews/acceptance/support.py b/backend/tests/reviews/acceptance/support.py index 730e09c6e..bc9ad80c6 100644 --- a/backend/tests/reviews/acceptance/support.py +++ b/backend/tests/reviews/acceptance/support.py @@ -7,7 +7,7 @@ from app.core.identifiers import new_record_id from app.modules.reviews.acceptance.models import FinalAcceptance -from app.modules.reviews.acceptance.schemas import FinalAcceptanceInput +from app.modules.reviews.api.acceptance import FinalAcceptanceInput from tests.reviews.decision.support import finding, insert_review, review_source diff --git a/backend/tests/reviews/acceptance/test_contracts.py b/backend/tests/reviews/acceptance/test_contracts.py index 9723dfaf8..80b343923 100644 --- a/backend/tests/reviews/acceptance/test_contracts.py +++ b/backend/tests/reviews/acceptance/test_contracts.py @@ -4,7 +4,7 @@ from pydantic import ValidationError from app.core.identifiers import new_record_id -from app.modules.reviews.acceptance.schemas import FinalAcceptanceInput +from app.modules.reviews.api.acceptance import FinalAcceptanceInput def values(source="human_review"): diff --git a/backend/tests/reviews/acceptance/test_participant_contracts.py b/backend/tests/reviews/acceptance/test_participant_contracts.py new file mode 100644 index 000000000..c0c6eefd2 --- /dev/null +++ b/backend/tests/reviews/acceptance/test_participant_contracts.py @@ -0,0 +1,119 @@ +"""Closed shared effects and structural exclusion from production composition.""" + +import ast +from pathlib import Path +from unittest.mock import AsyncMock + +import pytest +from pydantic import ValidationError + +from app.core.identifiers import new_record_id +from app.modules.authorization.catalogue import ACTION_BY_ID, ActionAvailability, ActionId +from app.modules.reviews.acceptance.participant import FinalAcceptanceParticipant +from app.modules.reviews.api.acceptance import FinalAcceptanceInput +from app.modules.reviews.api.acceptance import FinalAcceptanceConflict, FinalAcceptanceRequest +from app.modules.tasks.api.accepted_effects import TaskAcceptedEffectsRequest +from tests.reviews.acceptance.test_contracts import values + + +def request(): + source = FinalAcceptanceInput(**values()) + return FinalAcceptanceRequest( + acceptance=source, + task_effects=TaskAcceptedEffectsRequest( + project_id=source.project_id, task_id=source.task_id, + assignment_id=new_record_id(), submission_id=source.submission_id, + submission_version=1, contributor_id=source.accepted_submitter_id, + contribution_policy_version_id=new_record_id(), content_id=new_record_id(), + content_sha256="sha256:" + "a" * 64, final_acceptance_id=source.id, + expected_task_status="review_pending", + ), correlation_id=new_record_id(), expected_generation=0, + ) + + +@pytest.mark.parametrize("field", [ + "project_id", "task_id", "submission_id", "contributor_id", "final_acceptance_id", + "expected_task_status", "submission_version", "content_sha256", +]) +async def test_inconsistent_nested_effects_reject_before_any_owner(field): + original = request() + replacement = { + "expected_task_status": "evaluation_pending", "submission_version": True, + "content_sha256": "not-a-hash", + }.get(field, new_record_id()) + forged = original.model_copy(update={ + "task_effects": original.task_effects.model_copy(update={field: replacement}), + }) + with pytest.raises(ValidationError): + FinalAcceptanceRequest.model_validate(forged) + session, fence, tasks, contributions = (AsyncMock() for _ in range(4)) + owner = FinalAcceptanceParticipant(session, fence=fence, tasks=tasks, contributions=contributions) + with pytest.raises(FinalAcceptanceConflict): + await owner.participate(forged) + for collaborator in (session, fence, tasks, contributions): + assert collaborator.mock_calls == [] + + +@pytest.mark.parametrize("change", [ + {"expected_generation": True}, {"expected_generation": -1}, + {"correlation_id": str(new_record_id())}, {"authorized": True}, +]) +def test_shared_request_rejects_coercion_and_extra_authority(change): + with pytest.raises(ValidationError): + FinalAcceptanceRequest(**(request().model_dump() | change)) + + +def test_automated_source_transport_requires_evaluation_prestate_without_review(): + original = request() + source = original.acceptance.model_copy(update={ + "acceptance_source": "task_post_submit_route", + "source_review_id": None, + "source_routing_manifest_id": new_record_id(), + }) + automated = FinalAcceptanceRequest.model_validate(original.model_copy(update={ + "acceptance": source, + "task_effects": original.task_effects.model_copy(update={ + "expected_task_status": "evaluation_pending", + }), + })) + assert automated.acceptance.source_review_id is None + with pytest.raises(ValidationError, match="acceptance effect lineage differs"): + FinalAcceptanceRequest.model_validate(automated.model_copy(update={ + "task_effects": original.task_effects, + })) + + +def test_hidden_participant_has_no_production_entry_and_actions_remain_planned(): + app_root = Path(__file__).resolve().parents[3] / "app" + participant_path = app_root / "modules/reviews/acceptance/participant.py" + found_definition = False + for path in app_root.rglob("*.py"): + tree = ast.parse(path.read_text(), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == "FinalAcceptanceParticipant": + assert path == participant_path + found_definition = True + if isinstance(node, ast.ImportFrom): + assert node.module != "app.modules.reviews.acceptance.participant", str(path) + assert all(alias.name != "FinalAcceptanceParticipant" for alias in node.names), str(path) + if node.level and node.module in {"participant", "acceptance.participant"}: + assert "reviews" not in path.parts, str(path) + if isinstance(node, ast.Import): + assert all( + alias.name != "app.modules.reviews.acceptance.participant" + for alias in node.names + ), str(path) + if isinstance(node, ast.Call): + called = node.func + assert not ( + isinstance(called, ast.Name) and called.id == "FinalAcceptanceParticipant" + or isinstance(called, ast.Attribute) and called.attr == "FinalAcceptanceParticipant" + ), str(path) + if isinstance(node, ast.Constant) and isinstance(node.value, str): + assert node.value not in { + "app.modules.reviews.acceptance.participant", + "app.modules.reviews.acceptance.participant.FinalAcceptanceParticipant", + }, str(path) + assert found_definition + for action in (ActionId.REVIEW_DECISION, ActionId.TASK_POST_SUBMIT_ROUTE): + assert ACTION_BY_ID[action].availability is ActionAvailability.PLANNED diff --git a/backend/tests/reviews/acceptance/test_participation.py b/backend/tests/reviews/acceptance/test_participation.py new file mode 100644 index 000000000..c8443fd1c --- /dev/null +++ b/backend/tests/reviews/acceptance/test_participation.py @@ -0,0 +1,171 @@ +"""Real human-source shared-acceptance lifecycle and economic proof.""" + +from uuid import UUID + +import pytest +from sqlalchemy import text + +from app.core.identifiers import new_record_id +from app.modules.reviews.api.acceptance import FinalAcceptanceConflict +from tests.contributions.records.support import contribution_source + +from .participation_support import ( + participant, + prepare_review_pending, + request_for, + stored_effects, +) + + +@pytest.mark.parametrize( + "instruments", + [(), ("money",), ("money", "project_points")], + ids=("unpaid", "one-award", "two-awards"), +) +async def test_human_acceptance_creates_and_exactly_replays_complete_effects( + tmp_path, isolated_database_env, instruments +): + async with contribution_source( + tmp_path, + isolated_database_env, + contribution_awards=instruments, + persist_acceptance=False, + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + + async with h.factory() as session, session.begin(): + created = await participant(session).participate(request) + async with h.factory() as session, session.begin(): + replayed = await participant(session).participate(request) + + assert replayed == created + assert created.acceptance.id == request.acceptance.id + assert created.acceptance.accepted_at == replayed.acceptance.accepted_at + assert created.task_effects.request == request.task_effects + assert created.task_effects.task_status == "accepted" + assert created.task_effects.assignment_status == "completed" + assert created.participation.contribution.source_final_acceptance_id == created.acceptance.id + assert created.participation.contribution.source_task_assignment_id == ( + request.task_effects.assignment_id + ) + assert {award.instrument_type.value for award in created.participation.awards} == set( + instruments + ) + + async with h.factory() as session: + acceptance = ( + await session.execute( + text("SELECT * FROM public.final_acceptances WHERE id=:id"), + {"id": created.acceptance.id}, + ) + ).mappings().one() + for field in ( + "id", + "source_review_id", + "source_routing_manifest_id", + "policy_context_ref", + ): + assert acceptance[field] == getattr(created.acceptance, field) + for field in ( + "project_id", + "task_id", + "submission_id", + "accepted_submitter_id", + "recorded_by", + ): + assert UUID(str(acceptance[field])) == getattr(created.acceptance, field) + assert acceptance["acceptance_source"] == created.acceptance.acceptance_source + assert acceptance["accepted_at"] == created.acceptance.accepted_at + + contribution = ( + await session.execute( + text( + "SELECT id,created_at FROM public.contribution_records " + "WHERE source_final_acceptance_id=:id" + ), + {"id": created.acceptance.id}, + ) + ).one() + assert contribution == ( + created.participation.contribution.id, + created.participation.contribution.created_at, + ) + awards = ( + await session.execute( + text( + "SELECT id,created_at FROM public.compensation_awards " + "WHERE contribution_record_id=:id ORDER BY instrument_type" + ), + {"id": created.participation.contribution.id}, + ) + ).all() + assert awards == [ + (award.id, award.created_at) for award in created.participation.awards + ] + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "accepted", + "assignment_status": "completed", + "acceptances": 1, + "contributions": 1, + "reviewer_contributions": 0, + "awards": len(instruments), + } + assert await session.scalar(text("SELECT count(*) FROM public.reviews")) == 1 + + +async def test_human_source_rejects_foreign_review_identity_and_invalid_artifact_hash( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path / "local", isolated_database_env, persist_acceptance=False + ) as h: + async with contribution_source( + tmp_path / "foreign", + isolated_database_env, + persist_acceptance=False, + provision_services=False, + storage_settings=h.settings, + ) as foreign: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + + foreign_sources = ( + request.acceptance.model_copy( + update={"source_review_id": foreign.acceptance.source_review_id} + ), + request.acceptance.model_copy( + update={"recorded_by": foreign.acceptance.recorded_by} + ), + ) + for acceptance in foreign_sources: + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate( + request.model_copy(update={"acceptance": acceptance}) + ) + + changed_hash = "sha256:" + ( + "0" * 64 + if request.task_effects.content_sha256 != "sha256:" + "0" * 64 + else "1" * 64 + ) + changed_task = request.task_effects.model_copy( + update={"content_sha256": changed_hash} + ) + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate( + request.model_copy(update={"task_effects": changed_task}) + ) + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "review_pending", + "assignment_status": "active", + "acceptances": 0, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + assert await session.scalar(text("SELECT count(*) FROM public.reviews")) == 2 diff --git a/backend/tests/reviews/acceptance/test_participation_transactions.py b/backend/tests/reviews/acceptance/test_participation_transactions.py new file mode 100644 index 000000000..78f8bb73a --- /dev/null +++ b/backend/tests/reviews/acceptance/test_participation_transactions.py @@ -0,0 +1,356 @@ +"""Atomicity, concurrency, conflicting identity, and partial replay proof.""" + +import asyncio +from contextlib import suppress + +import pytest +from sqlalchemy import text +from sqlalchemy.exc import DBAPIError + +from app.core.identifiers import new_record_id +from app.modules.contributions.records.repository import SubmitterContributionRepository +from app.modules.reviews.api.acceptance import FinalAcceptanceConflict +from tests.contributions.records.support import ( + award_values, + contribution_source, + insert_award, + insert_record, + rows, +) +from tests.reviews.acceptance.support import insert_acceptance +from tests.reviews.packet.test_repository import wait_for_blocker + +from .participation_support import ( + participant, + prepare_review_pending, + request_for, + stage_terminal_task, + stored_effects, +) + + +async def test_caller_rollback_and_late_sql_failure_discard_every_new_effect( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, + isolated_database_env, + paid=True, + persist_acceptance=False, + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + + async with h.factory() as session: + await session.begin() + result = await participant(session).participate(request) + assert len(result.participation.awards) == 2 + await session.rollback() + + async with h.factory() as session: + with pytest.raises(DBAPIError, match="division by zero"): + async with session.begin(): + result = await participant(session).participate(request) + assert len(result.participation.awards) == 2 + await session.execute(text("SELECT 1 / 0")) + await session.rollback() + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "review_pending", + "assignment_status": "active", + "acceptances": 0, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +@pytest.mark.parametrize("commit_first", [True, False], ids=("commit-wins", "rollback-loses")) +async def test_concurrent_shared_acceptance_converges_on_committed_winner_ids( + tmp_path, isolated_database_env, commit_first +): + async with contribution_source( + tmp_path, + isolated_database_env, + paid=True, + persist_acceptance=False, + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + ready = asyncio.Future() + + async def competitor(): + async with h.factory() as session: + await session.begin() + ready.set_result(await session.scalar(text("SELECT pg_backend_pid()"))) + try: + result = await participant(session).participate(request) + await session.commit() + return result + except BaseException: + await session.rollback() + raise + + async with h.factory() as first_session: + await first_session.begin() + first = await participant(first_session).participate(request) + pending = asyncio.create_task(competitor()) + try: + await wait_for_blocker(h.factory, await ready) + await ( + first_session.commit() if commit_first else first_session.rollback() + ) + winner = await asyncio.wait_for(pending, 10) + finally: + if not pending.done(): + pending.cancel() + with suppress(asyncio.CancelledError): + await pending + + if commit_first: + assert winner == first + else: + assert winner.acceptance.id == first.acceptance.id == request.acceptance.id + assert winner.acceptance.accepted_at != first.acceptance.accepted_at + assert winner.participation.contribution.id != first.participation.contribution.id + assert {award.id for award in winner.participation.awards}.isdisjoint( + {award.id for award in first.participation.awards} + ) + async with h.factory() as session: + assert await session.scalar(text("SELECT id FROM public.final_acceptances")) == ( + winner.acceptance.id + ) + assert await session.scalar( + text("SELECT id FROM public.contribution_records") + ) == winner.participation.contribution.id + assert set( + await session.scalars(text("SELECT id FROM public.compensation_awards")) + ) == {award.id for award in winner.participation.awards} + + +async def test_terminal_replay_rejects_conflicting_preallocated_acceptance_identity( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, isolated_database_env, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + created = await participant(session).participate(request) + + conflicting_id = new_record_id() + conflict = request.model_copy( + update={ + "acceptance": request.acceptance.model_copy(update={"id": conflicting_id}), + "task_effects": request.task_effects.model_copy( + update={"final_acceptance_id": conflicting_id} + ), + } + ) + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(conflict) + + async with h.factory() as session: + assert await session.scalar(text("SELECT id FROM public.final_acceptances")) == ( + created.acceptance.id + ) + assert await session.scalar( + text("SELECT count(*) FROM public.contribution_records") + ) == 1 + + +async def test_recorded_acceptance_with_pending_task_rejects_without_terminal_repair( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, isolated_database_env, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + await insert_acceptance(session, request.acceptance) + + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "review_pending", + "assignment_status": "active", + "acceptances": 1, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +async def test_terminal_task_without_acceptance_rejects_without_source_or_economics( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, isolated_database_env, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + await stage_terminal_task(session, request) + + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "accepted", + "assignment_status": "completed", + "acceptances": 0, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +@pytest.mark.parametrize( + ("task_status", "assignment_status"), + (("accepted", "active"), ("review_pending", "completed")), +) +async def test_mixed_task_assignment_states_reject_without_missing_effects( + tmp_path, isolated_database_env, task_status, assignment_status +): + async with contribution_source( + tmp_path, isolated_database_env, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + await session.execute( + text("UPDATE public.workstream_tasks SET status=:status WHERE id=:id"), + {"status": task_status, "id": request.task_effects.task_id}, + ) + await session.execute( + text("UPDATE public.task_assignments SET status=:status WHERE id=:id"), + {"status": assignment_status, "id": request.task_effects.assignment_id}, + ) + + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + + async with h.factory() as session: + state = await stored_effects(session, request.task_effects.task_id) + assert state == { + "task_status": task_status, + "assignment_status": assignment_status, + "acceptances": 0, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +async def test_acceptance_and_terminal_task_without_contribution_rejects_and_commits_no_repair( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, isolated_database_env, paid=True, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + await stage_terminal_task(session, request) + await insert_acceptance(session, request.acceptance) + + async with h.factory() as session, session.begin(): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "accepted", + "assignment_status": "completed", + "acceptances": 1, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +async def test_partial_awards_in_same_transaction_reject_without_backfill( + tmp_path, isolated_database_env +): + async with contribution_source( + tmp_path, isolated_database_env, paid=True, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session: + await session.begin() + await stage_terminal_task(session, request) + await insert_acceptance(session, request.acceptance) + await insert_record(session, h.submitter_record) + expected_awards = await award_values(session, h.submitter_record) + await insert_award( + session, + expected_awards[0], + correlation_id=request.correlation_id, + ) + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + assert len(await rows(session, "compensation_awards")) == 1 + assert len(await rows(session, "contribution_records")) == 1 + await session.rollback() + + async with h.factory() as session: + assert await stored_effects(session, request.task_effects.task_id) == { + "task_status": "review_pending", + "assignment_status": "active", + "acceptances": 0, + "contributions": 0, + "reviewer_contributions": 0, + "awards": 0, + } + + +async def test_removing_replay_no_insert_guard_exposes_partial_repair( + tmp_path, isolated_database_env, monkeypatch +): + async with contribution_source( + tmp_path, isolated_database_env, paid=False, persist_acceptance=False + ) as h: + await prepare_review_pending(h) + request = await request_for(h, correlation_id=new_record_id()) + async with h.factory() as session, session.begin(): + await stage_terminal_task(session, request) + await insert_acceptance(session, request.acceptance) + + original = SubmitterContributionRepository.apply_acceptance_disposition + + async def insert_during_replay(repository, participation_request): + return await original( + repository, + participation_request.model_copy( + update={"acceptance_disposition": "new"} + ), + ) + + monkeypatch.setattr( + SubmitterContributionRepository, + "apply_acceptance_disposition", + insert_during_replay, + ) + async with h.factory() as session: + await session.begin() + with pytest.raises(pytest.fail.Exception, match="DID NOT RAISE"): + with pytest.raises(FinalAcceptanceConflict): + await participant(session).participate(request) + assert len(await rows(session, "contribution_records")) == 1 + assert await rows(session, "compensation_awards") == [] + state = await stored_effects(session, request.task_effects.task_id) + assert state["task_status"] == "accepted" + assert state["assignment_status"] == "completed" + assert state["acceptances"] == 1 + await session.rollback() diff --git a/backend/tests/tasks/accepted_effects/__init__.py b/backend/tests/tasks/accepted_effects/__init__.py new file mode 100644 index 000000000..1d4f19745 --- /dev/null +++ b/backend/tests/tasks/accepted_effects/__init__.py @@ -0,0 +1 @@ +"""Focused proof for hidden TASK accepted effects.""" diff --git a/backend/tests/tasks/accepted_effects/support.py b/backend/tests/tasks/accepted_effects/support.py new file mode 100644 index 000000000..1661c4ee7 --- /dev/null +++ b/backend/tests/tasks/accepted_effects/support.py @@ -0,0 +1,71 @@ +"""Real human-source TASK state for accepted-effects participant tests.""" + +from contextlib import asynccontextmanager +from uuid import UUID + +from sqlalchemy import text + +from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence +from app.modules.tasks.accepted_effects import TaskAcceptedEffectsParticipant +from app.modules.tasks.api import TaskAcceptedEffectsRequest +from tests.reviews.acceptance.support import acceptance_source + + +@asynccontextmanager +async def accepted_effects_source(tmp_path, database_url): + """Yield exact persisted TASK facts in the human acceptance prestate.""" + async with acceptance_source(tmp_path, database_url) as h: + async with h.factory() as session, session.begin(): + row = ( + await session.execute( + text( + """ + SELECT s.task_assignment_id, s.contributor_id, + s.contribution_policy_version_id, s.artifact_content_id, + c.sha256, t.locked_review_policy_id + FROM public.submissions s + JOIN public.workstream_tasks t ON t.id=s.task_id + JOIN public.artifact_contents c ON c.id=s.artifact_content_id + WHERE s.id=:submission_id + """ + ), + {"submission_id": h.review.submission_id}, + ) + ).mappings().one() + await session.execute( + text( + "UPDATE public.workstream_tasks SET status='review_pending' " + "WHERE id=:task_id" + ), + {"task_id": h.review.task_id}, + ) + await session.execute( + text( + "UPDATE public.task_assignments " + "SET accepted_at=coalesce(accepted_at, clock_timestamp()) " + "WHERE id=:assignment_id AND status='active' AND released_at IS NULL" + ), + {"assignment_id": row["task_assignment_id"]}, + ) + h.effects_request = TaskAcceptedEffectsRequest( + project_id=h.review.project_id, + task_id=h.review.task_id, + assignment_id=UUID(str(row["task_assignment_id"])), + submission_id=h.review.submission_id, + submission_version=h.review.submission_version, + contributor_id=UUID(str(row["contributor_id"])), + contribution_policy_version_id=row["contribution_policy_version_id"], + content_id=UUID(str(row["artifact_content_id"])), + content_sha256=row["sha256"], + final_acceptance_id=h.acceptance.id, + expected_task_status="review_pending", + ) + h.locked_review_policy_id = UUID(str(row["locked_review_policy_id"])) + yield h + + +def participant(session): + return TaskAcceptedEffectsParticipant( + session, + fence=PostgresJointLifecycleMutationFence(session), + ) diff --git a/backend/tests/tasks/accepted_effects/test_contracts.py b/backend/tests/tasks/accepted_effects/test_contracts.py new file mode 100644 index 000000000..cdba32803 --- /dev/null +++ b/backend/tests/tasks/accepted_effects/test_contracts.py @@ -0,0 +1,191 @@ +"""Strict public contract and hidden-construction proof.""" + +from inspect import iscoroutinefunction +from types import SimpleNamespace + +import pytest +from pydantic import ValidationError + +from app.core.identifiers import new_record_id +from app.modules.tasks import api as task_api +from app.modules.tasks.api import accepted_effects +from app.modules.tasks.accepted_effects import TaskAcceptedEffectsParticipant +from app.modules.tasks.api import ( + TaskAcceptedEffectsFence, + TaskAcceptedEffectsPort, + TaskAcceptedEffectsRequest, + TaskAcceptedEffectsResult, + TaskAcceptedEffectsUnavailable, + TaskAcceptedPreparation, +) + +SHA = "sha256:" + "a" * 64 + + +def values(**changes): + result = { + "project_id": new_record_id(), + "task_id": new_record_id(), + "assignment_id": new_record_id(), + "submission_id": new_record_id(), + "submission_version": 1, + "contributor_id": new_record_id(), + "contribution_policy_version_id": new_record_id(), + "content_id": new_record_id(), + "content_sha256": SHA, + "final_acceptance_id": new_record_id(), + "expected_task_status": "evaluation_pending", + } + result.update(changes) + return result + + +@pytest.mark.parametrize( + ("field", "value"), + ( + ("project_id", str(new_record_id())), + ("submission_version", "1"), + ("submission_version", True), + ("content_sha256", "sha256:" + "A" * 64), + ("expected_task_status", "accepted"), + ("routing_manifest_id", new_record_id()), + ), +) +def test_request_is_strict(field, value): + with pytest.raises(ValidationError): + TaskAcceptedEffectsRequest(**values(**{field: value})) + + +def test_two_phase_contract_is_exact_and_concrete_participant_is_hidden(): + request = TaskAcceptedEffectsRequest(**values()) + preparation = TaskAcceptedPreparation( + disposition="new", locked_review_policy_id=new_record_id() + ) + result = TaskAcceptedEffectsResult( + request=request, task_status="accepted", assignment_status="completed" + ) + + assert preparation.disposition == "new" + assert result.request == request + assert set(TaskAcceptedEffectsRequest.model_fields) == set(values()) + assert set(TaskAcceptedEffectsResult.model_fields) == { + "request", "task_status", "assignment_status", + } + assert set(TaskAcceptedPreparation.model_fields) == { + "disposition", "locked_review_policy_id", + } + for model, field, value in ( + (request, "expected_task_status", "review_pending"), + (result, "task_status", "review_pending"), + (preparation, "disposition", "replay"), + ): + with pytest.raises(ValidationError, match="frozen_instance"): + setattr(model, field, value) + for change in ( + {"task_status": "review_pending"}, + {"assignment_status": "accepted"}, + {"review_id": new_record_id()}, + ): + with pytest.raises(ValidationError): + TaskAcceptedEffectsResult(**(result.model_dump() | change)) + assert accepted_effects.__all__ == ( + "TaskAcceptedEffectsPort", "TaskAcceptedEffectsFence", "TaskAcceptedPreparation", + "TaskAcceptedEffectsRequest", "TaskAcceptedEffectsResult", "TaskAcceptedEffectsUnavailable", + ) + for name in accepted_effects.__all__: + assert getattr(task_api, name) is getattr(accepted_effects, name) + assert iscoroutinefunction(TaskAcceptedEffectsPort.lock_accepted_effects) + assert iscoroutinefunction(TaskAcceptedEffectsPort.apply_accepted_effects) + assert iscoroutinefunction(TaskAcceptedEffectsPort.require_routing_source) + assert iscoroutinefunction(TaskAcceptedEffectsFence.acquire) + assert issubclass(TaskAcceptedEffectsUnavailable, RuntimeError) + assert not hasattr(task_api, "TaskAcceptedEffectsParticipant") + for value in ( + {"disposition": "pending", "locked_review_policy_id": new_record_id()}, + {"disposition": "new", "locked_review_policy_id": str(new_record_id())}, + ): + with pytest.raises(ValidationError): + TaskAcceptedPreparation(**value) + + +def test_nested_request_instances_are_revalidated(): + request = TaskAcceptedEffectsRequest(**values()) + object.__setattr__(request, "expected_task_status", "accepted") + + with pytest.raises(ValidationError): + TaskAcceptedEffectsResult( + request=request, + task_status="accepted", + assignment_status="completed", + ) + + +class _RoutingManifestRead: + """Return one stored-shape transport row while recording owner qualification.""" + + def __init__(self, source): + self.source = source + self.scope = None + + async def read_routing_manifest(self, **scope): + self.scope = scope + return self.source + + +def _routing_source(request, **changes): + source = { + "project_id": str(request.project_id), + "task_id": str(request.task_id), + "submission_id": str(request.submission_id), + "submission_version": request.submission_version, + "assignment_id": str(request.assignment_id), + "contributor_id": str(request.contributor_id), + "contribution_policy_version_id": request.contribution_policy_version_id, + "content_sha256": request.content_sha256, + "human_review_required": False, + } + source.update(changes) + return SimpleNamespace(**source) + + +def _routing_owner(source): + repository = _RoutingManifestRead(source) + owner = object.__new__(TaskAcceptedEffectsParticipant) + owner._repository = repository + return owner, repository + + +async def test_exact_false_transport_row_is_owner_qualified_without_activation_claim(): + request = TaskAcceptedEffectsRequest(**values()) + manifest_id = new_record_id() + owner, repository = _routing_owner(_routing_source(request)) + + assert await owner.require_routing_source(request, manifest_id) is None + assert repository.scope == { + "manifest_id": manifest_id, + "project_id": request.project_id, + "task_id": request.task_id, + "submission_id": request.submission_id, + } + + +@pytest.mark.parametrize( + "changes", + ( + {"project_id": str(new_record_id())}, + {"task_id": str(new_record_id())}, + {"submission_id": str(new_record_id())}, + {"submission_version": 2}, + {"assignment_id": str(new_record_id())}, + {"contributor_id": str(new_record_id())}, + {"contribution_policy_version_id": new_record_id()}, + {"content_sha256": "sha256:" + "b" * 64}, + {"human_review_required": True}, + ), +) +async def test_each_stored_routing_source_mismatch_is_rejected(changes): + request = TaskAcceptedEffectsRequest(**values()) + owner, _ = _routing_owner(_routing_source(request, **changes)) + + with pytest.raises(TaskAcceptedEffectsUnavailable): + await owner.require_routing_source(request, new_record_id()) diff --git a/backend/tests/tasks/accepted_effects/test_postgresql.py b/backend/tests/tasks/accepted_effects/test_postgresql.py new file mode 100644 index 000000000..64e74e267 --- /dev/null +++ b/backend/tests/tasks/accepted_effects/test_postgresql.py @@ -0,0 +1,284 @@ +"""Real PostgreSQL TASK state, source, and lock-boundary proof.""" + +import pytest +from sqlalchemy import text +from sqlalchemy.exc import DBAPIError + +from app.core.identifiers import new_record_id +from app.modules.reviews.api.lifecycle import JointLifecycleUnavailable +from app.modules.tasks.api import TaskAcceptedEffectsUnavailable +from tests.tasks.post_submit_routing.support import insert_source + +from .support import accepted_effects_source, participant + + +@pytest.mark.parametrize("prestate", ("review_pending", "evaluation_pending")) +async def test_new_then_exact_replay_preserves_nonterminal_task_facts( + tmp_path, isolated_database_env, prestate +): + """Prove TASK mechanics for either prestate, without automated authority.""" + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + h.effects_request = h.effects_request.model_copy(update={ + "expected_task_status": prestate, + }) + async with h.factory() as session, session.begin(): + await session.execute( + text("UPDATE public.workstream_tasks SET status=:prestate WHERE id=:task_id"), + {"prestate": prestate, "task_id": h.effects_request.task_id}, + ) + async with h.factory() as session: + before = ( + await session.execute( + text( + """ + SELECT t.assigned_to, a.assigned_at, a.accepted_at, + a.released_at, s.status AS submission_status + FROM public.workstream_tasks t + JOIN public.task_assignments a ON a.id=:assignment_id + JOIN public.submissions s ON s.id=:submission_id + WHERE t.id=:task_id + """ + ), + h.effects_request.model_dump(), + ) + ).mappings().one() + + async with h.factory() as session, session.begin(): + owner = participant(session) + prepared = await owner.lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + assert prepared.disposition == "new" + assert prepared.locked_review_policy_id == h.locked_review_policy_id + result = await owner.apply_accepted_effects( + h.effects_request, disposition="new", expected_generation=0 + ) + assert result.task_status == "accepted" + assert result.assignment_status == "completed" + + async with h.factory() as session, session.begin(): + owner = participant(session) + prepared = await owner.lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + assert prepared.disposition == "replay" + assert await owner.apply_accepted_effects( + h.effects_request, disposition="replay", expected_generation=0 + ) == result + + async with h.factory() as session: + after = ( + await session.execute( + text( + """ + SELECT t.status AS task_status, t.assigned_to, + a.status AS assignment_status, a.assigned_at, + a.accepted_at, a.released_at, + s.status AS submission_status + FROM public.workstream_tasks t + JOIN public.task_assignments a ON a.id=:assignment_id + JOIN public.submissions s ON s.id=:submission_id + WHERE t.id=:task_id + """ + ), + h.effects_request.model_dump(), + ) + ).mappings().one() + assert after["task_status"] == "accepted" + assert after["assignment_status"] == "completed" + assert {key: after[key] for key in before} == dict(before) + + +@pytest.mark.parametrize( + "changes", + ( + {"submission_version": 2}, + {"submission_id": new_record_id()}, + {"assignment_id": new_record_id()}, + {"contributor_id": new_record_id()}, + {"contribution_policy_version_id": new_record_id()}, + {"content_id": new_record_id()}, + ), +) +async def test_exact_lineage_gates_reject_without_mutation( + tmp_path, isolated_database_env, changes +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + changed = h.effects_request.model_copy(update=changes) + async with h.factory() as session, session.begin(): + with pytest.raises(TaskAcceptedEffectsUnavailable): + await participant(session).lock_accepted_effects( + changed, expected_generation=0 + ) + async with h.factory() as session: + states = ( + await session.execute( + text( + "SELECT t.status, a.status FROM public.workstream_tasks t " + "JOIN public.task_assignments a ON a.id=:assignment_id " + "WHERE t.id=:task_id" + ), + h.effects_request.model_dump(), + ) + ).one() + assert states == ("review_pending", "active") + + +@pytest.mark.parametrize( + ("task_status", "assignment_status"), + (("accepted", "active"), ("review_pending", "completed")), +) +async def test_mixed_terminal_states_are_neither_new_nor_replay( + tmp_path, isolated_database_env, task_status, assignment_status +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session, session.begin(): + await session.execute( + text("UPDATE public.workstream_tasks SET status=:status WHERE id=:id"), + {"status": task_status, "id": h.effects_request.task_id}, + ) + await session.execute( + text("UPDATE public.task_assignments SET status=:status WHERE id=:id"), + {"status": assignment_status, "id": h.effects_request.assignment_id}, + ) + async with h.factory() as session, session.begin(): + with pytest.raises(TaskAcceptedEffectsUnavailable): + await participant(session).lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + + +@pytest.mark.parametrize("timestamp", ("accepted_at", "released_at")) +async def test_assignment_timestamp_preconditions_are_required( + tmp_path, isolated_database_env, timestamp +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + assignment = ( + "accepted_at=NULL" + if timestamp == "accepted_at" + else "released_at=clock_timestamp()" + ) + async with h.factory() as session, session.begin(): + await session.execute( + text( + f"UPDATE public.task_assignments SET {assignment} WHERE id=:id" + ), + {"id": h.effects_request.assignment_id}, + ) + async with h.factory() as session, session.begin(): + with pytest.raises(TaskAcceptedEffectsUnavailable): + await participant(session).lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + + +async def test_apply_rechecks_acceptance_disposition( + tmp_path, isolated_database_env +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session, session.begin(): + owner = participant(session) + assert ( + await owner.lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + ).disposition == "new" + with pytest.raises(TaskAcceptedEffectsUnavailable): + await owner.apply_accepted_effects( + h.effects_request, + disposition="replay", + expected_generation=0, + ) + + +async def test_foreign_project_is_rejected_before_the_actual_task_row_is_locked( + tmp_path, isolated_database_env +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + foreign = h.effects_request.model_copy(update={"project_id": new_record_id()}) + async with h.factory() as first: + await first.begin() + with pytest.raises(TaskAcceptedEffectsUnavailable): + await participant(first).lock_accepted_effects( + foreign, expected_generation=0 + ) + async with h.factory() as second, second.begin(): + locked = await second.scalar( + text( + "SELECT id FROM public.workstream_tasks " + "WHERE id=:id FOR UPDATE NOWAIT" + ), + {"id": h.effects_request.task_id}, + ) + assert locked == h.effects_request.task_id + await first.rollback() + + +@pytest.mark.parametrize( + ("table", "field"), + ( + ("workstream_tasks", "task_id"), + ("task_assignments", "assignment_id"), + ("submissions", "submission_id"), + ), +) +async def test_preparation_retains_each_owner_lock_until_caller_rollback( + tmp_path, isolated_database_env, table, field +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + async with h.factory() as first: + await first.begin() + await participant(first).lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + async with h.factory() as second: + await second.begin() + with pytest.raises(DBAPIError, match="could not obtain lock"): + await second.execute( + text(f"SELECT id FROM public.{table} WHERE id=:id FOR UPDATE NOWAIT"), + {"id": getattr(h.effects_request, field)}, + ) + await second.rollback() + await first.rollback() + + +async def test_stored_true_policy_manifest_cannot_prove_automated_source( + tmp_path, isolated_database_env +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + assert h.source["human_review_required"] is True + async with h.factory() as session, session.begin(): + await insert_source(session, h.source) + automated = h.effects_request.model_copy( + update={"expected_task_status": "evaluation_pending"} + ) + async with h.factory() as session, session.begin(): + with pytest.raises(TaskAcceptedEffectsUnavailable): + await participant(session).require_routing_source( + automated, h.source["id"] + ) + + +async def test_canonical_fence_rejects_missing_nested_and_stale_generation( + tmp_path, isolated_database_env +): + async with accepted_effects_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session: + with pytest.raises(JointLifecycleUnavailable, match="root transaction"): + await participant(session).lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + assert not session.in_transaction() + + async with h.factory() as session, session.begin(): + async with session.begin_nested(): + with pytest.raises(JointLifecycleUnavailable, match="root transaction"): + await participant(session).lock_accepted_effects( + h.effects_request, expected_generation=0 + ) + + async with h.factory() as session, session.begin(): + with pytest.raises(JointLifecycleUnavailable, match="generation changed"): + await participant(session).lock_accepted_effects( + h.effects_request, expected_generation=1 + ) diff --git a/backend/tests/tasks/post_submit_routing/test_contracts.py b/backend/tests/tasks/post_submit_routing/test_contracts.py index 18b5ce427..374af14ef 100644 --- a/backend/tests/tasks/post_submit_routing/test_contracts.py +++ b/backend/tests/tasks/post_submit_routing/test_contracts.py @@ -1,6 +1,6 @@ -"""Pure contract proof for detached routing source and accepted TASK effects.""" +"""Pure contract proof for the detached post-submit routing source.""" -from inspect import isclass, iscoroutinefunction +from inspect import isclass import pytest from pydantic import ValidationError @@ -9,33 +9,9 @@ from app.modules.actors.api import ServiceIdentity from app.modules.authorization.catalogue import (ActionId, PermissionId, ActionAvailability, ACTION_BY_ID, SERVICE_ACTIONS_BY_IDENTITY, resolve_executable_action) from app.modules.tasks import api as task_api -from app.modules.tasks.api import accepted_effects, post_submit_routing -from app.modules.tasks.api.accepted_effects import ( - TaskAcceptedEffectsPort, - TaskAcceptedEffectsRequest, - TaskAcceptedEffectsResult, - TaskAcceptedEffectsUnavailable, -) +from app.modules.tasks.api import post_submit_routing from app.modules.tasks.api.post_submit_routing import TaskPostSubmitManifestFacts -from tests.tasks.post_submit_routing.contract_fixtures import SHA_A, _lineage, _source_values - -def _effects_values(**changes: object) -> dict[str, object]: - values: dict[str, object] = { - "project_id": new_record_id(), - "task_id": new_record_id(), - "assignment_id": new_record_id(), - "submission_id": new_record_id(), - "submission_version": 1, - "contributor_id": new_record_id(), - "contribution_policy_version_id": new_record_id(), - "content_id": new_record_id(), - "content_sha256": SHA_A, - "final_acceptance_id": new_record_id(), - "expected_task_status": "evaluation_pending", - } - values.update(changes) - return values - +from tests.tasks.post_submit_routing.contract_fixtures import _lineage, _source_values @pytest.mark.parametrize( ("field", "value"), @@ -184,78 +160,13 @@ def test_false_source_value_is_transport_only() -> None: assert source.model_dump(mode="json")["human_review_required"] is False -@pytest.mark.parametrize( - ("field", "value"), - ( - ("project_id", str(new_record_id())), - ("submission_version", "1"), - ("submission_version", True), - ("content_sha256", "sha256:" + "A" * 64), - ("expected_task_status", "accepted"), - ("routing_manifest_id", new_record_id()), - ), -) -def test_accepted_effects_request_is_strict(field: str, value: object) -> None: - with pytest.raises(ValidationError): - TaskAcceptedEffectsRequest(**_effects_values(**{field: value})) - - -def test_accepted_effects_contract_is_source_neutral() -> None: - request = TaskAcceptedEffectsRequest(**_effects_values()) - review_request = TaskAcceptedEffectsRequest( - **_effects_values(expected_task_status="review_pending") - ) - result = TaskAcceptedEffectsResult( - request=request, - task_status="accepted", - assignment_status="completed", - ) - - assert result.request == request - assert review_request.expected_task_status == "review_pending" - assert set(TaskAcceptedEffectsRequest.model_fields) == set(_effects_values()) - assert set(TaskAcceptedEffectsResult.model_fields) == { - "request", - "task_status", - "assignment_status", - } - assert iscoroutinefunction(TaskAcceptedEffectsPort.apply_accepted_effects) - assert issubclass(TaskAcceptedEffectsUnavailable, RuntimeError) - with pytest.raises(ValidationError): - request.expected_task_status = "accepted" - with pytest.raises(ValidationError): - result.task_status = "review_pending" - for changes in ( - {"task_status": "review_pending"}, - {"assignment_status": "accepted"}, - {"review_id": new_record_id()}, - ): - values = { - "request": request, - "task_status": "accepted", - "assignment_status": "completed", - } - values.update(changes) - with pytest.raises(ValidationError): - TaskAcceptedEffectsResult(**values) - - -def test_source_foundation_has_no_runtime_entry() -> None: +def test_routing_source_foundation_has_no_runtime_entry() -> None: assert post_submit_routing.__all__ == ( "TaskPostSubmitManifestFacts", "task_post_submit_source_digest", "TaskRoutingSelection", "TaskRoutingRequestFacts", "task_routing_request_digest", ) - assert accepted_effects.__all__ == ( - "TaskAcceptedEffectsPort", - "TaskAcceptedEffectsRequest", - "TaskAcceptedEffectsResult", - "TaskAcceptedEffectsUnavailable", - ) - for name in post_submit_routing.__all__ + accepted_effects.__all__: - assert getattr(task_api, name) is getattr( - post_submit_routing if name in post_submit_routing.__all__ else accepted_effects, - name, - ) + for name in post_submit_routing.__all__: + assert getattr(task_api, name) is getattr(post_submit_routing, name) assert { name @@ -263,16 +174,6 @@ def test_source_foundation_has_no_runtime_entry() -> None: if isclass(value) and value.__module__ == post_submit_routing.__name__ } == {"TaskPostSubmitManifestFacts", "TaskRoutingSelection", "TaskRoutingRequestFacts"} assert not hasattr(task_api, "TaskRoutingRequests") - assert { - name - for name, value in vars(accepted_effects).items() - if isclass(value) and value.__module__ == accepted_effects.__name__ - } == { - "TaskAcceptedEffectsPort", - "TaskAcceptedEffectsRequest", - "TaskAcceptedEffectsResult", - "TaskAcceptedEffectsUnavailable", - } action = ActionId.TASK_POST_SUBMIT_ROUTE assert ACTION_BY_ID[action].permission_id is PermissionId.TASK_POST_SUBMIT_ROUTE assert ACTION_BY_ID[action].availability is ActionAvailability.PLANNED diff --git a/backend/tests/test_behavior_ownership.py b/backend/tests/test_behavior_ownership.py index 877d80ac3..6214ba309 100644 --- a/backend/tests/test_behavior_ownership.py +++ b/backend/tests/test_behavior_ownership.py @@ -2324,3 +2324,37 @@ def test_submitter_participation_has_exact_ownership_without_activation() -> Non ownership._validate_additive_partition_transition( _partition(sorted({retained, *expected, neighbor})), trusted ) + + +def test_shared_acceptance_has_exact_ownership_without_activation() -> None: + expected = { + "backend/app/modules/reviews/api/acceptance.py", + "backend/app/modules/reviews/acceptance/repository.py", + "backend/app/modules/reviews/acceptance/participant.py", + "backend/app/modules/tasks/accepted_effects.py", + } + assert ownership.REV_04C_PARTICIPATION_TARGETS == expected + assert {ownership.group_for_target(path) for path in expected} == {"lifecycle"} + retained = "backend/app/core/config.py" + trusted = _partition([retained]) + ownership._validate_additive_partition_transition( + _partition(sorted({retained, *expected})), trusted + ) + for neighbor in ( + "backend/app/modules/reviews/acceptance/router.py", + "backend/app/modules/reviews/acceptance/activation.py", + ): + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership._validate_additive_partition_transition( + _partition(sorted({retained, *expected, neighbor})), trusted + ) + + +def test_acceptance_contract_relocation_requires_public_replacement() -> None: + old = "backend/app/modules/reviews/acceptance/schemas.py" + new = "backend/app/modules/reviews/api/acceptance.py" + retained = "backend/app/core/config.py" + trusted = _partition(sorted({old, retained})) + ownership._validate_additive_partition_transition(_partition(sorted({new, retained})), trusted) + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership._validate_additive_partition_transition(_partition([retained]), trusted) diff --git a/backend/tests/test_ci_lane_catalogue.py b/backend/tests/test_ci_lane_catalogue.py index d134d1d74..85977e021 100644 --- a/backend/tests/test_ci_lane_catalogue.py +++ b/backend/tests/test_ci_lane_catalogue.py @@ -51,6 +51,11 @@ def test_measured_hotspots_have_explicit_semantic_owners() -> None: == modules_by_lane["project_lifecycle_b"] == modules_by_lane["project_lifecycle_c"] == { + "tests/reviews/acceptance/test_participant_contracts.py", + "tests/reviews/acceptance/test_participation.py", + "tests/reviews/acceptance/test_participation_transactions.py", + "tests/tasks/accepted_effects/test_contracts.py", + "tests/tasks/accepted_effects/test_postgresql.py", "tests/contributions/records/test_contracts.py", "tests/contributions/records/test_storage.py", "tests/contributions/records/test_migration.py", @@ -776,3 +781,15 @@ def test_routing_authorization_proofs_run_once_on_task_a(): for lane in LANES: assert set(lane.modules) & expected == (expected if lane.name == "task_lifecycle_a" else set()) assert not expected & set(catalogue.PARTITION_LANES_BY_MODULE) + + +def test_shared_acceptance_owner_proofs_are_in_partitioned_project_lanes(): + expected = { + "tests/reviews/acceptance/test_participant_contracts.py", + "tests/reviews/acceptance/test_participation.py", + "tests/reviews/acceptance/test_participation_transactions.py", + "tests/tasks/accepted_effects/test_contracts.py", + "tests/tasks/accepted_effects/test_postgresql.py", + } + assert expected <= set(catalogue.PROJECT_MODULES) + assert all(catalogue.PARTITION_LANES_BY_MODULE[path] == catalogue.PARTITIONED_PROJECT_LANES for path in expected) diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index a6018f15f..56b16a475 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -1758,10 +1758,11 @@ completion event and execute/finalize receipts, locked binding/content anchors and complete locked policy lineage; its sole recommendation is `allow_review`, which is evidence rather than permission. -The table has no deployable writer or reader, current pointer, handler, routing -authority, TASK transition or acceptance effect. `TaskAcceptedEffectsPort` is a -source-neutral type-only Protocol for a later REV-owned shared acceptance -operation; no adapter or participant exists yet. The valid storage graph is +The table has no general routing publication writer/reader, current pointer, +handler or routing authority. REV-04C uses its bounded exact-source verifier. +`TaskAcceptedEffectsPort` is the source-neutral contract used +by the REV-04C hidden FinalAcceptance/TASK/CON participant. That participant is +not a complete authorized operation or live route. The valid storage graph is currently limited to true policy. False is proven only as a strict scalar DTO value because guide activation still rejects it. @@ -1853,7 +1854,10 @@ mutation/deletion/truncation. Repository writes are caller-transaction operation creation checks the lease deadline against PostgreSQL time; exact replay retains the stored identity after expiry or closure. The detached identity is `packet_manifest_id`, matching AUTH. This storage is delivered; the resolver, -claim authority and byte capability remain future work. REV-04A immutable Review source storage is delivered; shared FinalAcceptance persistence is next. +claim authority and byte capability remain future work. REV-04A immutable Review source storage, REV-04B FinalAcceptance persistence and +REV-04C hidden acceptance/TASK/CON participation are delivered. Hidden routing +handlers are next; exact authority, complete-effect custody and activation +remain required before production use. ## Review @@ -2001,7 +2005,8 @@ direction, reason, and change summary. The [shared acceptance contract](spec_review_lifecycle.md#finalacceptance) defines both sources for this REV-owned fact. REV-04B delivers its immutable -storage foundation; the shared authorized operation remains planned. One schema and operation serve human accept and authorized `task.post_submit.route` with the +storage foundation; REV-04C supplies the hidden mechanical writer/participant, +while the complete authorized operation remains planned. One schema and operation serve human accept and authorized `task.post_submit.route` with the exact current successful routing manifest, locked `human_review_required=false` policy and originating AUTH decision event. Required-check success or raw checker output alone cannot create FinalAcceptance. No separate automated decision entity @@ -2024,9 +2029,10 @@ Fields: Purpose: -The future canonical operation creates this immutable REV-owned fact inside either -authorized trigger's shared transaction. Current raw storage has no production -writer or authority receipt: it cannot be consumed as canonical acceptance. +The REV-04C participant creates or exactly replays this immutable REV-owned fact +inside either caller-owned transaction and composes TASK/CON effects. It has no +authority receipt, routing handler or production consumer, so it cannot be +consumed as canonical acceptance. Before runtime, a same-table hardening migration must add exact mandatory AUTH custody and refuse retained pre-authority rows without backfill or deletion. Existing `Submission` is already the version identity, so the stored FK is `submission_id`; no SubmissionVersion entity or @@ -2047,8 +2053,9 @@ this record. CON-03C implements this immutable storage foundation. CON-07 adds the hidden submitter participant and complete frozen award staging/replay in the caller's -transaction. Shared acceptance/TASK effects, source AUTH hardening and runtime -composition remain unavailable; reviewer participation and fulfillment are +transaction. REV-04C composes it with hidden FinalAcceptance/TASK effects. +Source AUTH hardening, database complete-set enforcement, currentness proof and +runtime composition remain unavailable; reviewer participation and fulfillment are separate future work. Fields: diff --git a/docs/architecture_lifecycle_state_machine.md b/docs/architecture_lifecycle_state_machine.md index 4a83fad9f..c7bc0945b 100644 --- a/docs/architecture_lifecycle_state_machine.md +++ b/docs/architecture_lifecycle_state_machine.md @@ -6,7 +6,9 @@ These are target v0.1 states, not a claim of live route availability. The locked ReviewPolicy selects the success route: true requires human review; false uses the [same shared acceptance operation](spec_review_lifecycle.md#finalacceptance) after required post-submit checks pass. False activation remains unavailable -until its shared acceptance/CON/AUTH proof lands. +although the REV-04C hidden FinalAcceptance/TASK/CON participant is delivered; +mandatory AUTH receipt, database closure, currentness race, audit/outbox and +activation proof still must land. ```text DRAFT diff --git a/docs/architecture_lockdown.md b/docs/architecture_lockdown.md index 034821289..dfad4cdcb 100644 --- a/docs/architecture_lockdown.md +++ b/docs/architecture_lockdown.md @@ -65,7 +65,9 @@ authorized automated FinalAcceptance and submitter contribution after required post-submit checks pass, without human admission, a synthetic Review or a reviewer contribution. Its source/authority/CON design is defined in the [shared acceptance contract](spec_review_lifecycle.md#finalacceptance); -runtime implementation and activation remain pending. +REV-04C supplies its hidden FinalAcceptance/TASK/CON transaction participant, +while exact authority/evidence, routing handlers, currentness proof and activation +remain pending. This amendment does not enable raw checker results to create acceptance or change the existing human branch's implementation contract. diff --git a/docs/engineering/authorization_activation_custody.md b/docs/engineering/authorization_activation_custody.md index d329f4625..6220754ac 100644 --- a/docs/engineering/authorization_activation_custody.md +++ b/docs/engineering/authorization_activation_custody.md @@ -68,8 +68,12 @@ use the fixed `workstream.checker.post_submit` identity and phase-specific recei [ART-07A1](../../.commitrail/initiatives/WS-ART-001/WS-ART-001-07A1.md) delivers metadata-only packet types without authority or a resolver. REV-03B normalized -packet storage and REV-04A immutable Review source storage are delivered; REV-04B shared FinalAcceptance storage is delivered; CON persistence and participation are next; -hidden composition proof precedes exact activation. +packet storage, REV-04A immutable Review source storage, REV-04B FinalAcceptance +storage, CON-03C persistence, CON-07 participation and REV-04C hidden shared +acceptance/TASK/CON composition are delivered. Hidden routing handlers +ARCH-04E1B-B are next. Mandatory exact AUTH receipts, database complete-effect +custody, shared audit/outbox and fulfillment-root custody, and both TASK-before- +CHECKERS currentness races remain prerequisites of activation at ARCH-04E2-B. Runtime owner `WS-XINT-002-07` retains catalogue custody. The only approved v0.1 availability transition is 07A packet materialization. Evidence binding diff --git a/docs/glossary.md b/docs/glossary.md index d077fb9ef..f24318a55 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -370,8 +370,9 @@ context without rewriting prior work. ## FinalAcceptance The internal immutable accept-only fact and sole source of a submitter's -`accepted_submission` ContributionRecord. REV-04B delivers storage only; exact -AUTH custody and the canonical shared operation remain required before runtime use. See [Final Acceptance](#final-acceptance) +`accepted_submission` ContributionRecord. REV-04B delivers storage and REV-04C +delivers its hidden FinalAcceptance/TASK/CON participant; exact AUTH custody and +the complete canonical shared operation remain required before runtime use. See [Final Acceptance](#final-acceptance) for human and planned automated source boundaries. ## Revision Replay @@ -442,8 +443,9 @@ It is not a v0.1 review-transaction side effect. CON-03C supplies immutable storage; CON-07 supplies hidden submitter participation with complete frozen award staging and exact replay in the -caller's transaction. Live recognition still requires shared acceptance/TASK -effects, source authority and runtime composition. Reviewer participation +caller's transaction. REV-04C composes it with hidden FinalAcceptance/TASK +effects. Live recognition still requires source authority, database closure, +currentness proof, audit/outbox and runtime composition. Reviewer participation and fulfillment remain separate future work. The immutable, evidence-backed record of one completed contribution under locked @@ -470,8 +472,10 @@ TASK routing after required checks pass, without a Review, ReviewLease or reviewer contribution. The [canonical shared contract](spec_review_lifecycle.md#finalacceptance) defines the exclusive Review/routing-manifest provenance and atomic CON effects. The source flag records which trigger was used, not a second acceptance system. -False runtime activation remains unavailable until implementation proof lands; -raw checker success is not authority. +REV-04C supplies the hidden mechanical participant for both sources. False +runtime activation remains unavailable until mandatory AUTH receipt, database +closure, currentness race, audit/outbox and activation proof land; raw checker +success is not authority. ## Human Owner diff --git a/docs/operations_payment_reputation.md b/docs/operations_payment_reputation.md index 086a4ab22..8ad834a58 100644 --- a/docs/operations_payment_reputation.md +++ b/docs/operations_payment_reputation.md @@ -2,10 +2,12 @@ ## Status -Existing compensation records retain their owning implementation status. The -Review-, FinalAcceptance-, and revision-sourced behavior below is planned and -unavailable until its owning REV/CON chunks, exact AUTH activation, and REV-13 -joint release complete. Reputation behavior is deferred entirely. +Existing compensation records retain their owning implementation status. +The REV-04C hidden participant stages FinalAcceptance, TASK effects and the CON +submitter outcome in a caller-owned transaction. The authorized human/automatic +triggers, reviewer and revision behavior, audit/outbox closure, fulfillment and +joint release remain unavailable until their owning REV/CON/AUTH chunks complete. +Reputation behavior is deferred entirely. ## Compensation Principle diff --git a/docs/product_brief.md b/docs/product_brief.md index 89c8fa3ec..524c71001 100644 --- a/docs/product_brief.md +++ b/docs/product_brief.md @@ -59,9 +59,11 @@ deterministic rules or supported model-based checkers; the stage does not promise deterministic judgments. Neither stage replaces required human Review or independently authorizes acceptance. The versioned `human_review_required: true` in the existing locked ReviewPolicy may be set false for authorized automated -acceptance, without reviewer contribution. This is not live behavior; the +acceptance, without reviewer contribution. REV-04C supplies the hidden +FinalAcceptance/TASK/CON transaction participant, but this is not live behavior; the [bounded handoff](../.commitrail/changes/pre-review-plan-reconciliation.md#product-builder-handoff-implement-the-setting-next) -now provides configuration; activation of the shared acceptance/CON path remains pending. +now provides configuration; exact authority/evidence, routing handlers, +currentness proof and activation of the shared acceptance/CON path remain pending. See the [checker framework](architecture_checker_framework.md) and [current capability ledger](roadmap_status.md) for supported boundaries. diff --git a/docs/product_first_user_flows.md b/docs/product_first_user_flows.md index 10a9f7252..69a626a0a 100644 --- a/docs/product_first_user_flows.md +++ b/docs/product_first_user_flows.md @@ -14,8 +14,10 @@ default in the existing versioned ReviewPolicy setting. False is a separately planned post-check TASK handoff to authorized FinalAcceptance and CON, without human queues, leases, Reviews or reviewer contributions. Required checks and exact immutable evidence still apply. The [shared acceptance contract](spec_review_lifecycle.md#finalacceptance) -defines both triggers; runtime remains unavailable until its implementation -and exact shared release proof land. +defines both triggers. Runtime remains unavailable until the same participant +input/schema requires verified authority/evidence and gains routing/currentness and exact +shared release proof. REV-04C supplies only the hidden mechanical +FinalAcceptance/TASK/CON participant. The first user flows prove that Workstream can run real work from intake to acceptance. These flows come before any advanced routing or settlement. diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index a5583c68d..f1477bfe3 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -114,18 +114,23 @@ results, database-timed execution leases and atomic completion events. ARCH-04D2 and database-bound receipts; output-file authority remains unavailable. ARCH-04D1 enforces canonical ART material lineage for all terminal results retaining material; public intake remains deferred to the later cutover prerequisites. ARCH-04E1A adds one immutable route-neutral -TASK source table, detached source facts and a source-neutral type-only -accepted-effects Protocol. It adds no runtime writer, reader, handler, current -pointer, routing authority or acceptance behavior. Required success +TASK source table, detached source facts and source-neutral accepted-effects +contracts. REV-04C uses the bounded exact-source verifier, but there is no +general routing publication writer/reader, handler, current pointer or routing +authority. Required success then branches on the locked ReviewPolicy: true routes to human `allow_review`; false invokes shared authorized acceptance without a human Review. Both routing integrations remain planned; false has metadata and guard-reachability proof only and guide activation still rejects it. ART-07A1 supplies metadata-only reviewer packet types, with no resolver or byte authority. REV-03B packets and REV-04A immutable Review source storage are delivered; REV-04B shared acceptance storage, CON-03C contribution/award storage, REV-12A1 disabled controller/fence mechanics and CON-07 hidden source-neutral submitter participation with complete frozen award sets -are delivered. AUTH-19A supplies inert exact source commitments and a planned router +are delivered. [REV-04C](../.commitrail/initiatives/WS-REV-001/WS-REV-001-04C.md) +adds one hidden source-neutral participant that stages +FinalAcceptance, TASK accepted/completed effects and the complete CON outcome in +the caller's transaction. This mechanical core is not an authorized operation, +routing handler, currentness guarantee or live acceptance path. AUTH-19A supplies inert exact source commitments and a planned router identity; ARCH-04E2-A supplies hidden source preparation and a nominal receipt projection only after canonical consumption, which is unreachable while the action -is planned. Durable receipts commit only with the complete consequence. Human review/revision, acceptance composition, conditional +is planned. Durable receipts commit only with the complete consequence. Human review/revision, acceptance authority/evidence closure, conditional fulfillment, operations and release proof complete v0.1. The [independent MCP package](../mcp_server/README.md) implements nine self-service @@ -187,10 +192,10 @@ cannot be reused as post-submission review-gate evidence. See the | Contributor artifact preparation | **Hidden and proven** | One outer ZIP; bounded scratch inspection; canonical manifest; platform and project prechecks; unchanged-work rejection; durable put intent; verification; capacity-charged ready admission; hidden final handoff validates the exact activated historical guide through owner ports | Complete the later public admission-only cutover | | Pre-submission intake checking | **Hidden with approved-guide lineage** | Separate versioned pre-submission catalogue, locked effective-plan compilation, platform/project checks during continuous preparation, blocking feedback before Submission creation, and one internal phase command covering execution/replay with the JSON precheck removed; ARCH-03D connects approved-guide lineage through the final durable handoff | Complete the canonical public cutover after evaluation/remediation prerequisites; passing intake must never substitute for post-submit evaluation | | Immutable Submission creation | **Hidden foundation; public packet creation retired** | Contributor preparation authority; durable pre-submit reservation and exact completed-evidence recovery without rerunning checks; atomic admission consumption; TASK-owned admission-backed creation with exact assignment ContributionPolicyVersion and locked policy lineage; fixed-service artifact binding; replay/concurrency/rollback proof | Finish downstream evaluation and the canonical public integration. The retained submission-list GET is not a usable creation POST | -| Post-submission evaluation and `allow_review` | **Hidden exact preparation; routing planned** | One canonical CHECKER post-submit catalogue/compiler used by existing consumers, internal phase service with exact fixed-service post-submit authority, hidden value contracts and structural-handler conformance; ARCH-04B/04B2 input and output custody; ARCH-04C durable execution and current-result custody; ARCH-04D1 canonical ART material custody; ARCH-04D2 exact phase authority and receipts; ARCH-04E1A immutable route-neutral source table, detached facts and type-only accepted-effects Protocol; AUTH-19A inert source/receipt commitments and planned router identity; ARCH-04E1B-A immutable request/source-identity reservation and exact replay; ARCH-04E2-A strict resource/request/consequence matching and nominal adapter through canonical PREP, with planned denial before handle issuance | Next selected automated path: shared acceptance/TASK effects using delivered CON-07 participation -> hidden handlers 04E1B-B -> activation 04E2-B -> live 04E3. True routing uses the delivered preparation and can proceed to its handler after its own prerequisites without CON/shared acceptance. Mandatory persisted custody begins at activation using delivered REV-12A1 fencing, actual fulfillment-root/ordinal custody and scoped lifecycle activation; false guide activation also requires 04F remediation. Before publication, harden the same source table with mandatory exact route/owner receipts and refuse retained pre-authority rows. No source-manifest writer/reader, handler, current pointer, route or acceptance effect is live | +| Post-submission evaluation and `allow_review` | **Hidden exact preparation; routing planned** | One canonical CHECKER post-submit catalogue/compiler used by existing consumers, internal phase service with exact fixed-service post-submit authority, hidden value contracts and structural-handler conformance; ARCH-04B/04B2 input and output custody; ARCH-04C durable execution and current-result custody; ARCH-04D1 canonical ART material custody; ARCH-04D2 exact phase authority and receipts; ARCH-04E1A immutable route-neutral source table and detached facts; AUTH-19A inert source/receipt commitments and planned router identity; ARCH-04E1B-A immutable request/source-identity reservation and exact replay; ARCH-04E2-A strict preparation; REV-04C hidden FinalAcceptance/TASK/CON participant | Hidden handlers 04E1B-B are next. True routing remains independent of shared acceptance. False activation at 04E2-B requires the same strict participant input to carry the mandatory exact AUTH decision-event receipt, database-enforced FinalAcceptance/TASK/CON completeness, shared audit/outbox and scoped lifecycle/fulfillment custody. 04E1B-B must use TASK-before-CHECKERS locking and prove both currentness/successor-generation race orders. No handler, current pointer, route, currentness guarantee or acceptance effect is live; 04F remediation still precedes false guide activation | | Review queue and lease | **Hidden persistence foundation** | Queue/admission idempotency and ReviewLease/preference persistence; complete unavailable REV action/principal catalogue and typed AUTH contracts; ART-07A1 metadata-only packet contract and REV-03B immutable normalized packet persistence with live ingest custody; REV-04A Review/finding/resolution and completed request storage | Future resolver proof; canonical admission from `allow_review`; claim/lease/packet authority; lease copies the Submission-stamped policy version with no CON lookup | -| Review decision and revision | **Hidden source storage; runtime planned** | Review/revision policy identities and mutation authority; REV-04A immutable Review, findings, resolutions and completed request storage; REV-04B shared FinalAcceptance storage without authority or a production writer; approved same-task revision-rebase semantics | Authorized atomic decision composition; `accept`, `needs_revision`, and `reject`; complete-context revision preparation; finding responses; replacement contributor rules; replay and recovery | -| Contribution and compensation truth | **Hidden submitter participant plus public policy administration** | ContributionPolicyVersion persistence; lifecycle-audit participant; adapter bindings; public Finance policy administration; REV-04B shared acceptance source storage; CON-03C immutable ContributionRecord/CompensationAward storage; CON-07 source-neutral submitter participant and complete zero/one/two frozen award-set staging/replay | Compose one shared FinalAcceptance/TASK-effects operation for human accept or authorized false/pass routing. Only actual Reviews create reviewer records. Add fulfillment-root/ordinal custody before activation; no public recognition or fulfillment route is live | +| Review decision and revision | **Hidden acceptance core; runtime planned** | Review/revision policy identities and mutation authority; REV-04A immutable Review storage; REV-04B source storage; REV-04C hidden source-neutral FinalAcceptance/TASK/CON participant; approved same-task revision-rebase semantics | Add the mandatory exact AUTH receipt to the same participant input and complete database/audit/outbox closure before either trigger consumes it; then authorize atomic human decisions, revision preparation, replay and recovery | +| Contribution and compensation truth | **Hidden shared participant plus public policy administration** | ContributionPolicyVersion persistence; lifecycle-audit participant; adapter bindings; public Finance policy administration; REV-04B shared acceptance source storage; CON-03C immutable ContributionRecord/CompensationAward storage; CON-07 source-neutral submitter participant and complete award sets; REV-04C composes it with FinalAcceptance/TASK effects in the caller transaction | Add authority/evidence and complete-set database closure before production consumption. Only actual Reviews create reviewer records. Add audit/outbox and fulfillment-root/ordinal custody before activation; no public recognition or fulfillment route is live | | Fulfillment, reconciliation, and audit | **Planned** | Shared audit foundations, provider-neutral adapter convention, AUTH-OUTBOX-02 live dispatcher authority, retained phase audit decisions, Celery delivery/recovery scans and CON-02B custody | Feature-specific handlers and authority, conditional award fulfillment, callbacks, idempotent recovery, reconciliation, bounded operational reads, and release controls | | Runtime diagnostics | **Implemented foundation** | Closed structured logging, explicit API/Celery tracing, bounded metrics, safe correlation and optional typed OTLP export | Restrict broker publishers; configure and secure collector/log access, egress, encryption and finite deletion; prove diagnostics during the release drill; no deployed monitoring claim | | Frontend and pilot | **Planned after stable backend contracts** | React + Vite + TypeScript stack decision | Implement only stable backed surfaces, run the real internal pilot, repair findings, and complete release drills | @@ -501,15 +506,14 @@ The next dependency-safe product sequence is: retaining material, with database rejection, rollback and safe-upgrade proof. ARCH-04D2 supplies exact input/execute/finalize service authority and durable receipt custody. ARCH-04E1A supplies one immutable route-neutral source table, - detached facts and source-neutral accepted-effects types. It has no runtime - entry. CON-03C adds immutable contribution/award storage. REV-12A1 supplies disabled controller/fence mechanics with PostgreSQL-enforced root-transaction checks, including raw-SQL savepoint rejection. CON-07 supplies a hidden source-neutral submitter participant and complete frozen award-set staging/replay under that caller-root fence; it adds no acceptance/source writer, TASK effect, AUTH claim, route, fulfillment root or reviewer participant. + detached facts and source-neutral accepted-effects types. CON-03C adds immutable contribution/award storage. REV-12A1 supplies disabled controller/fence mechanics with PostgreSQL-enforced root-transaction checks, including raw-SQL savepoint rejection. CON-07 supplies a hidden source-neutral submitter participant and complete frozen award-set staging/replay under that caller-root fence. REV-04C composes FinalAcceptance, TASK accepted/completed effects and that CON participant in one hidden caller-owned transaction for either source. It adds no AUTH receipt, handler, route, currentness guarantee, audit/outbox consequence, fulfillment root or reviewer participant. Both branches have delivered request reservation 04E1B-A and hidden AUTH preparation 04E2-A before handlers 04E1B-B, activation 04E2-B and live 04E3. True routing may proceed after its own prerequisites to publish the exact TASK manifest and move `evaluation_pending -> review_pending` when no blocking - failure exists; this does not write the REV queue. Shared acceptance and TASK - terminal effects are additional false-branch requirements; true admission has - no CON prerequisite. + failure exists; this does not write the REV queue. The delivered shared + acceptance participant is an additional false-branch dependency; true + admission has no CON prerequisite. CHECKERS owns durable execution/currentness; the shared facade does not create a second result store. Work evaluation may be deterministic or use an explicitly implemented model judge. A structural presence check cannot @@ -523,12 +527,15 @@ The next dependency-safe product sequence is: (04E2-A) are delivered. They bind exact `TaskAcceptedEffectsRequest` values without a Review. CON-07 now uses the delivered REV-12A1 fence to stage or exactly replay the contribution and complete zero/one/two frozen award set; - paid replay checks its correlation and unpaid replay retains none. Complete - shared acceptance and TASK terminal effects, then hidden handlers 04E1B-B; - at activation 04E2-B enforce mandatory - receipt custody and prove a genuine allow with all acceptance effects in that - caller transaction. Only then wire live 04E3. CON-07 isolated storage controls - prove economic participation, not acceptance authority. Prove real scoped activation/drain and 04F remediation before + paid replay checks its correlation and unpaid replay retains none. REV-04C + now composes that result with FinalAcceptance and TASK effects. Hidden handlers + 04E1B-B are next and must lock TASK before CHECKERS currentness, proving both + acceptance/successor-generation race orders. At activation 04E2-B make the + exact AUTH decision-event receipt mandatory on the same strict input, with no + optional/default path; add database-enforced FinalAcceptance/TASK/CON + completeness, shared audit/outbox and prove a genuine allow with all effects + in that caller transaction. Only then wire live 04E3. Isolated hidden controls + prove mechanical participation, not acceptance authority. Prove real scoped activation/drain and 04F remediation before enabling false. This milestone creates the submitter contribution and applicable awards without live human queues/leases/decisions; it neither invents a reviewer nor removes the later human branch from v0.1. @@ -646,12 +653,14 @@ Delivered foundations (not a claim of full public integration) ARCH-04E1B-A immutable request and future source-ID reservation (no source publication) ARCH-04E2-A strict hidden preparation + nominal PREP adapter (action unavailable) CON-07 hidden source-neutral submitter participant + complete frozen award set + REV-04C hidden FinalAcceptance + TASK effects + CON composition (no authority/route) Remaining integration both branches: handlers 04E1B-B -> activation 04E2-B -> live 04E3 dispatch/routing true: own routing prerequisites; no CON/shared acceptance prerequisite - selected false path first: shared acceptance + TASK terminal effects -> handler - (first durable receipt commits with all consequence effects) + selected false path first: delivered shared participant -> handler + -> mandatory same-input AUTH receipt + DB complete-set closure + -> TASK-before-CHECKERS race proof + shared audit/outbox -> fulfillment-root ordinal custody and authorized lifecycle activation -> 04F remediation -> public intake and immutable admitted Submission cutover @@ -716,9 +725,10 @@ v0.1 is not ready until all of the following are true: distinct effects. - Every final review atomically creates the reviewer ContributionRecord; `accept` additionally creates FinalAcceptance and the submitter record. -- The delivered CON-07 participant's zero, one, or two frozen CompensationAwards - run inside the authorized shared-acceptance transaction without controlling - Workstream lifecycle truth; isolated hidden participation is not release proof. +- The delivered REV-04C participant composes FinalAcceptance, TASK effects and + CON-07's zero, one, or two frozen CompensationAwards in one caller transaction + without controlling Workstream lifecycle truth; isolated hidden participation + is not release proof. - Fulfillment and recovery are idempotent, observable, reconcilable, and safe under durable-job, provider, transaction, and unknown-commit failures. - Configure secured diagnostic collection with bounded retention and access, @@ -789,9 +799,9 @@ remaining trace sequence is: and [REV-04B acceptance storage](../.commitrail/initiatives/WS-REV-001/WS-REV-001-04B.md), followed by delivered [CON-03C contribution/award storage](../.commitrail/initiatives/WS-CON-001/WS-CON-001-03C.md), [REV-12A1 disabled controller/fence](../.commitrail/initiatives/WS-REV-001/WS-REV-001-12A1.md) - and [CON-07 hidden participation](../.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md) - support the additional false-path acceptance-source AUTH custody and shared - acceptance prerequisites. Both branches have delivered request reservation `04E1B-A` and AUTH preparation `04E2-A`, + and [CON-07 hidden participation](../.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md), + plus the REV-04C hidden FinalAcceptance/TASK/CON participant, support the + additional false-path acceptance-source AUTH custody and activation prerequisites. Both branches have delivered request reservation `04E1B-A` and AUTH preparation `04E2-A`, then handlers/activation/live `04E1B-B -> 04E2-B -> 04E3`; true routing proceeds after its own prerequisites without shared acceptance; 04F supplies remediation. The mandatory [04D1 canonical material custody](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04D1.md) @@ -806,17 +816,19 @@ remaining trace sequence is: proof does not make false activation available. Automatic `04E` delivery still needs its separately authorized checker-routing handler; the installed assignment handler does not supply that authority. These foundations do not require REV or fulfillment. - TASK request/source-identity reservation `04E1B-A` and hidden AUTH preparation - `04E2-A` and CON-07 hidden participation are delivered. The selected automated - path next completes shared acceptance/TASK effects, then handlers `04E1B-B`, activation `04E2-B` and - live integration `04E3`; a dispatcher cannot authorize TASK or CHECKERS mutations. + TASK request/source-identity reservation `04E1B-A`, hidden AUTH preparation + `04E2-A`, CON-07 participation and REV-04C hidden FinalAcceptance/TASK/CON + composition are delivered. The selected automated path next adds hidden + handlers `04E1B-B`, then mandatory authority/evidence closure and activation + at `04E2-B`, followed by live integration `04E3`; a dispatcher cannot authorize TASK or CHECKERS mutations. Later `04F` owns contributor-correctable remediation and admission-backed resubmission before public cutover; it does not block `allow_review` or replace human review/revision. - [CON-03C contribution/award storage](../.commitrail/initiatives/WS-CON-001/WS-CON-001-03C.md) supplies immutable source/economic equality. [CON-07 hidden participation](../.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md) - supplies source replay and complete frozen award sets, without runtime - recognition, acceptance authority or fulfillment. + supplies source replay and complete frozen award sets; REV-04C composes them + with hidden FinalAcceptance and TASK effects, without live recognition, + acceptance authority or fulfillment. - Review/revision: REV packet/schema foundations may proceed independently, but live admission starts after `ARCH-04E`; the first live Review commit also requires the atomic CON decision participant on the delivered CON-03C storage. diff --git a/docs/spec_artifact_storage_service.md b/docs/spec_artifact_storage_service.md index 1892aaaf1..1e87dba94 100644 --- a/docs/spec_artifact_storage_service.md +++ b/docs/spec_artifact_storage_service.md @@ -1628,9 +1628,10 @@ resolver or byte access. REV-03B now stores normalized packets referencing live guide ingests with committed-upload custody; retained extraction bindings remain read-only. REV-04A immutable Review source storage, REV-04B shared FinalAcceptance storage, CON-03C contribution/award storage and CON-07 hidden -submitter participation with complete frozen awards are delivered. Shared -acceptance/TASK effects and actual source authority remain prerequisites for -runtime recognition; reviewer participation and fulfillment remain separate. +submitter participation with complete frozen awards are delivered. REV-04C +composes hidden FinalAcceptance/TASK/CON effects. Exact source authority, +database complete-set enforcement, currentness proof, audit/outbox and activation +remain prerequisites for runtime recognition; reviewer participation and fulfillment remain separate. ART later supplies an exact, authorized reviewer-packet byte capability, while REV owns queueing, leases, decisions, and the reviewer note/findings. The approved v0.1 review flow does not upload a reviewer revision artifact. CON owns diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index 3b5d27c44..da0b88f47 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -540,8 +540,9 @@ In particular, the XINT-06B grouping corresponds to runtime activation contract is ARCH-04D2. Exact post-submit input, execute and finalize authority is delivered; checker-output write/bind remains planned because the current catalogue produces no output files. ARCH-04E1A route-neutral source -facts and type-only accepted-effects contracts are delivered without an action, -handler, current pointer or runtime composition. This does not reopen XINT-06B as a parallel implementation lane. Likewise ARCH-02G/02H +facts and accepted-effects contracts are delivered; REV-04C supplies the hidden +FinalAcceptance/TASK/CON participant without an action, handler, current pointer +or authorized runtime composition. This does not reopen XINT-06B as a parallel implementation lane. Likewise ARCH-02G/02H are replacement implementation boundaries, not automatic registry renames. Read exact runtime ownership from the typed catalogue. No planning-only change may promote or reassign an action. @@ -692,7 +693,7 @@ a planning document does not grant a service permission. | `task.assignment.authority_reconcile` | `workstream.task.assignment_reconciler` | Committed exact AUTH invalidation event, project/actor/grant-or-link, active pre-submit assignment; no wrong-role or submitted-history mutation | ARCH-03B9 hidden handler/fence and ARCH-03C1 real feature authority/decision receipts complete; ARCH-03C2 atomic producer wiring and registration | | `checker.post_submit.execute` | `workstream.checker.post_submit` | Immutable Submission/request/generation, locked compiled policy, attempt and admitted service; exact pre-I/O authority | Implemented by ARCH-04C/04D2; no dispatcher registration | | `checker.post_submit.finalize` | `workstream.checker.post_submit` | Exact execution request/fence, accepted result digest, retained material and original execute receipt; fresh post-I/O authority and atomic evidence; current outputs are empty | Implemented by ARCH-04C/04D2; no dispatcher registration | -| `task.post_submit.route` | `workstream.task.post_submit_router` | Committed completion event/claim, exact current CHECKER result/fence, immutable Submission, locked ReviewPolicy and TASK pre-review state; true permits only the TASK manifest transition to `review_pending`, false/pass binds exact accepted TASK effects for the future shared FinalAcceptance consequence; never a human review decision or generic CON write | AUTH-19A inert source/request commitments and planned identity, 04E1B-A request facts and 04E2-A strict hidden preparation are delivered. The nominal adapter uses canonical PREP, but planned denial prevents a handle, allow or receipt. CON-07 hidden submitter participation is delivered; shared acceptance/TASK effects still precede the false handler; 04E1B-B supplies handlers, 04E2-B enforces receipt custody and activates the first genuine atomic consequence, and 04E3 wires live composition | +| `task.post_submit.route` | `workstream.task.post_submit_router` | Committed completion event/claim, exact current CHECKER result/fence, immutable Submission, locked ReviewPolicy and TASK pre-review state; true permits only the TASK manifest transition to `review_pending`, false/pass binds exact accepted TASK effects for the shared FinalAcceptance consequence; never a human review decision or generic CON write | AUTH-19A inert source/request commitments, 04E1B-A request facts, 04E2-A strict preparation and the REV-04C hidden FinalAcceptance/TASK/CON participant are delivered. Planned denial still prevents a handle, allow or receipt. 04E1B-B supplies hidden handlers/currentness race proof; 04E2-B requires the exact AUTH receipt on the same strict input, adds database/audit/outbox closure and activates the first genuine atomic consequence; 04E3 wires live composition | Each action maps to the identically named permission in this table and only its singleton fixed-service row. Humans, dispatchers and unrelated services @@ -1661,9 +1662,9 @@ persist this new source commitment. ARCH-04E1B-A stages routing request facts; fixed-router adapter through canonical PREP. The action remains planned and the kernel rejects before handle issuance, so the adapter cannot return an allow or receipt and performs no source, publication or product write. CON-07 hidden -submitter participation is delivered; shared acceptance/TASK effects remain -next for the selected automated path. Mandatory -persisted source/FinalAcceptance custody follows at 04E2-B, +submitter participation and REV-04C hidden FinalAcceptance/TASK/CON composition +are delivered; hidden handlers are next for the selected automated path. +Mandatory exact same-input receipt and persisted source/FinalAcceptance custody follow at 04E2-B, with the first genuine allow and all governed effects in the same transaction, before production composition or consumption. No early standalone allow exists. The actor-vocabulary migration provisions nothing, and existing closed audit constraints still reject a route allow. diff --git a/docs/spec_chunk_4_task_queue_assignment.md b/docs/spec_chunk_4_task_queue_assignment.md index 1ee552d72..02dc37603 100644 --- a/docs/spec_chunk_4_task_queue_assignment.md +++ b/docs/spec_chunk_4_task_queue_assignment.md @@ -138,8 +138,10 @@ policy-governed routing; it does not own final acceptance. ARCH-04E1A persists that successful evidence in one immutable route-neutral TASK source table and exposes detached source facts plus a source-neutral, -type-only accepted-effects Protocol. It installs no writer, reader, handler, -current pointer, routing authority, TASK transition or acceptance participant. +accepted-effects Protocol. REV-04C supplies the hidden FinalAcceptance/TASK/CON +participant and uses the bounded exact-source verifier, but no general routing +publication writer/reader, handler, current pointer, routing authority or live +TASK transition. Before either true human admission or false automatic acceptance is published, the remaining ARCH-04E work must harden the same table with mandatory exact routing and owner-receipt custody and reject retained pre-authority rows. diff --git a/docs/spec_contribution_compensation.md b/docs/spec_contribution_compensation.md index 45adc5ea1..ebc803dba 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -275,7 +275,9 @@ depends on the binding. CON-03C delivers immutable source and award storage. CON-07 adds the hidden source-neutral accepted-submission participant and complete frozen award-set -owner. No production source writer, reader, recognition route, reviewer +owner. REV-04C composes that participant with hidden FinalAcceptance and TASK +accepted/completed effects in one caller-owned transaction for either source. +No production recognition route, authorized acceptance operation, reviewer participant or fulfillment consumer is registered. Before activation, originating Review/FinalAcceptance authority, mandatory source receipts and fulfillment-root ordinal custody must be installed; retained pre-authority @@ -438,7 +440,9 @@ owns the single FinalAcceptance schema, exclusive human/checker-policy source shapes, same-chain constraints, actor/authority provenance, currentness and transaction sequence. Both triggers use the same CON submitter operation; no new contribution type, synthetic Review or automatic reviewer award is added. -This is planned runtime behavior, not enabled false-policy activation. +REV-04C delivers the hidden mechanical transaction participant. The authority, +currentness, audit/outbox and activation behavior is still planned runtime +behavior, not enabled false-policy activation. CON MUST create `accepted_submission` only from the supplied locked FinalAcceptance and exact TaskAssignment. It MUST NOT infer submitter acceptance @@ -555,9 +559,12 @@ not ordinary review-claim selection. The target decision boundary has two ordered, operation-specific CON methods in the initiating command's caller-owned session. CON-07 delivers the source-neutral -submitter port; the reviewer participant remains future human-lifecycle work. -Human decision composition will use the reviewer method, while the shared -acceptance operation uses the submitter method for either trigger. A combined +submitter port, and REV-04C invokes it from the hidden source-neutral acceptance +participant; the reviewer participant remains future human-lifecycle work. +Human decision composition will use the reviewer method. Before either trigger +uses the delivered participant, its same input/schema must evolve to require the +verified AUTH event without an optional/default path, and the complete authorized +shared acceptance operation must add shared audit/outbox staging. A combined request carrying nullable FinalAcceptance or both actors' source and policy facts is prohibited. @@ -642,17 +649,19 @@ Reject MUST NOT change an actor grant, another task, or another assignment. ### Submitter operation -Runtime composition invokes the submitter operation only after shared acceptance -has created FinalAcceptance and applied accepted Task and completed -TaskAssignment effects. The delivered hidden participant itself consumes exact -stored lineage and neither creates nor authorizes those facts. +The delivered REV-04C composition invokes the submitter operation only after it +has created or exactly replayed FinalAcceptance and staged accepted Task and +completed TaskAssignment effects. CON's delivered hidden participant consumes +exact stored lineage and neither creates nor authorizes those enclosing facts. Its delivered typed input contains only immutable scalar IDs for project, task, Submission, FinalAcceptance, TaskAssignment, submitter and the assignment-frozen ContributionPolicyVersion, plus the stabilized artifact hash, caller correlation UUID and expected lifecycle generation. It carries no authorization decision, -receipt-shaped value or caller-selected contribution/award ID; future shared -acceptance owns authority and request idempotency. +receipt-shaped value or caller-selected contribution/award ID. Before production +consumption, the enclosing REV input must require the exact AUTH decision-event +receipt with no optional/default path; the complete shared operation owns +authority and request idempotency. It contains no direct Review or ReviewLease contribution-source fields. It creates or exactly replays one `accepted_submission`, evaluates only the frozen @@ -1039,7 +1048,8 @@ REV-12A owns the only `JointLifecycleReleaseControl` and generation, or availability writer. REV-12A1 delivers its disabled generation-zero persistence and caller-root mutation fence. The [shared acceptance order](spec_review_lifecycle.md#implementation-order-and-required-proof) -now includes delivered hidden AUTH preparation and CON-07 participation, while +now includes delivered hidden AUTH preparation, CON-07 participation and REV-04C +hidden FinalAcceptance/TASK/CON composition, while mandatory persisted receipt custody remains required before production composition or consumption. Actual CON root storage and ordinal allocation remain required before either trigger creates @@ -1172,11 +1182,14 @@ governs the false branch: delivered TASK ARCH-04E1A source schema/detached facts precede the delivered REV-04B acceptance, CON-03C contribution/award storage and CON-07 flush-only submitter participation. REV-12A1 supplies the disabled controller/fence mechanics used through CON's consumer-owned acquisition -Protocol; TASK request staging (04E1B-A) and hidden AUTH preparation (04E2-A) -are also delivered. Shared acceptance and TASK terminal effects precede the -04E1B-B handler. CON-07 isolated storage controls prove economic behavior, not -acceptance authority. At 04E2-B, mandatory -same-table receipt custody and exact activation prove the first genuine allow +Protocol; TASK request staging (04E1B-A), hidden AUTH preparation (04E2-A) and +REV-04C hidden shared composition are also delivered. Hidden handlers 04E1B-B +are next; they must lock TASK before CHECKERS currentness and prove both +acceptance/successor-generation race orders. These isolated controls prove +mechanical behavior, not acceptance authority. At 04E2-B, make the exact AUTH +decision-event receipt mandatory on the same strict input with no +optional/default path; add database-enforced FinalAcceptance/TASK/CON +complete-set closure, shared audit/outbox and exact activation to prove the first genuine allow with source, FinalAcceptance, TASK effects, CON rows and audit/outbox in one transaction. No standalone allow or fabricated authority fixture is permitted. Actual root ordinal custody and authorized lifecycle transition/drain proof precede live AUTH routing composition. diff --git a/docs/spec_review_lifecycle.md b/docs/spec_review_lifecycle.md index f4f5f07a6..1d69f260a 100644 --- a/docs/spec_review_lifecycle.md +++ b/docs/spec_review_lifecycle.md @@ -65,9 +65,11 @@ queue, lease and human-decision sections apply only to the human branch. The [policy setting](../.commitrail/changes/pre-review-plan-reconciliation.md#delivered-policy-setting-implementation) is persisted and versioned; false activation remains unavailable. The hidden source-neutral CON submitter participant and complete frozen award-set -port are delivered, but no shared acceptance writer, TASK terminal -effects, production authority, handler or route is implemented or enabled by -this note. In particular, +port are delivered. REV-04C now composes one source-neutral FinalAcceptance, +TASK accepted/completed effects and that CON outcome inside the caller's root +transaction. This is a hidden mechanical participant, not the complete +authorized `SharedFinalAcceptanceOperation`: no production authority, routing +handler, currentness guarantee or route is implemented or enabled. In particular, `allow_review` retains its human-admission meaning, and a checker pass alone does not authorize FinalAcceptance. No synthetic human Review or ReviewLease may be used for the false branch. @@ -393,7 +395,7 @@ a separate REV-owned intent and reviewed ART/AUTH contract. [REV-04A](../.commitrail/initiatives/WS-REV-001/WS-REV-001-04A.md) persists complete immutable Review, finding, resolution and completed request metadata. -It provides no production writer, AUTH action activation, CON participant or +It provides no production writer, AUTH action activation, CON reviewer participant or public decision route. Real PostgreSQL storage fixtures are not canonical product decisions. The transaction below remains mandatory for future runtime. @@ -496,8 +498,8 @@ effects, FinalAcceptance, contributions, awards, audit, and outbox together. `FinalAcceptance` is one internal immutable REV fact, created by one shared acceptance operation. There is no public/manual creation API, independent materialization action, second decision entity or automated acceptance engine. -The hidden CON participant that consumes this fact is delivered; the shared -writer and both runtime branches remain unavailable. +The hidden REV-04C participant writes this fact and composes TASK/CON effects; +both authorized runtime branches remain unavailable. | Trigger | Required source | Result | |---|---|---| @@ -567,7 +569,9 @@ immutable `final_acceptances` table and closed metadata inputs, with exclusive Review/TASK source lineage, exact locked policy and submitter, database-owned time and unique terminal source identities. It adds no writer, reader, shared operation or TASK effect. Direct SQL fixture rows prove relational -custody only; they are not authorized acceptance. +custody only; they are not authorized acceptance. That describes the REV-04B +slice itself; REV-04C is the later hidden +writer/participant described below. [CON-07](../.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md) delivers the hidden source-neutral submitter participant and complete frozen award-set port. @@ -579,9 +583,21 @@ correlation UUID; unpaid replay retains no correlation. It does not write FinalAcceptance, change TASK, evaluate AUTH, publish a source, fulfill an award or create a reviewer contribution. +[REV-04C](../.commitrail/initiatives/WS-REV-001/WS-REV-001-04C.md) delivers the +hidden source-neutral participant over those foundations. With a preallocated +acceptance ID and exact source/generation envelope, it locks the shared fence, +locks and validates TASK without mutation, validates the stored source, appends +or exactly replays FinalAcceptance, applies TASK accepted/completed effects, and +invokes CON with the enclosing create/replay disposition. +It flushes inside the caller's root transaction and rejects partial replay; it +does not commit, authorize a trigger, route a result, stage shared audit/outbox, +create reviewer participation or activate either branch. + `authorization_decision_event_id` above remains a required runtime field, absent from this foundation because neither originating authority is live. Before any -production consumer, harden this same table with a NOT NULL exact AUTH event and +production consumer, evolve this same strict participant input to require the +exact AUTH decision event, with no optional/default compatibility path, and +harden this same table with a NOT NULL exact AUTH event plus source/action/actor/request/resource checks. Human authority is the exact `review.decision` allow; automated authority is the exact fixed `workstream.task.post_submit_router` actor and `task.post_submit.route` allow. @@ -597,11 +613,13 @@ ancestry and the full authorized transaction remain required before activation. ### Shared transaction and dependency direction -The application composition root injects public caller-session ports into one -REV-owned `SharedFinalAcceptanceOperation`. Both callers invoke that same public -operation, not copied sequences. It appends the REV acceptance fact, invokes -TASK's accepted/completed-effects port, invokes CON's existing submitter -participant and stages shared audit/outbox effects. CON validates the fact and +REV-04C supplies the hidden transaction participant that appends the REV +acceptance fact and invokes TASK's accepted/completed-effects port plus CON's +existing submitter participant. At 04E2-B the same participant input/schema must +evolve to require the verified AUTH event, without an optional/default path, and +the complete `SharedFinalAcceptanceOperation` must stage shared audit/outbox +effects. Both callers invoke that same evolved public operation, not copied +sequences. CON validates the fact and exact assignment before appending the contribution and conditional awards. All participants flush only. The initiating command owns the single commit: human decision composition also stages the actual Review and reviewer contribution; TASK @@ -630,7 +648,9 @@ zero applicable `human_review` dispositions, both at guide activation and again at acceptance. Such a disposition is not a failed executable check and cannot be silently relabeled `supported_post_submit` or ignored because all executable checks passed. Never read the project's newest policy to choose the branch. -After acceptance, reject a new evaluation generation, resubmission or policy +This TASK-before-CHECKERS currentness coordination and both race orders remain +04E1B-B/04E2-B proof; REV-04C alone does not establish them. After acceptance, +reject a new evaluation generation, resubmission or policy rebase for that task. Racing acceptance, supersession and retries serialize: an earlier supersession rejects old evidence; an earlier acceptance prevents supersession from invalidating terminal truth. Exact duplicate delivery returns @@ -642,8 +662,8 @@ Extract foundations from existing owner work, not a new initiative: 1. ARCH-04E1A's TASK manifest persistence/detached scalar facts and narrow accepted-effects Protocol are delivered after CHECKERS-04C facts, without a - writer, reader, current pointer, routing handler, effects implementation or - REV FK. One manifest stores the locked `human_review_required` branch; it is + general routing publication writer/reader, current pointer or handler. Its + bounded exact-source verifier is used by REV-04C. One manifest stores the locked `human_review_required` branch; it is not restricted to human admission. False proof is scalar transport only while activation remains unavailable. This schema precedes the REV source FK. 2. [ART-07A1](../.commitrail/initiatives/WS-ART-001/WS-ART-001-07A1.md) supplies @@ -651,9 +671,10 @@ Extract foundations from existing owner work, not a new initiative: delivered. REV-04A immutable Review-source storage and REV-04B shared FinalAcceptance storage and CON-03C contribution/award persistence are delivered. Do not create an incomplete Review solely as an FK target. REV-12A1 supplies the disabled - controller and transaction fence. Exact hidden AUTH preparation and CON-07 - participation are delivered; mandatory persisted receipt custody precedes - production composition or consumption. These foundations require no live human claim or + controller and transaction fence. Exact hidden AUTH preparation, CON-07 + participation and REV-04C hidden FinalAcceptance/TASK/CON composition are + delivered; mandatory persisted receipt custody precedes production composition + or consumption. These foundations require no live human claim or decision endpoint. Hidden composition proof precedes exact AUTH activation; unavailable authority must not be replaced with fabricated allow evidence. 3. [REV-12A1](../.commitrail/initiatives/WS-REV-001/WS-REV-001-12A1.md) @@ -668,19 +689,25 @@ Extract foundations from existing owner work, not a new initiative: owner. Its consumer-owned Protocol acquires the supplied canonical fence before CON or compensation access; exact source replay preserves contribution and award IDs, paid replay checks correlation, and unpaid replay retains no - correlation. These isolated storage controls prove economic behavior without - claiming acceptance authority or fabricating allows. + correlation. REV-04C composes those effects with FinalAcceptance and TASK + accepted/completed effects for either source. These isolated controls prove + mechanical transaction behavior without claiming acceptance authority or + fabricating allows. Actual CON fulfillment roots own immutable ordinal allocation; no award or outbox row substitutes for a root. Authorized transition/drain composition and real root/cutoff proof extend this fence before activation, independently of live human queues or decisions. -4. The shared operation and TASK accepted/completed effects are next. ARCH-04E1B-B's - hidden routing handler invokes it for false/pass. True routing does not +4. ARCH-04E1B-B's hidden routing handler is next and invokes the delivered + participant for false/pass. It owns TASK-before-CHECKERS currentness and both + successor-generation race orders. True routing does not require CON or shared acceptance; it uses the delivered preparation, hidden 04E1B-B, exact AUTH 04E2-B and live 04E3 after its own prerequisites. The existing lifecycle-control command receives scoped AUTH activation for the proven shared manifest as specified below; - ARCH-04E2-B installs mandatory same-table receipt custody and activates the - existing action. Its first genuine allow commits in one caller transaction + ARCH-04E2-B evolves the same strict participant input to require the exact + AUTH decision-event receipt, with no optional/default path; installs + mandatory same-table receipt custody and database-enforced complete-set + closure across FinalAcceptance/TASK/CON; stages shared audit/outbox; and + activates the existing action. Its first genuine allow commits in one caller transaction with source publication, FinalAcceptance, TASK effects, CON rows and audit/ outbox; failure rolls all back. Retained pre-authority sources remain refused. ARCH-04E3 then proves live composition. @@ -724,8 +751,9 @@ another acceptance architecture. contract authority. This section defines REV's orchestration obligations at that boundary. Merged CON-01 publishes contracts only; it implements no policy persistence, contribution record, award, participant, or fulfillment runtime. -CON-07 delivers the source-neutral submitter port and complete frozen -award-set owner; reviewer participation and runtime composition remain future. +CON-07 delivers the source-neutral submitter port and complete frozen award-set +owner; REV-04C composes its submitter path with hidden FinalAcceptance/TASK +effects. Reviewer participation and authorized runtime composition remain future. Every committed Review creates exactly one reviewer `completed_review` ContributionRecord sourced directly from the Review and ReviewLease. Only @@ -742,7 +770,9 @@ The target CON boundary exposes two operation-specific flush-only inputs: accepted task effects, containing immutable scalar FinalAcceptance, TaskAssignment, submitter, assignment-frozen ContributionPolicyVersion, stabilized artifact, correlation and expected-generation facts. It carries no - authorization decision or receipt-shaped authority value. + authorization decision or receipt-shaped authority value. Before production + consumption, the enclosing REV participant input must separately require the + exact AUTH decision-event receipt; this absence is not an optional authority path. Database constraints keep the source shapes mutually exclusive and enforce one `completed_review` per Review and one `accepted_submission` per