From 559014ce3c0ec5eeddaaef0a8deadaab12dfd2a8 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:09:35 +0100 Subject: [PATCH 01/11] docs(plan): bound CON-07 submitter participation before shared acceptance --- .../initiatives/WS-CON-001/WS-CON-001-07.md | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md new file mode 100644 index 000000000..20106f1b2 --- /dev/null +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -0,0 +1,90 @@ +# CON-07 — Hidden submitter contribution and complete frozen awards + +- Initiative: `WS-CON-001` +- Durable disposition: `Planned` +- Intended merge outcome: one hidden flush-only submitter participant creates or exactly replays a FinalAcceptance-sourced contribution and its complete frozen award set; no runtime acceptance activation. + +## Intent and current behavior + +Finish the economic participant needed by shared acceptance before live human +review. The current ARCH dependency contract and CON overview supersede the +pre-cutover CON-07 human-review-first contract. `records/models.py`, +`compensation/awards/models.py` and migration 0015 enforce immutable source and +individual award lineage. They have no participant and do not require all +compensated definitions to have awards. REV-12A1 supplies a root-transaction +fence; ARCH-04E2-A supplies hidden exact AUTH preparation with planned denial. +These storage prerequisites do not authorize acceptance or payment. + +## Bounded change + +### Allowed + +- `backend/app/modules/contributions/api/participation.py` and `records/{participant,repository}.py`: source-neutral typed submitter input/result, exact source replay and frozen-rule selection. +- `backend/app/modules/compensation/api/awards.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. +- `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. +- `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, migration graph assertions if necessary: focused integration and regression proof. +- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and behavior-ownership records: exact new proof/owner registration only; no gate weakening. +- This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. + +### Not allowed + +- Production composition, routes, workers, AUTH allow/receipt fabrication or action activation; routing/source publication; FinalAcceptance writer; TASK state changes; live human reviewer participant/queues/revision; fulfillment roots, ordinals, payment delivery, reputation. +- Private REV/TASK/ART/AUTH repository imports or provider calls. No compatibility paths, second contribution store, silent incomplete-award repair, retained-data deletion, gate/cap weakening. + +## Design + +CON input is immutable scalar source lineage: project/task/submission, +FinalAcceptance, assignment, contributor, frozen policy version, stabilized +artifact hash and correlation UUID. It has no caller-chosen contribution/award +IDs and no receipt-shaped value purporting to authorize recognition. Existing +SQL source guards validate real stored ancestry. Future shared acceptance owns +actual source authority and TASK terminal-state proof; isolated storage controls +here do not claim those effects or permission. + +The caller owns one root AsyncSession transaction and the existing REV lifecycle +fence, acquired before owner locks; it stays open through later effects. No +participant commits or rolls back independently. No current policy selection +or adapter availability is re-evaluated: published/retired frozen definitions +remain due even after retirement or binding suspension. + +CON loads its exact frozen accepted-submission rule and immutable definitions, +serializes exact source insertion/replay, generates UUIDv7 IDs only for new rows, +and copies the supplied artifact digest unchanged. The compensation owner +receives strict contribution/definition facts through its public port, writes +all awards and returns exact immutable facts. Unpaid means zero; compensated +means one or two exact definitions. Exact replay recovers original IDs and +requires the complete stored award set; conflicts never backfill partial rows. +Returned facts supply later audit/outbox composition; this chunk does not stage +or claim authorized audit/outbox events. + +A deferred SQL constraint trigger verifies complete accepted-submission award +membership at commit, allowing the contribution and awards to be staged in +sequence. It also protects late inserts from changing a completed set. All +protected SQL references are schema-qualified and functions pin safe search_path. +Upgrade refuses incomplete retained accepted-submission rows unchanged. Reviewer +completeness remains with its future participant; existing reviewer storage +proof is retained. + +## Acceptance criteria and verification + +- [ ] Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. +- [ ] Source replay returns the same record and award IDs; each changed source fact and correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. +- [ ] Real stored foreign source substitutions reject at intended ownership guard, with valid controls; copied decimal quantity, unit, binding, definition and artifact hash equal canonical stored facts. +- [ ] Caller rollback and late database failure discard new FinalAcceptance/contribution/awards together. No participant commit, provider, ART, AUTH evaluation, Review or task mutation occurs. +- [ ] Missing root transaction/savepoints are rejected or fenced through the existing canonical root fence; independent sessions prove transaction-scoped serialization. +- [ ] Direct SQL partial compensated award set fails at deferred commit; full set and unpaid controls succeed; removing the completeness guard makes the negative proof fail. Retained incomplete migration fixture refuses upgrade without repair/deletion. +- [ ] New tests are registered once, all existing required storage proof remains, full hosted suite completes with zero skips/deselections. No percentages are acceptance gates. + +Planned executable proof: `cd backend && .venv/bin/python -m pytest -q tests/contributions/participation tests/contributions/records` with isolated PostgreSQL; focused contract tests without infrastructure where possible. Run Ruff, module/AUTH/structure/ownership checks, stale wording, Markdown links and Commitrail checks; then hosted full suite. Guard-removal and independent-session probes bind to the exact implementation candidate. + +## Risk and review routing + +- Risk class: L1 — economic lineage, immutable storage and caller transactions. +- Required reviews: architecture/reuse; security; QA/test-delta; CI integrity for catalogue/gates; documentation/product-operations for boundary claims. +- Human focus: no recognition is live; storage controls are not authority, existing facts are never repaired silently, and only the submitter participant is delivered here. + +## Reconciliation + +- Current main: 89dd8c91 includes merged routing preparation, observability and CLI self-service. Preserve their ownership and test registrations. +- Next: shared acceptance/TASK accepted effects and hidden handlers, actual authority custody and fulfillment-root/lifecycle proof before activation; later reviewer participant reuses these owners. +- Remaining risks: production authority, full acceptance atomicity and fulfillment are deliberately not established by this hidden participant. From 2af8a6c3bb2d66c9517bf55b283115f71301647b Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:12:58 +0100 Subject: [PATCH 02/11] docs(plan): make fence custody and unpaid replay limits explicit --- .../initiatives/WS-CON-001/WS-CON-001-07.md | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 20106f1b2..2c2f1bb22 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -19,11 +19,11 @@ These storage prerequisites do not authorize acceptance or payment. ### Allowed -- `backend/app/modules/contributions/api/participation.py` and `records/{participant,repository}.py`: source-neutral typed submitter input/result, exact source replay and frozen-rule selection. -- `backend/app/modules/compensation/api/awards.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. -- `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. +- `backend/app/modules/contributions/api/participation.py` , `api/__init__.py` and `records/{participant,repository}.py`: source-neutral typed submitter input/result, exact source replay and frozen-rule selection. +- `backend/app/modules/compensation/api/awards.py` , `api/__init__.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. +- `backend/alembic/env.py`, `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. - `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, migration graph assertions if necessary: focused integration and regression proof. -- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and behavior-ownership records: exact new proof/owner registration only; no gate weakening. +- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/{partition.v1.json,contributions/con07-submitter-participation.json}`: exact new proof/owner registration only; no gate weakening. - This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. ### Not allowed @@ -41,19 +41,28 @@ SQL source guards validate real stored ancestry. Future shared acceptance owns actual source authority and TASK terminal-state proof; isolated storage controls here do not claim those effects or permission. -The caller owns one root AsyncSession transaction and the existing REV lifecycle -fence, acquired before owner locks; it stays open through later effects. No +The caller owns one root AsyncSession transaction and supplies the existing REV +lifecycle fence bound to that same session. The participant calls its public +`acquire(expected_generation)` first, before any CON/COMPENSATION query or lock; +it never accepts constructible detached facts as proof of custody. The eventual +shared acceptance caller must acquire that same reentrant fence before source +and TASK locks. The transaction stays open through later effects. No participant commits or rolls back independently. No current policy selection or adapter availability is re-evaluated: published/retired frozen definitions remain due even after retirement or binding suspension. CON loads its exact frozen accepted-submission rule and immutable definitions, -serializes exact source insertion/replay, generates UUIDv7 IDs only for new rows, +uses source-unique PostgreSQL INSERT ON CONFLICT DO NOTHING plus exact winner +recovery (never catches a uniqueness failure in a broken root transaction), +generates UUIDv7 IDs only for new rows, and copies the supplied artifact digest unchanged. The compensation owner receives strict contribution/definition facts through its public port, writes all awards and returns exact immutable facts. Unpaid means zero; compensated means one or two exact definitions. Exact replay recovers original IDs and requires the complete stored award set; conflicts never backfill partial rows. +Correlation is new-award trace metadata: compensated replay checks stored +correlation equality; unpaid replay has no persisted correlation and does not +claim to compare it. Future shared acceptance owns its request idempotency. Returned facts supply later audit/outbox composition; this chunk does not stage or claim authorized audit/outbox events. @@ -68,12 +77,14 @@ proof is retained. ## Acceptance criteria and verification - [ ] Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. -- [ ] Source replay returns the same record and award IDs; each changed source fact and correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. +- [ ] Source replay returns the same record and award IDs; each changed source fact and compensated-award correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. - [ ] Real stored foreign source substitutions reject at intended ownership guard, with valid controls; copied decimal quantity, unit, binding, definition and artifact hash equal canonical stored facts. - [ ] Caller rollback and late database failure discard new FinalAcceptance/contribution/awards together. No participant commit, provider, ART, AUTH evaluation, Review or task mutation occurs. -- [ ] Missing root transaction/savepoints are rejected or fenced through the existing canonical root fence; independent sessions prove transaction-scoped serialization. +- [ ] The supplied canonical fence rejects missing root transaction and managed/raw SQL savepoints; independent sessions prove transaction-scoped serialization. - [ ] Direct SQL partial compensated award set fails at deferred commit; full set and unpaid controls succeed; removing the completeness guard makes the negative proof fail. Retained incomplete migration fixture refuses upgrade without repair/deletion. -- [ ] New tests are registered once, all existing required storage proof remains, full hosted suite completes with zero skips/deselections. No percentages are acceptance gates. +- [ ] Existing paid storage tests must stage complete accepted-submission award sets or retain invalid insert probes in a rolled-back transaction; no required assertion is removed. New tests are registered once in the project lifecycle group (schema has little timing headroom), all existing required storage proof remains, full hosted suite completes with zero skips/deselections. No percentages are acceptance gates. + +Fixtures use real retained human-source FinalAcceptance storage; automated no-Review production proof remains with false activation, which is currently blocked. No fixture changes true to false or fabricates AUTH allows. Planned executable proof: `cd backend && .venv/bin/python -m pytest -q tests/contributions/participation tests/contributions/records` with isolated PostgreSQL; focused contract tests without infrastructure where possible. Run Ruff, module/AUTH/structure/ownership checks, stale wording, Markdown links and Commitrail checks; then hosted full suite. Guard-removal and independent-session probes bind to the exact implementation candidate. From 6d4cc12216f922d2c065538c8fbedcd0176830cc Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:14:32 +0100 Subject: [PATCH 03/11] docs(plan): keep acceptance participant dependency acyclic --- .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 2c2f1bb22..334d0d578 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -29,21 +29,24 @@ These storage prerequisites do not authorize acceptance or payment. ### Not allowed - Production composition, routes, workers, AUTH allow/receipt fabrication or action activation; routing/source publication; FinalAcceptance writer; TASK state changes; live human reviewer participant/queues/revision; fulfillment roots, ordinals, payment delivery, reputation. -- Private REV/TASK/ART/AUTH repository imports or provider calls. No compatibility paths, second contribution store, silent incomplete-award repair, retained-data deletion, gate/cap weakening. +- REV imports (including its public lifecycle types), private TASK/ART/AUTH repository imports or provider calls. No compatibility paths, second contribution store, silent incomplete-award repair, retained-data deletion, gate/cap weakening. ## Design CON input is immutable scalar source lineage: project/task/submission, FinalAcceptance, assignment, contributor, frozen policy version, stabilized -artifact hash and correlation UUID. It has no caller-chosen contribution/award +artifact hash, correlation UUID and expected lifecycle generation. It has no caller-chosen contribution/award IDs and no receipt-shaped value purporting to authorize recognition. Existing SQL source guards validate real stored ancestry. Future shared acceptance owns actual source authority and TASK terminal-state proof; isolated storage controls here do not claim those effects or permission. The caller owns one root AsyncSession transaction and supplies the existing REV -lifecycle fence bound to that same session. The participant calls its public -`acquire(expected_generation)` first, before any CON/COMPENSATION query or lock; +lifecycle fence bound to that same session through a CON-owned acquisition-only +Protocol (`acquire(expected_generation: int) -> object`). External composition +passes the canonical REV implementation structurally; CON imports no REV type, +so the future REV -> CON dependency remains acyclic. No returned scalar is +interpreted as authority. The participant calls `acquire(expected_generation)` first, before any CON/COMPENSATION query or lock; it never accepts constructible detached facts as proof of custody. The eventual shared acceptance caller must acquire that same reentrant fence before source and TASK locks. The transaction stays open through later effects. No From 74fb454ca4aabc3c8dc6ee83281efcc3232cd48d Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:15:33 +0100 Subject: [PATCH 04/11] docs(plan): require partial replay rejection proof --- .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 334d0d578..f89dbcf82 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -82,6 +82,7 @@ proof is retained. - [ ] Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. - [ ] Source replay returns the same record and award IDs; each changed source fact and compensated-award correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. - [ ] Real stored foreign source substitutions reject at intended ownership guard, with valid controls; copied decimal quantity, unit, binding, definition and artifact hash equal canonical stored facts. +- [ ] In one real fenced root transaction, stage a valid compensated contribution with one of two canonical awards before deferred commit; exact-source replay must conflict without inserting the missing award, then caller rolls back. Removing replay completeness validation must fail the intended conflict assertion (whether the broken code returns a partial set or backfills). - [ ] Caller rollback and late database failure discard new FinalAcceptance/contribution/awards together. No participant commit, provider, ART, AUTH evaluation, Review or task mutation occurs. - [ ] The supplied canonical fence rejects missing root transaction and managed/raw SQL savepoints; independent sessions prove transaction-scoped serialization. - [ ] Direct SQL partial compensated award set fails at deferred commit; full set and unpaid controls succeed; removing the completeness guard makes the negative proof fail. Retained incomplete migration fixture refuses upgrade without repair/deletion. From 9b47c5540fae0cadce6c359ccca8a42dfa7a9e41 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:16:41 +0100 Subject: [PATCH 05/11] docs(plan): use canonical intent section --- .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index f89dbcf82..fba356bb7 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -4,7 +4,7 @@ - Durable disposition: `Planned` - Intended merge outcome: one hidden flush-only submitter participant creates or exactly replays a FinalAcceptance-sourced contribution and its complete frozen award set; no runtime acceptance activation. -## Intent and current behavior +## Intent Finish the economic participant needed by shared acceptance before live human review. The current ARCH dependency contract and CON overview supersede the From 61fd17c1ccc526a39faba7762e94b1018fcba664 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:17:01 +0100 Subject: [PATCH 06/11] docs(plan): preserve required verification headings --- .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index fba356bb7..60b267f51 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -77,7 +77,7 @@ Upgrade refuses incomplete retained accepted-submission rows unchanged. Reviewer completeness remains with its future participant; existing reviewer storage proof is retained. -## Acceptance criteria and verification +## Acceptance criteria - [ ] Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. - [ ] Source replay returns the same record and award IDs; each changed source fact and compensated-award correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. @@ -90,6 +90,8 @@ proof is retained. Fixtures use real retained human-source FinalAcceptance storage; automated no-Review production proof remains with false activation, which is currently blocked. No fixture changes true to false or fabricates AUTH allows. +## Evidence + Planned executable proof: `cd backend && .venv/bin/python -m pytest -q tests/contributions/participation tests/contributions/records` with isolated PostgreSQL; focused contract tests without infrastructure where possible. Run Ruff, module/AUTH/structure/ownership checks, stale wording, Markdown links and Commitrail checks; then hosted full suite. Guard-removal and independent-session probes bind to the exact implementation candidate. ## Risk and review routing From c690b7d53cfc6d8d008eb5b34603a506e987df30 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:46:50 +0100 Subject: [PATCH 07/11] feat(con): add hidden submitter participation and complete frozen awards --- .ci/behavior-ownership/partition.v1.json | 22 +- .commitrail/INDEX.md | 8 +- .../initiatives/WS-ARCH-001/OVERVIEW.md | 27 +- .../WS-ARCH-001/planning/CHUNK_MAP.md | 10 +- .../initiatives/WS-ARCH-001/planning/PLAN.md | 19 +- .../initiatives/WS-AUTH-001/OVERVIEW.md | 23 +- .../WS-AUTH-001/planning/CHUNK_MAP.md | 16 +- .../initiatives/WS-AUTH-001/planning/PLAN.md | 16 +- .../initiatives/WS-CON-001/OVERVIEW.md | 51 +-- .../initiatives/WS-CON-001/WS-CON-001-07.md | 20 +- .../initiatives/WS-POL-003/OVERVIEW.md | 24 +- .../WS-POL-003/planning/CHUNK_MAP.md | 14 +- .../initiatives/WS-POL-003/planning/PLAN.md | 14 +- .../initiatives/WS-REV-001/OVERVIEW.md | 33 +- README.md | 21 +- backend/alembic/env.py | 4 +- .../0019_submitter_award_completeness.py | 126 ++++++++ .../app/modules/compensation/api/__init__.py | 16 + .../app/modules/compensation/api/awards.py | 135 ++++++++ .../compensation/awards/participant.py | 122 +++++++ .../app/modules/contributions/api/__init__.py | 16 + .../contributions/api/participation.py | 94 ++++++ .../contributions/records/participant.py | 120 +++++++ .../contributions/records/repository.py | 196 ++++++++++++ backend/scripts/test_lane_catalogue.py | 10 +- .../contributions/participation/__init__.py | 1 + .../contributions/participation/support.py | 33 ++ .../participation/test_contracts.py | 86 +++++ .../participation/test_postgresql.py | 300 ++++++++++++++++++ .../participation/test_transactions.py | 121 +++++++ .../tests/contributions/records/support.py | 18 +- .../contributions/records/test_migration.py | 140 +++++++- .../contributions/records/test_storage.py | 108 ++++++- backend/tests/test_alembic.py | 2 +- backend/tests/test_ci_lane_catalogue.py | 9 +- docs/roadmap_status.md | 67 ++-- docs/spec_contribution_compensation.md | 80 +++-- docs/spec_review_lifecycle.md | 52 ++- 38 files changed, 1972 insertions(+), 202 deletions(-) create mode 100644 backend/alembic/versions/0019_submitter_award_completeness.py create mode 100644 backend/app/modules/compensation/api/awards.py create mode 100644 backend/app/modules/compensation/awards/participant.py create mode 100644 backend/app/modules/contributions/api/participation.py create mode 100644 backend/app/modules/contributions/records/participant.py create mode 100644 backend/app/modules/contributions/records/repository.py create mode 100644 backend/tests/contributions/participation/__init__.py create mode 100644 backend/tests/contributions/participation/support.py create mode 100644 backend/tests/contributions/participation/test_contracts.py create mode 100644 backend/tests/contributions/participation/test_postgresql.py create mode 100644 backend/tests/contributions/participation/test_transactions.py diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index afd2fb392..a6156b817 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -884,6 +884,10 @@ "group": "lifecycle", "target": "backend/app/modules/compensation/api/adapter_bindings.py" }, + { + "group": "shared", + "target": "backend/app/modules/compensation/api/awards.py" + }, { "group": "shared", "target": "backend/app/modules/compensation/api/instruments.py" @@ -896,6 +900,10 @@ "group": "lifecycle", "target": "backend/app/modules/compensation/awards/models.py" }, + { + "group": "shared", + "target": "backend/app/modules/compensation/awards/participant.py" + }, { "group": "shared", "target": "backend/app/modules/compensation/models.py" @@ -916,6 +924,10 @@ "group": "lifecycle", "target": "backend/app/modules/compensation/service.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/contributions/api/participation.py" + }, { "group": "lifecycle", "target": "backend/app/modules/contributions/api/policies.py" @@ -960,6 +972,14 @@ "group": "lifecycle", "target": "backend/app/modules/contributions/records/models.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/contributions/records/participant.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/contributions/records/repository.py" + }, { "group": "lifecycle", "target": "backend/app/modules/contributions/records/schemas.py" @@ -1629,7 +1649,7 @@ "target": "backend/scripts/validate_test_lane_evidence.py" } ], - "authority_digest": "f945d1495efbf8c69985a1e50669a5219bc3d8746556fe27c14d8238ba97ba86", + "authority_digest": "ce43271253a9f45a0477b003990161c7dca4eec4edee4ae0ad31e0ddafbfef3a", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/.commitrail/INDEX.md b/.commitrail/INDEX.md index cd6c19c63..03823ec5e 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 | Three self-service tools delivered through WS-MCP-002-02; 24 tools remain and WS-MCP-002-03 administrative reads are next | | [WS-CLI-001](initiatives/WS-CLI-001/OVERVIEW.md) | Planned | Two public Go CLI reads delivered through WS-CLI-001-01; profile editing and later public workflows remain | -| [WS-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | Source storage, inert AUTH contracts, TASK request reservation and hidden exact AUTH preparation are delivered; CON-07/shared acceptance prerequisites 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 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-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 and ARCH-04E2-A hidden strict PREP matching are delivered; the action remains unavailable, with CON-07/shared acceptance, 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 is delivered; CON-07 hidden atomic participation follows exact AUTH preparation; production consumption requires genuine authority and retained source receipts. | +| [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-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 are delivered; source receipt custody and shared acceptance participation remain, with human review/revision separately dependency-gated. | +| [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-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 d20c4af43..094b78264 100644 --- a/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A now delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical 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 proceeds to -CON-07/shared acceptance prerequisites. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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 +23,11 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Disposition: Planned - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies - metadata-only packet types. REV-03B 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. No packet resolver or human runtime is live. + 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 + delivered. Mandatory persisted custody accompanies the first authorized atomic + consequence at 04E2-B. No packet resolver or human runtime is live. - Completed boundary: through 02H, [CP05](WS-ARCH-001-CP05.md), [CP06](WS-ARCH-001-CP06.md), [CP07](WS-ARCH-001-CP07.md), [ARCH-03A](WS-ARCH-001-03A.md), and [ARCH-04A consolidation](WS-ARCH-001-04A.md) canonical post-submit contracts/conformance. @@ -46,9 +52,9 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), 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. [REV-04B](../WS-REV-001/WS-REV-001-04B.md) adds shared FinalAcceptance source - storage, without AUTH receipt custody or runtime consumers. -- Next usable boundary: for the selected automated-acceptance delivery sequence, CON-07/shared acceptance prerequisites using the delivered REV-12A1 disabled fence, - plus mandatory same-table acceptance authority hardening before shared + 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 authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers public manager activation @@ -77,12 +83,13 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), factory or authorization paths. 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, -CON participation and authorized activation remain separate required work. +generation-zero controller storage and transaction locking. Acceptance-source +AUTH custody, shared acceptance/TASK effects 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 -can proceed after its own prerequisites without CON-07/shared acceptance or +can proceed after its own prerequisites without CON/shared acceptance or scoped lifecycle activation. False routing adds those requirements. Human final acceptance later uses the same authorized shared acceptance/CON operation. diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md index cbc241fa5..34b0d506c 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md @@ -4,9 +4,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/shared acceptance -prerequisites are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. @@ -52,7 +54,7 @@ public intake remains deferred to ARCH-02I. | [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 CON-07/shared acceptance, mandatory persisted custody at activation, actual obligation custody and scoped lifecycle activation; true does not. False guide activation also requires 04F remediation | +| [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-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 97b6cbda6..72d7d6af8 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-ARCH-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. The action remains planned/unavailable, so no handle, allow, receipt, -source write or product effect is reachable. CON-07/shared acceptance prerequisites -are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. @@ -71,7 +73,7 @@ checker-remediation boundary before public Submission cutover. | [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-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 CON-07/shared acceptance using delivered storage/fence | Hidden TASK handlers; durable receipt proof follows at exact activation | +| 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-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 | @@ -112,10 +114,11 @@ 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. The current delivery -priority is the false acceptance branch: CON-07 and shared acceptance after -delivered exact AUTH preparation. 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-07 or +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. +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. See [ARCH-04E1B/04E2/04E3](chunks/WS-ARCH-001-04E-canonical-allow-review.md#current-bounded-sequence) and ARCH-04F. ARCH-04E1A is delivered as the source-only predecessor. diff --git a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md index 11d4bbef0..f77df9f39 100644 --- a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A now delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical 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 proceeds to -CON-07/shared acceptance prerequisites. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. @@ -23,7 +25,11 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), - Disposition: Planned - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies - metadata-only packet types. REV-03B 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. No packet resolver or human runtime is live. + 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 + delivered. Mandatory persisted custody accompanies the first authorized atomic + consequence at 04E2-B. No packet resolver or human runtime is live. - Intent: provide deny-default, project-scoped authority with canonical human and service identities and attributable audit evidence. @@ -67,7 +73,7 @@ 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: for the selected automated-acceptance delivery sequence, CON-07 using the delivered REV-12A1 disabled fence, before shared acceptance composition and hidden ARCH-04E1B-B; +- Next usable boundary: shared acceptance/TASK effects before hidden ARCH-04E1B-B; ARCH-04E2 then owns exact routing activation before 04E3 live composition. - Governing source: `docs/spec_authorization_service.md`, authorization code, migrations, and tests. @@ -76,12 +82,13 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), fail-closed action availability. 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, -CON participation and authorized activation remain separate required work. +generation-zero controller storage and transaction locking. Acceptance-source +AUTH custody, shared acceptance/TASK effects 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 -can proceed after its own prerequisites without CON-07/shared acceptance or +can proceed after its own prerequisites without CON/shared acceptance or scoped lifecycle activation. False routing adds those requirements. Human final acceptance later uses the same authorized shared acceptance/CON operation. diff --git a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md index d07061f67..803b87ec7 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md @@ -4,15 +4,21 @@ 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/shared acceptance -prerequisites are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. The delivered [ART-07A1 metadata contract](../../WS-ART-001/WS-ART-001-07A1.md) -and REV-03B normalized packet persistence are delivered. REV-04A Review source storage is also delivered; REV-04B shared acceptance 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. These are storage prerequisites; +and REV-03B normalized packet persistence are delivered. REV-04A Review source +storage, REV-04B acceptance 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 first +authorized atomic consequence at 04E2-B. These are hidden prerequisites; no live human-review queue or endpoint is required for automated acceptance. Use the [current plan](PLAN.md) and @@ -32,7 +38,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; false additionally requires acceptance-source AUTH custody, CON-07/shared acceptance, actual obligation 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 -> 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 | 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 efb738394..efc835ac1 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md @@ -4,15 +4,21 @@ 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/shared acceptance -prerequisites are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. The delivered [ART-07A1 metadata contract](../../WS-ART-001/WS-ART-001-07A1.md) -and REV-03B normalized packet persistence are delivered. REV-04A Review source storage is also delivered; REV-04B shared acceptance 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. These are storage prerequisites; +and REV-03B normalized packet persistence are delivered. REV-04A Review source +storage, REV-04B acceptance 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 first +authorized atomic consequence at 04E2-B. These are hidden prerequisites; no live human-review queue or endpoint is required for automated acceptance. The [cross-owner dependency contract](../../WS-ARCH-001/planning/PLAN.md#current-dependency-contract) @@ -40,7 +46,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 is delivered. Hidden handlers 04E1B-B, activation 04E2-B and live 04E3 remain. True routing proceeds on its own prerequisites. False additionally requires acceptance-source AUTH custody, CON-07/shared acceptance 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 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. - 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 cf0700863..59921c653 100644 --- a/.commitrail/initiatives/WS-CON-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-CON-001/OVERVIEW.md @@ -5,8 +5,10 @@ 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/shared acceptance prerequisites are next. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. @@ -16,7 +18,12 @@ and the [capability ledger](../../../docs/roadmap_status.md). - Disposition: Planned - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies - metadata-only packet types. REV-03B 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. No packet resolver or human runtime is live. + 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 + first authorized atomic consequence at 04E2-B. No packet resolver or human + runtime is live. - Completed boundary: public Finance ContributionPolicy administration, exact Finance Authority, CP06 selected-policy validation and CP07 internal guide @@ -28,8 +35,9 @@ and the [capability ledger](../../../docs/roadmap_status.md). - Delivered dependent boundary: ARCH-04E1A route-neutral source facts and the source-neutral accepted-effects Protocol follow [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and - finalize authority. No CON participant, FinalAcceptance, award or runtime - effects implementation is included. Output-file authority remains unavailable + finalize authority. It includes no FinalAcceptance writer or runtime effects + implementation. CON-07 now supplies the separate hidden CON participant and + award-set owner. 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 @@ -50,11 +58,14 @@ and the [capability ledger](../../../docs/roadmap_status.md). 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. [REV-04B](../WS-REV-001/WS-REV-001-04B.md) adds shared FinalAcceptance source - storage, without AUTH receipt custody or runtime consumers. - [CON-03C](WS-CON-001-03C.md) adds exact contribution sources and fixed awards; no recognition or fulfillment operation is live. -- Next usable boundary: for the selected automated-acceptance delivery sequence, CON-07 using the delivered REV-12A1 disabled fence, + storage, without AUTH receipt custody or a production writer. + [CON-03C](WS-CON-001-03C.md) adds exact contribution sources and fixed awards. + [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 - acceptance operation serves both human and automatic triggers. + operation serves both human and automatic triggers. - 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 @@ -62,12 +73,13 @@ and the [capability ledger](../../../docs/roadmap_status.md). projection in v0.1. 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, -CON participation and authorized activation remain separate required work. +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. 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 -can proceed after its own prerequisites without CON-07/shared acceptance or +can proceed after its own prerequisites without CON/shared acceptance or scoped lifecycle activation. False routing adds those requirements. Human final acceptance later uses the same authorized shared acceptance/CON operation. @@ -81,8 +93,8 @@ acceptance later uses the same authorized shared acceptance/CON operation. - Finance Authority adapter-binding and five policy actions are active through explicit AUTH composition. [CP05A](../WS-ARCH-001/WS-ARCH-001-CP05A.md) exposes public Finance policy administration and current draft/published selector - recovery. ContributionRecord, award and fulfillment behavior and their public - APIs remain unavailable; delivery of those economic effects is not activated. + recovery. CON-07 hidden ContributionRecord/award participation is delivered; + public recognition, fulfillment and their APIs remain unavailable. ## Remaining v0.1 sequence @@ -103,15 +115,18 @@ the internal policy operations; CP05A exposes their public Finance routes. CP09 removes the replaced legacy economic path only after all consumers are replaced, including CHECKERS and public Submission cutover; it does not block canonical `allow_review`. -2. [CON-03C](WS-CON-001-03C.md) delivers immutable ContributionRecord/CompensationAward storage after REV-04B. The disabled REV-12A1 fence and hidden AUTH preparation - are delivered before the CON-07 flush-only submitter participant. Mandatory persisted custody +2. [CON-03C](WS-CON-001-03C.md) delivers immutable ContributionRecord/CompensationAward storage after REV-04B. The disabled REV-12A1 fence, hidden AUTH preparation + and CON-07 flush-only submitter participant are delivered. CON-07 creates or + exactly replays a source-bound contribution and its complete frozen set of + zero, one or two awards in the caller's root transaction; paid replay checks + correlation and unpaid replay retains none. Mandatory persisted custody accompanies the first authorized atomic consequence at 04E2-B. These shared pieces do not require live human decision/queue/lease behavior. The locked ReviewPolicy boolean `human_review_required` defaults true; false permits authorized automated acceptance without a Review or reviewer contribution. Both use the same FinalAcceptance/submitter-contribution participant and applicable awards. - The delivered REV-12A1 fence precedes this operation. Actual obligation roots - and authorized transition/drain proof remain required before activation; live + The delivered REV-12A1 fence precedes this operation. Actual fulfillment-root + ordinal custody and authorized transition/drain proof remain required before activation; live fulfillment endpoints do not block hidden composition. Automated acceptance must not require live human-review infrastructure; see the [canonical source, authority and transaction contract](../../../docs/spec_review_lifecycle.md#finalacceptance). diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 60b267f51..2c7e03223 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -23,7 +23,7 @@ These storage prerequisites do not authorize acceptance or payment. - `backend/app/modules/compensation/api/awards.py` , `api/__init__.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. - `backend/alembic/env.py`, `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. - `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, migration graph assertions if necessary: focused integration and regression proof. -- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/{partition.v1.json,contributions/con07-submitter-participation.json}`: exact new proof/owner registration only; no gate weakening. +- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/partition.v1.json`: exact new proof/owner registration and move the three existing contribution record modules from schema to project partitions; no gate weakening. - This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. ### Not allowed @@ -100,6 +100,24 @@ Planned executable proof: `cd backend && .venv/bin/python -m pytest -q tests/con - Required reviews: architecture/reuse; security; QA/test-delta; CI integrity for catalogue/gates; documentation/product-operations for boundary claims. - Human focus: no recognition is live; storage controls are not authority, existing facts are never repaired silently, and only the submitter participant is delivered here. +## CI placement + +The prior exact-head hosted run placed schema at 1183.777 seconds under its +unchanged 1200-second cap; project lanes took 511.959/617.582/547.069 seconds. +Move the three existing contribution-record modules with the new participant +proof into project partitions. Their retained 17 tests include at least 32.61 +seconds of observed call time; this is not a total-runtime estimate. Preserve +all nodes and the partition algorithm; fresh hosted completion must establish +actual timing. No cap or completeness gate changes. + +## Review findings + +Plan review corrected an otherwise cyclic CON -> REV -> CON dependency by +using a consumer-owned acquisition interface with canonical fence composition. +It also narrowed unpaid correlation claims to what storage retains and added +a partial replay proof that reaches the guard before deferred commit. No +production authority or live automated acceptance is inferred from these tests. + ## Reconciliation - Current main: 89dd8c91 includes merged routing preparation, observability and CLI self-service. Preserve their ownership and test registrations. diff --git a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md index 66828d541..4e9aeb31f 100644 --- a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md +++ b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md @@ -5,9 +5,11 @@ planned router identity. ARCH-04E1B-A delivers caller-owned routing-request and future source-identity reservation. ARCH-04E2-A now delivers strict hidden resource/preparation matching and a nominal fixed-router adapter through canonical PREP while the action remains planned/unavailable. It issues no handle, allow or -receipt and writes no source or effect. The selected automated path proceeds to -CON-07/shared acceptance prerequisites. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. @@ -29,7 +31,11 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Disposition: Planned - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies - metadata-only packet types. REV-03B 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. No packet resolver or human runtime is live. + 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 + delivered. Mandatory persisted custody accompanies the first authorized atomic + consequence at 04E2-B. No packet resolver or human runtime is live. - Completed boundary: automatic unified execution, deterministic projections, immutable setup finalization, current-authority replay and one public guide @@ -75,7 +81,8 @@ 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: for the selected automated-acceptance delivery sequence, CON-07 using the delivered REV-12A1 disabled fence, then shared acceptance composition and ARCH-04E1B-B/04E2-B/04E3. +- Next usable boundary: shared acceptance/TASK effects using the delivered + REV-12A1 fence and CON-07 participant, then ARCH-04E1B-B/04E2-B/04E3. ARCH-04F remediation still precedes enabling false. - Governing sources: project-guide specifications, authorization and contribution-policy specifications, code, migrations, and tests. @@ -84,12 +91,13 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), adapter leakage. 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, -CON participation and authorized activation remain separate required work. +generation-zero controller storage and transaction locking. Acceptance-source +AUTH custody, shared acceptance/TASK effects 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 -can proceed after its own prerequisites without CON-07/shared acceptance or +can proceed after its own prerequisites without CON/shared acceptance or scoped lifecycle activation. False routing adds those requirements. Human final acceptance later uses the same authorized shared acceptance/CON operation. diff --git a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md index 6b77ad235..b61d92e5d 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md @@ -4,15 +4,21 @@ 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/shared acceptance -prerequisites are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. The delivered [ART-07A1 metadata contract](../../WS-ART-001/WS-ART-001-07A1.md) -and REV-03B normalized packet persistence are delivered. REV-04A Review source storage is also delivered; REV-04B shared acceptance 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. These are storage prerequisites; +and REV-03B normalized packet persistence are delivered. REV-04A Review source +storage, REV-04B acceptance 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 first +authorized atomic consequence at 04E2-B. These are hidden prerequisites; no live human-review queue or endpoint is required for automated acceptance. All chunks are L1 and one PR each. Product behavior is built hidden before AUTH diff --git a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md index db29838a0..7716031d7 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md +++ b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md @@ -4,15 +4,21 @@ 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/shared acceptance -prerequisites are next for the selected automated path. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 +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. The delivered [ART-07A1 metadata contract](../../WS-ART-001/WS-ART-001-07A1.md) -and REV-03B normalized packet persistence are delivered. REV-04A Review source storage is also delivered; REV-04B shared acceptance 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. These are storage prerequisites; +and REV-03B normalized packet persistence are delivered. REV-04A Review source +storage, REV-04B acceptance 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 first +authorized atomic consequence at 04E2-B. These are hidden prerequisites; no live human-review queue or endpoint is required for automated acceptance. ## Objective diff --git a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md index bd9da1c29..0f5a2a8c9 100644 --- a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md @@ -5,8 +5,10 @@ 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/shared acceptance prerequisites are next. Mandatory durable custody and its genuine positive -proof belong to consequence activation (04E2-B), before production consumption. Neither phase may +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 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. @@ -17,7 +19,11 @@ of review/revision behavior. The downstream owner contracts remain separate. - Disposition: Planned - Delivered prerequisite: [ART-07A1](../WS-ART-001/WS-ART-001-07A1.md) supplies - metadata-only packet types. REV-03B 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. No packet resolver or human runtime is live. + 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 + delivered. Mandatory persisted custody accompanies the first authorized atomic + consequence at 04E2-B. No packet resolver or human runtime is live. - Completed boundary: queue admission and ReviewLease persistence through 03A2, plus normalized immutable packets through 03B, Review sources through 04A @@ -31,8 +37,8 @@ of review/revision behavior. The downstream owner contracts remain separate. - 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: for the selected automated-acceptance delivery sequence, CON-07 using the delivered - REV-12A1 disabled controller/fence under the canonical order; human hidden behavior may continue independently behind exact AUTH, +- 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, ART and CON prerequisites. - Governing sources: `docs/spec_review_lifecycle.md`, `docs/engineering/review_authorization_action_custody.md`, code, migrations, @@ -41,12 +47,13 @@ of review/revision behavior. The downstream owner contracts remain separate. policy lineage; separation of duties; and atomic final acceptance effects. 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, -CON participation and authorized activation remain separate required work. +generation-zero controller storage and transaction locking. Acceptance-source +AUTH custody, shared acceptance/TASK effects 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 -can proceed after its own prerequisites without CON-07/shared acceptance or +can proceed after its own prerequisites without CON/shared acceptance or scoped lifecycle activation. False routing adds those requirements. Human final acceptance later uses the same authorized shared acceptance/CON operation. @@ -78,15 +85,15 @@ atomic operation. Do not build a separate workflow engine. 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 that acceptance provenance using the same atomic -submitter-contribution and applicable compensation participant. These effects -are planned runtime work; the source storage foundations are delivered. +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. The human branch below continues to use `allow_review`; it is not an automatic acceptance signal. The [versioned policy setting](../../changes/pre-review-plan-reconciliation.md#delivered-policy-setting-implementation) is available for draft configuration. Add mandatory shared acceptance authority custody and -the CON participant for both sources before enabling false. The automated branch must not depend on +compose the delivered CON participant for both sources before enabling false. The automated branch must not depend on live human queues, ReviewLeases or decision endpoints. Human lifecycle work remains required for v0.1, but need not delay the first automated end-to-end proof. No adjudication setting or behavior is included. @@ -97,7 +104,7 @@ 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 and ARCH-04E2-A exact source preparation are delivered before CON-07 hidden participation; 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 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 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/README.md b/README.md index 43c0b08c6..69acff879 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,10 @@ existing locked ReviewPolicy. After required post-submit checks pass, true requires human review; false uses an authorized automated FinalAcceptance and submitter ContributionRecord, with no reviewer contribution. This branch is not live yet: the [policy setting](.commitrail/changes/pre-review-plan-reconciliation.md#delivered-policy-setting-implementation) -is implemented for configuration; shared acceptance/CON integration remains -pending. Adjudication is excluded. +is implemented for configuration, and the hidden submitter participant can +stage or exactly replay its complete frozen award set. Shared acceptance, TASK +terminal effects, authority/source custody, routing activation and public +composition remain pending. Adjudication is excluded. The stage describes the purpose and lifecycle boundary, not a promise that all checks are deterministic. Deterministic compilation and policy routing do not @@ -174,14 +176,16 @@ Findings and policy proposals retain document-access evidence. [ART-07A1](.commitrail/initiatives/WS-ART-001/WS-ART-001-07A1.md) provides strict 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 and REV-12A1's disabled controller/transaction -fence are delivered. Exact hidden AUTH preparation is delivered before hidden participation; durable source custody accompanies the first atomic governed consequence. Acceptance storage has no production writer or AUTH receipt; mandatory exact +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 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, -atomic contribution/award creation and conditional fulfillment. +shared acceptance/TASK effects, 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. @@ -702,9 +706,10 @@ allow, receipt, source write, publication or effect is reachable. The true branc 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, -without fabricating a Review. CON-07/shared acceptance prerequisites are next; -actual receipt custody must commit with the governed consequence before shared -acceptance and contribution creation run. +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. ## v0.1 Success Standard diff --git a/backend/alembic/env.py b/backend/alembic/env.py index c6f5a6040..3364da7e2 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -21,7 +21,7 @@ target_metadata = Base.metadata _BASELINE_REVISION = "0001_uuid7_v01" -_CURRENT_HEAD_REVISION = "0018_task_routing_request" +_CURRENT_HEAD_REVISION = "0019_submitter_awards" _RECREATE_GUIDANCE = ( "Workstream v0.1 requires a fresh database; recreate this database before " "running the 0001_uuid7_v01 migration" @@ -52,7 +52,7 @@ def do_run_migrations(connection: Connection) -> None: .scalars() .all() ) - if revisions not in ((), (_BASELINE_REVISION,), ("0002_task_queue_authority",), ("0003_task_read_authority",), ("0004_task_context_authority",), ("0005_task_evidence_authority",), ("0006_history_read_authority",), ("0007_checker_output_custody",), ("0008_checker_execution",), ("0009_checker_material_lineage",), ("0010_post_submit_authority",), ("0011_task_routing_source",), ("0012_review_packet",), ("0013_review_source",), ("0014_final_acceptance",), ("0015_contribution_awards",), ("0016_review_lifecycle_fence",), ("0017_acceptance_source_contracts",), (_CURRENT_HEAD_REVISION,)): + if revisions not in ((), (_BASELINE_REVISION,), ("0002_task_queue_authority",), ("0003_task_read_authority",), ("0004_task_context_authority",), ("0005_task_evidence_authority",), ("0006_history_read_authority",), ("0007_checker_output_custody",), ("0008_checker_execution",), ("0009_checker_material_lineage",), ("0010_post_submit_authority",), ("0011_task_routing_source",), ("0012_review_packet",), ("0013_review_source",), ("0014_final_acceptance",), ("0015_contribution_awards",), ("0016_review_lifecycle_fence",), ("0017_acceptance_source_contracts",), ("0018_task_routing_request",), (_CURRENT_HEAD_REVISION,)): raise RuntimeError(_RECREATE_GUIDANCE) # The read-only preflight autobegins a SQLAlchemy transaction. End that # transaction before Alembic establishes the migration transaction; diff --git a/backend/alembic/versions/0019_submitter_award_completeness.py b/backend/alembic/versions/0019_submitter_award_completeness.py new file mode 100644 index 000000000..7315fba6a --- /dev/null +++ b/backend/alembic/versions/0019_submitter_award_completeness.py @@ -0,0 +1,126 @@ +"""Require complete frozen awards for accepted-submission contributions.""" + +from alembic import op + +revision = "0019_submitter_awards" +down_revision = "0018_task_routing_request" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute("SET LOCAL search_path = pg_catalog, public, pg_temp") + op.execute( + "LOCK TABLE public.contribution_records, public.compensation_awards " + "IN SHARE ROW EXCLUSIVE MODE" + ) + op.execute(""" +CREATE FUNCTION public.accepted_submission_award_set_is_complete(record_uuid uuid) +RETURNS boolean +LANGUAGE sql +STABLE +SET search_path = pg_catalog, public, pg_temp +AS $$ + SELECT NOT EXISTS ( + SELECT 1 + FROM public.contribution_records record + WHERE record.id = record_uuid + AND record.contribution_type = 'accepted_submission' + AND ( + EXISTS ( + SELECT 1 + FROM public.contribution_rules rule + JOIN public.contribution_award_definitions definition + ON definition.contribution_rule_id = rule.id + AND definition.contribution_policy_version_id = rule.contribution_policy_version_id + AND definition.project_id = rule.project_id + AND definition.contribution_type = rule.contribution_type + WHERE rule.contribution_policy_version_id = record.contribution_policy_version_id + AND rule.project_id = record.project_id + AND rule.contribution_type = record.contribution_type + AND NOT EXISTS ( + SELECT 1 + FROM public.compensation_awards award + WHERE award.contribution_record_id = record.id + AND award.award_definition_id = definition.id + ) + ) + OR EXISTS ( + SELECT 1 + FROM public.compensation_awards award + WHERE award.contribution_record_id = record.id + AND NOT EXISTS ( + SELECT 1 + FROM public.contribution_rules rule + JOIN public.contribution_award_definitions definition + ON definition.contribution_rule_id = rule.id + AND definition.contribution_policy_version_id = rule.contribution_policy_version_id + AND definition.project_id = rule.project_id + AND definition.contribution_type = rule.contribution_type + WHERE rule.contribution_policy_version_id = record.contribution_policy_version_id + AND rule.project_id = record.project_id + AND rule.contribution_type = record.contribution_type + AND definition.id = award.award_definition_id + ) + ) + ) + ) +$$ +""") + op.execute(""" +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 + FROM public.contribution_records record + WHERE record.contribution_type = 'accepted_submission' + AND NOT public.accepted_submission_award_set_is_complete(record.id) + ) THEN + RAISE EXCEPTION 'retained accepted-submission contribution has incomplete award set' + USING ERRCODE = '23514'; + END IF; +END +$$ +""") + op.execute(""" +CREATE FUNCTION public.require_accepted_submission_award_set() +RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp +AS $$ +DECLARE + record_uuid uuid; +BEGIN + IF TG_TABLE_SCHEMA = 'public' AND TG_TABLE_NAME = 'contribution_records' THEN + record_uuid := NEW.id; + ELSIF TG_TABLE_SCHEMA = 'public' AND TG_TABLE_NAME = 'compensation_awards' THEN + record_uuid := NEW.contribution_record_id; + ELSE + RAISE EXCEPTION 'accepted-submission award guard installed on unexpected table' + USING ERRCODE = '55000'; + END IF; + + IF NOT public.accepted_submission_award_set_is_complete(record_uuid) THEN + RAISE EXCEPTION 'accepted-submission contribution has incomplete award set' + USING ERRCODE = '23514'; + END IF; + RETURN NULL; +END +$$ +""") + op.execute(""" +CREATE CONSTRAINT TRIGGER accepted_submission_award_set_from_contribution +AFTER INSERT ON public.contribution_records +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION public.require_accepted_submission_award_set() +""") + op.execute(""" +CREATE CONSTRAINT TRIGGER accepted_submission_award_set_from_award +AFTER INSERT ON public.compensation_awards +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION public.require_accepted_submission_award_set() +""") + + +def downgrade() -> None: + raise RuntimeError("Workstream v0.1 migrations cannot be downgraded; recreate the database") diff --git a/backend/app/modules/compensation/api/__init__.py b/backend/app/modules/compensation/api/__init__.py index 1f126d1a3..8f532b82a 100644 --- a/backend/app/modules/compensation/api/__init__.py +++ b/backend/app/modules/compensation/api/__init__.py @@ -21,6 +21,15 @@ validate_adapter_route_key, ) from app.modules.compensation.api.instruments import CompensationInstrumentType +from app.modules.compensation.api.awards import ( + AwardContributionFacts, + AwardDefinitionFacts, + CompleteAwardSetPort, + CompleteAwardSetRequest, + CompleteAwardSetResult, + CompensationAwardConflict, + CompensationAwardFacts, +) from app.modules.compensation.api.policy_bindings import ( LockedPolicyAdapterBindingFacts, PolicyAdapterBindingPort, @@ -46,6 +55,13 @@ "AdapterBindingView", "DenyAdapterBindingAuthorization", "CompensationInstrumentType", + "AwardContributionFacts", + "AwardDefinitionFacts", + "CompleteAwardSetPort", + "CompleteAwardSetRequest", + "CompleteAwardSetResult", + "CompensationAwardConflict", + "CompensationAwardFacts", "LockedPolicyAdapterBindingFacts", "PolicyAdapterBindingPort", "PolicyAdapterBindingUnavailable", diff --git a/backend/app/modules/compensation/api/awards.py b/backend/app/modules/compensation/api/awards.py new file mode 100644 index 000000000..962dd78ad --- /dev/null +++ b/backend/app/modules/compensation/api/awards.py @@ -0,0 +1,135 @@ +"""Strict complete award-set contract for accepted-submission contributions.""" + +from decimal import Decimal +from typing import Annotated, Literal, Protocol, Self +from uuid import UUID + +from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, model_validator + +from app.modules.compensation.api.instruments import CompensationInstrumentType + +_STRICT_FROZEN = ConfigDict( + extra="forbid", frozen=True, strict=True, revalidate_instances="always" +) +_Quantity = Annotated[Decimal, Field(gt=0, allow_inf_nan=False)] + + +class CompensationAwardConflict(RuntimeError): + """The requested complete award set differs from immutable stored facts.""" + + +class AwardDefinitionFacts(BaseModel): + """One exact frozen definition selected by the contribution owner.""" + + model_config = _STRICT_FROZEN + + id: UUID + project_id: UUID + contribution_policy_version_id: UUID + contribution_type: Literal["accepted_submission"] + instrument_type: CompensationInstrumentType + unit_code: str = Field(min_length=1, max_length=32) + quantity: _Quantity + adapter_binding_id: UUID + + +class AwardContributionFacts(BaseModel): + """Exact immutable contribution lineage needed to copy award facts.""" + + model_config = _STRICT_FROZEN + + id: UUID + project_id: UUID + contributor_id: UUID + contribution_policy_version_id: UUID + contribution_type: Literal["accepted_submission"] + + +class CompleteAwardSetRequest(BaseModel): + """Create a new set or validate replay of an already complete set.""" + + model_config = _STRICT_FROZEN + + disposition: Literal["new", "replay"] + compensation_mode: Literal["unpaid", "compensated"] + contribution: AwardContributionFacts + definitions: tuple[AwardDefinitionFacts, ...] = Field(max_length=2) + correlation_id: UUID + + @model_validator(mode="after") + def validate_complete_definition_set(self) -> Self: + """Reject incomplete, duplicate, or crossed frozen definition facts.""" + definitions = self.definitions + instruments = {item.instrument_type for item in definitions} + expected = ( + self.compensation_mode == "unpaid" and not definitions + ) or ( + self.compensation_mode == "compensated" + and 1 <= len(definitions) <= 2 + and len(instruments) == len(definitions) + ) + if not expected or any( + item.project_id != self.contribution.project_id + or item.contribution_policy_version_id + != self.contribution.contribution_policy_version_id + or item.contribution_type != self.contribution.contribution_type + for item in definitions + ): + raise ValueError("award definitions differ from contribution lineage") + return self + + +class CompensationAwardFacts(BaseModel): + """Exact immutable award facts persisted by the compensation owner.""" + + model_config = _STRICT_FROZEN + + id: UUID + project_id: UUID + contribution_record_id: UUID + contributor_id: UUID + contribution_policy_version_id: UUID + award_definition_id: UUID + adapter_binding_id: UUID + instrument_type: CompensationInstrumentType + unit_code: str = Field(min_length=1, max_length=32) + quantity: _Quantity + created_at: AwareDatetime + correlation_id: UUID + + +class CompleteAwardSetResult(BaseModel): + """The exact complete stored set, ordered by instrument identity.""" + + model_config = _STRICT_FROZEN + + awards: tuple[CompensationAwardFacts, ...] = Field(max_length=2) + + @model_validator(mode="after") + def validate_unique_instruments(self) -> Self: + """A returned complete set cannot contain duplicate instruments.""" + instruments = {item.instrument_type for item in self.awards} + if len(instruments) != len(self.awards): + raise ValueError("award result contains duplicate instruments") + return self + + +class CompleteAwardSetPort(Protocol): + """Write or exactly replay awards in the caller's current transaction.""" + + async def complete_award_set( + self, request: CompleteAwardSetRequest + ) -> CompleteAwardSetResult: + """Create every new award or validate an existing complete set.""" + ... + + +__all__ = ( + "AwardContributionFacts", + "AwardDefinitionFacts", + "CompleteAwardSetPort", + "CompleteAwardSetRequest", + "CompleteAwardSetResult", + "CompensationAwardConflict", + "CompensationAwardFacts", +) diff --git a/backend/app/modules/compensation/awards/participant.py b/backend/app/modules/compensation/awards/participant.py new file mode 100644 index 000000000..1ba44a2a0 --- /dev/null +++ b/backend/app/modules/compensation/awards/participant.py @@ -0,0 +1,122 @@ +"""Flush-only owner of complete immutable compensation award sets.""" + +from uuid import UUID + +from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.identifiers import new_record_id +from app.modules.compensation.api import ( + CompleteAwardSetRequest, + CompleteAwardSetResult, + CompensationAwardConflict, + CompensationAwardFacts, + CompensationInstrumentType, +) +from app.modules.compensation.awards.models import CompensationAward + + +class CompensationAwardParticipant: + """Create all new awards or exactly validate an existing complete set.""" + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def complete_award_set( + self, request: CompleteAwardSetRequest + ) -> CompleteAwardSetResult: + """Copy frozen facts without committing or repairing a replay.""" + try: + checked = CompleteAwardSetRequest.model_validate(request) + except (TypeError, ValueError, ValidationError) as exc: + raise CompensationAwardConflict("compensation_award_conflict") from exc + if not self._session.in_transaction() or self._session.in_nested_transaction(): + raise CompensationAwardConflict("compensation_award_conflict") + + if checked.disposition == "new": + self._session.add_all( + [ + CompensationAward( + id=new_record_id(), + project_id=str(checked.contribution.project_id), + contribution_record_id=checked.contribution.id, + contributor_id=str(checked.contribution.contributor_id), + contribution_policy_version_id=( + checked.contribution.contribution_policy_version_id + ), + award_definition_id=definition.id, + adapter_binding_id=definition.adapter_binding_id, + instrument_type=definition.instrument_type.value, + unit_code=definition.unit_code, + quantity=definition.quantity, + correlation_id=checked.correlation_id, + ) + for definition in checked.definitions + ] + ) + await self._session.flush() + + awards = await self._load(checked) + self._require_exact_complete_set(checked, awards) + return CompleteAwardSetResult(awards=awards) + + async def _load( + self, request: CompleteAwardSetRequest + ) -> tuple[CompensationAwardFacts, ...]: + """Read the stored set after insert or for exact replay validation.""" + rows = ( + await self._session.scalars( + select(CompensationAward) + .where( + CompensationAward.contribution_record_id + == request.contribution.id + ) + .order_by(CompensationAward.instrument_type) + ) + ).all() + try: + return tuple( + CompensationAwardFacts( + id=row.id, + project_id=UUID(row.project_id), + contribution_record_id=row.contribution_record_id, + contributor_id=UUID(row.contributor_id), + contribution_policy_version_id=row.contribution_policy_version_id, + award_definition_id=row.award_definition_id, + adapter_binding_id=row.adapter_binding_id, + instrument_type=CompensationInstrumentType(row.instrument_type), + unit_code=row.unit_code, + quantity=row.quantity, + created_at=row.created_at, + correlation_id=row.correlation_id, + ) + for row in rows + ) + except (TypeError, ValueError, ValidationError) as exc: + raise CompensationAwardConflict("compensation_award_conflict") from exc + + @staticmethod + def _require_exact_complete_set( + request: CompleteAwardSetRequest, + awards: tuple[CompensationAwardFacts, ...], + ) -> None: + """Reject missing, extra, crossed, or correlation-conflicting awards.""" + definitions = {item.instrument_type: item for item in request.definitions} + if len(awards) != len(definitions): + raise CompensationAwardConflict("compensation_award_conflict") + for award in awards: + definition = definitions.get(award.instrument_type) + if definition is None or not ( + award.project_id == request.contribution.project_id + and award.contribution_record_id == request.contribution.id + and award.contributor_id == request.contribution.contributor_id + and award.contribution_policy_version_id + == request.contribution.contribution_policy_version_id + and award.award_definition_id == definition.id + and award.adapter_binding_id == definition.adapter_binding_id + and award.unit_code == definition.unit_code + and award.quantity == definition.quantity + and award.correlation_id == request.correlation_id + ): + raise CompensationAwardConflict("compensation_award_conflict") diff --git a/backend/app/modules/contributions/api/__init__.py b/backend/app/modules/contributions/api/__init__.py index 9ea43c916..fcef60b48 100644 --- a/backend/app/modules/contributions/api/__init__.py +++ b/backend/app/modules/contributions/api/__init__.py @@ -37,6 +37,15 @@ ContributionPolicyValidationPurpose, ContributionPolicyValidationRequest, ) +from app.modules.contributions.api.participation import ( + ContributionParticipationConflict, + ContributionParticipationUnavailable, + ParticipationLifecycleFence, + SubmitterContributionFacts, + SubmitterParticipationPort, + SubmitterParticipationRequest, + SubmitterParticipationResult, +) __all__ = ( "ContributionPolicyOperationsPort", @@ -46,6 +55,13 @@ "ContributionPolicyValidationPort", "ContributionPolicyValidationPurpose", "ContributionPolicyValidationRequest", + "ContributionParticipationConflict", + "ContributionParticipationUnavailable", + "ParticipationLifecycleFence", + "SubmitterContributionFacts", + "SubmitterParticipationPort", + "SubmitterParticipationRequest", + "SubmitterParticipationResult", "CompensationMode", "ContributionPolicyConflict", "ContributionPolicyCreateDraftRequest", diff --git a/backend/app/modules/contributions/api/participation.py b/backend/app/modules/contributions/api/participation.py new file mode 100644 index 000000000..b1253a15b --- /dev/null +++ b/backend/app/modules/contributions/api/participation.py @@ -0,0 +1,94 @@ +"""Hidden submitter participation contract; construction grants no authority.""" + +from typing import Annotated, 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" +) +_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)] + + +class ContributionParticipationUnavailable(RuntimeError): + """The exact frozen rule or participant transaction is unavailable.""" + + +class ContributionParticipationConflict(RuntimeError): + """A replay differs from the immutable contribution or award facts.""" + + +class ParticipationLifecycleFence(Protocol): + """Acquire only the canonical lifecycle fence owned by external composition.""" + + async def acquire(self, expected_generation: int) -> object: + """Hold the expected generation through the caller's root transaction.""" + ... + + +class SubmitterParticipationRequest(BaseModel): + """Exact scalar FinalAcceptance source lineage selected by its future owner.""" + + model_config = _STRICT_FROZEN + + project_id: UUID + task_id: UUID + submission_id: UUID + final_acceptance_id: UUID + task_assignment_id: UUID + contributor_id: UUID + contribution_policy_version_id: UUID + artifact_hash: _Digest + correlation_id: UUID + expected_generation: _Generation + + +class SubmitterContributionFacts(BaseModel): + """Exact accepted-submission contribution persisted for the submitter.""" + + model_config = _STRICT_FROZEN + + id: UUID + project_id: UUID + task_id: UUID + submission_id: UUID + contributor_id: UUID + source_final_acceptance_id: UUID + source_task_assignment_id: UUID + artifact_hash: _Digest + contribution_policy_version_id: UUID + created_at: AwareDatetime + + +class SubmitterParticipationResult(BaseModel): + """Immutable contribution and its exact complete frozen award set.""" + + model_config = _STRICT_FROZEN + + contribution: SubmitterContributionFacts + awards: tuple[CompensationAwardFacts, ...] = Field(max_length=2) + + +class SubmitterParticipationPort(Protocol): + """Create or exactly replay submitter economic facts without committing.""" + + async def participate_submitter( + self, request: SubmitterParticipationRequest + ) -> SubmitterParticipationResult: + """Flush exact facts in the caller's fenced root transaction.""" + ... + + +__all__ = ( + "ContributionParticipationConflict", + "ContributionParticipationUnavailable", + "ParticipationLifecycleFence", + "SubmitterContributionFacts", + "SubmitterParticipationPort", + "SubmitterParticipationRequest", + "SubmitterParticipationResult", +) diff --git a/backend/app/modules/contributions/records/participant.py b/backend/app/modules/contributions/records/participant.py new file mode 100644 index 000000000..cb5f81b7f --- /dev/null +++ b/backend/app/modules/contributions/records/participant.py @@ -0,0 +1,120 @@ +"""Hidden flush-only submitter contribution participant.""" + +from pydantic import ValidationError +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.compensation.api import ( + AwardContributionFacts, + CompleteAwardSetPort, + CompleteAwardSetRequest, + CompleteAwardSetResult, + CompensationAwardConflict, +) +from app.modules.contributions.api import ( + ContributionParticipationConflict, + ContributionParticipationUnavailable, + ParticipationLifecycleFence, + SubmitterContributionFacts, + SubmitterParticipationRequest, + SubmitterParticipationResult, +) +from app.modules.contributions.records.repository import ( + FrozenSubmitterRule, + SubmitterContributionRepository, +) + + +class SubmitterContributionParticipant: + """Create or replay exact submitter economic facts under an injected fence.""" + + def __init__( + self, + session: AsyncSession, + *, + fence: ParticipationLifecycleFence, + awards: CompleteAwardSetPort, + ) -> None: + """Bind every participant to the caller's explicitly composed owners.""" + self._repository = SubmitterContributionRepository(session) + self._fence = fence + self._awards = awards + + async def participate_submitter( + self, request: SubmitterParticipationRequest + ) -> SubmitterParticipationResult: + """Acquire first, then flush a contribution and its complete award set.""" + try: + checked = SubmitterParticipationRequest.model_validate(request) + except (TypeError, ValueError, ValidationError) as exc: + raise ContributionParticipationUnavailable( + "contribution_participation_unavailable" + ) from exc + + await self._fence.acquire(checked.expected_generation) + rule = await self._repository.get_frozen_rule( + checked.project_id, checked.contribution_policy_version_id + ) + contribution, disposition = await self._repository.insert_or_get(checked) + award_request = CompleteAwardSetRequest( + disposition=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_type="accepted_submission", + ), + definitions=rule.definitions, + correlation_id=checked.correlation_id, + ) + try: + award_result = CompleteAwardSetResult.model_validate( + await self._awards.complete_award_set(award_request) + ) + except (CompensationAwardConflict, TypeError, ValueError, ValidationError) as exc: + raise ContributionParticipationConflict( + "contribution_participation_conflict" + ) from exc + self._require_exact_awards(checked, contribution, rule, award_result) + return SubmitterParticipationResult( + contribution=contribution, + awards=award_result.awards, + ) + + @staticmethod + def _require_exact_awards( + request: SubmitterParticipationRequest, + contribution: SubmitterContributionFacts, + rule: FrozenSubmitterRule, + result: CompleteAwardSetResult, + ) -> None: + """Validate an injected award owner's response before exposing facts.""" + definitions = {item.instrument_type: item for item in rule.definitions} + if len(result.awards) != len(definitions): + raise ContributionParticipationConflict("contribution_participation_conflict") + for award in result.awards: + definition = definitions.get(award.instrument_type) + if definition is None or not _award_matches( + award, definition, contribution, request.correlation_id + ): + raise ContributionParticipationConflict("contribution_participation_conflict") + + +def _award_matches(award, definition, contribution, correlation_id) -> bool: + """Compare one returned award with its exact contribution and definition.""" + return ( + 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.award_definition_id == definition.id + and award.adapter_binding_id == definition.adapter_binding_id + and award.instrument_type == definition.instrument_type + and award.unit_code == definition.unit_code + and award.quantity == definition.quantity + and award.correlation_id == correlation_id + ) diff --git a/backend/app/modules/contributions/records/repository.py b/backend/app/modules/contributions/records/repository.py new file mode 100644 index 000000000..6a8336248 --- /dev/null +++ b/backend/app/modules/contributions/records/repository.py @@ -0,0 +1,196 @@ +"""Persistence for exact accepted-submission contribution participation.""" + +from dataclasses import dataclass +from typing import Literal +from uuid import UUID + +from pydantic import ValidationError +from sqlalchemy import select +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.identifiers import new_record_id +from app.modules.compensation.api import AwardDefinitionFacts, CompensationInstrumentType +from app.modules.contributions.api import ( + ContributionParticipationConflict, + ContributionParticipationUnavailable, + SubmitterContributionFacts, + SubmitterParticipationRequest, +) +from app.modules.contributions.models import ( + ContributionAwardDefinition, + ContributionPolicyVersion, + ContributionRule, +) +from app.modules.contributions.records.models import ContributionRecord + + +@dataclass(frozen=True, slots=True) +class FrozenSubmitterRule: + """The one accepted-submission rule and its complete definition set.""" + + compensation_mode: Literal["unpaid", "compensated"] + definitions: tuple[AwardDefinitionFacts, ...] + + +class SubmitterContributionRepository: + """Read the frozen rule and reserve the source-unique contribution row.""" + + def __init__(self, session: AsyncSession) -> None: + self._session = session + + async def get_frozen_rule( + self, project_id: UUID, contribution_policy_version_id: UUID + ) -> FrozenSubmitterRule: + """Read only the exact published or retired accepted-submission rule.""" + rule = ( + await self._session.execute( + select(ContributionRule.id, ContributionRule.compensation_mode) + .join( + ContributionPolicyVersion, + ContributionPolicyVersion.id + == ContributionRule.contribution_policy_version_id, + ) + .where( + ContributionPolicyVersion.id == contribution_policy_version_id, + ContributionPolicyVersion.project_id == str(project_id), + ContributionPolicyVersion.status.in_(("published", "retired")), + ContributionRule.project_id == str(project_id), + ContributionRule.contribution_type == "accepted_submission", + ) + ) + ).one_or_none() + if rule is None: + raise ContributionParticipationUnavailable("contribution_participation_unavailable") + + rows = ( + await self._session.execute( + select( + ContributionAwardDefinition.id, + ContributionAwardDefinition.project_id, + ContributionAwardDefinition.contribution_policy_version_id, + ContributionAwardDefinition.contribution_type, + ContributionAwardDefinition.instrument_type, + ContributionAwardDefinition.unit_code, + ContributionAwardDefinition.quantity, + ContributionAwardDefinition.adapter_binding_id, + ) + .where( + ContributionAwardDefinition.contribution_rule_id == rule.id, + ContributionAwardDefinition.project_id == str(project_id), + ContributionAwardDefinition.contribution_policy_version_id + == contribution_policy_version_id, + ContributionAwardDefinition.contribution_type == "accepted_submission", + ) + .order_by(ContributionAwardDefinition.instrument_type) + ) + ).all() + try: + definitions = tuple( + AwardDefinitionFacts( + id=row.id, + project_id=UUID(row.project_id), + contribution_policy_version_id=row.contribution_policy_version_id, + contribution_type=row.contribution_type, + instrument_type=CompensationInstrumentType(row.instrument_type), + unit_code=row.unit_code, + quantity=row.quantity, + adapter_binding_id=row.adapter_binding_id, + ) + for row in rows + ) + except (TypeError, ValueError, ValidationError) as exc: + raise ContributionParticipationUnavailable( + "contribution_participation_unavailable" + ) from exc + + instruments = {item.instrument_type for item in definitions} + complete = ( + rule.compensation_mode == "unpaid" and not definitions + ) or ( + rule.compensation_mode == "compensated" + and 1 <= len(definitions) <= 2 + and len(instruments) == len(definitions) + ) + if not complete: + raise ContributionParticipationUnavailable("contribution_participation_unavailable") + return FrozenSubmitterRule( + compensation_mode=rule.compensation_mode, + definitions=definitions, + ) + + async def insert_or_get( + self, request: SubmitterParticipationRequest + ) -> tuple[SubmitterContributionFacts, Literal["new", "replay"]]: + """Insert once by FinalAcceptance source, then compare the exact winner.""" + candidate_id = new_record_id() + inserted_id = await self._session.scalar( + insert(ContributionRecord) + .values( + id=candidate_id, + project_id=str(request.project_id), + task_id=str(request.task_id), + submission_id=str(request.submission_id), + contribution_type="accepted_submission", + contributor_id=str(request.contributor_id), + source_review_id=None, + source_review_lease_id=None, + source_final_acceptance_id=request.final_acceptance_id, + source_task_assignment_id=str(request.task_assignment_id), + 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] + ) + .returning(ContributionRecord.id) + ) + winner = 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) + ) + if winner is None or not self._matches(winner, request): + raise ContributionParticipationConflict("contribution_participation_conflict") + try: + facts = SubmitterContributionFacts( + id=winner.id, + project_id=UUID(winner.project_id), + task_id=UUID(winner.task_id), + submission_id=UUID(winner.submission_id), + contributor_id=UUID(winner.contributor_id), + source_final_acceptance_id=winner.source_final_acceptance_id, + source_task_assignment_id=UUID(winner.source_task_assignment_id), + artifact_hash=winner.artifact_hash, + contribution_policy_version_id=winner.contribution_policy_version_id, + 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" + + @staticmethod + def _matches( + winner: ContributionRecord, request: SubmitterParticipationRequest + ) -> bool: + """Compare every caller-supplied immutable contribution source fact.""" + return ( + winner.project_id == str(request.project_id) + and winner.task_id == str(request.task_id) + and winner.submission_id == str(request.submission_id) + and winner.contribution_type == "accepted_submission" + and winner.contributor_id == str(request.contributor_id) + and winner.source_review_id is None + and winner.source_review_lease_id is None + 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 + ) diff --git a/backend/scripts/test_lane_catalogue.py b/backend/scripts/test_lane_catalogue.py index 5e6d5c523..3573fb19b 100644 --- a/backend/scripts/test_lane_catalogue.py +++ b/backend/scripts/test_lane_catalogue.py @@ -251,6 +251,13 @@ class TestLane: ) PROJECT_MODULES = ( + # Frozen policy participation shares measured project capacity, not the full schema lane. + "tests/contributions/records/test_contracts.py", + "tests/contributions/records/test_storage.py", + "tests/contributions/records/test_migration.py", + "tests/contributions/participation/test_contracts.py", + "tests/contributions/participation/test_postgresql.py", + "tests/contributions/participation/test_transactions.py", "tests/projects/guide_activation/test_admission.py", "tests/projects/guide_activation/test_audit_contract.py", "tests/projects/guide_activation/test_concurrency.py", @@ -507,9 +514,6 @@ class TestLane: "tests/reviews/acceptance/test_contracts.py", "tests/reviews/acceptance/test_storage.py", "tests/reviews/acceptance/test_migration.py", - "tests/contributions/records/test_contracts.py", - "tests/contributions/records/test_storage.py", - "tests/contributions/records/test_migration.py", "tests/test_database_reset.py", "tests/test_artifact_architecture.py", "tests/architecture/test_module_boundaries.py", diff --git a/backend/tests/contributions/participation/__init__.py b/backend/tests/contributions/participation/__init__.py new file mode 100644 index 000000000..690eb5f2b --- /dev/null +++ b/backend/tests/contributions/participation/__init__.py @@ -0,0 +1 @@ +"""Focused hidden submitter participation proof.""" diff --git a/backend/tests/contributions/participation/support.py b/backend/tests/contributions/participation/support.py new file mode 100644 index 000000000..b9922881e --- /dev/null +++ b/backend/tests/contributions/participation/support.py @@ -0,0 +1,33 @@ +"""Real source-neutral composition for hidden submitter participation tests.""" + +from app.modules.compensation.awards.participant import CompensationAwardParticipant +from app.modules.contributions.api import SubmitterParticipationRequest +from app.modules.contributions.records.participant import SubmitterContributionParticipant +from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence + + +def request_for(h, **changes): + values = { + "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 + ), + "artifact_hash": h.submitter_record.artifact_hash, + "correlation_id": h.submitter_record.id, + "expected_generation": 0, + } + values.update(changes) + return SubmitterParticipationRequest(**values) + + +def participant(session): + return SubmitterContributionParticipant( + session, + fence=PostgresJointLifecycleMutationFence(session), + awards=CompensationAwardParticipant(session), + ) diff --git a/backend/tests/contributions/participation/test_contracts.py b/backend/tests/contributions/participation/test_contracts.py new file mode 100644 index 000000000..bd7ff3028 --- /dev/null +++ b/backend/tests/contributions/participation/test_contracts.py @@ -0,0 +1,86 @@ +"""Closed inputs and deny-before-work validation, without database claims.""" + +from decimal import Decimal +from unittest.mock import AsyncMock + +import pytest +from pydantic import ValidationError + +from app.core.identifiers import new_record_id +from app.modules.compensation.api import ( + AwardContributionFacts, AwardDefinitionFacts, CompleteAwardSetRequest, + CompensationInstrumentType, +) +from app.modules.contributions.api import ( + 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, + ) + + +@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()}, +]) +async def test_invalid_constructed_input_rejects_before_fence_or_owner_work(change): + original = request() + with pytest.raises(ValidationError): + SubmitterParticipationRequest(**(original.model_dump() | change)) + forged = original.model_copy(update=change) + session, fence, awards = AsyncMock(), AsyncMock(), AsyncMock() + participant = SubmitterContributionParticipant(session, fence=fence, awards=awards) + with pytest.raises(ContributionParticipationUnavailable): + await participant.participate_submitter(forged) + assert session.mock_calls == [] + assert fence.mock_calls == [] + assert awards.mock_calls == [] + + +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", + ) + definition = AwardDefinitionFacts( + 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(), + ) + return CompleteAwardSetRequest( + disposition="new", compensation_mode="compensated", contribution=contribution, + definitions=(definition,), correlation_id=new_record_id(), + ) + + +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": 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()}), + )}) + 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": ()}, + )) + assert unpaid.definitions == () + with pytest.raises(ValidationError): + 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 new file mode 100644 index 000000000..9ea48f075 --- /dev/null +++ b/backend/tests/contributions/participation/test_postgresql.py @@ -0,0 +1,300 @@ +"""Real PostgreSQL proof for hidden source-neutral submitter participation.""" + +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 ( + AwardContributionFacts, + AwardDefinitionFacts, + CompleteAwardSetRequest, + CompensationAwardFacts, + CompensationInstrumentType, + CompensationAwardConflict, +) +from app.modules.compensation.awards.participant import CompensationAwardParticipant +from app.modules.contributions.api import ( + ContributionParticipationConflict, + SubmitterContributionFacts, +) +from app.modules.reviews.lifecycle.fence import PostgresJointLifecycleMutationFence +from tests.contributions.records.support import ( + award_values, + contribution_source, + insert_award, + insert_record, + retire_policy_and_suspend_bindings, + rows, +) + +from .support import participant, request_for + +@pytest.mark.parametrize( + "instruments", + [(), ("money",), ("project_points",), ("money", "project_points")], + ids=("unpaid", "money", "points", "money-and-points"), +) +async def test_create_and_exact_replay_all_frozen_award_shapes( + tmp_path, isolated_database_env, instruments +): + async with contribution_source( + tmp_path, isolated_database_env, contribution_awards=instruments + ) as h: + request = request_for(h, 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) + + 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.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(""" + SELECT * FROM public.contribution_records + WHERE id=:record_id + """), {"record_id": created.contribution.id})).mappings().one() + stored_contribution = SubmitterContributionFacts( + id=record["id"], + project_id=record["project_id"], + task_id=record["task_id"], + submission_id=record["submission_id"], + contributor_id=record["contributor_id"], + source_final_acceptance_id=record["source_final_acceptance_id"], + source_task_assignment_id=record["source_task_assignment_id"], + artifact_hash=record["artifact_hash"], + contribution_policy_version_id=record["contribution_policy_version_id"], + created_at=record["created_at"], + ) + assert stored_contribution == created.contribution + 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(""" + SELECT a.*, d.quantity AS definition_quantity, + d.unit_code AS definition_unit, + d.adapter_binding_id AS definition_binding, + d.contribution_policy_version_id AS definition_policy, + d.project_id AS definition_project, + d.contribution_type AS definition_type, + d.instrument_type AS definition_instrument + FROM public.compensation_awards a + 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() + assert len(stored) == len(instruments) + stored_awards = tuple( + CompensationAwardFacts( + id=award["id"], + 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" + ], + award_definition_id=award["award_definition_id"], + adapter_binding_id=award["adapter_binding_id"], + instrument_type=CompensationInstrumentType( + award["instrument_type"] + ), + unit_code=award["unit_code"], + quantity=award["quantity"], + created_at=award["created_at"], + correlation_id=award["correlation_id"], + ) + for award in stored + ) + assert stored_awards == created.awards + for award in stored: + assert award["quantity"] == award["definition_quantity"] + assert award["unit_code"] == award["definition_unit"] + assert award["adapter_binding_id"] == award["definition_binding"] + assert award["contribution_policy_version_id"] == award["definition_policy"] + assert award["project_id"] == award["definition_project"] + 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 + + +async def test_retired_policy_and_suspended_bindings_keep_frozen_awards_due( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + 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()) + ) + assert {award.instrument_type.value for award in result.awards} == { + "money", + "project_points", + } + + +async def test_exact_replay_rejects_each_changed_source_fact_and_real_foreign_source( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + async with contribution_source( + tmp_path / "foreign", + isolated_database_env, + paid=True, + 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) + async with h.factory() as session, session.begin(): + created = await participant(session).participate_submitter(original) + + conflict_fields = ( + "task_id", + "submission_id", + "task_assignment_id", + "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() + 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() + 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"): + await participant(session).participate_submitter( + original.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 + } + ) + ) + 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 + + +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()) + 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()}) + ) + async with paid.factory() as session: + assert len(await rows(session, "compensation_awards")) == len(created.awards) == 2 + + async with contribution_source( + tmp_path / "unpaid", + isolated_database_env, + provision_services=False, + storage_settings=paid.settings, + ) as unpaid: + first_request = request_for(unpaid, 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()}) + ) + assert second == first + assert second.awards == () + + +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()) + async with h.factory() as session: + await session.begin() + await PostgresJointLifecycleMutationFence(session).acquire(0) + 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(ContributionParticipationConflict): + await participant(session).participate_submitter(request) + assert len(await rows(session, "compensation_awards")) == 1 + + owner = CompensationAwardParticipant(session) + definitions = tuple( + AwardDefinitionFacts( + id=award["award_definition_id"], + project_id=h.submitter_record.project_id, + contribution_policy_version_id=( + h.submitter_record.contribution_policy_version_id + ), + contribution_type="accepted_submission", + instrument_type=CompensationInstrumentType( + award["instrument_type"] + ), + unit_code=award["unit_code"], + quantity=award["quantity"], + adapter_binding_id=award["adapter_binding_id"], + ) + for award in expected_awards + ) + rule_request = CompleteAwardSetRequest( + disposition="replay", + compensation_mode="compensated", + contribution=AwardContributionFacts( + id=h.submitter_record.id, + project_id=h.submitter_record.project_id, + contributor_id=h.submitter_record.contributor_id, + contribution_policy_version_id=h.submitter_record.contribution_policy_version_id, + contribution_type="accepted_submission", + ), + definitions=definitions, + correlation_id=request.correlation_id, + ) + with pytest.raises(CompensationAwardConflict): + await owner.complete_award_set(rule_request) + assert len(await rows(session, "compensation_awards")) == 1 + + monkeypatch.setattr( + CompensationAwardParticipant, + "_require_exact_complete_set", + staticmethod(lambda request, awards: None), + ) + with pytest.raises(pytest.fail.Exception, match="DID NOT RAISE"): + with pytest.raises(CompensationAwardConflict): + await owner.complete_award_set(rule_request) + await session.rollback() diff --git a/backend/tests/contributions/participation/test_transactions.py b/backend/tests/contributions/participation/test_transactions.py new file mode 100644 index 000000000..0b2d0519f --- /dev/null +++ b/backend/tests/contributions/participation/test_transactions.py @@ -0,0 +1,121 @@ +"""Caller transaction, rollback, fence, and PostgreSQL winner 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.reviews.api.lifecycle import JointLifecycleUnavailable +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()) + async with h.factory() as session: + with pytest.raises(DBAPIError, match="division by zero"): + async with session.begin(): + await PostgresJointLifecycleMutationFence(session).acquire(0) + await insert_acceptance(session, h.acceptance) + result = await participant(session).participate_submitter(request) + assert len(result.awards) == 2 + await session.execute(text("SELECT 1 / 0")) + await session.rollback() + async with h.factory() as session: + assert await rows(session, "final_acceptances") == [] + assert await rows(session, "contribution_records") == [] + 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()) + async with h.factory() as session: + with pytest.raises(JointLifecycleUnavailable, match="root transaction"): + await participant(session).participate_submitter(request) + 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).participate_submitter(request) + + 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" + ): + await participant(session).participate_submitter(request) + await session.rollback() + + async with h.factory() as session, session.begin(): + result = await participant(session).participate_submitter(request) + assert result.awards == () diff --git a/backend/tests/contributions/records/support.py b/backend/tests/contributions/records/support.py index 16cd3f204..28e5edac0 100644 --- a/backend/tests/contributions/records/support.py +++ b/backend/tests/contributions/records/support.py @@ -14,10 +14,24 @@ @asynccontextmanager -async def contribution_source(tmp_path, url, *, decision="accept", paid=False, persist_acceptance=True, **options): +async def contribution_source( + tmp_path, + url, + *, + decision="accept", + paid=False, + contribution_awards=None, + persist_acceptance=True, + **options, +): + frozen_awards = ( + tuple(contribution_awards) + if contribution_awards is not None + else (("money", "project_points") if paid else ()) + ) async with acceptance_source( tmp_path, url, decision=decision, - contribution_awards=("money", "project_points") if paid else (), **options, + contribution_awards=frozen_awards, **options, ) as h: async with h.factory() as session: if decision == "accept" and persist_acceptance: diff --git a/backend/tests/contributions/records/test_migration.py b/backend/tests/contributions/records/test_migration.py index b0c0aaee3..cc02ea0a4 100644 --- a/backend/tests/contributions/records/test_migration.py +++ b/backend/tests/contributions/records/test_migration.py @@ -1,12 +1,20 @@ """Populated source upgrade preserves retained truth; downgrade never deletes it.""" import asyncio +from pathlib import Path import asyncpg import pytest from alembic import command +from sqlalchemy.exc import DBAPIError from app.db import session as db_session +from tests.contributions.records.support import ( + award_values, + contribution_source, + insert_award, + insert_record, +) from tests.migration_fixtures import _config from tests.reviews.acceptance.support import acceptance_source, insert_acceptance from tests.reviews.acceptance.test_migration import snapshot as parent_snapshot @@ -14,6 +22,19 @@ pytestmark = pytest.mark.postgres_schema_contract +def test_completeness_migration_locks_writers_before_retained_data_preflight(): + source = ( + Path(__file__).resolve().parents[3] + / "alembic/versions/0019_submitter_award_completeness.py" + ).read_text(encoding="utf-8") + lock = ( + '"LOCK TABLE public.contribution_records, public.compensation_awards "\n' + ' "IN SHARE ROW EXCLUSIVE MODE"' + ) + assert source.count(lock) == 1 + assert source.index(lock) < source.index("DO $$") + + async def snapshot(connection): result = await parent_snapshot(connection) result["final_acceptances"] = await connection.fetch( @@ -53,11 +74,126 @@ async def test_contribution_upgrade_preserves_sources(tmp_path, isolated_databas assert await connection.fetchval("SELECT version_num FROM alembic_version") == "0015_contribution_awards" finally: await connection.close() - with pytest.raises(RuntimeError, match="Workstream v0.1 migrations cannot be downgraded; recreate the database"): + with pytest.raises( + RuntimeError, + match="Workstream v0.1 migrations cannot be downgraded; recreate the database", + ): await asyncio.to_thread(command.downgrade, _config(), "0014_final_acceptance") connection = await asyncpg.connect(url) try: assert await snapshot(connection) == before - assert await connection.fetchval("SELECT version_num FROM alembic_version") == "0015_contribution_awards" + assert await connection.fetchval( + "SELECT version_num FROM alembic_version" + ) == "0015_contribution_awards" + finally: + await connection.close() + + +async def _award_snapshot(connection): + return { + table: [row["value"] for row in await connection.fetch( + f"SELECT to_jsonb(r)::text AS value FROM public.{table} r ORDER BY r.id" + )] + for table in ("contribution_records", "compensation_awards") + } + + +async def _reset_to_0018(database_url): + await db_session.dispose_engine() + connection = await asyncpg.connect(database_url.replace("+asyncpg", "")) + try: + await connection.execute("DROP SCHEMA public CASCADE; CREATE SCHEMA public") + finally: + await connection.close() + await asyncio.to_thread(command.upgrade, _config(), "0018_task_routing_request") + + +async def test_completeness_upgrade_preserves_complete_retained_awards( + tmp_path, isolated_database_env, migration_lock +): + with migration_lock(): + await _reset_to_0018(isolated_database_env) + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + async with h.factory() as session: + await insert_record(session, h.submitter_record) + for award in await award_values(session, h.submitter_record): + await insert_award(session, award) + await session.commit() + connection = await asyncpg.connect(isolated_database_env.replace("+asyncpg", "")) + try: + before = await _award_snapshot(connection) + assert len(before["contribution_records"]) == 1 + assert len(before["compensation_awards"]) == 2 + finally: + await connection.close() + + await asyncio.to_thread( + command.upgrade, _config(), "0019_submitter_awards" + ) + connection = await asyncpg.connect(isolated_database_env.replace("+asyncpg", "")) + try: + assert await _award_snapshot(connection) == before + assert await connection.fetchval( + "SELECT version_num FROM public.alembic_version" + ) == "0019_submitter_awards" + functions = await connection.fetch( + "SELECT p.proname, p.proconfig, pg_get_functiondef(p.oid) AS body " + "FROM pg_catalog.pg_proc p JOIN pg_catalog.pg_namespace n ON n.oid=p.pronamespace " + "WHERE n.nspname='public' AND p.proname=ANY($1::text[]) ORDER BY p.proname", + [ + "accepted_submission_award_set_is_complete", + "require_accepted_submission_award_set", + ], + ) + assert [row["proname"] for row in functions] == [ + "accepted_submission_award_set_is_complete", + "require_accepted_submission_award_set", + ] + for row in functions: + assert row["proconfig"] == ["search_path=pg_catalog, public, pg_temp"] + assert "public.contribution_records" in row["body"] + triggers = await connection.fetch( + "SELECT tgname, tgdeferrable, tginitdeferred FROM pg_catalog.pg_trigger " + "WHERE tgname LIKE 'accepted_submission_award_set_from_%' ORDER BY tgname" + ) + assert [tuple(row.values()) for row in triggers] == [ + ("accepted_submission_award_set_from_award", True, True), + ("accepted_submission_award_set_from_contribution", True, True), + ] + finally: + await connection.close() + + +async def test_completeness_upgrade_refuses_incomplete_retained_awards_unchanged( + tmp_path, isolated_database_env, migration_lock +): + with migration_lock(): + await _reset_to_0018(isolated_database_env) + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + async with h.factory() as session: + await insert_record(session, h.submitter_record) + await insert_award( + session, (await award_values(session, h.submitter_record))[0] + ) + await session.commit() + connection = await asyncpg.connect(isolated_database_env.replace("+asyncpg", "")) + try: + before = await _award_snapshot(connection) + finally: + await connection.close() + + with pytest.raises(DBAPIError, match="retained accepted-submission.*incomplete"): + await asyncio.to_thread( + command.upgrade, _config(), "0019_submitter_awards" + ) + connection = await asyncpg.connect(isolated_database_env.replace("+asyncpg", "")) + try: + assert await _award_snapshot(connection) == before + assert await connection.fetchval( + "SELECT version_num FROM public.alembic_version" + ) == "0018_task_routing_request" + assert await connection.fetchval( + "SELECT to_regprocedure('public.accepted_submission_award_set_is_complete(uuid)')" + ) is None finally: await connection.close() diff --git a/backend/tests/contributions/records/test_storage.py b/backend/tests/contributions/records/test_storage.py index e31eae0bd..9873f635b 100644 --- a/backend/tests/contributions/records/test_storage.py +++ b/backend/tests/contributions/records/test_storage.py @@ -139,23 +139,21 @@ async def test_contribution_source_substitution(tmp_path, isolated_database_env) async def test_award_definition_substitution(tmp_path, isolated_database_env): async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: async with h.factory() as session: - await insert_record(session, h.submitter_record) await insert_record(session, h.reviewer_record) - money, points = await award_values(session, h.submitter_record) reviewer_awards = await award_values(session, h.reviewer_record) await session.commit() async with h.factory() as session: + money, points = reviewer_awards for changes in ( {"quantity": money["quantity"] + Decimal("0.000000000000000001")}, {"unit_code": "EUR"}, {"adapter_binding_id": points["adapter_binding_id"]}, {"award_definition_id": points["award_definition_id"]}, - {"award_definition_id": reviewer_awards[0]["award_definition_id"]}, {"instrument_type": "project_points", "quantity": Decimal("7"), "unit_code": "PTS"}, ): await reject_award(session, money, "award frozen definition mismatch", **changes) await reject_award(session, money, "award contribution lineage mismatch", - contributor_id=h.reviewer_record.contributor_id) + contributor_id=h.submitter_record.contributor_id) assert await rows(session, "compensation_awards") == [] definition = await session.scalar(text( "SELECT pg_get_functiondef('public.guard_compensation_award()'::regprocedure)" @@ -172,7 +170,15 @@ async def test_award_definition_substitution(tmp_path, isolated_database_env): provision_services=False, storage_settings=h.settings, ) as foreign: async with h.factory() as session: + await insert_record(session, h.submitter_record) + money, points = await award_values(session, h.submitter_record) foreign_money = (await award_values(session, foreign.submitter_record))[0] + await reject_award( + session, + money, + "award frozen definition mismatch", + award_definition_id=reviewer_awards[0]["award_definition_id"], + ) await reject_award(session, money, "award contribution lineage mismatch", project_id=foreign_money["project_id"]) await reject_award(session, money, "award contribution lineage mismatch", @@ -181,9 +187,10 @@ async def test_award_definition_substitution(tmp_path, isolated_database_env): award_definition_id=foreign_money["award_definition_id"], adapter_binding_id=foreign_money["adapter_binding_id"]) assert await rows(session, "compensation_awards") == [] + await insert_award(session, money) + await insert_award(session, points) + await session.commit() async with h.factory() as session, session.begin(): - await insert_award(session, money) - await insert_award(session, points) await reject_award(session, money, "uq_compensation_awards", id=new_record_id()) @@ -211,8 +218,12 @@ async def test_contribution_award_immutability(tmp_path, isolated_database_env): async with h.factory() as session: before = await session.scalar(select(func.clock_timestamp())) await insert_record(session, h.submitter_record, created_at=datetime(2000, 1, 1, tzinfo=UTC)) - award = (await award_values(session, h.submitter_record))[0] - await insert_award(session, award, created_at=datetime(2000, 1, 1, tzinfo=UTC)) + awards = await award_values(session, h.submitter_record) + award = awards[0] + for values in awards: + await insert_award( + session, values, created_at=datetime(2000, 1, 1, tzinfo=UTC) + ) await session.commit() after = await session.scalar(select(func.clock_timestamp())) record = await session.get(ContributionRecord, h.submitter_record.id) @@ -235,11 +246,20 @@ async def test_contribution_award_immutability(tmp_path, isolated_database_env): async def test_contribution_award_concurrency(tmp_path, isolated_database_env, kind, commit_first): import asyncio from tests.reviews.packet.test_repository import wait_for_blocker - async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + # Contribution source uniqueness is isolated with an unpaid accepted-submission + # record; the participant owns complete paid-set assembly. Award uniqueness is + # isolated with a reviewer award until the future reviewer participant exists. + async with contribution_source( + tmp_path, isolated_database_env, paid=kind == "award" + ) as h: async with h.factory() as session: - award = (await award_values(session, h.submitter_record))[0] + award = ( + (await award_values(session, h.reviewer_record))[0] + if kind == "award" + else None + ) if kind == "award": - await insert_record(session, h.submitter_record) + await insert_record(session, h.reviewer_record) await session.commit() first_id = h.submitter_record.id if kind == "contribution" else award["id"] duplicate_id = new_record_id() @@ -277,8 +297,11 @@ async def competitor(): actual = await rows(session, table) assert [r["id"] for r in actual] == [str(first_id if commit_first else duplicate_id)] before = {name: await rows(session, name) for name in ("contribution_records", "compensation_awards")} - await insert_record(session, h.reviewer_record) - for values in await award_values(session, h.reviewer_record): + rollback_record = ( + h.reviewer_record if kind == "contribution" else h.submitter_record + ) + await insert_record(session, rollback_record) + for values in await award_values(session, rollback_record): await insert_award(session, values) await session.rollback() assert {name: await rows(session, name) for name in before} == before @@ -306,5 +329,62 @@ async def test_missing_contribution_parent(tmp_path, isolated_database_env): assert await rows(session, "compensation_awards") == [] await insert_acceptance(session, h.acceptance) await insert_record(session, h.submitter_record) - await insert_award(session, award) + for values in await award_values(session, h.submitter_record): + await insert_award(session, values) + await session.commit() + + +async def test_accepted_submission_complete_award_set_is_deferred_and_exact( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as paid: + async with paid.factory() as session: + awards = await award_values(session, paid.submitter_record) + await insert_record(session, paid.submitter_record) + await insert_award(session, awards[0]) + with pytest.raises(DBAPIError, match="incomplete award set"): + await session.commit() + await session.rollback() + async with paid.factory() as session: + assert await rows(session, "contribution_records") == [] + assert await rows(session, "compensation_awards") == [] + await insert_record(session, paid.submitter_record) + for award in awards: + await insert_award(session, award) await session.commit() + async with paid.factory() as session: + assert len(await rows(session, "contribution_records")) == 1 + assert len(await rows(session, "compensation_awards")) == 2 + + async with contribution_source( + tmp_path / "unpaid", isolated_database_env, provision_services=False, + storage_settings=paid.settings, + ) as unpaid: + async with unpaid.factory() as session: + await insert_record(session, unpaid.submitter_record) + await session.commit() + async with unpaid.factory() as session: + assert len(await rows(session, "contribution_records")) == 2 + assert len(await rows(session, "compensation_awards")) == 2 + + +async def test_removing_deferred_completeness_guard_breaks_partial_set_rejection( + tmp_path, isolated_database_env +): + async with contribution_source(tmp_path, isolated_database_env, paid=True) as h: + async with h.factory() as session: + awards = await award_values(session, h.submitter_record) + await session.execute(text( + "DROP TRIGGER accepted_submission_award_set_from_contribution " + "ON public.contribution_records" + )) + await session.execute(text( + "DROP TRIGGER accepted_submission_award_set_from_award " + "ON public.compensation_awards" + )) + await insert_record(session, h.submitter_record) + await insert_award(session, awards[0]) + with pytest.raises(pytest.fail.Exception, match="DID NOT RAISE"): + with pytest.raises(DBAPIError, match="incomplete award set"): + await session.execute(text("SET CONSTRAINTS ALL IMMEDIATE")) + await session.rollback() diff --git a/backend/tests/test_alembic.py b/backend/tests/test_alembic.py index b6d72f35f..e45ba82b7 100644 --- a/backend/tests/test_alembic.py +++ b/backend/tests/test_alembic.py @@ -79,7 +79,7 @@ def test_v01_graph_has_one_root_and_head() -> None: script = ScriptDirectory.from_config(config) revisions = list(script.walk_revisions()) - assert [revision.revision for revision in revisions] == [HEAD_REVISION, "0017_acceptance_source_contracts", "0016_review_lifecycle_fence", "0015_contribution_awards", "0014_final_acceptance", "0013_review_source", "0012_review_packet", "0011_task_routing_source", "0010_post_submit_authority", "0009_checker_material_lineage", "0008_checker_execution", "0007_checker_output_custody", "0006_history_read_authority", "0005_task_evidence_authority", "0004_task_context_authority", "0003_task_read_authority", "0002_task_queue_authority", BASELINE_REVISION] + assert [revision.revision for revision in revisions] == [HEAD_REVISION, "0018_task_routing_request", "0017_acceptance_source_contracts", "0016_review_lifecycle_fence", "0015_contribution_awards", "0014_final_acceptance", "0013_review_source", "0012_review_packet", "0011_task_routing_source", "0010_post_submit_authority", "0009_checker_material_lineage", "0008_checker_execution", "0007_checker_output_custody", "0006_history_read_authority", "0005_task_evidence_authority", "0004_task_context_authority", "0003_task_read_authority", "0002_task_queue_authority", BASELINE_REVISION] assert revisions[-1].down_revision is None assert script.get_heads() == [HEAD_REVISION] diff --git a/backend/tests/test_ci_lane_catalogue.py b/backend/tests/test_ci_lane_catalogue.py index 4a9da6a28..d134d1d74 100644 --- a/backend/tests/test_ci_lane_catalogue.py +++ b/backend/tests/test_ci_lane_catalogue.py @@ -51,6 +51,12 @@ def test_measured_hotspots_have_explicit_semantic_owners() -> None: == modules_by_lane["project_lifecycle_b"] == modules_by_lane["project_lifecycle_c"] == { + "tests/contributions/records/test_contracts.py", + "tests/contributions/records/test_storage.py", + "tests/contributions/records/test_migration.py", + "tests/contributions/participation/test_contracts.py", + "tests/contributions/participation/test_postgresql.py", + "tests/contributions/participation/test_transactions.py", "tests/projects/guide_activation/test_admission.py", "tests/projects/guide_activation/test_audit_contract.py", "tests/projects/guide_activation/test_concurrency.py", @@ -317,9 +323,6 @@ def test_measured_hotspots_have_explicit_semantic_owners() -> None: "tests/reviews/acceptance/test_contracts.py", "tests/reviews/acceptance/test_storage.py", "tests/reviews/acceptance/test_migration.py", - "tests/contributions/records/test_contracts.py", - "tests/contributions/records/test_storage.py", - "tests/contributions/records/test_migration.py", runner.ADMIN_RUNNER_MODULE, } | static_contracts | post_submit_storage_contracts | set(catalogue.OBSERVABILITY_MODULES) | set(catalogue.TASK_ROUTING_REQUEST_MODULES) == modules_by_lane[ "schema_contracts" diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index 41e157dd4..cc355da96 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -58,14 +58,15 @@ atomic contribution effects; checker success alone is not acceptance. The [canonical shared acceptance contract](spec_review_lifecycle.md#finalacceptance) defines one FinalAcceptance/submitter-contribution operation with two triggers, not a separate automated acceptance system. The human branch retains canonical -`allow_review`. Shared REV source persistence and CON participation precede -ARCH-04E false/pass integration; human queues/leases are not its prerequisites. +`allow_review`. Shared REV source persistence and hidden CON submitter +participation are delivered before ARCH-04E false/pass integration; human +queues/leases are not its prerequisites. The setting is configurable through the existing authorized policy writer; creation defaults true and omitted replacements inherit the predecessor. Versioned hashing preserves old policy hashes and locks. False remains blocked by the current activation service. See the [implementation record](../.commitrail/changes/pre-review-plan-reconciliation.md#delivered-policy-setting-implementation). Enabling false follows shared final-acceptance, -CON and exact AUTH integration proof, not live human-review infrastructure. +TASK effects, exact AUTH/source custody and routing proof, not live human-review infrastructure. The first complete contributor milestone is claim -> upload ZIP -> pre-submit feedback or immutable Submission -> automatic post-submit checking -> outcome. For an eligible project with locked `human_review_required=false`, success must @@ -120,12 +121,12 @@ 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 is delivered; CON-03C contribution/award storage and REV-12A1 disabled controller/fence mechanics +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 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, contribution and conditional -compensation effects, operations and release proof complete v0.1. +is planned. Durable receipts commit only with the complete consequence. Human review/revision, acceptance composition, conditional +fulfillment, operations and release proof complete v0.1. The [independent MCP package](../mcp_server/README.md) implements three self-service tools. The [Go CLI](../cli/README.md) provides caller-profile and @@ -185,10 +186,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: CON-07/shared acceptance prerequisites -> 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 obligation 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, 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 | | 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 runtime consumers; 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 | **Schema foundations 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 | Compose one shared FinalAcceptance/submitter operation for human accept or authorized false/pass routing. Only actual Reviews create reviewer records. Evaluate frozen actor rules into zero, one or two awards | +| 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 | | 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 | @@ -495,13 +496,14 @@ The next dependency-safe product sequence is: 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 without runtime writers or consumers. REV-12A1 supplies disabled controller/fence mechanics with PostgreSQL-enforced root-transaction checks, including raw-SQL savepoint rejection. + 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. 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. CON-07 and shared acceptance - are additional false-branch requirements, not prerequisites for true admission. + 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. 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 @@ -513,9 +515,11 @@ The next dependency-safe product sequence is: 04E1A source facts through the delivered REV-04B source FK; TASK request/source-identity reservation (04E1B-A) and the hidden `task.post_submit.route` preparation for `workstream.task.post_submit_router` (04E2-A) are delivered. They bind exact `TaskAcceptedEffectsRequest` values - without a Review and precede CON-07 and shared acceptance - using the delivered REV-12A1 fence; neither may commit a standalone allow. - Complete hidden handlers 04E1B-B, then at activation 04E2-B enforce mandatory + 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 @@ -629,19 +633,20 @@ Delivered foundations (not a claim of full public integration) REV-04A immutable Review/finding/resolution and completed request storage | v - REV-04B immutable shared acceptance source storage (no AUTH custody or runtime) - CON-03C immutable contribution records and fixed awards (no runtime consumers) + REV-04B immutable shared acceptance source storage (no AUTH custody or production writer) + CON-03C immutable contribution records and fixed awards REV-12A1 disabled generation-zero controller and transaction fence (no authority) AUTH-19A exact source/receipt contracts and planned router (no execution or receipt storage) 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 Remaining integration both branches: handlers 04E1B-B -> activation 04E2-B -> live 04E3 dispatch/routing - true: own routing prerequisites; no CON-07/shared acceptance prerequisite - selected false path first: CON-07 -> shared acceptance -> handler + 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) - -> actual obligation custody and authorized lifecycle activation + -> fulfillment-root ordinal custody and authorized lifecycle activation -> 04F remediation -> public intake and immutable admitted Submission cutover -> automatically consume the durable current result + required checks pass @@ -660,8 +665,8 @@ Remaining integration Human `needs_revision` follows controlled revision; `reject` records a rejected outcome. Every final human decision records reviewer work and any applicable reviewer award atomically; the false branch creates neither. Both acceptance -triggers use the same operation. Shared acceptance persistence -and CON participation must exist before false/pass routing is activated; human +triggers use the same operation. Shared acceptance persistence and hidden CON +participation are separate foundations before false/pass routing activation; human queues and leases are not prerequisites for that branch. The diagram shows the successful evaluation branches; failure/remediation and recovery remain required by the sequence and release gates below. Neither branch is live end to end yet. @@ -705,8 +710,9 @@ 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. -- Frozen policy rules produce zero, one, or two CompensationAwards without - controlling Workstream lifecycle truth. +- 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. - 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, @@ -776,8 +782,9 @@ remaining trace sequence is: [REV-04A Review source storage](../.commitrail/initiatives/WS-REV-001/WS-REV-001-04A.md) 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), - and [REV-12A1 disabled controller/fence](../.commitrail/initiatives/WS-REV-001/WS-REV-001-12A1.md) - support the additional false-path acceptance-source AUTH custody, CON-07 and shared + [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`, then handlers/activation/live `04E1B-B -> 04E2-B -> 04E3`; true routing proceeds after its own prerequisites without shared acceptance; 04F supplies remediation. The mandatory @@ -794,14 +801,16 @@ remaining trace sequence is: 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` are delivered. The selected automated path next completes CON-07/shared - acceptance prerequisites, then handlers `04E1B-B`, activation `04E2-B` and + `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. 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, without runtime recognition or fulfillment. + 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. - 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_contribution_compensation.md b/docs/spec_contribution_compensation.md index 01592259a..45adc5ea1 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -53,6 +53,17 @@ The delivered ARCH-04E1A route-neutral source row alone also cannot satisfy this boundary; later publication must add mandatory exact routing/owner-receipt custody to that same table before CON consumes the REV-owned acceptance fact. +The hidden CON submitter participant is delivered as a source-neutral, +flush-only consumer of a real stored FinalAcceptance and assignment lineage. It +runs only inside the caller-owned root transaction after acquiring the supplied +canonical lifecycle fence. It creates or exactly replays one +`accepted_submission` ContributionRecord and the complete frozen award set: +zero awards for unpaid work, one for money-only or points-only work, and two for +combined work. Compensated replay checks the original correlation UUID and +award IDs; unpaid replay retains no correlation to compare. This participant +adds no acceptance/source writer, TASK transition, authorization claim, route, +fulfillment root or reviewer contribution. + The boundary MUST preserve four distinct facts: 1. `Review` records the reviewer's decision. @@ -262,12 +273,14 @@ depends on the binding. ### ContributionRecord -CON-03C delivers immutable source and award storage only. No production writer, -reader, participant or fulfillment consumer is registered. Before activation, -originating Review/FinalAcceptance authority and the shared fence/ordinal must -be installed; retained pre-authority sources and dependent contribution/award -rows must cause refusal unchanged, never receipt backfill or deletion. CON-07 -must prove complete compensated award sets and whole-transaction rollback. +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 +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 +sources and dependent contribution/award rows must cause refusal unchanged, +never receipt backfill or deletion. Canonical fields: @@ -540,9 +553,10 @@ not ordinary review-claim selection. ## Atomic Review-To-Contribution Transaction -One mandatory `ContributionCompensationDecisionParticipant` exposes two -ordered, operation-specific methods in the initiating command's caller-owned -session. Human decision composition uses the reviewer method; the shared +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 request carrying nullable FinalAcceptance or both actors' source and policy facts is prohibited. @@ -628,21 +642,23 @@ Reject MUST NOT change an actor grant, another task, or another assignment. ### Submitter operation -The submitter operation exists only after the shared acceptance operation has created -FinalAcceptance and applied accepted Task and completed TaskAssignment effects. - -Its typed input contains only locked: +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. -- FinalAcceptance and TaskAssignment; -- existing versioned Submission, project, and task; -- submitter actor; -- assignment-frozen submitter ContributionPolicyVersion; -- the originating authorization decision, request, and correlation references; -- the same stabilized artifact hash. +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. It contains no direct Review or ReviewLease contribution-source fields. It -creates exactly one `accepted_submission`, evaluates only the submitter rule, -stages zero to two awards, returns typed audit/outbox inputs, and flushes. +creates or exactly replays one `accepted_submission`, evaluates only the frozen +submitter rule, stages the complete zero-to-two award set, returns immutable +contribution/award facts, and flushes. Paid replay checks correlation equality; +unpaid replay has no stored award correlation to compare. ### Atomicity @@ -893,8 +909,9 @@ not canonical catalogue identifiers and MUST NOT be registered by CON. `task.claim` currently has a stable PermissionId but no ActionId. CP08 lineage and ARCH-03B task-owned readiness/inheritance composition must merge before AUTH registers or activates that future action. `review.claim` and `review.decision` remain -planned until the inherited task-policy lineage, CON-07, and complete REV -composition merge. AUTH must transfer the complete +planned until complete REV composition and the reviewer participant merge; the +inherited task-policy lineage and CON-07 submitter participant are delivered. +AUTH must transfer the complete REV action set under its canonical custody contract; CON MUST NOT define a partial local transfer. @@ -1022,8 +1039,9 @@ 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) -requires hidden AUTH preparation/receipt staging before CON-07, and mandatory -persisted custody before production composition or consumption. Actual CON root +now includes delivered hidden AUTH preparation and CON-07 participation, 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 fulfillment obligations; neither awards nor generic outbox rows substitute. Later REV-12A drain/operator work extends this same controller; it is not a prerequisite on live human @@ -1151,11 +1169,13 @@ transaction participants do not wait for generic dispatch: The [shared acceptance order](spec_review_lifecycle.md#implementation-order-and-required-proof) governs the false branch: delivered TASK ARCH-04E1A source schema/detached facts -precede the delivered REV-04B acceptance and CON-03C contribution/award storage. -REV-12A1 delivers disabled controller/fence mechanics. TASK request staging (04E1B-A) and -hidden AUTH preparation/receipt staging (04E2-A) precede CON-07 flush-only -participation and the shared operation/04E1B-B handler. CON-07 isolated storage -controls prove economic behavior, not acceptance authority. At 04E2-B, mandatory +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 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 diff --git a/docs/spec_review_lifecycle.md b/docs/spec_review_lifecycle.md index 09421932a..f4f5f07a6 100644 --- a/docs/spec_review_lifecycle.md +++ b/docs/spec_review_lifecycle.md @@ -53,7 +53,7 @@ root-level version namespace do not create an alias. ## v0.1 Boundary -### Planned shared acceptance +### Shared acceptance boundary The accepted product direction uses `human_review_required: bool = true` in the existing guide-bound ReviewPolicy. After required post-submit checks pass, @@ -64,7 +64,10 @@ enum, policy entity or adjudication setting is introduced. The 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. -Automated acceptance is not implemented or enabled by this note. In particular, +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, `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. @@ -493,8 +496,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. -This is a target implementation contract; neither branch is enabled by this -document. +The hidden CON participant that consumes this fact is delivered; the shared +writer and both runtime branches remain unavailable. | Trigger | Required source | Result | |---|---|---| @@ -563,9 +566,19 @@ inferred from a FK. v0.1 has no reopen or replacement acceptance. 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, CON consumer or TASK effect. Direct SQL fixture rows prove relational +operation or TASK effect. Direct SQL fixture rows prove relational custody only; they are not authorized acceptance. +[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. +After acquiring the supplied canonical REV fence through a CON-owned Protocol, +it stages or exactly replays one accepted-submission contribution and zero, one +or two awards inside the caller's existing root transaction. Exact source +replay returns the original row IDs. Compensated replay requires the original +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. + `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 @@ -638,8 +651,9 @@ 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 is delivered before CON-07; 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 and CON-07 + participation 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) @@ -650,15 +664,19 @@ Extract foundations from existing owner work, not a new initiative: preparation matcher plus nominal `workstream.task.post_submit_router` adapter through canonical PREP. The action remains planned/unavailable and denial occurs before handle issuance; no allow, receipt, source publication or effect exists. - CON-07 follows as a flush-only participant; isolated storage controls prove - economic behavior without claiming acceptance authority or fabricating allows. + CON-07 then delivers the flush-only participant and complete frozen award-set + 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. 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 consumes those foundations for acceptance. ARCH-04E1B-B's +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 - require CON-07/shared acceptance; it uses the delivered preparation, hidden 04E1B-B, exact + 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 @@ -706,21 +724,25 @@ 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. Every committed Review creates exactly one reviewer `completed_review` ContributionRecord sourced directly from the Review and ReviewLease. Only FinalAcceptance creates a submitter `accepted_submission` ContributionRecord. CON never infers submitter acceptance from `Review.decision`. -The CON participant exposes two operation-specific flush-only inputs: +The target CON boundary exposes two operation-specific flush-only inputs: - reviewer input for every decision, containing Review, ReviewLease, reviewer, lease-frozen ContributionPolicyVersion, Submission/project/task lineage, AuthorizationDecision, request/correlation references, and stabilized `artifact_hash`; it contains no FinalAcceptance or submitter-policy facts; -- submitter input only after accept creates FinalAcceptance and applies accepted - task effects, containing FinalAcceptance, TaskAssignment, submitter, - assignment-frozen ContributionPolicyVersion, and the same locked lineage. +- delivered submitter input only after accept creates FinalAcceptance and applies + 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. Database constraints keep the source shapes mutually exclusive and enforce one `completed_review` per Review and one `accepted_submission` per From b51e23b31bfe8f53c494fe0bc124bb097cbed163 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:52:16 +0100 Subject: [PATCH 08/11] fix(ci): register exact submitter participant ownership --- .ci/behavior-ownership/partition.v1.json | 6 ++--- .../initiatives/WS-CON-001/WS-CON-001-07.md | 2 +- backend/scripts/behavior_ownership.py | 10 +++++++- backend/tests/test_behavior_ownership.py | 25 +++++++++++++++++++ 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index a6156b817..6d3e16510 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -885,7 +885,7 @@ "target": "backend/app/modules/compensation/api/adapter_bindings.py" }, { - "group": "shared", + "group": "lifecycle", "target": "backend/app/modules/compensation/api/awards.py" }, { @@ -901,7 +901,7 @@ "target": "backend/app/modules/compensation/awards/models.py" }, { - "group": "shared", + "group": "lifecycle", "target": "backend/app/modules/compensation/awards/participant.py" }, { @@ -1649,7 +1649,7 @@ "target": "backend/scripts/validate_test_lane_evidence.py" } ], - "authority_digest": "ce43271253a9f45a0477b003990161c7dca4eec4edee4ae0ad31e0ddafbfef3a", + "authority_digest": "fdbbf9eb7ded754eb10e532c3a3b811f4af227136fcd2870f7f638405656345b", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 2c7e03223..e62eee9c8 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -23,7 +23,7 @@ These storage prerequisites do not authorize acceptance or payment. - `backend/app/modules/compensation/api/awards.py` , `api/__init__.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. - `backend/alembic/env.py`, `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. - `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, migration graph assertions if necessary: focused integration and regression proof. -- `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/partition.v1.json`: exact new proof/owner registration and move the three existing contribution record modules from schema to project partitions; no gate weakening. +- `backend/scripts/behavior_ownership.py`, `backend/tests/test_behavior_ownership.py`, `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/partition.v1.json`: exact new proof/owner registration and move the three existing contribution record modules from schema to project partitions; no gate weakening. - This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. ### Not allowed diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index 6026a9658..ce288975f 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -148,6 +148,13 @@ } ) CON_03C_STORAGE_TARGETS = frozenset({"backend/app/modules/contributions/records/models.py", "backend/app/modules/contributions/records/schemas.py", "backend/app/modules/compensation/awards/models.py"}) +CON_07_PARTICIPATION_TARGETS = frozenset({ + "backend/app/modules/contributions/api/participation.py", + "backend/app/modules/contributions/records/participant.py", + "backend/app/modules/contributions/records/repository.py", + "backend/app/modules/compensation/api/awards.py", + "backend/app/modules/compensation/awards/participant.py", +}) ARCH_04E2A_ROUTING_AUTH_TARGETS = frozenset({ "backend/app/modules/authorization/domain/post_submit_routing.py", "backend/app/modules/authorization/post_submit_routing_authorization.py", @@ -623,7 +630,7 @@ def changed_callable_names(root: Path, base_sha: str, head_sha: str, target: str def group_for_target(target: str) -> str: """Assign one exact population group without wildcard authority.""" - if target in ARCH_CP02_CON_LIFECYCLE_TARGETS | CON_03C_STORAGE_TARGETS: + if target in ARCH_CP02_CON_LIFECYCLE_TARGETS | CON_03C_STORAGE_TARGETS | CON_07_PARTICIPATION_TARGETS: return "lifecycle" if "/authorization/" in target or target.endswith("/auth.py"): return "auth" @@ -762,6 +769,7 @@ def _validate_additive_partition_transition( | ARCH_CP04A_CONTRIBUTION_POLICY_TARGETS | ARCH_CP04B_CONTRIBUTION_POLICY_TARGETS | CON_03C_STORAGE_TARGETS + | CON_07_PARTICIPATION_TARGETS | AUTH_19A_SOURCE_CONTRACT_TARGETS | ARCH_04E2A_ROUTING_AUTH_TARGETS | REV_12A1_FENCE_TARGETS diff --git a/backend/tests/test_behavior_ownership.py b/backend/tests/test_behavior_ownership.py index 6f4d718bb..877d80ac3 100644 --- a/backend/tests/test_behavior_ownership.py +++ b/backend/tests/test_behavior_ownership.py @@ -2299,3 +2299,28 @@ def test_acceptance_source_contract_registration_excludes_execution() -> None: for neighbor in ("backend/app/modules/authorization/acceptance_source_authorization.py", "backend/app/modules/tasks/post_submit_routing/service.py"): with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): ownership._validate_additive_partition_transition(_partition(sorted({retained, *expected, neighbor})), trusted) + + +def test_submitter_participation_has_exact_ownership_without_activation() -> None: + expected = { + "backend/app/modules/contributions/api/participation.py", + "backend/app/modules/contributions/records/participant.py", + "backend/app/modules/contributions/records/repository.py", + "backend/app/modules/compensation/api/awards.py", + "backend/app/modules/compensation/awards/participant.py", + } + assert ownership.CON_07_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/contributions/records/router.py", + "backend/app/modules/compensation/awards/delivery.py", + ): + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership._validate_additive_partition_transition( + _partition(sorted({retained, *expected, neighbor})), trusted + ) From 445e627021cc2069ea6ef7d3a66e739f31b5fb22 Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 16:55:13 +0100 Subject: [PATCH 09/11] test(db): reconcile verified completeness schema fingerprint --- .commitrail/initiatives/WS-CON-001/WS-CON-001-07.md | 8 +++++++- backend/tests/conftest.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index e62eee9c8..2a75a856d 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -22,7 +22,7 @@ These storage prerequisites do not authorize acceptance or payment. - `backend/app/modules/contributions/api/participation.py` , `api/__init__.py` and `records/{participant,repository}.py`: source-neutral typed submitter input/result, exact source replay and frozen-rule selection. - `backend/app/modules/compensation/api/awards.py` , `api/__init__.py` and `awards/participant.py`: typed complete award-set port and owner implementation; no private cross-owner imports. - `backend/alembic/env.py`, `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. -- `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, migration graph assertions if necessary: focused integration and regression proof. +- `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, `backend/tests/conftest.py` exact schema fingerprint and migration graph assertions if necessary: focused integration and regression proof. - `backend/scripts/behavior_ownership.py`, `backend/tests/test_behavior_ownership.py`, `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/partition.v1.json`: exact new proof/owner registration and move the three existing contribution record modules from schema to project partitions; no gate weakening. - This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. @@ -110,6 +110,12 @@ seconds of observed call time; this is not a total-runtime estimate. Preserve all nodes and the partition algorithm; fresh hosted completion must establish actual timing. No cap or completeness gate changes. +The PostgreSQL 16 catalog comparison from 0018 to 0019 removes no objects. +Its only additions are the two named completeness functions (including their +namespace inventory entries) and two constraint triggers (including their +constraint entries). The reset harness fingerprint is updated from this exact +observed schema; its verification is not disabled. + ## Review findings Plan review corrected an otherwise cyclic CON -> REV -> CON dependency by diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 02e711645..d95d0031e 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -23,7 +23,7 @@ DDL_LOCK_DIRECTORY = Path("/tmp") # Match the PostgreSQL 16 engine used by Backend CI. Catalog identity rendering # differs across major versions; regenerate only after comparing actual objects. -EXPECTED_PUBLIC_SCHEMA_SHA256 = "bf7ef332ddfecb131cd288ea3ffe6a02c5d434c4d4dbb548c03c80b9b2078fcc" +EXPECTED_PUBLIC_SCHEMA_SHA256 = "3c87e2884fc6cf4d4be47c4a7ea5a65f918c5012098fae025f28cd75c571817c" PROTECTED_TEST_TABLES = ( "joint_lifecycle_release_control", "actor_profile_migration_state", From 5fa73a133dc2bb511232b33f511d9f079e30c44a Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 17:07:23 +0100 Subject: [PATCH 10/11] docs(con): reconcile delivered participation across current contracts --- .../initiatives/WS-CON-001/WS-CON-001-07.md | 37 +++++++++++++------ docs/architecture_data_model.md | 7 +++- docs/glossary.md | 7 +++- docs/spec_artifact_storage_service.md | 6 ++- docs/spec_authorization_service.md | 7 ++-- 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md index 2a75a856d..800ca8fcf 100644 --- a/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md +++ b/.commitrail/initiatives/WS-CON-001/WS-CON-001-07.md @@ -1,7 +1,7 @@ # CON-07 — Hidden submitter contribution and complete frozen awards - Initiative: `WS-CON-001` -- Durable disposition: `Planned` +- Durable disposition: `Complete` - Intended merge outcome: one hidden flush-only submitter participant creates or exactly replays a FinalAcceptance-sourced contribution and its complete frozen award set; no runtime acceptance activation. ## Intent @@ -24,7 +24,7 @@ These storage prerequisites do not authorize acceptance or payment. - `backend/alembic/env.py`, `backend/alembic/versions/0019_submitter_award_completeness.py`: deferred complete-set guard and fail-closed retained-data preflight for accepted-submission contributions. - `backend/tests/contributions/participation/**`, affected `backend/tests/contributions/records/**`, `backend/tests/conftest.py` exact schema fingerprint and migration graph assertions if necessary: focused integration and regression proof. - `backend/scripts/behavior_ownership.py`, `backend/tests/test_behavior_ownership.py`, `backend/scripts/test_lane_catalogue.py`, `backend/tests/test_ci_lane_catalogue.py` and `.ci/behavior-ownership/partition.v1.json`: exact new proof/owner registration and move the three existing contribution record modules from schema to project partitions; no gate weakening. -- This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. +- This record, current CON/AUTH/ARCH/POL/REV overview and planning navigation, `.commitrail/INDEX.md`, README, `docs/{roadmap_status,spec_contribution_compensation,spec_review_lifecycle,spec_authorization_service,spec_artifact_storage_service,architecture_data_model,glossary}.md`: affected delivery and dependency claims. Local spreadsheet exports only if present. ### Not allowed @@ -79,20 +79,30 @@ proof is retained. ## Acceptance criteria -- [ ] Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. -- [ ] Source replay returns the same record and award IDs; each changed source fact and compensated-award correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. -- [ ] Real stored foreign source substitutions reject at intended ownership guard, with valid controls; copied decimal quantity, unit, binding, definition and artifact hash equal canonical stored facts. -- [ ] In one real fenced root transaction, stage a valid compensated contribution with one of two canonical awards before deferred commit; exact-source replay must conflict without inserting the missing award, then caller rolls back. Removing replay completeness validation must fail the intended conflict assertion (whether the broken code returns a partial set or backfills). -- [ ] Caller rollback and late database failure discard new FinalAcceptance/contribution/awards together. No participant commit, provider, ART, AUTH evaluation, Review or task mutation occurs. -- [ ] The supplied canonical fence rejects missing root transaction and managed/raw SQL savepoints; independent sessions prove transaction-scoped serialization. -- [ ] Direct SQL partial compensated award set fails at deferred commit; full set and unpaid controls succeed; removing the completeness guard makes the negative proof fail. Retained incomplete migration fixture refuses upgrade without repair/deletion. -- [ ] Existing paid storage tests must stage complete accepted-submission award sets or retain invalid insert probes in a rolled-back transaction; no required assertion is removed. New tests are registered once in the project lifecycle group (schema has little timing headroom), all existing required storage proof remains, full hosted suite completes with zero skips/deselections. No percentages are acceptance gates. +- Unpaid, money-only, points-only and combined frozen rules create exact records/awards; retired policy and suspended binding do not alter frozen obligations. +- Source replay returns the same record and award IDs; each changed source fact and compensated-award correlation conflict rejects without new rows. Concurrent same-source calls converge through PostgreSQL, not fake locks. +- Real stored foreign source substitutions reject at intended ownership guard, with valid controls; copied decimal quantity, unit, binding, definition and artifact hash equal canonical stored facts. +- In one real fenced root transaction, stage a valid compensated contribution with one of two canonical awards before deferred commit; exact-source replay must conflict without inserting the missing award, then caller rolls back. Removing replay completeness validation must fail the intended conflict assertion (whether the broken code returns a partial set or backfills). +- Caller rollback and late database failure discard new FinalAcceptance/contribution/awards together. No participant commit, provider, ART, AUTH evaluation, Review or task mutation occurs. +- The supplied canonical fence rejects missing root transaction and managed/raw SQL savepoints; independent sessions prove transaction-scoped serialization. +- Direct SQL partial compensated award set fails at deferred commit; full set and unpaid controls succeed; removing the completeness guard makes the negative proof fail. Retained incomplete migration fixture refuses upgrade without repair/deletion. +- Existing paid storage tests must stage complete accepted-submission award sets or retain invalid insert probes in a rolled-back transaction; no required assertion is removed. New tests are registered once in the project lifecycle group (schema has little timing headroom), all existing required storage proof remains, full hosted suite completes with zero skips/deselections. No percentages are acceptance gates. Fixtures use real retained human-source FinalAcceptance storage; automated no-Review production proof remains with false activation, which is currently blocked. No fixture changes true to false or fabricates AUTH allows. ## Evidence -Planned executable proof: `cd backend && .venv/bin/python -m pytest -q tests/contributions/participation tests/contributions/records` with isolated PostgreSQL; focused contract tests without infrastructure where possible. Run Ruff, module/AUTH/structure/ownership checks, stale wording, Markdown links and Commitrail checks; then hosted full suite. Guard-removal and independent-session probes bind to the exact implementation candidate. +The focused proof lives in `backend/tests/contributions/participation/` and +`backend/tests/contributions/records/`, with migration graph assertions in +`backend/tests/test_alembic.py`. Use the isolated PostgreSQL runner for storage, +rollback, concurrent replay, deferred completeness and retained-data upgrade +cases. Pure contract validation needs no database. The replay and SQL guard +removal probes exercise distinct failure boundaries. + +Verification also includes Ruff, module/AUTH/structure/ownership checks, stale +wording, Markdown links, Commitrail and full hosted suite completeness. Exact +head results and internal reviewer verdicts belong in the PR evidence; this +record describes the durable behavior and its proof obligations. ## Risk and review routing @@ -123,9 +133,12 @@ using a consumer-owned acquisition interface with canonical fence composition. It also narrowed unpaid correlation claims to what storage retains and added a partial replay proof that reaches the guard before deferred commit. No production authority or live automated acceptance is inferred from these tests. +Implementation review also reconciled the current authorization, artifact, data +model and glossary entries so they distinguish delivered hidden participation +from pending shared acceptance and live recognition. ## Reconciliation -- Current main: 89dd8c91 includes merged routing preparation, observability and CLI self-service. Preserve their ownership and test registrations. +- Reconciled main: bcd0bd49 includes merged routing preparation, observability, CLI self-service and MCP administrative reads. Their ownership, test registrations and current navigation are preserved. - Next: shared acceptance/TASK accepted effects and hidden handlers, actual authority custody and fulfillment-root/lifecycle proof before activation; later reviewer participant reuses these owners. - Remaining risks: production authority, full acceptance atomicity and fulfillment are deliberately not established by this hidden participant. diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index b0e34d549..a6018f15f 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -2045,8 +2045,11 @@ this record. ## ContributionRecord -CON-03C implements this immutable storage foundation. Runtime recognition, -source AUTH hardening and the atomic CON participant remain unavailable. +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 +separate future work. Fields: diff --git a/docs/glossary.md b/docs/glossary.md index 852080299..d077fb9ef 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -440,8 +440,11 @@ It is not a v0.1 review-transaction side effect. ## Contribution Record -CON-03C supplies immutable storage only; live recognition and atomic -contribution creation remain unavailable until the shared participant is implemented. +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 +and fulfillment remain separate future work. The immutable, evidence-backed record of one completed contribution under locked project context. `completed_review` is created for every valid recorded human diff --git a/docs/spec_artifact_storage_service.md b/docs/spec_artifact_storage_service.md index 55164a08b..1892aaaf1 100644 --- a/docs/spec_artifact_storage_service.md +++ b/docs/spec_artifact_storage_service.md @@ -1626,7 +1626,11 @@ prevents a live contributor route whose mandatory checker read is unavailable. metadata-only packet membership types and a type-only port. It implements no 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 is delivered; REV-04B shared FinalAcceptance storage is delivered; CON-03C contribution/award storage is delivered; atomic participation remains next. +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. 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 3e9b5ce88..3b5d27c44 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -692,7 +692,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/shared acceptance precedes 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 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 | Each action maps to the identically named permission in this table and only its singleton fixed-service row. Humans, dispatchers and unrelated services @@ -1660,8 +1660,9 @@ persist this new source commitment. ARCH-04E1B-A stages routing request facts; 04E2-A provides strict hidden AUTH resource/preparation matching and the nominal 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/shared -acceptance prerequisites follow for the selected automated path. Mandatory +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, 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 From 20b679f9eb714327d6f077cd3efe4ecb62bc145c Mon Sep 17 00:00:00 2001 From: Commitrail Probe Date: Sun, 4 Oct 2026 17:25:04 +0100 Subject: [PATCH 11/11] test(con): verify each migrated function at its own boundary --- .../contributions/records/test_migration.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/backend/tests/contributions/records/test_migration.py b/backend/tests/contributions/records/test_migration.py index cc02ea0a4..47cedd604 100644 --- a/backend/tests/contributions/records/test_migration.py +++ b/backend/tests/contributions/records/test_migration.py @@ -151,7 +151,23 @@ async def test_completeness_upgrade_preserves_complete_retained_awards( ] for row in functions: assert row["proconfig"] == ["search_path=pg_catalog, public, pg_temp"] - assert "public.contribution_records" in row["body"] + bodies = {row["proname"]: row["body"] for row in functions} + helper_body = bodies["accepted_submission_award_set_is_complete"] + for relation in ( + "contribution_records", + "contribution_rules", + "contribution_award_definitions", + "compensation_awards", + ): + assert f"public.{relation}" in helper_body + trigger_body = bodies["require_accepted_submission_award_set"] + for relation in ("contribution_records", "compensation_awards"): + assert ( + f"TG_TABLE_SCHEMA = 'public' AND TG_TABLE_NAME = '{relation}'" + ) in trigger_body + assert ( + "public.accepted_submission_award_set_is_complete(record_uuid)" + ) in trigger_body triggers = await connection.fetch( "SELECT tgname, tgdeferrable, tginitdeferred FROM pg_catalog.pg_trigger " "WHERE tgname LIKE 'accepted_submission_award_set_from_%' ORDER BY tgname"