diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index a2d42ed4d..be940f9bf 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -1288,6 +1288,10 @@ "group": "lifecycle", "target": "backend/app/modules/reviews/schemas.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/api/accepted_effects.py" + }, { "group": "lifecycle", "target": "backend/app/modules/tasks/api/assignment_invalidation.py" @@ -1304,6 +1308,10 @@ "group": "lifecycle", "target": "backend/app/modules/tasks/api/management_queue.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/api/post_submit_routing.py" + }, { "group": "lifecycle", "target": "backend/app/modules/tasks/api/ready_queue.py" @@ -1352,6 +1360,10 @@ "group": "lifecycle", "target": "backend/app/modules/tasks/models.py" }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/post_submit_routing/models.py" + }, { "group": "lifecycle", "target": "backend/app/modules/tasks/queue_router.py" @@ -1521,7 +1533,7 @@ "target": "backend/scripts/validate_test_lane_evidence.py" } ], - "authority_digest": "941150a6b283032aefd8f41e1be6d5aaf1113d90349e943a0d2c8d9efe177fb2", + "authority_digest": "fa372bfafa2649a9e94a4bea1d1a0009f4734e5073ff1766c41184ebf6da1801", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/.commitrail/INDEX.md b/.commitrail/INDEX.md index 610b1e226..c4bdfbd5a 100644 --- a/.commitrail/INDEX.md +++ b/.commitrail/INDEX.md @@ -8,13 +8,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-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | Public guide activation and task reads, hidden approved-guide intake, exact post-submit input materialization and hidden ARCH-04B2 checker-output custody delivered; ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical material custody delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next | -| [WS-ART-001](initiatives/WS-ART-001/OVERVIEW.md) | Planned | Hidden exact post-submit input materialization and ARCH-04B2 output custody delivered; ARCH-04C hidden durable execution delivered; ARCH-04D1 canonical material custody delivered; ARCH-04D2 exact input/execution/finalization authority delivered; ARCH-04E1A routing-source facts next | -| [WS-AUTH-001](initiatives/WS-AUTH-001/OVERVIEW.md) | Planned | Manager guide activation, public task authority, dispatcher mechanics and hidden intake are delivered; ARCH-04B input and ARCH-04B2 output custody exist; only output-file authority remains unavailable; ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical material custody delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next | -| [WS-CON-001](initiatives/WS-CON-001/OVERVIEW.md) | Planned | ContributionPolicy administration and guide binding plus hidden intake, post-submit input and ARCH-04B2 output custody delivered; ARCH-04C hidden durable execution delivered; ARCH-04D2 input/execution/finalization authority delivered; ARCH-04E1A precedes shared FinalAcceptance, submitter ContributionRecord and applicable awards | -| [WS-AUTH-003](initiatives/WS-AUTH-003/OVERVIEW.md) | Planned | TASK/checker canonical history authority delivered and alternate gate removed; AUTH-18 public manager activation and ARCH-03D hidden intake delivered; ARCH-04B hidden input and ARCH-04B2 output custody delivered; continue boundary recovery with ARCH-04C hidden durable execution delivered; ARCH-04D1 canonical material custody delivered; ARCH-04D2 exact input/execution/finalization authority delivered; ARCH-04E1A routing-source facts next | -| [WS-POL-003](initiatives/WS-POL-003/OVERVIEW.md) | Planned | Unified setup, public manager policy operations, phase composition, public guide activation, hidden intake, post-submit input and ARCH-04B2 output custody delivered; ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical material custody delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next | -| [WS-REV-001](initiatives/WS-REV-001/OVERVIEW.md) | Planned | Shared acceptance/source and existing fence foundations; human hidden review work remains independently dependency-gated | +| [WS-ARCH-001](initiatives/WS-ARCH-001/OVERVIEW.md) | Planned | ARCH-04E1A immutable route-neutral TASK source storage, detached facts and type-only accepted-effects contract are delivered; next build shared REV-04B/CON-03C/07 and REV-12A/CON fence foundations before 04E1B/04E2/04E3 routing and 04F remediation | +| [WS-ART-001](initiatives/WS-ART-001/OVERVIEW.md) | Planned | ARCH-04E1A source facts now retain canonical material lineage without publishing a route; 04F remediation and the later public intake cutover remain | +| [WS-AUTH-001](initiatives/WS-AUTH-001/OVERVIEW.md) | Planned | ARCH-04E1A source facts are delivered without routing authority; shared acceptance foundations and hidden 04E1B proof precede exact 04E2 activation and 04E3 live composition | +| [WS-CON-001](initiatives/WS-CON-001/OVERVIEW.md) | Planned | ARCH-04E1A source facts are delivered; REV-04B source/FinalAcceptance persistence, CON-03C/07 and the shared REV-12A/CON fence foundation are next before either acceptance trigger composes shared effects | +| [WS-AUTH-003](initiatives/WS-AUTH-003/OVERVIEW.md) | Planned | ARCH-04E1A source facts and source-neutral types are delivered without runtime composition; continue boundary recovery through the shared acceptance foundations and later 04E1B/04E2/04E3 route | +| [WS-POL-003](initiatives/WS-POL-003/OVERVIEW.md) | Planned | ARCH-04E1A source facts are delivered, but false activation remains unavailable until shared acceptance, exact routing authority, live composition and 04F remediation are proven | +| [WS-REV-001](initiatives/WS-REV-001/OVERVIEW.md) | Planned | ARCH-04E1A TASK source foundation delivered; REV-04B source/FinalAcceptance, CON-03C/07 and existing REV-12A/CON fence foundations are next; human hidden review work remains independently 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 582385aa9..8e414e82b 100644 --- a/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md @@ -12,12 +12,20 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), [ARCH-04A consolidation](WS-ARCH-001-04A.md) canonical post-submit contracts/conformance. - Intent: keep product modules behind explicit ports and composition roots. - Current boundary: one CHECKERS catalogue, compiler/parser and implementation - per checker ID serve active policy consumers; hidden post-submit execution now - has exact fixed-service authority. Automatic dispatch/routing remains unavailable. + per checker ID serve active policy consumers; hidden post-submit execution has + exact fixed-service authority. ARCH-04E1A adds immutable route-neutral TASK + source storage, detached facts and source-neutral accepted-effects types. + Automatic dispatch/routing and acceptance remain unavailable. - Delivered storage boundary: hidden [ARCH-04B2 checker-output custody](WS-ARCH-001-04B2.md), following hidden input materialization (ARCH-04B). Typed store, byte-free recovery and flush-only verified binding exist; the CHECKERS zero-slot reservation reader is implemented; output write/bind authority remains unavailable. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered - [ARCH-04D2](WS-ARCH-001-04D2.md) exact input, execute and finalize authority. - Output-file authority remains unavailable for the zero-output catalogue. +- Delivered source boundary: [ARCH-04E1A routing-source facts](WS-ARCH-001-04E1A.md) + follow [ARCH-04D2](WS-ARCH-001-04D2.md) exact input, execute and finalize + authority. The source table has no writer, reader, handler, current pointer, + routing authority or acceptance effect implementation. False is proven only as + a scalar DTO value because activation still rejects it. +- Next usable boundary: shared REV-04B source/FinalAcceptance persistence, + CON-03C/07 and the existing REV-12A/CON fence foundation 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 context and exact guide activation using the existing CP07 operation. [CP05A](WS-ARCH-001-CP05A.md) supplies public Finance policy administration and recoverable draft selectors. diff --git a/.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04E1A.md b/.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04E1A.md new file mode 100644 index 000000000..aa08b4e4d --- /dev/null +++ b/.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04E1A.md @@ -0,0 +1,363 @@ +# ARCH-04E1A — Immutable post-submit routing source foundation + +- Initiative: `WS-ARCH-001` +- Durable disposition: `Complete` +- Risk: L1 (retained lineage and schema). +- Intended merge outcome: one TASK source schema and detached internal contracts support the later shared acceptance source FK; routing, acceptance and public intake remain unavailable. + +## Intent + +Continue claim -> ZIP intake -> immutable Submission -> automatic checking -> +policy-governed outcome. Establish the source identity needed by REV without +making that identity an authorization token or adding a second acceptance path. + +## Starting point + +Main `d5bf3460` includes ARCH-04D2. CHECKERS owns immutable completed results, +material custody, completion events, execute/finalize receipts and currentness. +TASK owns Submission/assignment/policy lineage and `SubmittedBundleFacts`. +There is no routing manifest or routing service/action. The zero-output +catalogue has no output-file authority requirement. + +The old 04E skeleton promises complete routing authority before AUTH registration. +It also promises original Submission/binding/materialization decision IDs which +current owner contracts discard. Do not fabricate them or find substitute allows +by reverse audit-log search. The later 04E1B/04E2 publication work must propagate +those exact receipt IDs from their owning operations and retain them before +claiming a complete authorized manifest. CHECKERS execute/finalize receipts +remain distinct and cannot authorize routing. + +## Bounded change + +### Allowed implementation and proof files + +- `backend/app/modules/tasks/post_submit_routing/models.py` +- `backend/app/modules/tasks/post_submit_routing/__init__.py` +- `backend/app/modules/tasks/api/post_submit_routing.py` +- `backend/app/modules/tasks/api/accepted_effects.py` +- `backend/app/modules/tasks/api/__init__.py` +- `backend/app/db/models.py` +- `backend/alembic/versions/0011_task_routing_source.py` +- `backend/alembic/env.py` (advance the exact accepted migration-head inventory) +- `backend/tests/tasks/post_submit_routing/__init__.py` +- `backend/tests/tasks/post_submit_routing/support.py` +- `backend/tests/tasks/post_submit_routing/test_contracts.py` +- `backend/tests/tasks/post_submit_routing/test_storage.py` +- `backend/tests/tasks/post_submit_routing/test_migration.py` +- `backend/tests/conftest.py` (exact table/trigger inventory and generated schema fingerprint) +- `backend/tests/test_alembic.py` (exact revision graph) +- `backend/tests/test_coverage_contract.py` (current-head fixture only) +- `backend/tests/checkers/execution/test_migration.py` (current-head assertion) +- `backend/tests/authorization/post_submit/test_migration.py` (pin 0009 -> 0010 proof to its actual target) +- `backend/scripts/test_lane_catalogue.py` (source inventory and measured static-contract scheduling) +- `backend/scripts/behavior_ownership.py` (exact three-file additive registration) +- `backend/tests/test_behavior_ownership.py` (closed registration and neighbor rejection) +- `backend/tests/test_ci_lane_catalogue.py` (exact inventory and static-contract lane membership) +- `.ci/behavior-ownership/partition.v1.json` (three additive TASK module registrations and digest) + +Reuse `tests/post_submit_materialization_helpers.py`, the real live executor and +existing guide/policy fixtures without changing their behavior. In the new local +`support.py`, replace the helper's default request before reservation using +`make_post_submit_request`: retain its exact project/task/assignment/Submission, +content/binding and verified bytes, and supply archive-backed +`PostSubmitEvidenceEntry` values plus matching required-evidence policy inputs +derived from the locked effective policy. Reserve and execute that canonically +rehashed request; require `allow_review` explicitly in the valid control. The +helper's empty evidence/default policy inputs are not a success fixture. No +false-policy fixture extension is allowed; current activation forbids that graph. + +### Allowed documentation and navigation files + +- This record; `.commitrail/INDEX.md`. +- `.commitrail/initiatives/WS-ARCH-001/OVERVIEW.md`, `planning/PLAN.md`, + `planning/CHUNK_MAP.md`, `planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md`. +- `.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md`, `planning/PLAN.md`, `planning/CHUNK_MAP.md`. +- `.commitrail/initiatives/WS-POL-003/OVERVIEW.md`, `planning/PLAN.md`, `planning/CHUNK_MAP.md`. +- `.commitrail/initiatives/WS-ART-001/OVERVIEW.md`. +- `.commitrail/initiatives/WS-CON-001/OVERVIEW.md`. +- `.commitrail/initiatives/WS-AUTH-003/OVERVIEW.md`. +- `.commitrail/initiatives/WS-REV-001/OVERVIEW.md`. +- `README.md`, `docs/roadmap_status.md`, `docs/architecture_data_model.md`, + `docs/spec_chunk_4_task_queue_assignment.md`, `docs/spec_review_lifecycle.md`, + `docs/spec_contribution_compensation.md`, `docs/spec_authorization_service.md`, + `docs/engineering/authorization_activation_custody.md`. +- Ignored local `sheets/workstream_roadmap.xlsx` and `sheets/workstream_roadmap.csv` + only if already present; preserve one `WorkStream RoadMap` sheet. + +### Prohibited + +No AUTH registration, public route, writer, reader, application composition, +handler, current pointer, TASK transition, REV/CON record, output-file authority, +controller, data deletion, compatibility code or second manifest table. The +accepted-effects port is type-only, without an adapter/default/no-op implementation. + +## Design and decisions + +### One route-neutral source + +Create one `task_post_submit_routing_manifests` table. It is source evidence, +not current routing, human admission or acceptance. No deployable product path +can populate or consume it in this step. Tests insert real source facts directly +only to prove storage. Later publication must harden this SAME table with +mandatory exact routing/other required receipt custody before installing a writer +or reader. That migration must refuse any retained pre-authority rows rather +than backfill, mutate or delete them. No pending state or nullable future receipt. + +### Exact field ownership + +Every persisted column below is non-null. `id` uses UUIDv7; creation time is +PostgreSQL insertion time, unconditionally stamped by the insert guard even +when the caller supplies a past, future or null timestamp. All record +keys/references are native UUID. ORM references preserve each existing owner's +string or Python UUID representation; the detached API uses strict Python UUIDs. + +| Persisted fields | Canonical equality | +|---|---| +| `id`, `created_at` | TASK source identity and database timestamp | +| `project_id`, `task_id`, `submission_id`, `submission_version` | Exact Submission joined to its TASK project | +| `assignment_id`, `contributor_id`, `contribution_policy_version_id` | Submission's exact assignment/submitter; Task, Assignment and Submission frozen policy IDs agree at insertion | +| `checker_run_id`, `evaluation_request_id`, `request_digest`, `evaluation_generation`, `result_id`, `result_digest` | Same-Submission immutable completed CheckerRun; run ID is the attempt ID, not a second identity | +| `completion_event_id`, `execute_evidence_id`, `finalize_evidence_id` | Exact distinct receipt IDs and completion event retained on that run; no audit search | +| `human_review_required` | Exact Submission-locked ReviewPolicy ID/generation/hash and its persisted boolean; no default | +| `replica_id`, `content_sha256`, `byte_count`, `semantic_manifest_sha256` | Run's validated `material_custody`, backed by existing `public.art_submission_material_matches`; never caller `Submission.package_hash` | + +Public `TaskPostSubmitManifestFacts` contains these same scalar fields plus the +following immutable join-only facts (not extra stored copies): + +- `predecessor_submission_id`, `predecessor_submission_version`: Submission's + exact same-task predecessor; both null for the initial Submission. +- `admission_id`, `binding_id`, `content_id`: immutable Submission ART anchors, + equal to validated run material. No private provider coordinates. +- `locked_policy: TaskPolicyLineage`: reuse TASK's existing strict frozen public + value in `api/transition_audit.py`, populated from Submission's locked guide, + source snapshot, effective/pre/post/review/revision identities and hashes; + its contribution-policy field equals the stored source policy ID. The existing + locked post-policy hash is the compiled policy's sole `policy_hash`, not another + computed plan hash. Review/revision generations and guide version remain exact. +- `routing_recommendation`: closed literal `allow_review`; this is CHECKERS' + success evidence, not permission to admit a human or accept the work. + +No packet, policy body, member results or arbitrary metadata is copied. There +is no runtime projection builder in this step. Future composition supplies these +same detached fields only after owner-qualified reads and live currentness checks. + +### Database custody + +Declare the single `id` primary key on its Alembic column, matching the existing +identifier inventory reader. Preserve byte-identical PostgreSQL DDL and the +existing schema fingerprint; do not add an inventory exception or parser path. + +Composite FKs anchor source project/task/Submission/version, exact assignment/ +contributor, CHECKERS ownership and frozen contribution-policy project. Other +retained record refs use restrictive FKs. A schema-qualified insert guard validates +all equality above using `IS DISTINCT FROM` or explicitly coalesced predicates; +NULL must deny, never bypass a three-valued condition. Require completed +`allow_review`, non-null material, exact phase receipts and completion event. +Reuse the canonical ART material predicate rather than another material parser. + +The locked guide must carry its retained activation operation/receipt and may +be active or superseded. Never consult the project's latest guide/policy. Existing +PROJECTS custody guards protect that immutable activation; draft/unactivated +sources deny. Match the exact locked ReviewPolicy tuple, not an arbitrary sibling +policy with the same boolean. Do not lock foreign TASK/ART/PROJECTS rows or query +private runtime owners; this is a database constraint over immutable source refs. + +Uniqueness: `(submission_id, checker_run_id, result_digest)`. Updates, deletes +and truncation deny. Retained source validity does not claim currentness; a later +checker generation does not rewrite or invalidate this historical row. The later +routing transaction must acquire TASK Submission/current pointer then CHECKERS +currentness through the owner port. No currentness pointer or new lock protocol +is implemented here. All SQL protected refs are schema-qualified, with safe +function search paths and `pg_temp` last. + +### Shared accepted-effects contract + +Put the source-neutral contract in `tasks/api/accepted_effects.py`, so neither +human acceptance nor routing owns the shared TASK participant. +`TaskAcceptedEffectsRequest` is strict/frozen and contains `project_id`, `task_id`, +`assignment_id`, `submission_id`, positive `submission_version`, `contributor_id`, +`contribution_policy_version_id`, `content_id`, `content_sha256`, +`final_acceptance_id`, and `expected_task_status` limited to `evaluation_pending` +or `review_pending`. IDs are UUIDs and hashes use the existing SHA-256 syntax. +`TaskAcceptedEffectsResult` retains that request identity, reports only task +`accepted` and assignment `completed`; it does not reuse Assignment.accepted_at, +which currently records claim acceptance, as a completion timestamp. + +`TaskAcceptedEffectsPort.apply_accepted_effects(request)` returns that result or +`TaskAcceptedEffectsUnavailable`. Its future participant requires the caller's +one root transaction, validates exact state/lineage, flushes only, and never +commits, authorizes, creates REV/CON facts or calls back into routing. The shared +REV acceptance command owns composition and authorization. No implementation or +claim of terminal state support is included in 04E1A. + +## Acceptance criteria + +Named tests below cover independent labeled cases. Related SQL rejection cases +reuse a genuine parent graph with separate rolled-back transactions, avoiding a +full project setup per scalar without losing valid controls or failure attribution. + +| Exact future test | Boundary and discriminating proof | +|---|---| +| `test_source_matches_real_completed_run` | Real `material_fixture` + live execution + stored true-policy source; compare every source scalar and join-only public fact with its canonical parent | +| `test_source_creation_time_is_database_owned` | Explicit past/future/null timestamps are overwritten and bounded by database-clock samples; an omitted timestamp follows the same rule | +| `test_source_rejects_null_scalar` | Each required caller-supplied column independently NULL; non-null constraint denies while otherwise valid row rolls back. The generated timestamp has separate overwrite proof | +| `test_source_rejects_scalar_substitution` | Separate well-shaped request ID/digest/generation/result ID/digest, content hash/bytes/semantic hash, replica and review-boolean substitutions reach the named semantic guard | +| `test_source_rejects_foreign_lineage` | Two real stored graphs: independent project/task/Submission/version/assignment/contributor/contribution-policy swaps reject, including coherent same-project sibling ownership | +| `test_source_rejects_sibling_completion_event` | Real same-project sibling completion event, all other facts valid, rejects at source guard | +| `test_source_rejects_phase_receipt` | Separate execute substitution, finalize substitution and execute/finalize swap using existing real stored allow IDs; no invented/missing event | +| `test_source_rejects_ineligible_checker_source` | Real queued/running/infrastructure-failed or blocking completed evidence cannot become successful source; select a registered failing-check fixture for the blocking case | +| `test_source_rejects_unactivated_guide` | Deliberately inconsistent storage fixture isolates source activation guard, not a claim of a valid false-policy product graph; restoring real activation permits the same source | +| `test_source_retains_historical_guide_and_generation` | Real successor activation/generation leaves original source IDs and policy tuple intact; no currentness claim or effects | +| `test_source_is_immutable` | Each direct-SQL mutation denied with exact row preserved | +| `test_source_uniqueness_and_caller_rollback` | Duplicate exact source rejected; failed enclosing transaction leaves source/effect counts unchanged and valid insertion remains possible | +| `test_source_contract_is_strict_and_detached` | Strict UUID/hash/version/boolean, unknown/private fields, nested lineage mismatch, predecessor shape; exact frozen value | +| `test_false_source_value_is_transport_only` | False scalar transports without coercion; no claim of activated false storage/routing. Real false proof remains 04E2/04E3 after activation becomes reachable | +| `test_accepted_effects_contract_is_source_neutral` | Exact input/result identity and closed prestates/poststates; imports no REV or routing API | +| `test_source_foundation_has_no_runtime_entry` | No registration, route, composition, reader/writer, current pointer or effects implementation; proposed AUTH identifiers remain absent | +| `test_upgrade_preserves_existing_sources_without_publishing` | Actual 0010 -> 0011 upgrade retains prior Submission/checker/AUTH bytes and adds an empty source table; no routing/review/acceptance effects | + +Guard-removal probes: remove only source scalar equality, phase-receipt equality, +historical activation check, database timestamp stamping or immutable trigger in an isolated test database; +its corresponding named regression must fail at the intended assertion. Keep +valid controls enabled and exclude fixture/setup errors from proof. FK/NOT NULL +proof names their own boundary; it must not be mislabeled as semantic-trigger proof. + +## Complete-suite scheduling + +Place these static contracts once in the existing `schema_contracts` lane: + +- `tests/test_artifact_architecture.py` +- `tests/architecture/test_module_boundaries.py` +- `tests/architecture/test_authorization_boundary.py` +- `tests/architecture/test_test_structure_boundary.py` +- `tests/test_identifier_inventory.py` +- `tests/test_record_id_collection.py` +- `tests/test_ci_lane_catalogue.py` +- `tests/test_ci_test_lanes.py` +- `tests/test_test_lane_evidence.py` +- `tests/test_merge_test_lane_evidence.py` + +These cover repository boundaries, schema identifiers and CI collection/evidence +custody. Reuse the lane's existing coverage, PostgreSQL and MinIO execution +custody. Do not add a lane, change the 1,200-second execution limit, alter node +hashing, change services, weaken aggregation, or remove/skip any test. + +With the first two modules included, the hosted schema lane completed 169 tests +in 343.189 seconds. Coverage-enabled profiles of the remaining boundary pair +completed 84 tests in 87.44 seconds. The identifier/collection/evidence profile +completed 66 tests in 42.48 seconds, exposing the identifier inventory mismatch +repaired by the equivalent column-level primary-key declaration. The existing +catalogue/runner/evidence suite completed 113 tests in 27.16 seconds. Summing +these overlapping measurements conservatively estimates less than 510 seconds +for the schema lane, leaving more than 690 seconds below the unchanged limit. +This supports placement, not a guarantee of final hosted runtime. + +The original artifact/module-boundary profiles were retained in tool-session +output, not raw log files. The subsequent profiles and canonical collection +comparisons have saved local logs; final hosted artifacts remain authoritative +for complete execution. + +Extend `test_measured_hotspots_have_explicit_semantic_owners` to require the +exact static module set in schema and neither shared partition. Retain +`test_committed_lanes_cover_recursive_inventory_exactly_once`. Compare canonical +collection before and after the additional eight-module reassignment: all 8,047 +nodes and execution kinds must remain identical, exactly 207 nodes move to +schema, and every other node keeps its lane. Shared partition counts may vary +with head-seeded parameter IDs; compare assignments using one collection head. +The resulting schema lane has 376 nodes. Final acceptance requires the complete +nine-lane hosted run with no skips or deselections. + +## Risk and review routing + +Required focused plan/candidate tracks: architecture/reuse, security, QA/test +delta, docs/product operations and CI integrity. Lead owns shared checks and +exact clean candidates. Human focus: one durable source identity without +premature routing authority, exact receipt/material/locked-policy custody and +honest false-policy limits. + +## Evidence + +Use the existing isolated runner with locally configured test PostgreSQL/MinIO; +never commit credentials. From `backend/`: + +```sh +.venv/bin/python scripts/run_isolated_tests.py --metadata-json /tmp/arch04e1a-tests.json --timeout-seconds 1200 -- .venv/bin/python -m pytest tests/tasks/post_submit_routing tests/test_identifier_schema.py -q --tb=short +.venv/bin/ruff check app/modules/tasks/api/post_submit_routing.py app/modules/tasks/api/accepted_effects.py app/modules/tasks/post_submit_routing/models.py tests/tasks/post_submit_routing alembic/versions/0011_task_routing_source.py +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q -p pytest_asyncio.plugin -p pytest_cov.plugin --cov=app --cov-report= --durations=0 tests/test_artifact_architecture.py +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 .venv/bin/python -m pytest -q -p pytest_asyncio.plugin -p pytest_cov.plugin --cov=app --cov-report= --durations=0 tests/architecture/test_module_boundaries.py +.venv/bin/python -m pytest tests/test_ci_lane_catalogue.py tests/test_ci_test_lanes.py tests/test_test_lane_evidence.py tests/test_merge_test_lane_evidence.py -q +.venv/bin/python -m scripts.run_test_lanes --collect-only --metadata-dir /tmp/arch04e1a-governance-before --summary-json /tmp/arch04e1a-governance-before-summary.json +# Repeat after catalogue reassignment, before changing the collection's Git head: +.venv/bin/python -m scripts.run_test_lanes --collect-only --metadata-dir /tmp/arch04e1a-governance-after --summary-json /tmp/arch04e1a-governance-after-summary.json +.venv/bin/python -m scripts.module_boundaries validate --protected-base origin/main +.venv/bin/python -m scripts.behavior_ownership validate +.venv/bin/python -m scripts.test_structure_boundary validate --policy ../.ci/auth-boundaries/TEST_STRUCTURE_POLICY.md --ledger ../.ci/auth-boundaries/TEST_STRUCTURE_DEBT.json +``` + +Compare the two manifests from `backend/`: + +```sh +.venv/bin/python - <<'PY' +import json +from collections import Counter +from pathlib import Path +moved = { + "tests/architecture/test_authorization_boundary.py", + "tests/architecture/test_test_structure_boundary.py", + "tests/test_identifier_inventory.py", + "tests/test_record_id_collection.py", + "tests/test_ci_lane_catalogue.py", + "tests/test_ci_test_lanes.py", + "tests/test_test_lane_evidence.py", + "tests/test_merge_test_lane_evidence.py", +} +read = lambda side: json.loads(Path(f"/tmp/arch04e1a-governance-{side}/manifest.json").read_text())["nodes"] +before, after = read("before"), read("after") +assert len(before) == len(after) == 8047 +old, new = ({row["nodeid"]: row for row in rows} for rows in (before, after)) +assert len(old) == len(new) == 8047 and old.keys() == new.keys() +changes = Counter() +for node, row in old.items(): + expected = row | {"lane": "schema_contracts"} if row["module"] in moved else row + assert new[node] == expected + if new[node] != row: + changes[row["lane"]] += 1 +assert sum(changes.values()) == 207 +assert set(changes) == {"shared_foundations_a", "shared_foundations_b"} +assert sum(row["lane"] == "schema_contracts" for row in after) == 376 +PY +``` + +From repository root: + +```sh +.venv/bin/python scripts/check_commitrail_records.py --base-ref origin/main +python3 scripts/check_markdown_links.py +git diff --check +``` + +Run affected schema/head, catalogue and retained activation-denial tests through +the same isolated runner. Full hosted semantic lanes and aggregate reconcile +all collected nodes with zero skips/deselections; coverage is diagnostic. Run +stale-wording scans against changed current records. No new percentage gate. + +The metadata registry consumes the canonical `post_submit_routing/models.py` +module; no boundary exception or debt entry is added. The ownership validator +registers only the three new TASK modules and rejects adjacent unapproved files. + +## Plan-review reconciliation + +Architecture findings PLAN-001..005 and security findings SEC-04E1A-001..003 +require exact headings, files, fields, source-neutral effect types, null-safe +custody and independent named proof. This record incorporates those requirements. +The parent 04E contract must describe this source-only boundary and explicitly +assign complete receipt propagation to 04E1B/04E2. No reviewer result is asserted +here; exact freshness and execution evidence remain in the eventual PR. + +## Reconciliation + +Next: existing shared REV/CON source/acceptance and fence foundations before +false-handler composition, then 04E1B/04E2/04E3 and remediation 04F. Live human +queues or leases are not prerequisites of automated acceptance. Public intake +remains behind complete outcomes and remediation. Navigation distinguishes +this source schema from authoritative publication and deployed behavior. diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md index e45e0d5e1..66ee57e23 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/CHUNK_MAP.md @@ -4,7 +4,7 @@ Use the [current dependency contract](PLAN.md#current-dependency-contract). The [preserved map](../pre-cutover/CHUNK_MAP.md) retains the complete original work accounting. Foundations through 02H and CP04B are complete; none restart. AUTH-18 public manager activation/context and ARCH-03D hidden approved-guide intake are delivered. -ARCH-04B hidden exact post-submit input and ARCH-04B2 output custody are delivered. ARCH-04C hidden durable execution is delivered; ARCH-04D1 canonical material custody is delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next; +ARCH-04B hidden exact post-submit input and ARCH-04B2 output custody are delivered. ARCH-04C hidden durable execution, ARCH-04D1 canonical material custody, ARCH-04D2 exact input/execution/finalization authority and ARCH-04E1A source-only facts/types are delivered; public intake remains deferred to ARCH-02I. | Boundary | Owner outcome | Risk | Current dependency | @@ -32,7 +32,7 @@ public intake remains deferred to ARCH-02I. | [WS-ARCH-001-03C4](../WS-ARCH-001-03C4.md) | Exact-authorized public task queues | L1 | Complete; contributor, manager and operational projections with signed pagination | | [WS-ARCH-001-03C5](../WS-ARCH-001-03C5.md) | Exact-authorized Contributor and Manager detail and requirements | L1 | Complete; canonical projections, historical policy custody and atomic read evidence | | [WS-ARCH-001-03C6](../WS-ARCH-001-03C6.md) | Distinct exact-authorized locked-context reads | L1 | Complete; bounded Audit Authority history access delivered by 03C7 | -| [WS-ARCH-001-03C7](../WS-ARCH-001-03C7.md) | Exact-authorized bounded task history | L1 | Complete; AUTH-18 public guide activation, ARCH-03D hidden approved-guide intake, hidden exact post-submit materialization and ARCH-04B2 output custody delivered; ARCH-04C hidden execution delivered; ARCH-04D1 canonical custody delivered; ARCH-04D2 authority delivered; ARCH-04E1A next | +| [WS-ARCH-001-03C7](../WS-ARCH-001-03C7.md) | Exact-authorized bounded task history | L1 | Complete; AUTH-18 public guide activation, ARCH-03D hidden approved-guide intake, 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 | | [WS-ARCH-001-03C](chunks/WS-ARCH-001-03C-auth-task-readiness.md) | Exact task/assignment public activation and integrated readiness proof | L1 | Complete through 03C7 audit history, 03C4 queues, 03C5 detail/requirements and 03C6 locked-context reads; AUTH-18 public guide activation delivered; ARCH-03D hidden intake delivered; public intake cutover remains separate | | [WS-ARCH-001-04A](../WS-ARCH-001-04A.md) | CHECKER post-submit contract and registered evaluator conformance | L1 | Complete canonical catalogue, phase facts and structural conformance; consumed by delivered POL-04B and POL-07B | | [WS-ARCH-001-04B](../WS-ARCH-001-04B.md) | ART exact verified Submission materialization | L1 | Complete hidden verified input with async scoped reads; exact input authority delivered by 04D2 | @@ -40,8 +40,9 @@ public intake remains deferred to ARCH-02I. | [WS-ARCH-001-04C](chunks/WS-ARCH-001-04C-checker-current-result.md) | CHECKER hidden durable current output and supersession behavior | L1 | Complete hidden request/lease/result custody with exact empty-output support and atomic completion; exact service authority delivered by 04D2 | | [WS-ARCH-001-04D1](../WS-ARCH-001-04D1.md) | Canonical terminal ART material custody | L1 | Complete; valid retained history preserved; invalid upgrades refused | | [WS-ARCH-001-04D2](../WS-ARCH-001-04D2.md) | AUTH exact fixed-service post-submit activation (replaces XINT-06B) | L1 | Complete: exact input, execute and finalize authority; output write/bind remains unavailable | -| [WS-ARCH-001-04E](chunks/WS-ARCH-001-04E-canonical-allow-review.md) | TASK current routing: true to canonical `allow_review`, false/pass to shared acceptance | L1 | Source 04E1A -> hidden handlers 04E1B -> AUTH 04E2 -> live 04E3, plus 04D2/OUTBOX-02; false consumes shared REV/CON/fence proof and 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 and ARCH-04B2 output custody delivered; ARCH-04C hidden execution delivered; ARCH-04D1 canonical custody delivered; ARCH-04D2 authority delivered; ARCH-04E1A next, public cutover remains deferred | +| [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 | Delivered source 04E1A -> shared REV-04B/CON-03C/07 plus REV-12A/CON fence foundation -> hidden 04E1B -> AUTH 04E2 -> live 04E3, plus 04D2/OUTBOX-02; false 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` | CP09, 04E and 04F are coordination parents, not permission for multi-owner PRs. diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md b/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md index a06b451e5..f269d57dc 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md @@ -44,8 +44,8 @@ checker-remediation boundary before public Submission cutover. | [ARCH-03C4](../WS-ARCH-001-03C4.md) | ARCH-03C3 | Complete: exact-authorized contributor, manager and operational public queues | | [ARCH-03C5](../WS-ARCH-001-03C5.md) | ARCH-03C4 | Complete: exact-authorized Contributor/Manager detail and requirements | | [ARCH-03C6](../WS-ARCH-001-03C6.md) | ARCH-03C5 | Complete: distinct exact-authorized locked-context reads | -| [ARCH-03C7](../WS-ARCH-001-03C7.md) | ARCH-03C6 and hidden TASK owner contracts | Complete: bounded Audit Authority history access; public guide activation, ARCH-03D hidden intake, hidden post-submit materialization and ARCH-04B2 output custody delivered; ARCH-04C hidden execution delivered; ARCH-04D1 canonical custody delivered; ARCH-04D2 authority delivered; ARCH-04E1A next | -| [CP05A](../WS-ARCH-001-CP05A.md) | CP05, existing policy owners | Complete: public Finance policy administration and selector recovery; public manager activation, ARCH-03D hidden intake, hidden post-submit materialization and ARCH-04B2 output custody delivered; ARCH-04C hidden execution delivered; ARCH-04D1 canonical custody delivered; ARCH-04D2 authority delivered; ARCH-04E1A next | +| [ARCH-03C7](../WS-ARCH-001-03C7.md) | ARCH-03C6 and hidden TASK owner contracts | Complete: bounded Audit Authority history access; public guide activation, ARCH-03D hidden intake, hidden post-submit materialization, ARCH-04B2 output custody, ARCH-04C execution, ARCH-04D1/04D2 custody/authority and ARCH-04E1A source-only facts/types delivered | +| [CP05A](../WS-ARCH-001-CP05A.md) | CP05, existing policy owners | Complete: public Finance policy administration and selector recovery; public manager activation, ARCH-03D hidden intake, hidden post-submit materialization, ARCH-04B2 output custody, ARCH-04C execution, ARCH-04D1/04D2 custody/authority and ARCH-04E1A source-only facts/types delivered | | CP09 (later cleanup coordination) | All legacy consumers replaced, including CHECKER and public 02I path | Physical economic deletion; not on the allow_review critical path | | [ARCH-03D](../WS-ARCH-001-03D.md) | AUTH-18, ARCH-03A, CP08 and merged ART preparation | Complete: hidden durable intake uses exact historical TASK/PROJECTS ports; public cutover remains ARCH-02I | | ARCH-04B | ARCH-04A, POL-07, ARCH-03C, merged ARCH-02H | Complete: ART hidden verified Submission input; exact live authority is delivered by ARCH-04D2 | @@ -56,7 +56,7 @@ checker-remediation boundary before public Submission cutover. | AUTH-OUTBOX-01 | Merged shared outbox persistence and AUTH service/PREP foundations | Complete: planned dispatcher identity/action/matrix and unavailable typed authority contract | | CON-02B | AUTH-OUTBOX-01 | Complete: shared hidden dispatcher/claim fencing, typed handlers and recovery | | AUTH-OUTBOX-02 | CON-02B exact hidden manifest | Exact dispatcher mechanics only; no feature authority | -| ARCH-04E1A | ARCH-04C | TASK routing-manifest schema/public facts and accepted-effects port, no handlers or REV dependency | +| [ARCH-04E1A](../WS-ARCH-001-04E1A.md) | ARCH-04C/04D2 | Complete: route-neutral immutable TASK source schema/detached facts and source-neutral accepted-effects types; no runtime writer/reader, handlers, current pointer, routing authority, acceptance implementation or REV dependency | | ARCH-04E1B | ARCH-04E1A, CON-02B hidden contract; shared REV-04B + CON-03C/07 + REV-12A shared fence foundation for false | TASK hidden handlers; consume one shared acceptance operation on false/pass | | 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 | ARCH-04E1B; scoped XINT-003-08B controller activation for false | AUTH exact TASK routing authority | @@ -88,7 +88,7 @@ ARCH-03C1 completes exact reconciler authority and decision-bound receipts. ARCH-03C2 supplies originating-transaction-only per-assignment producer events and first handler registration with enforced prefork topology; it must never backfill or dispatch retained invalidation rows. Public TASK activation is complete through ARCH-03C7. AUTH-18 delivers public -manager guide activation/context; ARCH-03D hidden intake, hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 authority is delivered; ARCH-04E1A routing-source facts are next. Subsequent +manager guide activation/context; ARCH-03D hidden intake, hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden execution is delivered; ARCH-04D1 canonical custody, ARCH-04D2 authority and ARCH-04E1A source-only facts/types are delivered. Subsequent PR-sized contracts name exact files, public types, current migration head and runnable proof before implementation; they refine this design, not create a new permission requirement. @@ -98,8 +98,10 @@ new permission requirement. Delivered supporting foundations are [ARCH-04B2](../WS-ARCH-001-04B2.md), [AUTH-OUTBOX-01/02](../../WS-AUTH-001/planning/PLAN.md#ws-auth-001-outbox-01--unavailable-dispatcher-contract), and [CON-02B](../../WS-CON-001/OVERVIEW.md#con-02b-current-dispatcher-contract). -The remaining routing sequence is -[ARCH-04E1A/04E1B/04E2/04E3](chunks/WS-ARCH-001-04E-canonical-allow-review.md#current-bounded-sequence). +The remaining routing sequence starts with shared REV-04B/CON-03C/07 and the +existing REV-12A/CON fence foundation before shared acceptance composition, +then [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. Each numbered section is a current bounded design, expanded into its own change record on implementation; the parent is not a multi-owner implementation PR. diff --git a/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md b/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md index d2151d802..f27366d24 100644 --- a/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md +++ b/.commitrail/initiatives/WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md @@ -24,9 +24,10 @@ false uses the same shared FinalAcceptance/CON operation as human accept, not enqueue human review or treat `allow_review` as acceptance authority. The current TASK children do not implement REV/CON internals: consume the [canonical shared participants and authority contract](../../../../../docs/spec_review_lifecycle.md#finalacceptance). -First deliver 04E1A's TASK manifest schema/public facts and narrow accepted-effects -port after 04C, without REV dependency or handlers. REV-04B can then reference -that schema. Shared REV-04B/CON-03C/07 and the early existing REV-12A/CON fence +04E1A's TASK manifest schema/detached facts and narrow accepted-effects Protocol +are delivered after 04C, without REV dependency, runtime participant or handlers. +REV-04B can now reference that schema. Shared REV-04B/CON-03C/07 and the early +existing REV-12A/CON fence foundation are hard dependencies of false handler composition, not of this early schema or true admission. This breaks the source-FK dependency cycle. False guide activation stays unavailable until that path is proven. Avoid @@ -38,9 +39,16 @@ false. Before its acceptance participant is implemented and activated, false has no live success route; an unexpected false attempt fails closed without creating human admission, acceptance or contribution effects. -1. **ARCH-04E1A — TASK source foundation.** After 04C, publish the routing - manifest persistence/public facts and narrow accepted-effects port described - above. No handler, REV source FK or acceptance operation dependency. +1. **[ARCH-04E1A — TASK source foundation](../../WS-ARCH-001-04E1A.md) — Complete.** + One route-neutral immutable source schema and detached internal facts, plus + source-neutral accepted-effects types, follow 04C/04D2. There is no runtime + writer/reader/composition, current pointer, routing allow or accepted effects. + Source evidence alone never admits a human or authorizes acceptance. + The current true-policy graph supplies real SQL proof; false is value-shape + transport only because guide activation still rejects it. Before publication, + 04E1B/04E2 must harden this same table with mandatory exact route and + owner-receipt custody and refuse every retained pre-authority source row, + without backfill, mutation, deletion or a parallel table. 2. **ARCH-04E1B — hidden TASK handlers.** After 04E1A and CON-02B's handler/claim contract (plus shared acceptance foundations for false), TASK implements unavailable request/event @@ -88,6 +96,16 @@ Each child uses a separate implementation record/PR at start with exact files and relevant reviewers. The graph does not require live routing to authorize its own hidden handler. No extra planning-only approval PR is implied. +The remaining sections specify the final publication boundary after 04E1A; +its source-only schema does not claim these effects or receipt guarantees. +04E1B/04E2 must propagate and retain exact decision IDs from `submission.create`, +ART admission/binding and post-submit materialization owners before publication. +Current contracts discard those IDs; reverse audit-log search or borrowing a +checker receipt is prohibited. Execute/finalize IDs already have durable custody. +For the current zero-output catalogue, the output-binding tuple is empty and no +output write/bind allows can be required. A future registered output producer +must implement and authorize those operations before using their evidence. + TASKS owns one evaluation-request/dispatch record and a current routing projection; the shared outbox owns its event rows, uniqueness and delivery. CHECKERS owns the result and recommendation. The success manifest references the exact @@ -112,11 +130,10 @@ dispatcher has mechanics-only authority; event payloads confer no permissions. No private TASK outbox consumer, dynamic handler loading or second worker registry belongs here. -Canonical Submission composition must stop reaching the old direct -`enqueue_pre_review_gate` scheduling path and CHECKERS -`_apply_pre_review_gate_result` TASK mutations. The replacement is one durable -event/handler route, not a second path alongside those calls. Public legacy -route removal remains 02I, but canonical-path reachability is cut over here. +The obsolete direct checker scheduling and TASK-mutation paths have already +been removed. Use the existing shared durable event/handler route when wiring +canonical Submission composition; do not recreate those removed paths. Public +intake remains the later 02I boundary. ## Distinct idempotency and uniqueness custody @@ -218,8 +235,9 @@ contributor-readable checker-remediation lineage for final needs-revision checker results without creating Review, ReviewFinding, or RevisionContextPreparation records. -Before implementation, replace this skeleton with a current-main contract that -enumerates exact files, commands, migration head and reviewers. +This is a coordination contract. Each remaining child record, beginning with +04E1B after the shared acceptance foundations, supplies its current-main exact +files, commands, migration head and reviewers before implementation. ## Merge state diff --git a/.commitrail/initiatives/WS-ART-001/OVERVIEW.md b/.commitrail/initiatives/WS-ART-001/OVERVIEW.md index 53bbc0eb4..982ded3d4 100644 --- a/.commitrail/initiatives/WS-ART-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-ART-001/OVERVIEW.md @@ -12,9 +12,14 @@ and the [capability ledger](../../../docs/roadmap_status.md). [ARCH-04B2 checker-output custody](../WS-ARCH-001/WS-ARCH-001-04B2.md). The CHECKERS zero-slot reservation reader is implemented; checker-output write/bind authority remains deny-only. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered - [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and finalize authority. - Output-file authority remains unavailable for the zero-output catalogue. +- Delivered dependent boundary: ARCH-04E1A route-neutral source facts retain the + exact canonical material lineage supplied after + [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md), without a runtime reader, + writer or published route. Output-file authority remains unavailable for the + zero-output catalogue. +- Next usable boundary: ARCH-04F checker remediation through existing ART ports, + after the shared acceptance and 04E routing sequence; public intake remains a + later cutover. - Governing sources: artifact specifications, `ArtifactStore`, `ArtifactScratchManager`, code, migrations, and artifact tests. - Preserve: SHA-256/byte-count identity, reread verification, isolation, @@ -37,9 +42,9 @@ ART retains the merged default-plus-project intake compiler/executor; POL-07 is a facade, not a replacement or second precheck run. New unified generations must prove exact approved lineage at preparation, consumption and binding. -1. ARCH-04B hidden exact Submission materialization and ARCH-04B2 hidden output - custody are delivered. ARCH-04C hidden durable execution is delivered. Continue with - fixed-service authority. +1. ARCH-04B hidden exact Submission materialization, ARCH-04B2 hidden output + custody, ARCH-04C hidden durable execution, ARCH-04D2 fixed-service authority + and ARCH-04E1A source-only material lineage are delivered. 2. ARCH-04F owns checker-remediation resubmission using existing ART ports; later reviewer-requested revision remains a separate REV boundary. Add those dependencies before public Submission cutover. diff --git a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md index c04af7f72..b4948a18a 100644 --- a/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-AUTH-001/OVERVIEW.md @@ -23,9 +23,12 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), - Public post-policy composition: POL-06B delivered using existing AUTH-12G. - Completed activation boundary: AUTH-12H exact-project manager authority for CP07 complete-guide activation/binding, with live-authority replay. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered - [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and finalize authority. - Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers +- Delivered dependent boundary: ARCH-04E1A immutable route-neutral source facts + and type-only accepted-effects contracts follow + [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and + finalize authority. They add no routing action, handler or deployable effects + participant. Output-file authority remains unavailable for the zero-output + 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 ContributionPolicy administration and recovery of a draft selector, a published @@ -42,6 +45,9 @@ Historical pre-cutover work records: [`STATUS.md`](pre-cutover/STATUS.md), and minimal writers and ARCH-03A internal guide context. POL-07B internal phase composition is delivered. The dispatcher registers only exact assignment invalidation. Future checker routing still requires its separate exact authority and handler. +- Next usable boundary: shared REV-04B/CON-03C/07 and REV-12A/CON fence + foundations precede shared acceptance composition and hidden ARCH-04E1B; + ARCH-04E2 then owns exact routing activation before 04E3 live composition. - Governing source: `docs/spec_authorization_service.md`, authorization code, migrations, and tests. - Preserve: Flow token verification only, no Workstream login/session system, @@ -78,5 +84,5 @@ manager proposal review, pre-submit approval and manual correction dispatch. AUTH-OUTBOX-01/02 bracket hidden CON-02B dispatch; ARCH-04E2 activates only the proven TASK routing handler before ARCH-04E3 live composition. TASK queue/read exposure is complete through ARCH-03C7. AUTH-18 public - manager guide activation/context is delivered; ARCH-03D hidden intake, hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden execution is delivered; ARCH-04D1 canonical material custody is delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next. + manager guide activation/context is delivered; ARCH-03D hidden intake, hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C hidden execution is delivered; ARCH-04D1 canonical material custody is delivered; ARCH-04D2 exact input/execution/finalization authority and ARCH-04E1A source-only facts/types are delivered. Routing authority remains ARCH-04E2 after hidden 04E1B proof and the shared acceptance foundations. Remaining work must use its exact owner, not the superseded broad designs. diff --git a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md index c23036bb9..774f457ec 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/CHUNK_MAP.md @@ -13,11 +13,11 @@ work and historical proposals. | [AUTH-12H](../WS-AUTH-001-12H.md) | Complete: exact manager authority for CP07; AUTH-18 exposes manager activation and selections publicly | | CP05 | Complete: exact five policy actions; public Finance exposure delivered by CP05A | | ARCH-03C | Complete through 03C7: task/assignment authority, public queues, projections and audit reads; replaces broad AUTH-13 | -| [AUTH-18](../WS-AUTH-001-18.md) | Complete: public manager activation/context over CP07 and AUTH-12H; ARCH-03D hidden intake, ARCH-04B input 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 is delivered; ARCH-04E1A routing-source facts are next | +| [AUTH-18](../WS-AUTH-001-18.md) | Complete: public manager activation/context over CP07 and AUTH-12H; ARCH-03D hidden intake, ARCH-04B input, ARCH-04B2 output custody, ARCH-04C hidden execution, ARCH-04D1 canonical custody, ARCH-04D2 exact input/execution/finalization authority and ARCH-04E1A source-only facts/types are delivered | | [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) | Exact TASK routing handler authority after hidden 04E1, before live 04E3 | +| [ARCH-04E2](../../WS-ARCH-001/planning/chunks/WS-ARCH-001-04E-canonical-allow-review.md#current-bounded-sequence) | Exact TASK routing handler authority after delivered source-only 04E1A, shared acceptance foundations and hidden 04E1B proof, before live 04E3 | 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 28d9082cf..7e79be84c 100644 --- a/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md +++ b/.commitrail/initiatives/WS-AUTH-001/planning/PLAN.md @@ -25,7 +25,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 is delivered; ARCH-04E1A routing-source facts are next. + 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 and ARCH-04E1A source-only facts/types are delivered. Shared REV/CON/fence foundations and hidden 04E1B proof precede 04E2 routing authority. - 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 @@ -53,7 +53,7 @@ and public JSON-packet Submission creation. Admission-backed creation stays hidd ARCH-03B/03C reuse these exact actions and command owners; public TASK access and authority are delivered through 03C7, with invalidation wiring in 03C2. CP08 delivered ContributionPolicyVersion lineage. ARCH-03D completes hidden approved-guide intake. ARCH-04B hidden exact post-submit -input 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 is delivered; ARCH-04E1A routing-source facts are next; public intake remains deferred to ARCH-02I. Do not restore eligibility +input 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 and ARCH-04E1A source-only facts/types are delivered; public intake remains deferred to ARCH-02I. Do not restore eligibility or register replacement aliases. ## WS-AUTH-001-OUTBOX-01 — unavailable dispatcher contract diff --git a/.commitrail/initiatives/WS-AUTH-003/OVERVIEW.md b/.commitrail/initiatives/WS-AUTH-003/OVERVIEW.md index fb80d9a1a..97c39ca90 100644 --- a/.commitrail/initiatives/WS-AUTH-003/OVERVIEW.md +++ b/.commitrail/initiatives/WS-AUTH-003/OVERVIEW.md @@ -8,9 +8,14 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), - Completed boundary: recovery foundation and [TASK/checker authorization cleanup](WS-AUTH-003-TASKCHECKER.md). - Intent: route public authorization capability through `authorization.api` and remove cross-module repository/model coupling. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered ARCH-04D2 exact service authority, ARCH-04C hidden execution and ARCH-04B - hidden input, [ARCH-04B2 output custody](../WS-ARCH-001/WS-ARCH-001-04B2.md), ARCH-03D hidden intake and - [AUTH-18 public manager activation](../WS-AUTH-001/WS-AUTH-001-18.md). +- Delivered dependent boundary: ARCH-04E1A route-neutral source facts and + source-neutral accepted-effects types follow ARCH-04D2 exact service authority, + ARCH-04C hidden execution, ARCH-04B hidden input, + [ARCH-04B2 output custody](../WS-ARCH-001/WS-ARCH-001-04B2.md), ARCH-03D hidden + intake and [AUTH-18 public manager activation](../WS-AUTH-001/WS-AUTH-001-18.md). + They install no routing action, handler or runtime composition. +- Next usable boundary: continue canonical boundary recovery through the shared + REV/CON/fence foundations and later ARCH-04E1B/04E2/04E3 routing sequence. Submission/checker history uses canonical authority; the alternate gate lifecycle is removed. Continue shrinking the canonical import ledger as implementation reaches each remaining consumer. diff --git a/.commitrail/initiatives/WS-CON-001/OVERVIEW.md b/.commitrail/initiatives/WS-CON-001/OVERVIEW.md index 07a3fd0f7..23023f406 100644 --- a/.commitrail/initiatives/WS-CON-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-CON-001/OVERVIEW.md @@ -11,9 +11,12 @@ and the [capability ledger](../../../docs/roadmap_status.md). immutable ContributionRecords and optional project-policy-driven compensation awards without coupling lifecycle truth to an economic provider. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered - [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and finalize authority. - Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers +- 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 + 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 ContributionPolicy administration and recovery of a draft selector, a published @@ -29,6 +32,9 @@ and the [capability ledger](../../../docs/roadmap_status.md). and minimal writers, ARCH-03A internal guide context, [CP07 activation/binding](../WS-ARCH-001/WS-ARCH-001-CP07.md) and [AUTH-12H live authority](../WS-AUTH-001/WS-AUTH-001-12H.md), before task readiness. +- Next usable boundary: REV-04B source/FinalAcceptance persistence, then + CON-03C/07 and the existing shared REV-12A/CON fence foundation before one + shared acceptance operation serves both the 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 diff --git a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md index d76afa709..67e2ed554 100644 --- a/.commitrail/initiatives/WS-POL-003/OVERVIEW.md +++ b/.commitrail/initiatives/WS-POL-003/OVERVIEW.md @@ -28,9 +28,12 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), and routing remain unavailable. - Intent: compile one locked guide and its policies into authoritative, versioned project behavior without circular subsystem authority. -- Next usable boundary: ARCH-04E1A routing-source facts after delivered - [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and finalize authority. - Output-file authority remains unavailable for the zero-output catalogue. [AUTH-18](../WS-AUTH-001/WS-AUTH-001-18.md) delivers +- Delivered dependent boundary: ARCH-04E1A route-neutral source facts and + type-only accepted-effects contracts follow + [ARCH-04D2](../WS-ARCH-001/WS-ARCH-001-04D2.md) exact input, execute and + finalize authority. They add no route, acceptance path or activation support. + 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 ContributionPolicy administration and recovery of a draft selector, a published @@ -52,7 +55,11 @@ Exact pre-cutover work record: [`STATUS.md`](pre-cutover/STATUS.md), Live setup consumes the [consolidated catalogue](../WS-ARCH-001/WS-ARCH-001-04A.md) and completed AUTH-12B2. Earlier development schemas require no backward-compatibility paths. - The existing ReviewPolicy boolean is delivered; automated acceptance remains unavailable. + The existing ReviewPolicy boolean is delivered; false has scalar DTO proof + only and automated acceptance remains unavailable. +- Next usable boundary: shared REV-04B/CON-03C/07 and REV-12A/CON fence + foundations, then shared acceptance composition and ARCH-04E1B/04E2/04E3. + ARCH-04F remediation still precedes enabling false. - Governing sources: project-guide specifications, authorization and contribution-policy specifications, code, migrations, and tests. - Preserve: trusted policy compilation, explicit ownership, atomic persistence, diff --git a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md index 70b22af18..0e6d16efc 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md +++ b/.commitrail/initiatives/WS-POL-003/planning/CHUNK_MAP.md @@ -29,7 +29,7 @@ post-task requirement. Historical split parents 05/06 are not extra PRs. | `WS-AUTH-001-12G` | Activate exact fixed-service projection plus PM approval/correction authority for the hidden 06A manifest. | 06A | | `WS-POL-003-06B` | Live deterministic post-submit projection/approval cutover with zero additional inference. | 06A + AUTH-12G | | `WS-POL-003-07` | One typed facade over existing ART pre and CHECKER post contracts; no post-result persistence. | 06B + ARCH-04A registered capability proof + merged ART-04B1-04B3 | -| `WS-AUTH-001-12H` / [AUTH-18](../../WS-AUTH-001/WS-AUTH-001-18.md) | Complete: exact guide activation authority and public manager activation/context over the approved unified chain. CP08 lineage and ARCH-03 task authority/projections are delivered; ARCH-03D hidden intake, ARCH-04B input and ARCH-04B2 output custody are delivered; ARCH-04C hidden execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 authority is delivered; ARCH-04E1A routing-source facts are next and public intake remains ARCH-02I. CP09 remains later. | POL-07 + corrected AUTH-12B2 + CP05 active ContributionPolicy behavior + CP06 validation + CP07 ProjectGuide binding | +| `WS-AUTH-001-12H` / [AUTH-18](../../WS-AUTH-001/WS-AUTH-001-18.md) | Complete: exact guide activation authority and public manager activation/context over the approved unified chain. CP08 lineage, ARCH-03 task authority/projections, ARCH-03D hidden intake, ARCH-04B/04B2 custody, ARCH-04C execution, ARCH-04D1/04D2 custody and authority, and ARCH-04E1A source-only facts/types are delivered. Public intake remains ARCH-02I; CP09 remains later. | POL-07 + corrected AUTH-12B2 + CP05 active ContributionPolicy behavior + CP06 validation + CP07 ProjectGuide binding | | `WS-POL-003-08` | Supplementary visibility; separate remaining cleanup is parked and handled within each affected module. Essential review/correction belongs to 05A/05B and 06A/06B. | Planned after 07 + AUTH-12H + canonical WS-ARCH-001-04E manifest; any separately authorized retained-data change requires CP09's inventory, mapping and readability/recoverability proof for affected facts; no cleanup prerequisite for 04B | The 04E manifest proves canonical routing, not legacy-history preservation. @@ -57,5 +57,6 @@ ARCH-04B delivers hidden exact ART post-submit input with exact ARCH-04D2 servic authority. ARCH-04B2 hidden output custody and ARCH-04C hidden durable CHECKER execution/results with exact empty-output support are delivered. WS-ARCH-001-04D2 delivers exact input/execute/finalize authority; ARCH-04E1A -routing-source facts are next. Output-file authority remains unavailable. Historical +source-only facts/types are delivered without a route or acceptance path. +Output-file authority remains unavailable. Historical XINT-06B and AUTH-14 contracts are superseded/non-executable. diff --git a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md index d71773a1f..de75e7268 100644 --- a/.commitrail/initiatives/WS-POL-003/planning/PLAN.md +++ b/.commitrail/initiatives/WS-POL-003/planning/PLAN.md @@ -57,7 +57,7 @@ approval authority. AUTH-12H supplies exact guide activation authority; AUTH-18 exposes manager activation and selection discovery publicly. ARCH-03D completes hidden approved-guide intake integration. ARCH-04B hidden exact post-submit materialization and ARCH-04B2 output custody are delivered; ARCH-04C -hidden execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 exact input/execution/finalization authority is delivered; ARCH-04E1A routing-source facts are next; public intake remains deferred to ARCH-02I. +hidden execution is delivered; ARCH-04D1 canonical custody is delivered; ARCH-04D2 exact input/execution/finalization authority and ARCH-04E1A source-only facts/types are delivered; public intake remains deferred to ARCH-02I. The sequence through POL-04B is complete; POL-04B1 supplies automatic request custody for the delivered live cutover: diff --git a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md index e9b888e1b..3333a8ffe 100644 --- a/.commitrail/initiatives/WS-REV-001/OVERVIEW.md +++ b/.commitrail/initiatives/WS-REV-001/OVERVIEW.md @@ -8,9 +8,14 @@ of review/revision behavior. The downstream owner contracts remain separate. - Completed boundary: queue admission and ReviewLease persistence through 03A2. - Intent: ensure the authorized reviewer evaluates the exact verified artifact under the locked policy version and produces attributable outcomes. -- Next usable boundary: prepare shared acceptance/source and existing fence - foundations under the canonical order; human hidden behavior may continue - independently behind exact AUTH, ART and CON prerequisites. +- Delivered upstream boundary: ARCH-04E1A provides immutable route-neutral TASK + source storage, detached source facts and the type-only accepted-effects + Protocol. It provides no writer, reader, handler, routing authority, current + pointer or acceptance implementation. +- Next usable boundary: REV-04B shared source/FinalAcceptance persistence, + CON-03C/07 and the existing REV-12A/CON fence foundation under the canonical + order; 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, and tests. @@ -56,8 +61,9 @@ proof. No adjudication setting or behavior is included. packet-membership contract. 2. Continue hidden claim/revision behavior against canonical `allow_review`, copying the Submission policy version without a current-policy lookup. -3. After TASK's early 04E1A source schema/public port, implement the REV-04B - shared source/FinalAcceptance persistence foundation, +3. TASK's early 04E1A source schema/detached facts and source-neutral accepted- + effects types are delivered. Implement the REV-04B shared + source/FinalAcceptance persistence foundation next, then CON-03C/CON-07 persistence and submitter participation. This foundation can precede human runtime: ARCH-04E uses it for false/pass acceptance without live queues, leases or decisions. Pull the existing REV-12A/CON shared diff --git a/README.md b/README.md index cb0403d6e..1d322845a 100644 --- a/README.md +++ b/README.md @@ -162,8 +162,10 @@ Implemented foundations on `main` include external Flow-token verification, canonical local actors and authorization, project guides and task records, submission packets, immutable artifact storage, pre-submit intake checks, and authorized retained submission/checker history. ARCH-04C implements hidden -durable post-submit execution and unfinished-attempt recovery. ARCH-04D2 supplies exact service authority; -automatic dispatch and routing remain unavailable. Project-guide ingestion stores original documents, +durable post-submit execution and unfinished-attempt recovery. ARCH-04D2 supplies +exact service authority. ARCH-04E1A adds immutable route-neutral TASK source +storage, detached source facts and a type-only accepted-effects Protocol; +automatic dispatch, routing and acceptance remain unavailable. Project-guide ingestion stores original documents, records immutable metadata and provides authorized exact-file reads to the unified setup agent. Guide metadata in PostgreSQL also holds at least one required task example; the agent assesses the examples with the uploaded guide documents. @@ -552,8 +554,9 @@ correction. Both correction origins use the same explicit manual dispatch. Derivation, reads, approval and correction creation do not invoke inference or post-submit evaluators. A periodic scan recovers publication failures from committed approvals; duplicate delivery retains one policy and receipt. -Hidden post-submission execution and completion custody are implemented; live -ARCH-04D2 supplies exact service authority; automatic routing remains ARCH-04E work. +Hidden post-submission execution and completion custody are implemented. +ARCH-04D2 supplies exact service authority and ARCH-04E1A supplies source-only +facts/types without a runtime entry. Automatic routing remains ARCH-04E work. The local Celery command above includes Beat; start it before creating guide sources. The Beat scheduler must run alongside the Celery execution processes so @@ -568,8 +571,9 @@ checker phase service and removes the standalone JSON precheck. Both fresh pre-submit execution and completed replay use that service, with ART retaining canonical evidence ownership. The obsolete checker worker, manual execution and submission-finalize repair routes are removed. `evaluate_post_submission` now -uses hidden durable execution with exact ARCH-04D2 service authority; automatic -dispatch and routing remain ARCH-04E work. +uses hidden durable execution with exact ARCH-04D2 service authority. ARCH-04E1A +retains route-neutral source evidence without a writer, reader, handler or +current pointer; automatic dispatch and routing remain ARCH-04E work. Submission and checker history use live exact-project Submitter authority for the original contributor. Separate `/projects/{project_id}` reads require a covering Project Manager grant and expose fixed management fields. Token roles confer no @@ -661,8 +665,9 @@ exact request/execution-lease custody, immutable member results and atomic compl The structural catalogue produces no output files, so output write/bind authority remains unavailable. ARCH-04D1 validates retained terminal material against canonical ART lineage. ARCH-04D2 supplies fixed-service input, execute and finalize authority, -with PostgreSQL enforcement of exact execution/finalization receipts. Automatic -dispatch and routing remain ARCH-04E work. +with PostgreSQL enforcement of exact execution/finalization receipts. ARCH-04E1A +adds immutable source storage and detached contracts only. Automatic dispatch, +routing and acceptance remain ARCH-04E work. ## v0.1 Success Standard diff --git a/backend/alembic/env.py b/backend/alembic/env.py index c897490f8..1892affdb 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 = "0010_post_submit_authority" +_CURRENT_HEAD_REVISION = "0011_task_routing_source" _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",), (_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",), (_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/0011_task_routing_source.py b/backend/alembic/versions/0011_task_routing_source.py new file mode 100644 index 000000000..9fd4b67a3 --- /dev/null +++ b/backend/alembic/versions/0011_task_routing_source.py @@ -0,0 +1,343 @@ +"""Install the immutable route-neutral TASK post-submit source foundation.""" + +from alembic import op +import sqlalchemy as sa + + +revision = "0011_task_routing_source" +down_revision = "0010_post_submit_authority" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Create detached source custody without installing a writer or reader.""" + op.execute("SET LOCAL search_path = pg_catalog, public, pg_temp") + op.create_table( + "task_post_submit_routing_manifests", + sa.Column("id", sa.Uuid(), nullable=False, primary_key=True), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + server_default=sa.text("clock_timestamp()"), + nullable=False, + ), + sa.Column("project_id", sa.Uuid(), nullable=False), + sa.Column("task_id", sa.Uuid(), nullable=False), + sa.Column("submission_id", sa.Uuid(), nullable=False), + sa.Column("submission_version", sa.Integer(), nullable=False), + sa.Column("assignment_id", sa.Uuid(), nullable=False), + sa.Column("contributor_id", sa.Uuid(), nullable=False), + sa.Column("contribution_policy_version_id", sa.Uuid(), nullable=False), + sa.Column("checker_run_id", sa.Uuid(), nullable=False), + sa.Column("evaluation_request_id", sa.Uuid(), nullable=False), + sa.Column("request_digest", sa.String(length=71), nullable=False), + sa.Column("evaluation_generation", sa.Integer(), nullable=False), + sa.Column("result_id", sa.Uuid(), nullable=False), + sa.Column("result_digest", sa.String(length=71), nullable=False), + sa.Column("completion_event_id", sa.Uuid(), nullable=False), + sa.Column("execute_evidence_id", sa.Uuid(), nullable=False), + sa.Column("finalize_evidence_id", sa.Uuid(), nullable=False), + sa.Column("human_review_required", sa.Boolean(), nullable=False), + sa.Column("replica_id", sa.Uuid(), nullable=False), + sa.Column("content_sha256", sa.String(length=71), nullable=False), + sa.Column("byte_count", sa.BigInteger(), nullable=False), + sa.Column("semantic_manifest_sha256", sa.String(length=71), nullable=False), + sa.CheckConstraint( + "(get_byte(uuid_send(id), 6) >> 4) = 7 and (get_byte(uuid_send(id), 8) & 192) = 128", + name="id_uuid7", + ), + sa.CheckConstraint("submission_version > 0", name="submission_version_positive"), + sa.CheckConstraint("evaluation_generation > 0", name="evaluation_generation_positive"), + sa.CheckConstraint("byte_count >= 0", name="byte_count_nonnegative"), + sa.CheckConstraint( + "request_digest ~ '^sha256:[0-9a-f]{64}$' and " + "result_digest ~ '^sha256:[0-9a-f]{64}$' and " + "content_sha256 ~ '^sha256:[0-9a-f]{64}$' and " + "semantic_manifest_sha256 ~ '^sha256:[0-9a-f]{64}$'", + name="sha256_shapes", + ), + sa.ForeignKeyConstraint( + ["task_id", "project_id"], + ["public.workstream_tasks.id", "public.workstream_tasks.project_id"], + name="fk_task_routing_manifest_task_project", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["submission_id", "task_id", "submission_version"], + ["public.submissions.id", "public.submissions.task_id", "public.submissions.version"], + name="fk_task_routing_manifest_submission_version", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["assignment_id", "task_id", "contributor_id"], + [ + "public.task_assignments.id", + "public.task_assignments.task_id", + "public.task_assignments.contributor_id", + ], + name="fk_task_routing_manifest_assignment_identity", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["contribution_policy_version_id", "project_id"], + [ + "public.contribution_policy_versions.id", + "public.contribution_policy_versions.project_id", + ], + name="fk_task_routing_manifest_contribution_project", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["checker_run_id", "task_id", "submission_id"], + [ + "public.checker_runs.id", + "public.checker_runs.task_id", + "public.checker_runs.submission_id", + ], + name="fk_task_routing_manifest_checker_source", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["completion_event_id"], + ["public.outbox_events.event_id"], + name="fk_task_routing_manifest_completion_event", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["execute_evidence_id"], + ["public.audit_events.id"], + name="fk_task_routing_manifest_execute_evidence", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["finalize_evidence_id"], + ["public.audit_events.id"], + name="fk_task_routing_manifest_finalize_evidence", + ondelete="RESTRICT", + ), + sa.ForeignKeyConstraint( + ["replica_id"], + ["public.artifact_replicas.id"], + name="fk_task_routing_manifest_replica", + ondelete="RESTRICT", + ), + sa.UniqueConstraint( + "submission_id", + "checker_run_id", + "result_digest", + name="uq_task_routing_manifest_source", + ), + schema="public", + ) + _install_source_guard() + _install_immutability_guard() + + +def _install_source_guard() -> None: + op.execute( + """ +CREATE FUNCTION public.guard_task_post_submit_routing_source() RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, public, pg_temp AS $$ +DECLARE + run public.checker_runs%ROWTYPE; + expected_material jsonb; +BEGIN + -- The database owns creation time even when an insert supplies a value. + NEW.created_at := pg_catalog.clock_timestamp(); + -- Preserve NOT NULL as the owner of missing caller-supplied scalars. + IF NEW.id IS NULL OR NEW.project_id IS NULL + OR NEW.task_id IS NULL OR NEW.submission_id IS NULL + OR NEW.submission_version IS NULL OR NEW.assignment_id IS NULL + OR NEW.contributor_id IS NULL OR NEW.contribution_policy_version_id IS NULL + OR NEW.checker_run_id IS NULL OR NEW.evaluation_request_id IS NULL + OR NEW.request_digest IS NULL OR NEW.evaluation_generation IS NULL + OR NEW.result_id IS NULL OR NEW.result_digest IS NULL + OR NEW.completion_event_id IS NULL OR NEW.execute_evidence_id IS NULL + OR NEW.finalize_evidence_id IS NULL OR NEW.human_review_required IS NULL + OR NEW.replica_id IS NULL OR NEW.content_sha256 IS NULL + OR NEW.byte_count IS NULL OR NEW.semantic_manifest_sha256 IS NULL + THEN + RETURN NEW; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM public.submissions AS submission + JOIN public.workstream_tasks AS task + ON task.id = submission.task_id + JOIN public.task_assignments AS assignment + ON assignment.id = submission.task_assignment_id + AND assignment.task_id = submission.task_id + AND assignment.contributor_id = submission.contributor_id + WHERE submission.id IS NOT DISTINCT FROM NEW.submission_id + AND submission.version IS NOT DISTINCT FROM NEW.submission_version + AND submission.task_id IS NOT DISTINCT FROM NEW.task_id + AND task.project_id IS NOT DISTINCT FROM NEW.project_id + AND submission.task_assignment_id IS NOT DISTINCT FROM NEW.assignment_id + AND submission.contributor_id IS NOT DISTINCT FROM NEW.contributor_id + AND assignment.submitter_contribution_policy_version_id + IS NOT DISTINCT FROM NEW.contribution_policy_version_id + AND submission.contribution_policy_version_id + IS NOT DISTINCT FROM NEW.contribution_policy_version_id + AND task.locked_contribution_policy_version_id + IS NOT DISTINCT FROM NEW.contribution_policy_version_id + ) THEN + RAISE EXCEPTION 'task post-submit routing source lineage mismatch' + USING ERRCODE = '23514'; + END IF; + + SELECT candidate.* INTO run + FROM public.checker_runs AS candidate + WHERE candidate.id IS NOT DISTINCT FROM NEW.checker_run_id + AND candidate.project_id IS NOT DISTINCT FROM NEW.project_id + AND candidate.task_id IS NOT DISTINCT FROM NEW.task_id + AND candidate.submission_id IS NOT DISTINCT FROM NEW.submission_id + AND candidate.submission_version IS NOT DISTINCT FROM NEW.submission_version + AND candidate.evaluation_request_id IS NOT DISTINCT FROM NEW.evaluation_request_id + AND candidate.request_digest IS NOT DISTINCT FROM NEW.request_digest + AND candidate.evaluation_generation IS NOT DISTINCT FROM NEW.evaluation_generation + AND candidate.result_id IS NOT DISTINCT FROM NEW.result_id + AND candidate.result_digest IS NOT DISTINCT FROM NEW.result_digest + AND candidate.completion_event_id IS NOT DISTINCT FROM NEW.completion_event_id + AND candidate.execute_evidence_id IS NOT DISTINCT FROM NEW.execute_evidence_id + AND candidate.finalize_evidence_id IS NOT DISTINCT FROM NEW.finalize_evidence_id + AND candidate.execute_evidence_id IS DISTINCT FROM candidate.finalize_evidence_id + AND candidate.status IS NOT DISTINCT FROM 'completed' + AND candidate.routing_recommendation IS NOT DISTINCT FROM 'allow_review' + AND candidate.outcome_source IS NOT DISTINCT FROM 'auto_checker' + AND candidate.material_custody::jsonb IS NOT NULL + AND public.checker_post_submit_receipt_valid(candidate, 'execute', false) + AND public.checker_post_submit_receipt_valid(candidate, 'finalize', false); + IF NOT FOUND THEN + RAISE EXCEPTION 'task post-submit routing checker source mismatch' + USING ERRCODE = '23514'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM public.submissions AS submission + JOIN public.workstream_tasks AS task ON task.id = submission.task_id + JOIN public.project_guides AS guide + ON guide.project_id = task.project_id + AND guide.version = submission.locked_guide_version + JOIN public.guide_mutation_idempotency_records AS activation + ON activation.operation_id = guide.activation_operation_id + AND activation.project_id = guide.project_id + AND activation.resource_id = guide.id + WHERE submission.id IS NOT DISTINCT FROM NEW.submission_id + AND submission.task_id IS NOT DISTINCT FROM NEW.task_id + AND submission.version IS NOT DISTINCT FROM NEW.submission_version + AND guide.status IN ('active', 'superseded') + AND guide.activation_operation_id IS NOT NULL + AND activation.action_id IS NOT DISTINCT FROM 'project.guide.activate' + AND activation.status IS NOT DISTINCT FROM 'committed' + AND activation.activation_facts_json IS NOT NULL + AND activation.activation_authority_json IS NOT NULL + ) THEN + RAISE EXCEPTION 'task post-submit routing guide activation mismatch' + USING ERRCODE = '23514'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM public.submissions AS submission + JOIN public.workstream_tasks AS task ON task.id = submission.task_id + JOIN public.project_guides AS guide + ON guide.project_id = task.project_id + AND guide.version = submission.locked_guide_version + JOIN public.review_policies AS review + ON review.project_id = guide.project_id + AND review.guide_version = guide.version + AND review.id = guide.selected_review_policy_id + AND review.policy_generation = guide.selected_review_policy_generation + AND review.policy_hash = guide.selected_review_policy_hash + WHERE submission.id IS NOT DISTINCT FROM NEW.submission_id + AND submission.task_id IS NOT DISTINCT FROM NEW.task_id + AND submission.version IS NOT DISTINCT FROM NEW.submission_version + AND submission.locked_review_policy_id IS NOT DISTINCT FROM review.id + AND submission.locked_review_policy_generation + IS NOT DISTINCT FROM review.policy_generation + AND submission.locked_review_policy_hash IS NOT DISTINCT FROM review.policy_hash + AND task.locked_review_policy_id IS NOT DISTINCT FROM review.id + AND task.locked_review_policy_generation IS NOT DISTINCT FROM review.policy_generation + AND task.locked_review_policy_hash IS NOT DISTINCT FROM review.policy_hash + AND run.locked_review_policy_id IS NOT DISTINCT FROM review.id + AND run.locked_review_policy_generation IS NOT DISTINCT FROM review.policy_generation + AND run.locked_review_policy_hash IS NOT DISTINCT FROM review.policy_hash + AND review.human_review_required IS NOT DISTINCT FROM NEW.human_review_required + ) THEN + RAISE EXCEPTION 'task post-submit routing review policy mismatch' + USING ERRCODE = '23514'; + END IF; + + expected_material := pg_catalog.jsonb_build_object( + 'submission_id', NEW.submission_id, + 'submission_version', NEW.submission_version, + 'admission_id', (run.material_custody::jsonb)->'admission_id', + 'binding_id', (run.material_custody::jsonb)->'binding_id', + 'content_id', (run.material_custody::jsonb)->'content_id', + 'replica_id', NEW.replica_id, + 'content_sha256', NEW.content_sha256, + 'byte_count', NEW.byte_count, + 'semantic_manifest_sha256', NEW.semantic_manifest_sha256 + ); + IF run.material_custody::jsonb IS DISTINCT FROM expected_material + OR public.art_submission_material_matches( + NEW.project_id, + NEW.task_id, + NEW.submission_id, + NEW.submission_version, + expected_material + ) IS DISTINCT FROM true + THEN + RAISE EXCEPTION 'task post-submit routing material mismatch' + USING ERRCODE = '23514'; + END IF; + RETURN NEW; +END +$$; +""" + ) + op.execute( + """ +CREATE TRIGGER task_routing_manifest_source_guard +BEFORE INSERT ON public.task_post_submit_routing_manifests +FOR EACH ROW EXECUTE FUNCTION public.guard_task_post_submit_routing_source(); +""" + ) + + +def _install_immutability_guard() -> None: + op.execute( + """ +CREATE FUNCTION public.protect_task_post_submit_routing_manifest() RETURNS trigger +LANGUAGE plpgsql +SET search_path = pg_catalog, pg_temp AS $$ +BEGIN + RAISE EXCEPTION 'task post-submit routing source is immutable' + USING ERRCODE = '23514'; +END +$$; +""" + ) + op.execute( + """ +CREATE TRIGGER task_routing_manifest_immutable +BEFORE UPDATE OR DELETE ON public.task_post_submit_routing_manifests +FOR EACH ROW EXECUTE FUNCTION public.protect_task_post_submit_routing_manifest(); +""" + ) + op.execute( + """ +CREATE TRIGGER task_routing_manifest_no_truncate +BEFORE TRUNCATE ON public.task_post_submit_routing_manifests +FOR EACH STATEMENT EXECUTE FUNCTION public.protect_task_post_submit_routing_manifest(); +""" + ) + + +def downgrade() -> None: + """Reject destructive downgrade of retained immutable source evidence.""" + raise RuntimeError("Workstream v0.1 migrations cannot be downgraded; recreate the database") diff --git a/backend/app/db/models.py b/backend/app/db/models.py index b5c0a3740..2d60a76dd 100644 --- a/backend/app/db/models.py +++ b/backend/app/db/models.py @@ -83,6 +83,9 @@ TaskAssignment, WorkstreamTask, ) +from app.modules.tasks.post_submit_routing.models import ( # noqa: F401 + TaskPostSubmitRoutingManifest, +) from app.modules.projects.guide_compilation.models import ( # noqa: F401 ProjectGuideRuntimeAllocation, ProjectGuideDocumentAccess, diff --git a/backend/app/modules/tasks/api/__init__.py b/backend/app/modules/tasks/api/__init__.py index ca7af1868..e9487ee89 100644 --- a/backend/app/modules/tasks/api/__init__.py +++ b/backend/app/modules/tasks/api/__init__.py @@ -1,6 +1,18 @@ """Dependency-safe public API for the TASKS business module.""" -from app.modules.tasks.api.transition_audit import TaskTransitionAuditPort, TaskTransitionFacts +from app.modules.tasks.api.accepted_effects import ( + TaskAcceptedEffectsPort, + TaskAcceptedEffectsRequest, + TaskAcceptedEffectsResult, + TaskAcceptedEffectsUnavailable, +) +from app.modules.tasks.api.post_submit_routing import TaskPostSubmitManifestFacts + +from app.modules.tasks.api.transition_audit import ( + TaskPolicyLineage, + TaskTransitionAuditPort, + TaskTransitionFacts, +) from app.modules.tasks.api.authorization import ( TaskAuthorizationPort, @@ -34,7 +46,11 @@ ) from app.modules.tasks.api.ready_queue import ( - TaskQueueCursor, ReadyTaskPage, ReadyTaskQueuePort, TaskQueueRequest, ReadyTaskSummary, + TaskQueueCursor, + ReadyTaskPage, + ReadyTaskQueuePort, + TaskQueueRequest, + ReadyTaskSummary, ) from app.modules.tasks.api.management_queue import ( @@ -47,32 +63,53 @@ ) from app.modules.tasks.api.task_detail import ( - ContributorTaskDetail, ContributorTaskDetailRequest, ContributorTaskDetailPort, - ManagementTaskDetail, ManagementTaskDetailRequest, ManagementTaskDetailPort, + ContributorTaskDetail, + ContributorTaskDetailRequest, + ContributorTaskDetailPort, + ManagementTaskDetail, + ManagementTaskDetailRequest, + ManagementTaskDetailPort, ) from app.modules.tasks.api.audit_evidence import ( - AuditTaskEvidence, AuditTaskEvidencePage, AuditTaskEvidencePort, - AuditTaskEvidenceRequest, TaskEvidenceCursor, TaskEvidenceInvalid, + AuditTaskEvidence, + AuditTaskEvidencePage, + AuditTaskEvidencePort, + AuditTaskEvidenceRequest, + TaskEvidenceCursor, + TaskEvidenceInvalid, ) __all__ = ( - "AuditTaskEvidence", "AuditTaskEvidencePage", "AuditTaskEvidencePort", - "AuditTaskEvidenceRequest", "TaskEvidenceCursor", "TaskEvidenceInvalid", + "AuditTaskEvidence", + "AuditTaskEvidencePage", + "AuditTaskEvidencePort", + "AuditTaskEvidenceRequest", + "TaskEvidenceCursor", + "TaskEvidenceInvalid", "ContributorTaskDetail", "ContributorTaskDetailRequest", "ContributorTaskDetailPort", "ManagementTaskDetail", "ManagementTaskDetailRequest", "ManagementTaskDetailPort", - "ManagementTaskPage", "ManagementTaskQueuePort", "ManagementTaskSummary", "OperationalTaskPage", "OperationalTaskQueuePort", "OperationalTaskSummary", - "TaskQueueCursor", "ReadyTaskPage", "ReadyTaskQueuePort", "TaskQueueRequest", "ReadyTaskSummary", + "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsRequest", + "TaskAcceptedEffectsResult", + "TaskAcceptedEffectsUnavailable", + "TaskPolicyLineage", + "TaskPostSubmitManifestFacts", + "TaskQueueCursor", + "ReadyTaskPage", + "ReadyTaskQueuePort", + "TaskQueueRequest", + "ReadyTaskSummary", "TaskTransitionAuditPort", "TaskTransitionFacts", "TaskAuthorizationPort", diff --git a/backend/app/modules/tasks/api/accepted_effects.py b/backend/app/modules/tasks/api/accepted_effects.py new file mode 100644 index 000000000..2ce78bce1 --- /dev/null +++ b/backend/app/modules/tasks/api/accepted_effects.py @@ -0,0 +1,63 @@ +"""Source-neutral TASK participant contract for future final acceptance.""" + +from typing import Annotated, Literal, Protocol +from uuid import UUID + +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr + +_Sha256 = Annotated[StrictStr, Field(pattern=r"^sha256:[0-9a-f]{64}$")] +_PositiveVersion = Annotated[StrictInt, Field(ge=1, le=2_147_483_647)] + + +class TaskAcceptedEffectsUnavailable(RuntimeError): + """Conceal absent, foreign, stale, or invalid TASK acceptance state.""" + + +class TaskAcceptedEffectsRequest(BaseModel): + """Exact TASK identity and lineage selected by the acceptance owner.""" + + model_config = ConfigDict(extra="forbid", frozen=True, strict=True) + + project_id: UUID + task_id: UUID + assignment_id: UUID + submission_id: UUID + submission_version: _PositiveVersion + contributor_id: UUID + contribution_policy_version_id: UUID + content_id: UUID + content_sha256: _Sha256 + final_acceptance_id: UUID + expected_task_status: Literal["evaluation_pending", "review_pending"] + + +class TaskAcceptedEffectsResult(BaseModel): + """Exact accepted request identity and the two TASK-owned terminal states.""" + + model_config = ConfigDict(extra="forbid", frozen=True, strict=True) + + request: TaskAcceptedEffectsRequest + task_status: Literal["accepted"] + assignment_status: Literal["completed"] + + +class TaskAcceptedEffectsPort(Protocol): + """Apply flush-only TASK effects inside the caller's one root transaction. + + The participant never commits, authorizes, creates REV or CON facts, or calls + back into routing. + """ + + async def apply_accepted_effects( + self, request: TaskAcceptedEffectsRequest + ) -> TaskAcceptedEffectsResult: + """Validate exact TASK state and lineage, then flush accepted effects.""" + ... + + +__all__ = ( + "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsRequest", + "TaskAcceptedEffectsResult", + "TaskAcceptedEffectsUnavailable", +) diff --git a/backend/app/modules/tasks/api/post_submit_routing.py b/backend/app/modules/tasks/api/post_submit_routing.py new file mode 100644 index 000000000..5a172dd89 --- /dev/null +++ b/backend/app/modules/tasks/api/post_submit_routing.py @@ -0,0 +1,89 @@ +"""Detached TASK source facts for future post-submit routing.""" + +from typing import Annotated, Literal, Self +from uuid import UUID + +from pydantic import ( + AwareDatetime, + BaseModel, + ConfigDict, + Field, + StrictBool, + StrictInt, + StrictStr, + model_validator, +) + +from app.modules.tasks.api.transition_audit import TaskPolicyLineage + +_Sha256 = Annotated[StrictStr, Field(pattern=r"^sha256:[0-9a-f]{64}$")] +_PositiveVersion = Annotated[StrictInt, Field(ge=1, le=2_147_483_647)] +_ByteCount = Annotated[StrictInt, Field(ge=0, le=9_223_372_036_854_775_807)] + + +class TaskPostSubmitManifestFacts(BaseModel): + """Immutable persisted and owner-joined source facts without routing authority.""" + + model_config = ConfigDict(extra="forbid", frozen=True, strict=True) + + id: UUID + created_at: AwareDatetime + project_id: UUID + task_id: UUID + submission_id: UUID + submission_version: _PositiveVersion + assignment_id: UUID + contributor_id: UUID + contribution_policy_version_id: UUID + checker_run_id: UUID + evaluation_request_id: UUID + request_digest: _Sha256 + evaluation_generation: _PositiveVersion + result_id: UUID + result_digest: _Sha256 + completion_event_id: UUID + execute_evidence_id: UUID + finalize_evidence_id: UUID + human_review_required: StrictBool + replica_id: UUID + content_sha256: _Sha256 + byte_count: _ByteCount + semantic_manifest_sha256: _Sha256 + + predecessor_submission_id: UUID | None + predecessor_submission_version: _PositiveVersion | None + admission_id: UUID + binding_id: UUID + content_id: UUID + locked_policy: TaskPolicyLineage + routing_recommendation: Literal["allow_review"] + + @model_validator(mode="after") + def validate_detached_lineage(self) -> Self: + """Keep nested policy and immediate predecessor facts internally exact.""" + if ( + self.locked_policy.locked_contribution_policy_version_id + != self.contribution_policy_version_id + ): + raise ValueError("routing source contribution policy lineage differs") + if self.execute_evidence_id == self.finalize_evidence_id: + raise ValueError("routing source phase receipts are not distinct") + + has_predecessor_id = self.predecessor_submission_id is not None + has_predecessor_version = self.predecessor_submission_version is not None + if has_predecessor_id != has_predecessor_version: + raise ValueError("routing source predecessor identity is incomplete") + if self.submission_version == 1: + if has_predecessor_id: + raise ValueError("initial routing source has a predecessor") + return self + if not has_predecessor_id: + raise ValueError("successor routing source lacks a predecessor") + if self.predecessor_submission_id == self.submission_id: + raise ValueError("routing source predecessor equals submission") + if self.predecessor_submission_version != self.submission_version - 1: + raise ValueError("routing source predecessor version is inconsistent") + return self + + +__all__ = ("TaskPostSubmitManifestFacts",) diff --git a/backend/app/modules/tasks/post_submit_routing/__init__.py b/backend/app/modules/tasks/post_submit_routing/__init__.py new file mode 100644 index 000000000..722fdcd07 --- /dev/null +++ b/backend/app/modules/tasks/post_submit_routing/__init__.py @@ -0,0 +1 @@ +"""TASK-owned detached post-submit routing source storage.""" diff --git a/backend/app/modules/tasks/post_submit_routing/models.py b/backend/app/modules/tasks/post_submit_routing/models.py new file mode 100644 index 000000000..0a5c83490 --- /dev/null +++ b/backend/app/modules/tasks/post_submit_routing/models.py @@ -0,0 +1,139 @@ +"""Detached immutable TASK source evidence for future post-submit routing.""" + +from __future__ import annotations + +from datetime import datetime +from uuid import UUID + +from sqlalchemy import ( + BigInteger, + Boolean, + CheckConstraint, + DateTime, + ForeignKey, + ForeignKeyConstraint, + Integer, + String, + UniqueConstraint, + Uuid, +) +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.sql import func + +from app.db.base import Base + + +class TaskPostSubmitRoutingManifest(Base): + """One immutable route-neutral source fact; no routing authority or currentness.""" + + __tablename__ = "task_post_submit_routing_manifests" + __table_args__ = ( + CheckConstraint( + "(get_byte(uuid_send(id), 6) >> 4) = 7 and (get_byte(uuid_send(id), 8) & 192) = 128", + name="id_uuid7", + ), + CheckConstraint("submission_version > 0", name="submission_version_positive"), + CheckConstraint("evaluation_generation > 0", name="evaluation_generation_positive"), + CheckConstraint("byte_count >= 0", name="byte_count_nonnegative"), + CheckConstraint( + "request_digest ~ '^sha256:[0-9a-f]{64}$' and " + "result_digest ~ '^sha256:[0-9a-f]{64}$' and " + "content_sha256 ~ '^sha256:[0-9a-f]{64}$' and " + "semantic_manifest_sha256 ~ '^sha256:[0-9a-f]{64}$'", + name="sha256_shapes", + ), + ForeignKeyConstraint( + ["task_id", "project_id"], + ["workstream_tasks.id", "workstream_tasks.project_id"], + name="fk_task_routing_manifest_task_project", + ondelete="RESTRICT", + ), + ForeignKeyConstraint( + ["submission_id", "task_id", "submission_version"], + ["submissions.id", "submissions.task_id", "submissions.version"], + name="fk_task_routing_manifest_submission_version", + ondelete="RESTRICT", + ), + ForeignKeyConstraint( + ["assignment_id", "task_id", "contributor_id"], + ["task_assignments.id", "task_assignments.task_id", "task_assignments.contributor_id"], + name="fk_task_routing_manifest_assignment_identity", + ondelete="RESTRICT", + ), + ForeignKeyConstraint( + ["contribution_policy_version_id", "project_id"], + ["contribution_policy_versions.id", "contribution_policy_versions.project_id"], + name="fk_task_routing_manifest_contribution_project", + ondelete="RESTRICT", + ), + ForeignKeyConstraint( + ["checker_run_id", "task_id", "submission_id"], + ["checker_runs.id", "checker_runs.task_id", "checker_runs.submission_id"], + name="fk_task_routing_manifest_checker_source", + ondelete="RESTRICT", + ), + UniqueConstraint( + "submission_id", + "checker_run_id", + "result_digest", + name="uq_task_routing_manifest_source", + ), + ) + + id: Mapped[UUID] = mapped_column(Uuid(), primary_key=True) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.clock_timestamp() + ) + project_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + task_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + submission_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + submission_version: Mapped[int] = mapped_column(Integer, nullable=False) + assignment_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + contributor_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + contribution_policy_version_id: Mapped[UUID] = mapped_column(Uuid(), nullable=False) + checker_run_id: Mapped[str] = mapped_column(Uuid(as_uuid=False), nullable=False) + evaluation_request_id: Mapped[UUID] = mapped_column(Uuid(), nullable=False) + request_digest: Mapped[str] = mapped_column(String(71), nullable=False) + evaluation_generation: Mapped[int] = mapped_column(Integer, nullable=False) + result_id: Mapped[UUID] = mapped_column(Uuid(), nullable=False) + result_digest: Mapped[str] = mapped_column(String(71), nullable=False) + completion_event_id: Mapped[UUID] = mapped_column( + Uuid(), + ForeignKey( + "outbox_events.event_id", + name="fk_task_routing_manifest_completion_event", + ondelete="RESTRICT", + ), + nullable=False, + ) + execute_evidence_id: Mapped[str] = mapped_column( + Uuid(as_uuid=False), + ForeignKey( + "audit_events.id", + name="fk_task_routing_manifest_execute_evidence", + ondelete="RESTRICT", + ), + nullable=False, + ) + finalize_evidence_id: Mapped[str] = mapped_column( + Uuid(as_uuid=False), + ForeignKey( + "audit_events.id", + name="fk_task_routing_manifest_finalize_evidence", + ondelete="RESTRICT", + ), + nullable=False, + ) + human_review_required: Mapped[bool] = mapped_column(Boolean, nullable=False) + replica_id: Mapped[str] = mapped_column( + Uuid(as_uuid=False), + ForeignKey( + "artifact_replicas.id", + name="fk_task_routing_manifest_replica", + ondelete="RESTRICT", + ), + nullable=False, + ) + content_sha256: Mapped[str] = mapped_column(String(71), nullable=False) + byte_count: Mapped[int] = mapped_column(BigInteger, nullable=False) + semantic_manifest_sha256: Mapped[str] = mapped_column(String(71), nullable=False) diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index 1479600c8..9ae0ecd14 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -146,6 +146,12 @@ "backend/app/modules/authorization/api/outbox_dispatch.py", } ) +ARCH_04E1A_SOURCE_TARGETS = frozenset({ + "backend/app/modules/tasks/api/accepted_effects.py", + "backend/app/modules/tasks/api/post_submit_routing.py", + "backend/app/modules/tasks/post_submit_routing/models.py", +}) + ARCH_04D2_AUTHORITY_TARGETS = frozenset({ "backend/app/modules/authorization/domain/post_submit.py", "backend/app/modules/authorization/post_submit_authorization.py", @@ -731,6 +737,7 @@ def _validate_additive_partition_transition( | ARCH_CP03B_ADAPTER_BINDING_AUTH_TARGETS | ARCH_CP04A_CONTRIBUTION_POLICY_TARGETS | ARCH_CP04B_CONTRIBUTION_POLICY_TARGETS + | ARCH_04E1A_SOURCE_TARGETS | ARCH_04D2_AUTHORITY_TARGETS | ARCH_04C_EXECUTION_TARGETS | ARCH_04B2_OUTPUT_TARGETS diff --git a/backend/scripts/test_lane_catalogue.py b/backend/scripts/test_lane_catalogue.py index 627765cb6..994ccbf98 100644 --- a/backend/scripts/test_lane_catalogue.py +++ b/backend/scripts/test_lane_catalogue.py @@ -62,16 +62,10 @@ class TestLane: "tests/test_api_contract_e2e.py", "tests/test_api_controls.py", "tests/test_identifiers.py", - "tests/test_identifier_inventory.py", "tests/test_identifier_schema.py", "tests/test_identifier_schema_postgresql.py", - "tests/test_record_id_collection.py", "tests/test_app.py", - "tests/test_artifact_architecture.py", - "tests/architecture/test_authorization_boundary.py", - "tests/architecture/test_module_boundaries.py", "tests/architecture/test_cp04a_file_structure.py", - "tests/architecture/test_test_structure_boundary.py", "tests/test_artifact_authorization.py", "tests/test_artifact_internal_authorization.py", "tests/test_artifact_cleanup_wiring.py", @@ -82,8 +76,6 @@ class TestLane: "tests/test_artifacts.py", "tests/test_assertion_helpers.py", "tests/test_aws_credential_isolation.py", - "tests/test_ci_test_lanes.py", - "tests/test_ci_lane_catalogue.py", "tests/test_config.py", "tests/test_compensation.py", "tests/compensation/test_adapter_binding_api.py", @@ -129,7 +121,6 @@ class TestLane: "tests/test_guide_artifacts.py", "tests/test_guide_formats.py", "tests/test_local_artifact_store.py", - "tests/test_merge_test_lane_evidence.py", "tests/migrations/test_task_queue_authority.py", "tests/migrations/test_task_read_authority.py", "tests/migrations/test_task_context_authority.py", @@ -141,7 +132,6 @@ class TestLane: "tests/test_submission_archive.py", "tests/test_submission_change_gate.py", "tests/test_submission_manifest.py", - "tests/test_test_lane_evidence.py", "tests/authentication/test_local_verifiers.py", "tests/authentication/test_token_contract.py", "tests/authentication/test_jwks_cache.py", @@ -377,6 +367,9 @@ class TestLane: ) TASK_MODULES = ( + "tests/tasks/post_submit_routing/test_contracts.py", + "tests/tasks/post_submit_routing/test_storage.py", + "tests/tasks/post_submit_routing/test_migration.py", "tests/tasks/test_public_queues.py", "tests/authorization/task_reads/test_authority.py", "tests/authorization/task_reads/test_contracts.py", @@ -478,6 +471,16 @@ class TestLane: ( SCHEMA_MODULE, "tests/test_database_reset.py", + "tests/test_artifact_architecture.py", + "tests/architecture/test_module_boundaries.py", + "tests/architecture/test_authorization_boundary.py", + "tests/architecture/test_test_structure_boundary.py", + "tests/test_identifier_inventory.py", + "tests/test_record_id_collection.py", + "tests/test_ci_lane_catalogue.py", + "tests/test_ci_test_lanes.py", + "tests/test_test_lane_evidence.py", + "tests/test_merge_test_lane_evidence.py", ADMIN_RUNNER_MODULE, ), ), diff --git a/backend/tests/authorization/post_submit/test_migration.py b/backend/tests/authorization/post_submit/test_migration.py index a8b9591e6..c20a7ade6 100644 --- a/backend/tests/authorization/post_submit/test_migration.py +++ b/backend/tests/authorization/post_submit/test_migration.py @@ -34,10 +34,10 @@ async def test_actual_upgrade_preserves_or_refuses_without_repair(tmp_path, isol before = await retained_snapshot(h.factory) if unprovable_receipt: with pytest.raises(IntegrityError, match="retained checker authorization receipts are unprovable"): - await asyncio.to_thread(command.upgrade, _config(), "head") + await asyncio.to_thread(command.upgrade, _config(), "0010_post_submit_authority") assert await retained_snapshot(h.factory) == before else: - await asyncio.to_thread(command.upgrade, _config(), "head") + await asyncio.to_thread(command.upgrade, _config(), "0010_post_submit_authority") after = await retained_snapshot(h.factory) assert after[0] == before[0] assert after[1] == "0010_post_submit_authority" @@ -67,7 +67,7 @@ def pause_after_scan(operations, sql, *args, **kwargs): assert resume.wait(20), "receipt migration scan was not released" return result monkeypatch.setattr(Operations, "execute", pause_after_scan) - migration = asyncio.create_task(asyncio.to_thread(command.upgrade, _config(), "head")) + migration = asyncio.create_task(asyncio.to_thread(command.upgrade, _config(), "0010_post_submit_authority")) writer = None writer_pid = asyncio.Queue() async def write_predecessor_receipt(): diff --git a/backend/tests/checkers/execution/test_migration.py b/backend/tests/checkers/execution/test_migration.py index cd369da8b..ffbd4c530 100644 --- a/backend/tests/checkers/execution/test_migration.py +++ b/backend/tests/checkers/execution/test_migration.py @@ -144,7 +144,7 @@ async def test_empty_database_installs_execution_custody(isolated_database_env, try: assert ( await conn.fetchval("select version_num from alembic_version") - == "0010_post_submit_authority" + == "0011_task_routing_source" ) assert await conn.fetchval("select count(*) from checker_submission_fences") == 0 columns = set( diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index b70a6ced8..de18d9edb 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 = "cd42c4edbe412cee9b5f139732e55359a4381337d924ed35af78992d34811e79" +EXPECTED_PUBLIC_SCHEMA_SHA256 = "d9c94fbdce2a2fa9aa822b9e0f528f4497362ed0c6b2aef1946f454ea250b354" PROTECTED_TEST_TABLES = ( "actor_profile_migration_state", "alembic_version", @@ -117,6 +117,7 @@ "submissions", "task_assignments", "task_command_receipts", + "task_post_submit_routing_manifests", "workstream_tasks", ) TRUNCATE_GUARDED_TABLES = ( @@ -126,6 +127,7 @@ "checker_submission_fences", "checker_results", "task_command_receipts", + "task_post_submit_routing_manifests", "checker_policies", "admin_role_grants", "audit_events", diff --git a/backend/tests/tasks/post_submit_routing/__init__.py b/backend/tests/tasks/post_submit_routing/__init__.py new file mode 100644 index 000000000..9e3c8554a --- /dev/null +++ b/backend/tests/tasks/post_submit_routing/__init__.py @@ -0,0 +1 @@ +"""Proof for immutable TASK post-submit routing source contracts.""" diff --git a/backend/tests/tasks/post_submit_routing/support.py b/backend/tests/tasks/post_submit_routing/support.py new file mode 100644 index 000000000..c1230b80b --- /dev/null +++ b/backend/tests/tasks/post_submit_routing/support.py @@ -0,0 +1,536 @@ +"""Real completed checker sources and direct-SQL routing-source helpers.""" + +from contextlib import asynccontextmanager +from dataclasses import asdict +from types import SimpleNamespace +from uuid import UUID + +from sqlalchemy import select, text + +from app.adapters.tasks import submitted_bundle_port +from app.api.deps.authorization import compose_hidden_submission_creation_command +from app.core.identifiers import new_record_id +from app.modules.actors.models import ActorIdentityLink +from app.modules.artifacts.api import SubmissionBundlePreparationRequest +from app.modules.authorization.api import ActorIdentityFacts, ActorKind +from app.modules.checkers.api import ( + ExpectedPostSubmitContext, + ObservedPostSubmitContext, + PostSubmitEvidenceEntry, + PostSubmitPolicyInputs, +) +from app.modules.checkers.models import CheckerRun +from app.modules.projects.models import ( + EffectiveProjectSubmissionArtifactPolicy, + ReviewPolicy, +) +from app.modules.tasks.api import SubmissionCreationRequest +from app.modules.tasks.api.post_submit_routing import TaskPostSubmitManifestFacts +from app.modules.tasks.api.submitted_bundle import SubmittedBundleRequest +from app.modules.tasks.api.transition_audit import TaskPolicyLineage +from app.modules.tasks.models import Submission, TaskAssignment, WorkstreamTask +from tests.checkers.execution.support import live_executor, reserve +from tests.checkers.post_submit.support import change_request +from tests.post_submit_materialization_helpers import material_fixture +from tests.tasks.lineage_fixtures import seed_started_task_for_artifact_test +from tests.tasks.submission_lineage_support import _verified_admission +from tests.test_artifact_admission import _context +from tests.test_default_pre_submit_execution import _bytes + + +SOURCE_COLUMNS = ( + "id", + "created_at", + "project_id", + "task_id", + "submission_id", + "submission_version", + "assignment_id", + "contributor_id", + "contribution_policy_version_id", + "checker_run_id", + "evaluation_request_id", + "request_digest", + "evaluation_generation", + "result_id", + "result_digest", + "completion_event_id", + "execute_evidence_id", + "finalize_evidence_id", + "human_review_required", + "replica_id", + "content_sha256", + "byte_count", + "semantic_manifest_sha256", +) + +_INSERT_SOURCE_WITH_CREATED_AT = text( + "INSERT INTO public.task_post_submit_routing_manifests (" + + ",".join(SOURCE_COLUMNS) + + ") VALUES (" + + ",".join(f":{column}" for column in SOURCE_COLUMNS) + + ")" +) +_DEFAULTED_SOURCE_COLUMNS = tuple( + column for column in SOURCE_COLUMNS if column != "created_at" +) +_INSERT_SOURCE = text( + "INSERT INTO public.task_post_submit_routing_manifests (" + + ",".join(_DEFAULTED_SOURCE_COLUMNS) + + ") VALUES (" + + ",".join(f":{column}" for column in _DEFAULTED_SOURCE_COLUMNS) + + ")" +) + + +def as_uuid(value) -> UUID: + return value if isinstance(value, UUID) else UUID(str(value)) + + +def other_hash(value: str) -> str: + """Return another syntactically valid SHA-256 token.""" + suffix = "0" if value[-1] != "0" else "1" + return value[:-1] + suffix + + +async def insert_source(session, values: dict) -> None: + """Insert exactly one source row through the public SQL boundary.""" + statement = _INSERT_SOURCE_WITH_CREATED_AT if "created_at" in values else _INSERT_SOURCE + await session.execute(statement, values) + + +async def source_rows(session) -> list[dict]: + return list( + ( + await session.execute( + text( + "SELECT to_jsonb(source) FROM " + "public.task_post_submit_routing_manifests AS source ORDER BY source.id" + ) + ) + ).scalars() + ) + + +async def rebuild_real_request(h) -> None: + """Replace permissive helper inputs with archive-backed locked-policy facts.""" + async with h.factory() as session: + submission = await session.get(Submission, str(h.request.submission_id)) + effective = await session.get( + EffectiveProjectSubmissionArtifactPolicy, + submission.locked_effective_project_submission_artifact_policy_id, + ) + assert effective is not None + assert effective.effective_policy_hash == h.request.expected_context.effective_policy_hash + policy = effective.effective_policy + + manifest = {item.artifact: item for item in h.request.structural_input.manifest} + evidence = [] + required_evidence = [ + str(item["key"]) + for item in policy.get("required_evidence", ()) + if item.get("required", True) + ] + for key in required_evidence: + path = "evidence/" + key + entry = manifest[path] + evidence.append( + PostSubmitEvidenceEntry( + label=key, + type="file", + uri=path, + hash=entry.hash, + key=key, + required_evidence_key=key, + ) + ) + policy_inputs = PostSubmitPolicyInputs( + required_evidence_keys=tuple(required_evidence), + required_artifact_paths=tuple( + str(item["path"]) + for item in policy.get("required_artifacts", ()) + if item.get("required", True) + ), + forbidden_artifact_patterns=tuple( + str(item["pattern"]) + for item in policy.get("forbidden_artifacts", ()) + if item.get("pattern") + ), + required_attestation_terms=tuple( + str(item) for item in policy.get("attestation_terms", ()) + ), + ) + h.request = change_request( + h.request, + structural_input=h.request.structural_input.model_copy( + update={"evidence": tuple(evidence), "policy_inputs": policy_inputs} + ), + ) + assert h.request.request_sha256 == change_request(h.request).request_sha256 + + +async def next_request(h, *, structural_input=None): + """Build a genuine later generation for the same immutable Submission.""" + request = change_request( + h.request, + evaluation_request_id=new_record_id(), + evaluation_generation=h.request.evaluation_generation + 1, + **({"structural_input": structural_input} if structural_input is not None else {}), + ) + h.request = request + return request + + +async def source_values(h, run_id=None) -> dict: + """Read one row's values only from canonical persisted owners.""" + async with h.factory() as session: + run = await session.get(CheckerRun, str(run_id or h.result.attempt_id)) + submission = await session.get(Submission, str(h.request.submission_id)) + task = await session.get(WorkstreamTask, submission.task_id) + assignment = await session.get(TaskAssignment, submission.task_assignment_id) + review = await session.get(ReviewPolicy, submission.locked_review_policy_id) + assert all(item is not None for item in (run, submission, task, assignment, review)) + material = run.material_custody or h.material + return { + "id": new_record_id(), + "project_id": as_uuid(run.project_id), + "task_id": as_uuid(run.task_id), + "submission_id": as_uuid(run.submission_id), + "submission_version": run.submission_version, + "assignment_id": as_uuid(submission.task_assignment_id), + "contributor_id": as_uuid(submission.contributor_id), + "contribution_policy_version_id": as_uuid( + submission.contribution_policy_version_id + ), + "checker_run_id": as_uuid(run.id), + "evaluation_request_id": as_uuid(run.evaluation_request_id), + "request_digest": run.request_digest, + "evaluation_generation": run.evaluation_generation, + "result_id": as_uuid(run.result_id), + "result_digest": run.result_digest or h.source["result_digest"], + "completion_event_id": as_uuid( + run.completion_event_id or h.source["completion_event_id"] + ), + "execute_evidence_id": as_uuid( + run.execute_evidence_id or h.source["execute_evidence_id"] + ), + "finalize_evidence_id": as_uuid( + run.finalize_evidence_id or h.source["finalize_evidence_id"] + ), + "human_review_required": review.human_review_required, + "replica_id": as_uuid(material["replica_id"]), + "content_sha256": material["content_sha256"], + "byte_count": material["byte_count"], + "semantic_manifest_sha256": material["semantic_manifest_sha256"], + } + + +async def joined_source_facts(h, stored: dict) -> TaskPostSubmitManifestFacts: + """Construct the public detached value from canonical join-only owners.""" + async with h.factory() as session: + submission = await session.get(Submission, str(stored["submission_id"])) + task = await session.get(WorkstreamTask, submission.task_id) + predecessor = ( + await session.get(Submission, submission.supersedes_submission_id) + if submission.supersedes_submission_id + else None + ) + run = await session.get(CheckerRun, str(stored["checker_run_id"])) + material = run.material_custody + lineage = TaskPolicyLineage( + locked_guide_version=submission.locked_guide_version, + locked_guide_source_snapshot_id=as_uuid( + submission.locked_guide_source_snapshot_id + ), + locked_guide_source_snapshot_hash=submission.locked_guide_source_snapshot_hash, + locked_effective_project_submission_artifact_policy_id=as_uuid( + submission.locked_effective_project_submission_artifact_policy_id + ), + locked_effective_project_submission_artifact_policy_hash=( + submission.locked_effective_project_submission_artifact_policy_hash + ), + locked_pre_submit_checker_policy_id=as_uuid( + submission.locked_pre_submit_checker_policy_id + ), + locked_pre_submit_checker_bundle_hash=submission.locked_pre_submit_checker_bundle_hash, + locked_post_submit_checker_policy_id=as_uuid( + submission.locked_post_submit_checker_policy_id + ), + locked_post_submit_checker_policy_version=( + submission.locked_post_submit_checker_policy_version + ), + locked_post_submit_checker_policy_hash=( + submission.locked_post_submit_checker_policy_hash + ), + locked_review_policy_id=as_uuid(submission.locked_review_policy_id), + locked_review_policy_generation=submission.locked_review_policy_generation, + locked_review_policy_hash=submission.locked_review_policy_hash, + locked_revision_policy_id=as_uuid(submission.locked_revision_policy_id), + locked_revision_policy_generation=submission.locked_revision_policy_generation, + locked_revision_policy_hash=submission.locked_revision_policy_hash, + locked_contribution_policy_version_id=as_uuid( + task.locked_contribution_policy_version_id + ), + ) + return TaskPostSubmitManifestFacts( + **{ + column: as_uuid(stored[column]) if column.endswith("_id") else stored[column] + for column in SOURCE_COLUMNS + }, + predecessor_submission_id=( + as_uuid(submission.supersedes_submission_id) + if submission.supersedes_submission_id + else None + ), + predecessor_submission_version=predecessor.version if predecessor else None, + admission_id=as_uuid(material["admission_id"]), + binding_id=as_uuid(material["binding_id"]), + content_id=as_uuid(material["content_id"]), + locked_policy=lineage, + routing_recommendation=run.routing_recommendation, + ) + + +@asynccontextmanager +async def completed_source( + tmp_path, + database_url, + *, + provision_services=True, + storage_settings=None, +): + """Yield one real authorized allow-review run and its valid source scalars.""" + async with material_fixture( + tmp_path, + database_url, + provision_services=provision_services, + storage_settings=storage_settings, + ) as h: + await rebuild_real_request(h) + await reserve(h) + result = await live_executor(h).evaluate_post_submission(h.request) + async with h.factory() as session: + run = await session.get(CheckerRun, str(result.attempt_id)) + assert result.outcome == "completed" + assert run.routing_recommendation == "allow_review" + material = dict(run.material_custody) + h.result, h.material = result, material + h.source = {} + h.source = await source_values(h) + yield h + + +async def _create_sibling_submission(h, context, task_id, assignment_id): + """Admit the ZIP and create its exact sibling Submission through owner services.""" + preparation = SubmissionBundlePreparationRequest( + actor=ActorIdentityFacts( + context.actor_profile_id, + context.identity_link_id, + ActorKind.HUMAN, + ), + request_id=context.request_id, + correlation_id=context.correlation_id, + task_id=task_id, + assignment_id=assignment_id, + predecessor_submission_id=None, + idempotency_key=new_record_id(), + summary=h.request.structural_input.summary, + contributor_attestation=h.request.structural_input.worker_attestation, + media_type="application/zip", + byte_source=_bytes(h.data), + ) + admission_id = await _verified_admission( + h.factory, + h.store, + h.namespace, + h.settings, + context, + preparation, + ) + async with h.factory() as session: + created = await compose_hidden_submission_creation_command( + session, + context, + request_id=new_record_id(), + correlation_id=new_record_id(), + ).create( + SubmissionCreationRequest( + task_id=task_id, + assignment_id=assignment_id, + contributor_id=context.actor_profile_id, + predecessor_submission_id=None, + admission_id=admission_id, + summary=preparation.summary, + contributor_attestation=preparation.contributor_attestation, + ) + ) + return created + + +async def completed_sibling_source(h): + """Build a real allow-review source for another task in the same project.""" + task_id, assignment_id = new_record_id(), new_record_id() + async with h.factory() as session: + original = await session.get(Submission, str(h.request.submission_id)) + identity_link_id = await session.scalar( + select(ActorIdentityLink.id).where( + ActorIdentityLink.actor_profile_id == original.contributor_id, + ActorIdentityLink.status == "active", + ) + ) + assert identity_link_id is not None + context = _context( + actor_profile_id=as_uuid(original.contributor_id), + identity_link_id=as_uuid(identity_link_id), + ) + async with h.engine.begin() as connection: + await seed_started_task_for_artifact_test( + connection, + { + "task": str(task_id), + "assignment": str(assignment_id), + "project": str(h.request.project_id), + "actor": str(context.actor_profile_id), + }, + ) + + created = await _create_sibling_submission(h, context, task_id, assignment_id) + async with h.factory() as session: + facts = await submitted_bundle_port(session).read( + SubmittedBundleRequest( + h.request.project_id, + task_id, + created.submission_id, + ) + ) + expected = ExpectedPostSubmitContext(**asdict(facts.context)) + structural_input = h.request.structural_input.model_copy( + update={"observed_context": ObservedPostSubmitContext(**asdict(facts.context))} + ) + request = change_request( + h.request, + evaluation_request_id=new_record_id(), + evaluation_generation=1, + project_id=facts.project_id, + task_id=facts.task_id, + assignment_id=facts.assignment_id, + submission_id=facts.submission_id, + submission_version=facts.submission_version, + content_id=facts.content_id, + binding_id=facts.binding_id, + expected_context=expected, + structural_input=structural_input, + ) + sibling = SimpleNamespace( + factory=h.factory, + service=h.service, + request=request, + source={}, + ) + await reserve(sibling) + result = await live_executor(sibling).evaluate_post_submission(request) + async with h.factory() as session: + run = await session.get(CheckerRun, str(result.attempt_id)) + assert result.outcome == "completed" + assert run.routing_recommendation == "allow_review" + sibling.material = dict(run.material_custody) + sibling.result = result + sibling.source = await source_values(sibling) + return sibling + + +async def source_count(session) -> int: + return int( + await session.scalar( + text("SELECT count(*) FROM public.task_post_submit_routing_manifests") + ) + ) + + +async def activate_successor_guide(h): + """Activate a genuine v2 guide in the source Submission's project.""" + from uuid import uuid4 + + from sqlalchemy import select + + from app.interfaces.project_agents import SubmissionArtifactPolicyProposal + from app.modules.actors.models import ActorIdentityLink, ActorProfile + from app.modules.projects.api.post_policy import PostPolicyApproval + from app.modules.projects.models import ProjectGuide + from tests.authorization.guide_activation.pg_support import activate + from tests.projects.guide_activation.pg_support import ( + activation_command, + publish_policy, + ) + from tests.projects.guide_activation.source_fixtures import create_compiled_guide + from tests.projects.guide_compilation.helpers import ids, service_actor + from tests.projects.guide_compilation.proposals.pg_support import ( + seed_review_actor, + seed_selected_review_revision_inputs, + ) + from tests.projects.post_policy.pg_support import operate, prepare_post_policy + + values = ids() + values["project"] = h.request.project_id + async with h.factory() as session: + setup_actor, setup_link = ( + await session.execute( + select(ActorProfile.id, ActorIdentityLink.id) + .join( + ActorIdentityLink, + ActorIdentityLink.actor_profile_id == ActorProfile.id, + ) + .where( + ActorProfile.service_identity == "workstream.project.setup", + ActorIdentityLink.status == "active", + ) + ) + ).one() + values.update(actor=as_uuid(setup_actor), link=as_uuid(setup_link)) + manager, grant = await seed_review_actor(h.factory, h.request.project_id) + proposal = SubmissionArtifactPolicyProposal( + maximum_file_size_bytes=1_000_000, + maximum_package_size_bytes=5_000_000, + required_artifacts=("task.toml",), + required_evidence=("results",), + attestation_terms=("rights_confirmed",), + ) + values, finalization = await create_compiled_guide( + h.factory, + values, + manager, + version="v2", + artifact_proposal=proposal, + ) + await seed_selected_review_revision_inputs(h.factory, finalization, manager) + _, derived = await prepare_post_policy( + h.factory, + finalization, + manager, + grant, + service_actor(values), + ) + approved = await operate( + h.factory, + manager, + finalization.project_id, + grant, + "approve", + PostPolicyApproval(target=derived.target, idempotency_key=uuid4()), + ) + _, contribution_policy = await publish_policy(h.factory, finalization.project_id) + command = await activation_command(h.factory, approved, contribution_policy) + async with h.factory() as session: + current = await session.scalar( + select(ProjectGuide).where( + ProjectGuide.project_id == str(h.request.project_id), + ProjectGuide.status == "active", + ) + ) + command = command.model_copy( + update={ + "expected_previous_active_guide_id": as_uuid(current.id), + "expected_previous_active_guide_generation": current.mutation_generation, + } + ) + return await activate(h.factory, manager, command) diff --git a/backend/tests/tasks/post_submit_routing/test_contracts.py b/backend/tests/tasks/post_submit_routing/test_contracts.py new file mode 100644 index 000000000..548912020 --- /dev/null +++ b/backend/tests/tasks/post_submit_routing/test_contracts.py @@ -0,0 +1,344 @@ +"""Pure contract proof for detached routing source and accepted TASK effects.""" + +from datetime import UTC, datetime +from inspect import isclass, iscoroutinefunction +from uuid import UUID + +import pytest +from pydantic import ValidationError + +from app.core.identifiers import new_record_id +from app.modules.actors.api import ServiceIdentity +from app.modules.authorization.catalogue import ActionId, PermissionId +from app.modules.tasks import api as task_api +from app.modules.tasks.api import accepted_effects, post_submit_routing +from app.modules.tasks.api.accepted_effects import ( + TaskAcceptedEffectsPort, + TaskAcceptedEffectsRequest, + TaskAcceptedEffectsResult, + TaskAcceptedEffectsUnavailable, +) +from app.modules.tasks.api.post_submit_routing import TaskPostSubmitManifestFacts +from app.modules.tasks.api.transition_audit import TaskPolicyLineage + +SHA_A = "sha256:" + "a" * 64 +SHA_B = "sha256:" + "b" * 64 + + +def _lineage(*, contribution_policy_version_id: UUID) -> TaskPolicyLineage: + return TaskPolicyLineage( + locked_guide_version="guide-v1", + locked_guide_source_snapshot_id=new_record_id(), + locked_guide_source_snapshot_hash=SHA_A, + locked_effective_project_submission_artifact_policy_id=new_record_id(), + locked_effective_project_submission_artifact_policy_hash=SHA_A, + locked_pre_submit_checker_policy_id=new_record_id(), + locked_pre_submit_checker_bundle_hash=SHA_A, + locked_post_submit_checker_policy_id=new_record_id(), + locked_post_submit_checker_policy_version="post-v1", + locked_post_submit_checker_policy_hash=SHA_A, + locked_review_policy_id=new_record_id(), + locked_review_policy_generation=1, + locked_review_policy_hash=SHA_A, + locked_revision_policy_id=new_record_id(), + locked_revision_policy_generation=1, + locked_revision_policy_hash=SHA_A, + locked_contribution_policy_version_id=contribution_policy_version_id, + ) + + +def _source_values(**changes: object) -> dict[str, object]: + contribution_policy_version_id = new_record_id() + values: dict[str, object] = { + "id": new_record_id(), + "created_at": datetime(2026, 1, 2, tzinfo=UTC), + "project_id": new_record_id(), + "task_id": new_record_id(), + "submission_id": new_record_id(), + "submission_version": 1, + "assignment_id": new_record_id(), + "contributor_id": new_record_id(), + "contribution_policy_version_id": contribution_policy_version_id, + "checker_run_id": new_record_id(), + "evaluation_request_id": new_record_id(), + "request_digest": SHA_A, + "evaluation_generation": 1, + "result_id": new_record_id(), + "result_digest": SHA_B, + "completion_event_id": new_record_id(), + "execute_evidence_id": new_record_id(), + "finalize_evidence_id": new_record_id(), + "human_review_required": True, + "replica_id": new_record_id(), + "content_sha256": SHA_A, + "byte_count": 0, + "semantic_manifest_sha256": SHA_B, + "predecessor_submission_id": None, + "predecessor_submission_version": None, + "admission_id": new_record_id(), + "binding_id": new_record_id(), + "content_id": new_record_id(), + "locked_policy": _lineage(contribution_policy_version_id=contribution_policy_version_id), + "routing_recommendation": "allow_review", + } + values.update(changes) + return values + + +def _effects_values(**changes: object) -> dict[str, object]: + values: dict[str, object] = { + "project_id": new_record_id(), + "task_id": new_record_id(), + "assignment_id": new_record_id(), + "submission_id": new_record_id(), + "submission_version": 1, + "contributor_id": new_record_id(), + "contribution_policy_version_id": new_record_id(), + "content_id": new_record_id(), + "content_sha256": SHA_A, + "final_acceptance_id": new_record_id(), + "expected_task_status": "evaluation_pending", + } + values.update(changes) + return values + + +@pytest.mark.parametrize( + ("field", "value"), + ( + ("id", str(new_record_id())), + ("created_at", "2026-01-02T00:00:00Z"), + ("submission_version", "1"), + ("evaluation_generation", True), + ("request_digest", "a" * 64), + ("human_review_required", 1), + ("byte_count", False), + ("routing_recommendation", "needs_revision"), + ), +) +def test_source_contract_is_strict_and_detached(field: str, value: object) -> None: + with pytest.raises(ValidationError): + TaskPostSubmitManifestFacts(**_source_values(**{field: value})) + + valid = _source_values() + valid["private_provider_key"] = "must-not-cross-owner-boundary" + with pytest.raises(ValidationError): + TaskPostSubmitManifestFacts(**valid) + + +def test_source_contract_is_an_exact_frozen_value() -> None: + source = TaskPostSubmitManifestFacts(**_source_values()) + + with pytest.raises(ValidationError): + source.human_review_required = False + + assert set(TaskPostSubmitManifestFacts.model_fields) == { + "id", + "created_at", + "project_id", + "task_id", + "submission_id", + "submission_version", + "assignment_id", + "contributor_id", + "contribution_policy_version_id", + "checker_run_id", + "evaluation_request_id", + "request_digest", + "evaluation_generation", + "result_id", + "result_digest", + "completion_event_id", + "execute_evidence_id", + "finalize_evidence_id", + "human_review_required", + "replica_id", + "content_sha256", + "byte_count", + "semantic_manifest_sha256", + "predecessor_submission_id", + "predecessor_submission_version", + "admission_id", + "binding_id", + "content_id", + "locked_policy", + "routing_recommendation", + } + + +def test_source_rejects_nested_lineage_mismatch() -> None: + with pytest.raises(ValidationError, match="contribution policy lineage differs"): + TaskPostSubmitManifestFacts( + **_source_values(locked_policy=_lineage(contribution_policy_version_id=new_record_id())) + ) + + +@pytest.mark.parametrize("successor", (False, True)) +def test_source_rejects_equal_phase_receipts(successor: bool) -> None: + receipt_id = new_record_id() + changes: dict[str, object] = { + "execute_evidence_id": receipt_id, + "finalize_evidence_id": receipt_id, + } + if successor: + changes.update( + submission_version=2, + predecessor_submission_id=new_record_id(), + predecessor_submission_version=1, + ) + + with pytest.raises(ValidationError, match="phase receipts are not distinct"): + TaskPostSubmitManifestFacts(**_source_values(**changes)) + + +@pytest.mark.parametrize( + "changes", + ( + {"predecessor_submission_id": new_record_id()}, + {"predecessor_submission_version": 1}, + { + "submission_version": 1, + "predecessor_submission_id": new_record_id(), + "predecessor_submission_version": 1, + }, + {"submission_version": 2}, + { + "submission_version": 3, + "predecessor_submission_id": new_record_id(), + "predecessor_submission_version": 1, + }, + ), +) +def test_source_rejects_inconsistent_predecessor_shape( + changes: dict[str, object], +) -> None: + with pytest.raises(ValidationError, match="predecessor"): + TaskPostSubmitManifestFacts(**_source_values(**changes)) + + +def test_source_accepts_exact_immediate_predecessor() -> None: + predecessor_id = new_record_id() + source = TaskPostSubmitManifestFacts( + **_source_values( + submission_version=2, + predecessor_submission_id=predecessor_id, + predecessor_submission_version=1, + ) + ) + + assert source.predecessor_submission_id == predecessor_id + assert source.predecessor_submission_version == source.submission_version - 1 + + +def test_source_rejects_its_own_submission_as_predecessor() -> None: + submission_id = new_record_id() + with pytest.raises(ValidationError, match="predecessor equals submission"): + TaskPostSubmitManifestFacts( + **_source_values( + submission_id=submission_id, + submission_version=2, + predecessor_submission_id=submission_id, + predecessor_submission_version=1, + ) + ) + + +def test_false_source_value_is_transport_only() -> None: + source = TaskPostSubmitManifestFacts(**_source_values(human_review_required=False)) + + assert source.human_review_required is False + assert source.model_dump(mode="json")["human_review_required"] is False + + +@pytest.mark.parametrize( + ("field", "value"), + ( + ("project_id", str(new_record_id())), + ("submission_version", "1"), + ("submission_version", True), + ("content_sha256", "sha256:" + "A" * 64), + ("expected_task_status", "accepted"), + ("routing_manifest_id", new_record_id()), + ), +) +def test_accepted_effects_request_is_strict(field: str, value: object) -> None: + with pytest.raises(ValidationError): + TaskAcceptedEffectsRequest(**_effects_values(**{field: value})) + + +def test_accepted_effects_contract_is_source_neutral() -> None: + request = TaskAcceptedEffectsRequest(**_effects_values()) + review_request = TaskAcceptedEffectsRequest( + **_effects_values(expected_task_status="review_pending") + ) + result = TaskAcceptedEffectsResult( + request=request, + task_status="accepted", + assignment_status="completed", + ) + + assert result.request == request + assert review_request.expected_task_status == "review_pending" + assert set(TaskAcceptedEffectsRequest.model_fields) == set(_effects_values()) + assert set(TaskAcceptedEffectsResult.model_fields) == { + "request", + "task_status", + "assignment_status", + } + assert iscoroutinefunction(TaskAcceptedEffectsPort.apply_accepted_effects) + assert issubclass(TaskAcceptedEffectsUnavailable, RuntimeError) + with pytest.raises(ValidationError): + request.expected_task_status = "accepted" + with pytest.raises(ValidationError): + result.task_status = "review_pending" + for changes in ( + {"task_status": "review_pending"}, + {"assignment_status": "accepted"}, + {"review_id": new_record_id()}, + ): + values = { + "request": request, + "task_status": "accepted", + "assignment_status": "completed", + } + values.update(changes) + with pytest.raises(ValidationError): + TaskAcceptedEffectsResult(**values) + + +def test_source_foundation_has_no_runtime_entry() -> None: + assert post_submit_routing.__all__ == ("TaskPostSubmitManifestFacts",) + assert accepted_effects.__all__ == ( + "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsRequest", + "TaskAcceptedEffectsResult", + "TaskAcceptedEffectsUnavailable", + ) + for name in post_submit_routing.__all__ + accepted_effects.__all__: + assert getattr(task_api, name) is getattr( + post_submit_routing if name.startswith("TaskPostSubmit") else accepted_effects, + name, + ) + + assert { + name + for name, value in vars(post_submit_routing).items() + if isclass(value) and value.__module__ == post_submit_routing.__name__ + } == {"TaskPostSubmitManifestFacts"} + assert { + name + for name, value in vars(accepted_effects).items() + if isclass(value) and value.__module__ == accepted_effects.__name__ + } == { + "TaskAcceptedEffectsPort", + "TaskAcceptedEffectsRequest", + "TaskAcceptedEffectsResult", + "TaskAcceptedEffectsUnavailable", + } + for closed_enum, proposed_identifier in ( + (ActionId, "task.post_submit.route"), + (PermissionId, "task.post_submit.route"), + (ServiceIdentity, "workstream.task.post_submit_router"), + ): + with pytest.raises(ValueError): + closed_enum(proposed_identifier) diff --git a/backend/tests/tasks/post_submit_routing/test_migration.py b/backend/tests/tasks/post_submit_routing/test_migration.py new file mode 100644 index 000000000..0564b4855 --- /dev/null +++ b/backend/tests/tasks/post_submit_routing/test_migration.py @@ -0,0 +1,108 @@ +"""Actual predecessor upgrade proof for the route-neutral TASK source table.""" + +import asyncio + +import asyncpg +import pytest +from alembic import command + +from app.db import session as db_session +from tests.migration_fixtures import _config + +from .support import completed_source + + +pytestmark = pytest.mark.postgres_schema_contract + + +async def _snapshot(connection, h): + identifiers = { + "submission": h.request.submission_id, + "run": h.result.attempt_id, + "execute": h.source["execute_evidence_id"], + "finalize": h.source["finalize_evidence_id"], + "completion": h.source["completion_event_id"], + "task": h.request.task_id, + "assignment": h.request.assignment_id, + } + return { + "submission": await connection.fetchval( + "select to_jsonb(row_value)::text from submissions row_value where id=$1", + identifiers["submission"], + ), + "checker_run": await connection.fetchval( + "select to_jsonb(row_value)::text from checker_runs row_value where id=$1", + identifiers["run"], + ), + "checker_results": await connection.fetch( + "select to_jsonb(row_value)::text as value from checker_results row_value " + "where checker_run_id=$1 order by member_order", + identifiers["run"], + ), + "authority": await connection.fetch( + "select to_jsonb(row_value)::text as value from audit_events row_value " + "where id = any($1::uuid[]) order by id", + [identifiers["execute"], identifiers["finalize"]], + ), + "completion": await connection.fetchval( + "select to_jsonb(row_value)::text from outbox_events row_value where event_id=$1", + identifiers["completion"], + ), + "task": await connection.fetchval( + "select to_jsonb(row_value)::text from workstream_tasks row_value where id=$1", + identifiers["task"], + ), + "assignment": await connection.fetchval( + "select to_jsonb(row_value)::text from task_assignments row_value where id=$1", + identifiers["assignment"], + ), + "review_counts": tuple( + await connection.fetchrow( + "select " + "(select count(*) from review_queue_entries)," + "(select count(*) from review_admission_idempotency_records)," + "(select count(*) from review_leases)" + ) + ), + } + + +async def test_upgrade_preserves_existing_sources_without_publishing( + tmp_path, isolated_database_env, migration_lock +): + url = isolated_database_env.replace("+asyncpg", "") + with migration_lock(): + await db_session.dispose_engine() + connection = await asyncpg.connect(url) + try: + await connection.execute("drop schema public cascade; create schema public") + finally: + await connection.close() + await asyncio.to_thread(command.upgrade, _config(), "0010_post_submit_authority") + + async with completed_source(tmp_path, isolated_database_env) as h: + connection = await asyncpg.connect(url) + try: + assert await connection.fetchval( + "select version_num from alembic_version" + ) == "0010_post_submit_authority" + assert await connection.fetchval( + "select to_regclass('public.task_post_submit_routing_manifests')" + ) is None + before = await _snapshot(connection, h) + finally: + await connection.close() + + await asyncio.to_thread(command.upgrade, _config(), "0011_task_routing_source") + + connection = await asyncpg.connect(url) + try: + assert await connection.fetchval( + "select version_num from alembic_version" + ) == "0011_task_routing_source" + assert await connection.fetchval( + "select count(*) from task_post_submit_routing_manifests" + ) == 0 + assert await _snapshot(connection, h) == before + finally: + await connection.close() diff --git a/backend/tests/tasks/post_submit_routing/test_storage.py b/backend/tests/tasks/post_submit_routing/test_storage.py new file mode 100644 index 000000000..59c89fee7 --- /dev/null +++ b/backend/tests/tasks/post_submit_routing/test_storage.py @@ -0,0 +1,555 @@ +"""PostgreSQL proof for immutable route-neutral TASK source custody.""" + +from datetime import UTC, datetime + +import pytest +from sqlalchemy import func, select, text +from sqlalchemy.exc import DBAPIError, IntegrityError + +from app.core.identifiers import new_record_id +from app.modules.artifacts.models import ArtifactReplica +from app.modules.checkers.api.execution import FinalizeFacts +from app.modules.checkers.models import CheckerRun +from app.modules.checkers.post_submit_contracts import make_post_submit_result +from app.modules.projects.models import ProjectGuide +from app.modules.reviews.models import ReviewQueueEntry +from app.modules.tasks.api.transition_audit import TaskPolicyLineage +from app.modules.tasks.models import Submission, TaskAssignment, WorkstreamTask +from app.modules.tasks.post_submit_routing.models import TaskPostSubmitRoutingManifest +from tests.checkers.execution.support import live_executor, reserve + +from .support import ( + SOURCE_COLUMNS, + activate_successor_guide, + as_uuid, + completed_sibling_source, + completed_source, + insert_source, + joined_source_facts, + next_request, + other_hash, + source_count, + source_rows, + source_values, +) + + +pytestmark = pytest.mark.postgres_schema_contract + + +async def _reject(session, values, message: str) -> None: + with pytest.raises((DBAPIError, IntegrityError), match=message): + async with session.begin_nested(): + await insert_source(session, values) + + +async def test_source_matches_real_completed_run(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session, session.begin(): + before = await session.scalar(select(func.clock_timestamp())) + await insert_source(session, h.source) + after = await session.scalar(select(func.clock_timestamp())) + async with h.factory() as session: + stored = await session.get(TaskPostSubmitRoutingManifest, h.source["id"]) + assert stored is not None + values = {column: getattr(stored, column) for column in SOURCE_COLUMNS} + submission = await session.get(Submission, str(stored.submission_id)) + task = await session.get(WorkstreamTask, str(stored.task_id)) + facts = await joined_source_facts(h, values) + expected_lineage = TaskPolicyLineage( + locked_guide_version=submission.locked_guide_version, + locked_guide_source_snapshot_id=as_uuid( + submission.locked_guide_source_snapshot_id + ), + locked_guide_source_snapshot_hash=submission.locked_guide_source_snapshot_hash, + locked_effective_project_submission_artifact_policy_id=as_uuid( + submission.locked_effective_project_submission_artifact_policy_id + ), + locked_effective_project_submission_artifact_policy_hash=( + submission.locked_effective_project_submission_artifact_policy_hash + ), + locked_pre_submit_checker_policy_id=as_uuid( + submission.locked_pre_submit_checker_policy_id + ), + locked_pre_submit_checker_bundle_hash=( + submission.locked_pre_submit_checker_bundle_hash + ), + locked_post_submit_checker_policy_id=as_uuid( + submission.locked_post_submit_checker_policy_id + ), + locked_post_submit_checker_policy_version=( + submission.locked_post_submit_checker_policy_version + ), + locked_post_submit_checker_policy_hash=( + submission.locked_post_submit_checker_policy_hash + ), + locked_review_policy_id=as_uuid(submission.locked_review_policy_id), + locked_review_policy_generation=submission.locked_review_policy_generation, + locked_review_policy_hash=submission.locked_review_policy_hash, + locked_revision_policy_id=as_uuid(submission.locked_revision_policy_id), + locked_revision_policy_generation=( + submission.locked_revision_policy_generation + ), + locked_revision_policy_hash=submission.locked_revision_policy_hash, + locked_contribution_policy_version_id=as_uuid( + task.locked_contribution_policy_version_id + ), + ) + + assert set(values) == set(SOURCE_COLUMNS) + assert before <= values["created_at"] <= after + assert facts.model_dump(include=set(SOURCE_COLUMNS)) == { + column: as_uuid(value) if column.endswith("_id") else value + for column, value in values.items() + } + assert facts.project_id == h.request.project_id + assert facts.task_id == h.request.task_id + assert facts.assignment_id == h.request.assignment_id + assert facts.submission_id == h.request.submission_id + assert facts.submission_version == h.request.submission_version + assert facts.checker_run_id == h.result.attempt_id + assert facts.evaluation_request_id == h.request.evaluation_request_id + assert facts.request_digest == h.request.request_sha256 + assert facts.result_id == h.result.result_id + assert facts.result_digest == h.result.result_digest + assert facts.predecessor_submission_id is None + assert facts.predecessor_submission_version is None + assert facts.admission_id == h.created.admission_id + assert facts.binding_id == h.created.artifact_binding_id + assert facts.content_id == h.created.artifact_content_id + assert facts.locked_policy == expected_lineage + assert facts.routing_recommendation == "allow_review" + + +async def test_source_rejects_null_scalar(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session: + for field in (column for column in SOURCE_COLUMNS if column != "created_at"): + bad = h.source | {"id": new_record_id(), field: None} + await _reject(session, bad, f'null value in column "{field}"') + assert await source_count(session) == 0, field + await insert_source(session, h.source) + await session.commit() + async with h.factory() as session: + assert await source_count(session) == 1 + + +async def test_source_creation_time_is_database_owned( + tmp_path, isolated_database_env +): + async with completed_source(tmp_path, isolated_database_env) as h: + supplied_values = ( + ("past", datetime(2000, 1, 1, tzinfo=UTC)), + ("future", datetime(2100, 1, 1, tzinfo=UTC)), + ("null", None), + ) + for label, supplied in supplied_values: + async with h.factory() as session: + transaction = await session.begin() + try: + before = await session.scalar(select(func.clock_timestamp())) + source_id = new_record_id() + await insert_source( + session, + h.source | {"id": source_id, "created_at": supplied}, + ) + stored = await session.get(TaskPostSubmitRoutingManifest, source_id) + after = await session.scalar(select(func.clock_timestamp())) + assert before <= stored.created_at <= after, label + assert stored.created_at != supplied, label + finally: + await transaction.rollback() + async with h.factory() as session: + assert await source_count(session) == 0, label + + +async def test_source_rejects_scalar_substitution(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session: + other_replica = await session.scalar( + select(ArtifactReplica.id) + .where(ArtifactReplica.id != str(h.source["replica_id"])) + .limit(1) + ) + assert other_replica is not None + cases = ( + ("evaluation_request_id", new_record_id(), "checker source mismatch"), + ("request_digest", other_hash(h.source["request_digest"]), "checker source mismatch"), + ("evaluation_generation", h.source["evaluation_generation"] + 1, "checker source mismatch"), + ("result_id", new_record_id(), "checker source mismatch"), + ("result_digest", other_hash(h.source["result_digest"]), "checker source mismatch"), + ("content_sha256", other_hash(h.source["content_sha256"]), "material mismatch"), + ("byte_count", h.source["byte_count"] + 1, "material mismatch"), + ( + "semantic_manifest_sha256", + other_hash(h.source["semantic_manifest_sha256"]), + "material mismatch", + ), + ("replica_id", as_uuid(other_replica), "material mismatch"), + ("human_review_required", False, "review policy mismatch"), + ) + for field, replacement, message in cases: + bad = h.source | {"id": new_record_id(), field: replacement} + await _reject(session, bad, message) + assert await source_count(session) == 0, field + await insert_source(session, h.source) + await session.commit() + + +async def test_source_rejects_foreign_lineage(tmp_path, isolated_database_env): + async with completed_source(tmp_path / "one", isolated_database_env) as first: + sibling = await completed_sibling_source(first) + async with completed_source( + tmp_path / "two", + isolated_database_env, + provision_services=False, + storage_settings=first.settings, + ) as other: + async with first.factory() as session: + cases = ( + ("project_id", other.source["project_id"]), + ("task_id", sibling.source["task_id"]), + ("submission_id", sibling.source["submission_id"]), + ("submission_version", first.source["submission_version"] + 1), + ("assignment_id", sibling.source["assignment_id"]), + ("contributor_id", other.source["contributor_id"]), + ( + "contribution_policy_version_id", + other.source["contribution_policy_version_id"], + ), + ) + for field, replacement in cases: + bad = first.source | {"id": new_record_id(), field: replacement} + await _reject(session, bad, "source lineage mismatch") + assert await source_count(session) == 0, field + + await insert_source(session, sibling.source) + assert await source_count(session) == 1 + for label, ownership in ( + ("other_project", other.source), + ("same_project_sibling", sibling.source), + ): + coherent = first.source | { + "id": new_record_id(), + **{ + field: ownership[field] + for field in ( + "project_id", + "task_id", + "submission_id", + "submission_version", + "assignment_id", + "contributor_id", + "contribution_policy_version_id", + ) + }, + } + await _reject(session, coherent, "checker source mismatch") + assert await source_count(session) == 1, label + await insert_source(session, first.source) + await session.commit() + async with first.factory() as session: + assert await source_count(session) == 2 + + +async def _completed_successor(h): + await next_request(h) + await reserve(h) + result = await live_executor(h).evaluate_post_submission(h.request) + assert result.outcome == "completed" + async with h.factory() as session: + run = await session.get(CheckerRun, str(result.attempt_id)) + assert run.routing_recommendation == "allow_review" + h.result = result + return await source_values(h) + + +async def test_source_rejects_sibling_completion_event(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + original = dict(h.source) + sibling = await completed_sibling_source(h) + async with h.factory() as session: + await insert_source(session, sibling.source) + bad = original | { + "id": new_record_id(), + "completion_event_id": sibling.source["completion_event_id"], + } + await _reject(session, bad, "checker source mismatch") + assert await source_count(session) == 1 + await insert_source(session, original) + await session.commit() + async with h.factory() as session: + assert await source_count(session) == 2 + + +async def test_source_rejects_phase_receipt(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + original = dict(h.source) + successor = await _completed_successor(h) + cases = ( + ("execute", {"execute_evidence_id": successor["execute_evidence_id"]}), + ("finalize", {"finalize_evidence_id": successor["finalize_evidence_id"]}), + ( + "swap", + { + "execute_evidence_id": original["finalize_evidence_id"], + "finalize_evidence_id": original["execute_evidence_id"], + }, + ), + ) + async with h.factory() as session: + for label, changes in cases: + await _reject( + session, + original | {"id": new_record_id(), **changes}, + "checker source mismatch", + ) + assert await source_count(session) == 0, label + await insert_source(session, original) + await session.commit() + + +async def test_source_rejects_ineligible_checker_source(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + original = dict(h.source) + executor = live_executor(h) + await next_request(h) + reservation = await reserve(h) + queued = await source_values(h, reservation.attempt_id) + async with h.factory() as session: + await _reject( + session, + queued | {"id": new_record_id()}, + "checker source mismatch", + ) + assert await source_count(session) == 0 + + lease, replay = await executor._claim(h.request) + assert replay is None + running = await source_values(h, reservation.attempt_id) + async with h.factory() as session: + await _reject( + session, + running | {"id": new_record_id()}, + "checker source mismatch", + ) + assert await source_count(session) == 0 + + failure = make_post_submit_result( + request_id=h.request.evaluation_request_id, + request_digest=h.request.request_sha256, + attempt_id=reservation.attempt_id, + result_id=reservation.result_id, + evaluation_generation=h.request.evaluation_generation, + outcome="infrastructure_failed", + member_results=(), + infrastructure_failure_code="deadline_exceeded", + ) + await executor.finalize( + FinalizeFacts( + request=h.request, + lease=lease, + result=failure, + material=None, + output_binding_ids=(), + ) + ) + infrastructure_failed = await source_values(h, reservation.attempt_id) + async with h.factory() as session: + await _reject( + session, + infrastructure_failed | {"id": new_record_id()}, + "checker source mismatch", + ) + assert await source_count(session) == 0 + + empty_evidence = h.request.structural_input.model_copy(update={"evidence": ()}) + await next_request(h, structural_input=empty_evidence) + await reserve(h) + blocked_result = await live_executor(h).evaluate_post_submission(h.request) + async with h.factory() as session: + blocked_run = await session.get(CheckerRun, str(blocked_result.attempt_id)) + assert blocked_result.outcome == "completed" + assert blocked_run.routing_recommendation == "needs_revision" + blocking_completed = await source_values(h, blocked_result.attempt_id) + + async with h.factory() as session: + await _reject( + session, + blocking_completed | {"id": new_record_id()}, + "checker source mismatch", + ) + assert await source_count(session) == 0 + await insert_source(session, original) + await session.commit() + + +async def test_source_rejects_unactivated_guide(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session: + transaction = await session.begin() + try: + guide = await session.scalar( + select(ProjectGuide).where( + ProjectGuide.project_id == str(h.source["project_id"]), + ProjectGuide.version == h.request.expected_context.guide_version, + ) + ) + guide_id = guide.id + activation_operation_id = guide.activation_operation_id + assert guide.status == "active" + await session.execute( + text( + "ALTER TABLE public.project_guides " + "DISABLE TRIGGER guide_lineage_lifecycle_guard" + ) + ) + await session.execute( + text("UPDATE public.project_guides SET status='draft' WHERE id=:id"), + {"id": guide.id}, + ) + await _reject(session, h.source, "guide activation mismatch") + assert await source_count(session) == 0 + finally: + await transaction.rollback() + + async with h.factory() as session, session.begin(): + restored = await session.get(ProjectGuide, guide_id) + trigger_enabled = await session.scalar( + text( + "SELECT tgenabled='O' FROM pg_trigger " + "WHERE tgrelid='public.project_guides'::regclass " + "AND tgname='guide_lineage_lifecycle_guard'" + ) + ) + assert trigger_enabled is True + assert restored.status == "active" + assert restored.activation_operation_id == activation_operation_id + await insert_source(session, h.source) + async with h.factory() as session: + assert await source_count(session) == 1 + + +async def test_source_retains_historical_guide_and_generation( + tmp_path, isolated_database_env +): + async with completed_source(tmp_path, isolated_database_env) as h: + original = dict(h.source) + async with h.factory() as session: + guide = await session.scalar( + select(ProjectGuide).where( + ProjectGuide.project_id == str(original["project_id"]), + ProjectGuide.version == h.request.expected_context.guide_version, + ) + ) + guide_id = guide.id + activation = guide.activation_operation_id + + async with h.factory() as session, session.begin(): + await insert_source(session, original) + async with h.factory() as session: + stored = await session.get(TaskPostSubmitRoutingManifest, original["id"]) + before_values = { + column: getattr(stored, column) for column in SOURCE_COLUMNS + } + before_facts = await joined_source_facts(h, before_values) + + successor_guide = await activate_successor_guide(h) + successor_run = await _completed_successor(h) + async with h.factory() as session, session.begin(): + await insert_source(session, successor_run) + + assert successor_guide.command.target.proposal.guide_version == "v2" + assert successor_run["evaluation_generation"] == original["evaluation_generation"] + 1 + assert successor_run["checker_run_id"] != original["checker_run_id"] + async with h.factory() as session: + stored = await session.get(TaskPostSubmitRoutingManifest, original["id"]) + after_values = { + column: getattr(stored, column) for column in SOURCE_COLUMNS + } + successor_stored = await session.get( + TaskPostSubmitRoutingManifest, successor_run["id"] + ) + successor_values = { + column: getattr(successor_stored, column) for column in SOURCE_COLUMNS + } + retained = await session.get(ProjectGuide, guide_id) + assert retained.activation_operation_id == activation + assert retained.status == "superseded" + assert await source_count(session) == 2 + after_facts = await joined_source_facts(h, after_values) + successor_facts = await joined_source_facts(h, successor_values) + assert after_values == before_values + assert after_facts.model_dump() == before_facts.model_dump() + assert after_facts.locked_policy == before_facts.locked_policy + assert successor_facts.locked_policy == before_facts.locked_policy + + +async def test_source_is_immutable(tmp_path, isolated_database_env): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session, session.begin(): + await insert_source(session, h.source) + async with h.factory() as session: + before = await source_rows(session) + statements = ( + "UPDATE public.task_post_submit_routing_manifests SET human_review_required=false", + "DELETE FROM public.task_post_submit_routing_manifests", + "TRUNCATE public.task_post_submit_routing_manifests", + ) + for statement in statements: + async with h.factory() as session: + with pytest.raises(DBAPIError, match="source is immutable"): + await session.execute(text(statement)) + await session.commit() + await session.rollback() + assert await source_rows(session) == before + + +async def test_source_uniqueness_and_caller_rollback( + tmp_path, isolated_database_env +): + async with completed_source(tmp_path, isolated_database_env) as h: + async with h.factory() as session: + task_status = await session.scalar( + select(WorkstreamTask.status).where( + WorkstreamTask.id == str(h.source["task_id"]) + ) + ) + assignment_status = await session.scalar( + select(TaskAssignment.status).where( + TaskAssignment.id == str(h.source["assignment_id"]) + ) + ) + review_count = await session.scalar( + select(func.count()).select_from(ReviewQueueEntry) + ) + + async with h.factory() as session: + transaction = await session.begin() + try: + await insert_source(session, h.source) + duplicate = h.source | {"id": new_record_id()} + with pytest.raises(IntegrityError, match="uq_task_routing_manifest_source"): + await insert_source(session, duplicate) + finally: + await transaction.rollback() + + async with h.factory() as session: + assert await source_count(session) == 0 + assert await session.scalar( + select(WorkstreamTask.status).where( + WorkstreamTask.id == str(h.source["task_id"]) + ) + ) == task_status + assert await session.scalar( + select(TaskAssignment.status).where( + TaskAssignment.id == str(h.source["assignment_id"]) + ) + ) == assignment_status + assert await session.scalar( + select(func.count()).select_from(ReviewQueueEntry) + ) == review_count + await insert_source(session, h.source) + await session.commit() + async with h.factory() as session: + assert await source_count(session) == 1 diff --git a/backend/tests/test_alembic.py b/backend/tests/test_alembic.py index 71bfafb51..d5480eab9 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, "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, "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_behavior_ownership.py b/backend/tests/test_behavior_ownership.py index facb2ad10..51c149ef2 100644 --- a/backend/tests/test_behavior_ownership.py +++ b/backend/tests/test_behavior_ownership.py @@ -2188,3 +2188,25 @@ def test_post_submit_authority_partition_replacement_is_exact() -> None: ) with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): ownership._validate_additive_partition_transition(_partition(sorted(additions)), trusted) + + +def test_routing_source_registration_rejects_adjacent_runtime_targets(): + expected = { + "backend/app/modules/tasks/api/accepted_effects.py", + "backend/app/modules/tasks/api/post_submit_routing.py", + "backend/app/modules/tasks/post_submit_routing/models.py", + } + assert ownership.ARCH_04E1A_SOURCE_TARGETS == expected + 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/tasks/post_submit_routing/service.py", + "backend/app/modules/tasks/api/routing_authority.py", + ): + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership._validate_additive_partition_transition( + _partition(sorted({retained, *expected, neighbor})), trusted + ) diff --git a/backend/tests/test_ci_lane_catalogue.py b/backend/tests/test_ci_lane_catalogue.py index 61affafa8..1c9a03b43 100644 --- a/backend/tests/test_ci_lane_catalogue.py +++ b/backend/tests/test_ci_lane_catalogue.py @@ -178,6 +178,9 @@ def test_measured_hotspots_have_explicit_semantic_owners() -> None: "tests/authorization/submission_history/test_migration.py", "tests/authorization/submission_history/test_absence.py", "tests/authorization/submission_history/test_failures.py", + "tests/tasks/post_submit_routing/test_contracts.py", + "tests/tasks/post_submit_routing/test_storage.py", + "tests/tasks/post_submit_routing/test_migration.py", "tests/tasks/test_contribution_lineage.py", "tests/tasks/test_project_display.py", "tests/tasks/test_ready_queue.py", @@ -281,11 +284,24 @@ def test_measured_hotspots_have_explicit_semantic_owners() -> None: "tests/authorization/setup_finalization/test_resource_context.py", "tests/authorization/setup_finalization/test_structure.py", }.issubset(shared_a) + static_contracts = { + "tests/test_artifact_architecture.py", + "tests/architecture/test_module_boundaries.py", + "tests/architecture/test_authorization_boundary.py", + "tests/architecture/test_test_structure_boundary.py", + "tests/test_identifier_inventory.py", + "tests/test_record_id_collection.py", + "tests/test_ci_lane_catalogue.py", + "tests/test_ci_test_lanes.py", + "tests/test_test_lane_evidence.py", + "tests/test_merge_test_lane_evidence.py", + } + assert static_contracts.isdisjoint(shared_a | shared_b) assert { "tests/test_alembic.py", "tests/test_database_reset.py", runner.ADMIN_RUNNER_MODULE, - } == modules_by_lane["schema_contracts"] + } | static_contracts == modules_by_lane["schema_contracts"] assert { "tests/authorization/admin_access/test_bootstrap_cli.py", "tests/authorization/admin_access/test_api_journey.py", diff --git a/backend/tests/test_coverage_contract.py b/backend/tests/test_coverage_contract.py index b585f18a7..1ee497360 100644 --- a/backend/tests/test_coverage_contract.py +++ b/backend/tests/test_coverage_contract.py @@ -12,7 +12,7 @@ sys.path.insert(0, str(SCRIPTS)) import coverage_policy as policy # noqa: E402 -HEAD = "0010_post_submit_authority" +HEAD = "0011_task_routing_source" SHA = "a" * 40 PEP695_INVALID = sys.version_info < (3, 12) diff --git a/docs/architecture_data_model.md b/docs/architecture_data_model.md index d79956ffa..098487bb5 100644 --- a/docs/architecture_data_model.md +++ b/docs/architecture_data_model.md @@ -1745,22 +1745,38 @@ If added later, the readiness certificate records the exact checker run and server-generated manifest/binding identity that allowed a submission to enter human review. -For v0.1, the final current `CheckerRun` is the checker proof. Planned ARCH-04E -adds the TASK-owned immutable routing manifest that binds that result, exact -Submission/binding/policy/authority lineage and evaluation generation, plus a -separate current routing pointer. TASK publishes manifest, pointer and -`review_pending` atomically after consuming CHECKERS facts. This is the canonical -handoff to REV, not an optional signed ReadinessCertificate or a replacement -authorization system. Its implementation remains planned. Any submitted -artifact change requires a new Submission and checker run. +For v0.1, the final current `CheckerRun` is the checker proof. ARCH-04E1A adds +the TASK-owned immutable `task_post_submit_routing_manifests` table as +route-neutral source evidence. It binds the exact Submission/assignment/ +contributor/contribution-policy lineage, CHECKERS run/request/generation/result, +completion event and execute/finalize receipts, locked +`human_review_required` scalar, and canonical ART material identity. Detached +`TaskPostSubmitManifestFacts` additionally joins the predecessor, ART admission/ +binding/content anchors and complete locked policy lineage; its sole +recommendation is `allow_review`, which is evidence rather than permission. + +The table has no deployable writer or reader, current pointer, handler, routing +authority, TASK transition or acceptance effect. `TaskAcceptedEffectsPort` is a +source-neutral type-only Protocol for a later REV-owned shared acceptance +operation; no adapter or participant exists yet. The valid storage graph is +currently limited to true policy. False is proven only as a strict scalar DTO +value because guide activation still rejects it. + +Before publication, ARCH-04E1B/04E2 must harden this same table with mandatory +exact routing and owner-receipt custody. The migration must refuse every retained +pre-authority row; it may not backfill, mutate or delete one, and no parallel +manifest table is allowed. Later routing will publish the hardened source, +current pointer and `review_pending` transition atomically after currentness and +authority checks. Any submitted artifact change requires a new Submission and +checker run. ## ReviewQueueEntry And ReviewLease `ReviewQueueEntry` immutably anchors one exact finalized Submission/version, Task, project, and its current successful `allow_review` CheckerRun. The 03A1 -foundation does not yet implement the later ARCH-04E routing-manifest input; -live admission must consume that exact TASK handoff while retaining these -immutable CheckerRun/binding anchors. This adoption is an upstream dependency, +foundation does not yet consume the delivered ARCH-04E1A source table; +live admission must consume its later authority-hardened TASK handoff while +retaining these immutable CheckerRun/binding anchors. This adoption is an upstream dependency, not activation of REV behavior. The 03A1 foundation persists only `pending` and `closed` queue state plus open/preferred routing metadata; it exposes no route, selection behavior, or lease shape. diff --git a/docs/engineering/authorization_activation_custody.md b/docs/engineering/authorization_activation_custody.md index 626315a33..b5715ce8b 100644 --- a/docs/engineering/authorization_activation_custody.md +++ b/docs/engineering/authorization_activation_custody.md @@ -59,7 +59,7 @@ of every typed runtime `ActionOwner`. XINT-06B groups runtime `WS-AUTH-001-ART-06A` post-submit materialization and `WS-AUTH-001-ART-06B` output write/binding. ARCH-04D2 replaces that grouping for exact input/execute/finalize authority; output write/bind remains unavailable for the current zero-output catalogue. ARCH-04B hidden input and ARCH-04B2 hidden output custody -and ARCH-04C hidden durable execution/results are delivered; ARCH-04D1 canonical terminal material custody is delivered; ARCH-04D2 exact service authority is delivered; ARCH-04E1A routing-source facts are next. ARCH-04B2 owns +and ARCH-04C hidden durable execution/results are delivered; ARCH-04D1 canonical terminal material custody is delivered; ARCH-04D2 exact service authority and ARCH-04E1A source-only facts/types are delivered. ARCH-04E1A adds no AUTH action, route or handler; ARCH-04B2 owns fresh authority participants internally for each store, recovery and binding phase; public requests carry selectors and byte sources, never PREP handles. The CHECKERS zero-slot reservation reader is implemented. Output authority remains deny-only. Production materialization requires real diff --git a/docs/roadmap_status.md b/docs/roadmap_status.md index bf078863a..6485f95bd 100644 --- a/docs/roadmap_status.md +++ b/docs/roadmap_status.md @@ -108,10 +108,14 @@ put/verification. ARCH-04C supplies hidden durable evaluation, immutable ordered results, database-timed execution leases and atomic completion events. ARCH-04D2 supplies exact fixed-service input, execute and finalize authority and database-bound receipts; output-file authority remains unavailable. ARCH-04D1 enforces canonical ART material lineage for all terminal results retaining material; public intake remains deferred to -the later cutover prerequisites. Required success +the later cutover prerequisites. ARCH-04E1A adds one immutable route-neutral +TASK source table, detached source facts and a source-neutral type-only +accepted-effects Protocol. It adds no runtime writer, reader, handler, current +pointer, routing authority or acceptance behavior. Required success 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. Human review/revision, contribution and conditional +integrations remain planned; false has scalar DTO proof only and guide activation +still rejects it. Human review/revision, contribution and conditional compensation effects, operations and release proof complete v0.1. The [independent MCP package](../mcp_server/README.md) implements one profile @@ -132,7 +136,7 @@ implementation and policy binding and is not claimed live here. | Stage | Purpose and examples | Policy and execution boundary | Outcome | | --- | --- | --- | --- | | Pre-submission intake checks | Is this package acceptable to submit? Check completeness, required/forbidden files, evidence integrity, and configured intake-quality rules. | The locked `PreSubmitCheckerPolicy` and effective artifact policy drive the pre-submission catalogue during continuous artifact preparation, before a Submission exists. | Blocking failures return correction feedback and prevent Submission creation. Passing intake does not prove the task is accepted or ready for review. | -| Post-submission evaluation | Does the submitted work meet the configured task/project checks? Evaluate the exact stored work and evidence under the locked requirements. | The Submission-stamped `PostSubmitCheckerPolicy` drives the durable checker registry after immutable Submission creation. Only supported, registered checks execute. | Persist current evidence. Required success routes by the locked ReviewPolicy: true produces human `allow_review`; false invokes shared acceptance under TASK authority. CHECKERS never writes acceptance itself. Both routes remain planned. | +| Post-submission evaluation | Does the submitted work meet the configured task/project checks? Evaluate the exact stored work and evidence under the locked requirements. | The Submission-stamped `PostSubmitCheckerPolicy` drives the durable checker registry after immutable Submission creation. Only supported, registered checks execute. | Persist current evidence. The route-neutral TASK source schema exists, with publication still deferred. Later routing branches on the locked ReviewPolicy: true produces human `allow_review`; false invokes shared acceptance under TASK authority. CHECKERS never writes acceptance itself. Both routes remain planned. | The unified guide agent proposes both sets of policy bindings in one setup result. Trusted compilation, validation, and the governing approval path turn @@ -162,7 +166,7 @@ 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` | **Planned; immediate integration milestone** | 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 hidden exact verified Submission materialization; ARCH-04B2 hidden typed output store/recovery and verified binding; ARCH-04C hidden durable execution, exact current-result custody, zero-output support and atomic completion events; ARCH-04D1 canonical ART material custody; ARCH-04D2 exact materialization/execute/finalize authority and durable receipt custody | ARCH-04E1A supplies routing-source facts; ARCH-04E dispatches evaluation and publishes the canonical `allow_review` manifest | +| Post-submission evaluation and `allow_review` | **Hidden source foundation; 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 | Build shared REV-04B/CON-03C/07 and REV-12A/CON fence foundations, then 04E1B/04E2/04E3 dispatch and routing plus 04F remediation. Before publication, harden the same source table with mandatory exact route/owner receipts and refuse retained pre-authority rows. No 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 | Packet-membership contract and manifest; Review schema; canonical admission from `allow_review`; claim/lease/packet authority; lease copies the Submission-stamped policy version with no CON lookup | | Review decision and revision | **Planned** | Review/revision policy identities and mutation authority; approved same-task revision-rebase semantics | Immutable findings and decisions; `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 | Persist ContributionRecord/CompensationAward and 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 | @@ -428,7 +432,9 @@ their evidence is linked under [completed work](#what-has-been-completed). The [dependency and ownership plan](../.commitrail/initiatives/WS-ARCH-001/planning/PLAN.md#current-dependency-contract) owns implementation sequencing. The existing checker phase service supports hidden pre-submit execution/replay and hidden durable post-submit execution. -Exact post-submit service authority is implemented; automatic dispatch and routing remain unavailable. Live setup does not wait for downstream task/checker execution. +Exact post-submit service authority and ARCH-04E1A source-only facts/types are +implemented; automatic dispatch, routing and acceptance remain unavailable. +Live setup does not wait for downstream task/checker execution. The next dependency-safe product sequence is: @@ -456,9 +462,11 @@ The next dependency-safe product sequence is: ARCH-04D1 enforces canonical ART material lineage for every terminal result retaining material, with database rejection, rollback and safe-upgrade proof. ARCH-04D2 supplies exact input/execute/finalize service authority and durable - receipt custody. Next, ARCH-04E1A supplies routing-source facts; ARCH-04E then dispatches evaluation - and publishes an exact human `allow_review` manifest on true when no blocking - failure exists. + receipt custody. ARCH-04E1A supplies one immutable route-neutral source table, + detached facts and source-neutral accepted-effects types. It has no runtime + entry. Shared REV-04B/CON-03C/07 and the existing REV-12A/CON fence foundation + come next; ARCH-04E1B/04E2/04E3 then dispatch evaluation and publish an exact + human `allow_review` manifest on true when no blocking failure exists. 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 @@ -466,10 +474,10 @@ The next dependency-safe product sequence is: evaluators block the affected guide until implemented and included in a new approved catalogue-bound generation. Infrastructure retries and project setup faults are not contributor failures; `allow_review` is not acceptance. - **For the first false-policy acceptance path:** publish TASK 04E1A source - facts before REV-04B's source FK; complete CON-03C/07 and the existing shared - fence/controller slice, then wire one shared acceptance operation through - 04E1B/04E2/04E3. Prove real scoped activation/drain and 04F remediation before + **For the first false-policy acceptance path:** consume the delivered TASK + 04E1A source facts in REV-04B's source FK; complete CON-03C/07 and the existing + shared fence/controller slice, then wire one shared acceptance operation + through 04E1B/04E2/04E3. Prove real scoped activation/drain and 04F remediation before enabling false. This milestone creates the submitter contribution and applicable awards without live human queues/leases/decisions; it neither invents a reviewer nor removes the later human branch from v0.1. @@ -569,10 +577,13 @@ Delivered foundations (not a claim of full public integration) canonical contributor/manager Submission and checker history; obsolete gate removed ARCH-04D1 canonical ART material custody at terminal CHECKERS commit ARCH-04D2 exact input/execute/finalize authority + durable receipts + ARCH-04E1A immutable route-neutral TASK source facts + type-only effects port | v Remaining integration - routing-source facts (ARCH-04E1A) -> automatic dispatch/routing + remediation + shared REV-04B/CON-03C/07 + REV-12A/CON fence foundation + -> shared acceptance composition -> 04E1B/04E2/04E3 dispatch/routing + -> 04F remediation -> public intake and immutable admitted Submission cutover -> automatically consume the durable current result + required checks pass | @@ -621,6 +632,9 @@ v0.1 is not ready until all of the following are true: post-submit result. Locked true produces human `allow_review` when eligible; locked false with supported requirements invokes the shared atomic acceptance operation with no human Review/lease/reviewer contribution. +- Before either route is published, the existing TASK source table gains + mandatory exact routing and owner-receipt custody and rejects retained + pre-authority rows; no parallel manifest or permissive backfill is introduced. - A reviewer can claim only that admitted version, access only its bounded packet, and record one immutable final decision. - `needs_revision` safely continues or rebases the same assignment while @@ -686,7 +700,10 @@ remaining trace sequence is: and [ARCH-04B2 output custody](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04B2.md) and [ARCH-04C durable execution](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04C.md) and [ARCH-04D2 exact service authority](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04D2.md) - lead to `04E1A -> 04E1B -> 04E2 -> 04E3` for dispatch and routing. The mandatory + lead to delivered + [ARCH-04E1A source facts](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04E1A.md). + Shared REV-04B/CON-03C/07 and the REV-12A/CON fence foundation precede + `04E1B -> 04E2 -> 04E3` dispatch and routing; 04F supplies remediation. The mandatory [04D1 canonical material custody](../.commitrail/initiatives/WS-ARCH-001/WS-ARCH-001-04D1.md) closes the three-field ART database guarantee before authority activation. ARCH-04C accepts the exact empty output set from the current structural @@ -695,10 +712,11 @@ remaining trace sequence is: delivery/recovery scans over CON-02B. Delivery termination is bounded by a 300-second hard limit under prefork. ARCH-03C2 enforces a dedicated non-eager prefork delivery queue. - Automatic `04E` delivery still needs its + ARCH-04E1A proves source storage and detached contracts only; its false scalar + proof does not make false activation available. Automatic `04E` delivery still needs its separately authorized checker-routing handler; the installed assignment handler does not supply that authority. These foundations do not require REV or fulfillment. - `04E` is hidden TASK handler `04E1`, exact AUTH activation `04E2`, then live + Remaining `04E` is hidden TASK handler `04E1B`, exact AUTH activation `04E2`, then 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 diff --git a/docs/spec_authorization_service.md b/docs/spec_authorization_service.md index ef1126552..e91accb45 100644 --- a/docs/spec_authorization_service.md +++ b/docs/spec_authorization_service.md @@ -539,7 +539,9 @@ In particular, the XINT-06B grouping corresponds to runtime `WS-AUTH-001-ART-06B` for checker-output write/binding. The current replacement activation contract is ARCH-04D2. Exact post-submit input, execute and finalize authority is delivered; checker-output write/bind remains planned because the -current catalogue produces no output files. ARCH-04E1A routing-source facts are next. This does not reopen XINT-06B as a parallel implementation lane. Likewise ARCH-02G/02H +current catalogue produces no output files. ARCH-04E1A route-neutral source +facts and type-only accepted-effects contracts are delivered without an action, +handler, current pointer or runtime composition. This does not reopen XINT-06B as a parallel implementation lane. Likewise ARCH-02G/02H are replacement implementation boundaries, not automatic registry renames. Read exact runtime ownership from the typed catalogue. No planning-only change may promote or reassign an action. diff --git a/docs/spec_chunk_4_task_queue_assignment.md b/docs/spec_chunk_4_task_queue_assignment.md index 67fe7a3cf..d51f84b33 100644 --- a/docs/spec_chunk_4_task_queue_assignment.md +++ b/docs/spec_chunk_4_task_queue_assignment.md @@ -136,6 +136,14 @@ Pre-submission intake failures prevent Submission creation. Post-submission evaluation concerns the submitted work and supplies evidence for policy-governed routing; it does not own final acceptance. +ARCH-04E1A persists that successful evidence in one immutable route-neutral +TASK source table and exposes detached source facts plus a source-neutral, +type-only accepted-effects Protocol. It installs no writer, reader, handler, +current pointer, routing authority, TASK transition or acceptance participant. +Before either true human admission or false automatic acceptance is published, +the remaining ARCH-04E work must harden the same table with mandatory exact +routing and owner-receipt custody and reject retained pre-authority rows. + ## Required verification - Real exact-project grants permit the supported commands without a worker diff --git a/docs/spec_contribution_compensation.md b/docs/spec_contribution_compensation.md index 7ac8615a6..f56c01b21 100644 --- a/docs/spec_contribution_compensation.md +++ b/docs/spec_contribution_compensation.md @@ -49,6 +49,9 @@ The false branch requires the exact current successful routing manifest, locked `human_review_required=false` policy and originating AUTH decision event under the [shared acceptance contract](spec_review_lifecycle.md#finalacceptance). Required-check success or raw checker output alone cannot create FinalAcceptance. +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 boundary MUST preserve four distinct facts: @@ -1134,8 +1137,8 @@ The core dependency order is a partial order. Persistence and flush-only 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: TASK ARCH-04E1A source schema/public facts precede -REV-04B acceptance persistence, then CON-03C/07 plus the existing shared fence +governs the false branch: delivered TASK ARCH-04E1A source schema/detached facts +precede REV-04B acceptance persistence, then CON-03C/07 plus the existing shared fence foundation, then the shared operation and ARCH-04E1B/AUTH routing composition. False guide activation follows joint proof. A stable Review FK target is not live ReviewLease/queue/decision behavior. The shared lifecycle/obligation diff --git a/docs/spec_review_lifecycle.md b/docs/spec_review_lifecycle.md index 82246db04..968b573cf 100644 --- a/docs/spec_review_lifecycle.md +++ b/docs/spec_review_lifecycle.md @@ -266,8 +266,9 @@ earlier Submission and ReviewLease lineage remains immutable. Only a durable, final, current post-submit CheckerRun outcome of `allow_review` may admit the exact immutable Submission to human review. Admission records the -exact CheckerRun ID and verified binding facts through the TASK-owned canonical -`allow_review` routing manifest delivered by ARCH-04E. The manifest binds the +exact CheckerRun ID and verified binding facts through the later authority- +hardened TASK-owned canonical `allow_review` routing manifest delivered by the +remaining ARCH-04E sequence. The manifest binds the current evaluation generation/result and immutable Submission; it does not replace CHECKERS truth or grant review authority. REV validates the current TASK handoff through its public port before recording admission. A retry, supersession, or @@ -483,10 +484,13 @@ ReviewPolicy governing that Submission. `acceptance_source` is provenance, not a configurable policy or workflow mode: `human_review` requires `source_review_id` and forbids `source_routing_manifest_id`; `task_post_submit_route` requires -`source_routing_manifest_id` and forbids `source_review_id`. The existing TASK -manifest identifies the exact Submission, run, request/generation, final-result -hash, output bindings and persisted authority-event references. Do not create -an AutomatedDecision table or copy checker results into REV. The AUTH event +`source_routing_manifest_id` and forbids `source_review_id`. The delivered +ARCH-04E1A TASK source identifies the exact Submission, run, +request/generation, final-result and canonical material lineage, but it is not +routing authority. Before runtime REV acceptance may consume it, ARCH-04E1B/04E2 must harden the +same table with mandatory exact route and owner-receipt custody and refuse +retained pre-authority rows. Do not create a second manifest, an +AutomatedDecision table or copied checker results in REV. The later AUTH event reference binds the exact source, operation, actor and resource; a checker finalization allow cannot substitute for routing authority. @@ -546,10 +550,12 @@ the same outcome; a changed envelope under the same identity denies. Extract foundations from existing owner work, not a new initiative: -1. ARCH-04E1A's TASK manifest persistence/public scalar facts and narrow accepted - effects port follow CHECKERS-04C facts, without routing handlers or REV FKs. - One manifest stores the locked `human_review_required` branch; it is not - restricted to human admission. This schema precedes the REV source FK. +1. ARCH-04E1A's TASK manifest persistence/detached scalar facts and narrow + accepted-effects Protocol are delivered after CHECKERS-04C facts, without a + writer, reader, current pointer, routing handler, effects implementation or + REV FK. One manifest stores the locked `human_review_required` branch; it is + not restricted to human admission. False proof is scalar transport only + while activation remains unavailable. This schema precedes the REV source FK. 2. REV-04B shared source/FinalAcceptance persistence follows that schema, then CON-03C contribution/award persistence and CON-07 submitter participation. A Review FK target may require a table, not live claim or review endpoints. @@ -571,7 +577,7 @@ manifest schema depend on its later handler or the early shared fence depend on live human review. Human runtime later adds its decision/lease proof and reuses the same operation. Fulfillment/read endpoints remain downstream. -Future implementation tests (not executed by this planning change): +Remaining implementation tests: | Owner / future test | Required discriminating proof | |---|---|