From 0c8272ccbf697b41b74b8debefe212016526a295 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 00:43:13 +0100 Subject: [PATCH 01/27] refactor(artifacts): expose preparation public boundary --- ...ARCH-001-02D-art-preparation-public-api.md | 52 ++- .../IMPORT_LEDGER.md | 10 - .../TEST_STRUCTURE_DEBT.json | 14 +- .../private-edge-debt.v1.json | 204 ----------- backend/app/adapters/artifacts/__init__.py | 33 +- backend/app/adapters/checkers/__init__.py | 74 ++++ backend/app/adapters/projects/__init__.py | 13 + backend/app/adapters/tasks/__init__.py | 11 + backend/app/api/deps/authorization.py | 18 + backend/app/api/router.py | 2 + .../app/api/routes/artifact_submissions.py | 111 ++++++ backend/app/interfaces/artifact_operations.py | 59 ---- backend/app/modules/artifacts/api/__init__.py | 17 + .../artifacts/api/submission_preparation.py | 54 +++ .../app/modules/artifacts/authorization.py | 9 +- .../modules/artifacts/pre_submit_evidence.py | 319 ++++++++++++++---- .../modules/artifacts/submission_admission.py | 198 +++++++---- .../artifacts/submission_authorization.py | 35 +- .../artifacts/submission_materialization.py | 179 +++++++--- backend/app/modules/tasks/router.py | 103 +----- backend/scripts/module_boundaries.py | 13 + .../architecture/test_module_boundaries.py | 31 +- backend/tests/pre_submit_test_helpers.py | 71 ++++ backend/tests/test_artifact_architecture.py | 17 +- .../test_default_pre_submit_execution.py | 193 ++++++----- .../test_effective_pre_submit_execution.py | 47 +-- .../tests/test_submission_bundle_admission.py | 144 ++++---- 27 files changed, 1205 insertions(+), 826 deletions(-) create mode 100644 backend/app/adapters/checkers/__init__.py create mode 100644 backend/app/adapters/projects/__init__.py create mode 100644 backend/app/adapters/tasks/__init__.py create mode 100644 backend/app/api/routes/artifact_submissions.py create mode 100644 backend/app/modules/artifacts/api/__init__.py create mode 100644 backend/app/modules/artifacts/api/submission_preparation.py create mode 100644 backend/tests/pre_submit_test_helpers.py diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index d19a29c6d..c9953e245 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -39,22 +39,54 @@ PROJECT, and CHECKER public fact manifests. ```text backend/app/modules/artifacts/api/** +backend/app/modules/artifacts/authorization.py +backend/app/modules/artifacts/router.py +backend/app/modules/artifacts/pre_submit_evidence.py backend/app/modules/artifacts/submission_admission.py backend/app/modules/artifacts/submission_authorization.py +backend/app/modules/artifacts/submission_materialization.py backend/app/modules/artifacts/schemas.py backend/app/adapters/artifacts/__init__.py +backend/app/adapters/checkers/__init__.py +backend/app/adapters/projects/__init__.py +backend/app/adapters/tasks/__init__.py +backend/app/api/deps/authorization.py backend/app/interfaces/artifact_operations.py backend/app/modules/tasks/router.py +backend/app/api/router.py +backend/app/api/routes/artifact_submissions.py backend/tests/architecture/test_module_boundaries.py +backend/scripts/module_boundaries.py +backend/tests/test_artifact_architecture.py backend/tests/test_submission_bundle_admission.py backend/tests/test_default_pre_submit_execution.py +backend/tests/pre_submit_test_helpers.py .ci/module-boundaries/private-edge-debt.v1.json +.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md +.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json .ci/behavior-ownership/** +.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md docs/spec_artifact_storage_service.md ``` +## Public and private contract split + +- `artifacts.api` owns only dependency-safe route-facing request/result/error + values and capability ports. It does not re-export private ART scratch, + inspection, custody, manifest, pass-capability, repository, session, or + provider types. +- Process-local sealed preparation and materialization values move out of the + repository-global `app.interfaces.artifact_operations` surface and remain + ART-private with one physical definition. +- AUTH handles remain opaque `object` values at ART seams. ART does not import, + reconstruct, serialize, or inspect AUTH's private handle class; the concrete + transaction-bound AUTH adapter owns validation and consumption. +- The composition root may import concrete TASK, PROJECT, CHECKER, AUTH, and + ART implementations to construct public ports. Route-facing and product + module code may depend only on owner public APIs. + ## Not allowed Action activation; public OpenAPI exposure; Submission creation/binding; @@ -64,7 +96,9 @@ cross-module imports; serialized prepared handles; compatibility facades. ## Acceptance criteria - [ ] The route and composition code depend on `artifacts.api`; ART depends on - owner public APIs only. + owner public APIs only. The composition root may instantiate concrete + implementations but may not hide a service locator or second factory + behind the public API. - [ ] The public port shape places preflight before byte acceptance and final prepared-authority consumption in the durable-intent transaction before capacity, put attempt, or provider I/O, but production remains deny-only @@ -74,9 +108,19 @@ cross-module imports; serialized prepared handles; compatibility facades. - [ ] `artifact.submission_bundle.prepare` remains planned/unavailable and the route remains hidden. - [ ] Every touched private edge is removed. -- [ ] Submission-preparation types in - `app.interfaces.artifact_operations` migrate to `artifacts.api`; no - parallel legacy/public ART contract remains. +- [ ] Route-facing submission-preparation types in + `app.interfaces.artifact_operations` migrate to `artifacts.api`. + Process-local sealed types become ART-private; no parallel legacy/public + ART contract remains. +- [ ] After byte materialization and before evidence persistence, ART re-locks + TASK assignment/predecessor facts and PROJECT locked-policy facts through + their public ports and compares them with the original CHECKER plan + lineage. Stale lineage fails before durable evidence or provider I/O. +- [ ] CHECKER execution is injected at the composition root and returns only + public CHECKER result facts. ART constructs and retains its own custody + facts; neither module imports the other's private implementation. +- [ ] `artifacts.api` imports no private module and exposes no ORM, session, + repository, provider, scratch path, byte buffer, or serialized handle. - [ ] Record the exact preparation resource/port manifest in `.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md`. diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md index 5b96a732d..17f1d99e0 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md @@ -66,14 +66,6 @@ backend/app/modules/artifacts/service.py app.modules.authorization.catalogue app.modules.authorization.prepared app.modules.authorization.runtime -backend/app/modules/artifacts/submission_admission.py - app.modules.authorization.prepared -backend/app/modules/artifacts/submission_authorization.py - app.modules.authorization.prepared - app.modules.authorization.runtime -backend/app/modules/artifacts/submission_materialization.py - app.modules.authorization.catalogue - app.modules.authorization.prepared backend/app/modules/projects/authorization_reads.py app.modules.authorization.catalogue app.modules.authorization.kernel @@ -115,8 +107,6 @@ backend/app/modules/projects/sufficiency_mutation_service.py app.modules.authorization.runtime backend/app/workers/project_setup.py app.modules.authorization.prepared -backend/app/modules/tasks/router.py - app.modules.authorization.runtime ``` The ledger validator decodes numeric Markdown entities before comparing exact diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index aa0964b01..1b45f5363 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "ca1f6ec87557ec015061506dd7a1e9562b129bdde8ce82150664c459756566a3", - "end_line": 1728, + "content_sha256": "07f1e5ebe7404bb7cca904259434abb10abbd783808aee5e1e8d0a55b7e548d0", + "end_line": 1723, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1728, + "observed_lines": 1723, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -758,15 +758,15 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "1ef3d6ee06ed0999a5752b168ba34b0a2b847793b386bc20fe72ecc335234bc5", - "end_line": 1132, + "content_sha256": "4cf5ee426ef4eab663019f2315012ace5a5d04444ea2cc5f713254ce1f268fe1", + "end_line": 1130, "hard_limit": 120, "kind": "test_function", - "observed_lines": 701, + "observed_lines": 697, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 432 + "start_line": 434 }, { "capability": "unassigned_legacy_auth", diff --git a/.ci/module-boundaries/private-edge-debt.v1.json b/.ci/module-boundaries/private-edge-debt.v1.json index a30ef18dc..7d26608b0 100644 --- a/.ci/module-boundaries/private-edge-debt.v1.json +++ b/.ci/module-boundaries/private-edge-debt.v1.json @@ -7,96 +7,12 @@ "imported_private_path": "app.modules.actors.service_identities", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.authorization", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.preparation", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.schemas", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.service", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_admission", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_archive", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_authorization", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/__init__.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_materialization", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/adapters/artifacts/internal_workers.py", "target_module": "actors", "imported_private_path": "app.modules.actors.service_identities", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.authorization", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.guide_setup", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.models", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.preparation", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.schemas", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/internal_workers.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.service", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/adapters/artifacts/internal_workers.py", "target_module": "projects", @@ -109,30 +25,6 @@ "imported_private_path": "app.modules.projects.models", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/adapters/artifacts/local.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.preparation", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/local.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.sources", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/s3_compatible.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.preparation", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/adapters/artifacts/s3_compatible.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.sources", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/api/deps/api_controls.py", "target_module": "api_controls", @@ -235,36 +127,6 @@ "imported_private_path": "app.modules.projects.service", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/interfaces/artifact_operations.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.sources", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/interfaces/artifact_operations.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_archive", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/interfaces/artifact_operations.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_manifest", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/interfaces/artifact_operations.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.effective_plan", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/interfaces/artifact_operations.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.pre_submit_execution", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/interfaces/artifacts.py", "target_module": "artifacts", @@ -373,24 +235,6 @@ "imported_private_path": "app.modules.tasks.models", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/modules/artifacts/pre_submit_evidence.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.effective_plan", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/artifacts/pre_submit_evidence.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.pre_submit_execution", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/artifacts/pre_submit_evidence.py", - "target_module": "tasks", - "imported_private_path": "app.modules.tasks.pre_submit_context", - "repair_owner": "WS-ARCH-001-03" - }, { "source_file": "backend/app/modules/artifacts/repository.py", "target_module": "checkers", @@ -445,36 +289,6 @@ "imported_private_path": "app.modules.tasks.pre_submit_context", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/modules/artifacts/submission_admission.py", - "target_module": "tasks", - "imported_private_path": "app.modules.tasks.pre_submit_context", - "repair_owner": "WS-ARCH-001-03" - }, - { - "source_file": "backend/app/modules/artifacts/submission_materialization.py", - "target_module": "actors", - "imported_private_path": "app.modules.actors.service_identities", - "repair_owner": "WS-ARCH-001-03" - }, - { - "source_file": "backend/app/modules/artifacts/submission_materialization.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.catalogue", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/artifacts/submission_materialization.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.effective_plan", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/artifacts/submission_materialization.py", - "target_module": "checkers", - "imported_private_path": "app.modules.checkers.pre_submit_execution", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/modules/audit/repository.py", "target_module": "tasks", @@ -721,24 +535,6 @@ "imported_private_path": "app.modules.actors.service", "repair_owner": "WS-ARCH-001-03" }, - { - "source_file": "backend/app/modules/tasks/router.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.authorization", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/tasks/router.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.schemas", - "repair_owner": "WS-ARCH-001-04" - }, - { - "source_file": "backend/app/modules/tasks/router.py", - "target_module": "artifacts", - "imported_private_path": "app.modules.artifacts.submission_admission", - "repair_owner": "WS-ARCH-001-04" - }, { "source_file": "backend/app/modules/tasks/service.py", "target_module": "actors", diff --git a/backend/app/adapters/artifacts/__init__.py b/backend/app/adapters/artifacts/__init__.py index 02b6ef0ec..ad4d457a6 100644 --- a/backend/app/adapters/artifacts/__init__.py +++ b/backend/app/adapters/artifacts/__init__.py @@ -11,8 +11,13 @@ from app.core.config import Settings from app.core.api_controls import request_ids +from app.api.deps.authorization import get_authorization_actor_identity +from app.adapters.checkers import PreSubmitCheckerExecutionAdapter +from app.adapters.projects import project_locked_policy_context_port +from app.adapters.tasks import task_submission_context_port from app.db.session import get_db_session from app.interfaces.artifact_operations import GuideArtifactIngestCommand +from app.modules.artifacts.api import SubmissionBundlePreparationCommand from app.interfaces.artifacts import ( ARTIFACT_STORE_CAPABILITY_KEY, ArtifactConfigurationError, @@ -30,7 +35,6 @@ from app.modules.artifacts.submission_authorization import ( SubmissionBundlePreparationAuthorization, ) -from app.modules.artifacts.submission_admission import SubmissionBundlePreparationCommand from app.modules.artifacts.schemas import ( ArtifactInternalAuthority, ) @@ -40,6 +44,15 @@ get_guide_artifact_prepared_authorization, ) from app.modules.actors.service_identities import ServiceIdentity +from app.modules.authorization.api import ActorIdentityFacts +async def get_submission_bundle_preparation_actor( + actor: Annotated[ + ActorIdentityFacts, + Depends(get_authorization_actor_identity), + ], +) -> ActorIdentityFacts: + """Return dependency-safe AUTH facts to the route-facing ART command.""" + return actor def create_artifact_store_bootstrap(settings: Settings) -> ArtifactStoreBootstrap: @@ -249,9 +262,10 @@ def get_submission_bundle_preparation_command( PreparedSubmissionBundlePreparationCommand, SubmissionBundlePreparationRuntime, ) - settings = request.app.state.settings request_id, correlation_id = (UUID(value) for value in request_ids(request)) + task_contexts = task_submission_context_port(session) + project_contexts = project_locked_policy_context_port(session) @asynccontextmanager async def runtime(): @@ -273,6 +287,11 @@ async def runtime(): ) preparation = ArtifactPreparationService(manager) catalogue = request.app.state.pre_submission_checker_catalogue + inspector = SubmissionArchiveInspector(submission_archive_limits(settings)) + checker_execution = PreSubmitCheckerExecutionAdapter( + catalogue=catalogue, + archive_inspector=inspector, + ) storage_schemes = {"local": "local", "s3_compatible": "s3"} try: storage_scheme = storage_schemes[settings.artifact_store_backend] @@ -281,8 +300,7 @@ async def runtime(): materialization = PreparedBundleMaterializationService( authorization=materialization_authority, preparation=preparation, - archive_inspector=SubmissionArchiveInspector(submission_archive_limits(settings)), - catalogue=catalogue, + checker_execution=checker_execution, storage_scheme=storage_scheme, ) admission = ArtifactAdmissionService(session, settings, namespace) @@ -295,12 +313,15 @@ async def runtime(): ) yield SubmissionBundlePreparationRuntime( preparation=preparation, - inspector=SubmissionArchiveInspector(submission_archive_limits(settings)), + inspector=inspector, catalogue=catalogue, materialization=materialization, evidence=PreparedBundlePreSubmitEvidenceService( session=session, materialization=materialization, + preparation_authorization=authority, + task_contexts=task_contexts, + project_contexts=project_contexts, ), durable_put=SubmissionBundleDurablePutService( session=session, @@ -317,6 +338,8 @@ async def runtime(): return PreparedSubmissionBundlePreparationCommand( session=session, authority=authority, + task_contexts=task_contexts, + project_contexts=project_contexts, runtime_factory=runtime, ) diff --git a/backend/app/adapters/checkers/__init__.py b/backend/app/adapters/checkers/__init__.py new file mode 100644 index 000000000..ce8918f13 --- /dev/null +++ b/backend/app/adapters/checkers/__init__.py @@ -0,0 +1,74 @@ +"""CHECKER-owned composition adapters.""" + +from typing import Any, BinaryIO, Protocol +from pathlib import Path + +from app.modules.checkers.api import ( + PreSubmissionExecutionFacts, + PreSubmissionInfrastructureUnavailableError, +) +from app.modules.checkers.catalogue import PreSubmissionCheckerCatalogue +from app.modules.checkers.pre_submit_execution import ( + DefaultPreSubmissionExecutionInput, + EffectivePreSubmissionProcessor, + PreSubmissionInfrastructureUnavailable, +) + + +class _ExecutionRequest(Protocol): + plan: Any + commitment: Any + inspection: Any + manifest: Any + change_gate: Any + packet: Any + prepared_generation_id: Any + storage_scheme: str + + +class _PublicFactsCheckerProcessor: + """Project the private CHECKER processor result to public facts.""" + + def __init__(self, processor: EffectivePreSubmissionProcessor) -> None: + self._processor = processor + + def abort(self) -> None: + self._processor.abort() + + def process_blocking( + self, reader: BinaryIO, workspace: Path + ) -> PreSubmissionExecutionFacts: + try: + return self._processor.process_blocking(reader, workspace).bounded_facts() + except PreSubmissionInfrastructureUnavailable as exc: + raise PreSubmissionInfrastructureUnavailableError(str(exc)) from exc + + +class PreSubmitCheckerExecutionAdapter: + """Build the private CHECKER processor behind dependency-safe facts.""" + + def __init__(self, *, catalogue: PreSubmissionCheckerCatalogue, archive_inspector: Any): + self._catalogue = catalogue + self._archive_inspector = archive_inspector + + @property + def catalogue_manifest_sha256(self) -> str: + return self._catalogue.manifest_sha256 + + def build(self, request: _ExecutionRequest) -> _PublicFactsCheckerProcessor: + return _PublicFactsCheckerProcessor( + EffectivePreSubmissionProcessor( + archive_inspector=self._archive_inspector, + catalogue=self._catalogue, + execution_input=DefaultPreSubmissionExecutionInput( + plan=request.plan, + commitment=request.commitment, + inspection=request.inspection, + manifest=request.manifest, + change_gate=request.change_gate, + packet=request.packet, + prepared_generation_id=request.prepared_generation_id, + storage_scheme=request.storage_scheme, + ), + ) + ) diff --git a/backend/app/adapters/projects/__init__.py b/backend/app/adapters/projects/__init__.py new file mode 100644 index 000000000..27ace0515 --- /dev/null +++ b/backend/app/adapters/projects/__init__.py @@ -0,0 +1,13 @@ +"""PROJECT-owned composition adapters.""" + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.projects.api import ProjectLockedPolicyContextPort +from app.modules.projects.locked_policy_repository import ProjectLockedPolicyRepository + + +def project_locked_policy_context_port( + session: AsyncSession, +) -> ProjectLockedPolicyContextPort: + """Bind the public PROJECT locked-policy port to its repository.""" + return ProjectLockedPolicyRepository(session) diff --git a/backend/app/adapters/tasks/__init__.py b/backend/app/adapters/tasks/__init__.py new file mode 100644 index 000000000..b78d1a5eb --- /dev/null +++ b/backend/app/adapters/tasks/__init__.py @@ -0,0 +1,11 @@ +"""TASK-owned composition adapters.""" + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.modules.tasks.api import TaskSubmissionContextPort +from app.modules.tasks.repository import TaskRepository + + +def task_submission_context_port(session: AsyncSession) -> TaskSubmissionContextPort: + """Bind the public TASK submission-context port to its repository.""" + return TaskRepository(session) diff --git a/backend/app/api/deps/authorization.py b/backend/app/api/deps/authorization.py index cf78d2c0e..e8394d408 100644 --- a/backend/app/api/deps/authorization.py +++ b/backend/app/api/deps/authorization.py @@ -29,6 +29,7 @@ ) from app.modules.api_controls.service import FIRST_ACCESS_SCOPE, RateControlService from app.modules.authorization.kernel import AuthorizationService +from app.modules.authorization.api import ActorIdentityFacts, ActorKind as PublicActorKind from app.modules.authorization.prepared import PreparedAuthorizationService from app.modules.authorization.repository import AdminAuthorizationRepository from app.modules.authorization.runtime import ( @@ -85,6 +86,23 @@ async def get_authorization_actor( return await resolve_authorization_actor(request, result, session, rate_control) +async def get_authorization_actor_identity( + resolved: Annotated[ResolvedActor, Depends(get_authorization_actor)], +) -> ActorIdentityFacts: + """Project the resolved request actor into the dependency-safe AUTH facts.""" + service_identity = resolved.profile.service_identity + return ActorIdentityFacts( + actor_profile_id=UUID(resolved.profile.id), + identity_link_id=UUID(resolved.identity_link.id), + actor_kind=( + PublicActorKind.SERVICE + if resolved.profile.actor_kind == ActorKind.SERVICE + else PublicActorKind.HUMAN + ), + service_identity=service_identity, + ) + + async def resolve_authorization_actor( request: Request, result: AuthVerificationResult, diff --git a/backend/app/api/router.py b/backend/app/api/router.py index 92a61445c..b4f08564f 100644 --- a/backend/app/api/router.py +++ b/backend/app/api/router.py @@ -7,6 +7,7 @@ from app.api.routes.auth import actors_router, router as auth_router from app.api.routes.health import router as health_router from app.modules.checkers.router import router as checkers_router +from app.api.routes.artifact_submissions import router as artifact_submission_router from app.modules.artifacts.router import router as artifacts_router from app.modules.authorization.router import router as authorization_router from app.modules.projects.router import router as projects_router @@ -28,3 +29,4 @@ api_router.include_router(tasks_router, prefix="/api/v1") api_router.include_router(checkers_router, prefix="/api/v1") api_router.include_router(artifacts_router, prefix="/api/v1") +api_router.include_router(artifact_submission_router, prefix="/api/v1") diff --git a/backend/app/api/routes/artifact_submissions.py b/backend/app/api/routes/artifact_submissions.py new file mode 100644 index 000000000..788dc7155 --- /dev/null +++ b/backend/app/api/routes/artifact_submissions.py @@ -0,0 +1,111 @@ +"""Hidden delivery surface for ART-owned contributor bundle preparation.""" + +from typing import Annotated +from uuid import UUID + +from fastapi import APIRouter, Depends, Header, HTTPException, Request, status +from pydantic import BaseModel, ConfigDict + +from app.adapters.artifacts import ( + get_submission_bundle_preparation_actor, + get_submission_bundle_preparation_command, +) +from app.core.api_controls import request_ids +from app.modules.artifacts.api import ( + SubmissionBundlePreparationCommand, + SubmissionBundlePreparationRejected, + SubmissionBundlePreparationRequest, + SubmissionBundlePreparationUnavailable, +) +from app.modules.authorization.api import ActorIdentityFacts + +router = APIRouter(tags=["tasks"]) + + +class SubmissionBundlePreparationResponse(BaseModel): + """Bounded hidden operation state without provider or scratch coordinates.""" + + model_config = ConfigDict(extra="forbid", from_attributes=True) + + put_attempt_id: UUID + admission_id: UUID | None + status: str + replayed: bool + + +def _require_ascii_submission_packet_headers(summary: str, attestation: str) -> None: + """Reject lossy HTTP-header decoding before immutable evidence hashing.""" + try: + summary.encode("ascii") + attestation.encode("ascii") + except UnicodeEncodeError as exc: + raise HTTPException( + status_code=422, + detail="submission_bundle_packet_header_encoding_invalid", + ) from exc + + +@router.post( + "/tasks/{task_id}/submission-bundle-preparations", + response_model=SubmissionBundlePreparationResponse, + status_code=status.HTTP_202_ACCEPTED, + include_in_schema=False, +) +async def prepare_submission_bundle( + task_id: str, + request: Request, + actor: Annotated[ActorIdentityFacts, Depends(get_submission_bundle_preparation_actor)], + command: Annotated[ + SubmissionBundlePreparationCommand, + Depends(get_submission_bundle_preparation_command), + ], + assignment_id: Annotated[str | None, Header(alias="X-Task-Assignment-Id")] = None, + idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None, + summary: Annotated[str | None, Header(alias="X-Submission-Summary")] = None, + contributor_attestation: Annotated[ + str | None, Header(alias="X-Contributor-Attestation") + ] = None, + predecessor_submission_id: Annotated[ + str | None, Header(alias="X-Predecessor-Submission-Id") + ] = None, +) -> SubmissionBundlePreparationResponse: + """Run the hidden continuous ZIP preparation surface; AUTH remains fail closed.""" + if None in (assignment_id, idempotency_key, summary, contributor_attestation): + raise HTTPException(status_code=404, detail="Task not found") + assert assignment_id is not None and idempotency_key is not None + assert summary is not None and contributor_attestation is not None + _require_ascii_submission_packet_headers(summary, contributor_attestation) + try: + identifiers = ( + UUID(task_id), + UUID(assignment_id), + UUID(idempotency_key), + UUID(predecessor_submission_id) if predecessor_submission_id else None, + ) + except ValueError as exc: + raise HTTPException(status_code=404, detail="Task not found") from exc + try: + request_id, correlation_id = (UUID(value) for value in request_ids(request)) + result = await command.prepare( + SubmissionBundlePreparationRequest( + actor=actor, + request_id=request_id, + correlation_id=correlation_id, + task_id=identifiers[0], + assignment_id=identifiers[1], + predecessor_submission_id=identifiers[3], + idempotency_key=identifiers[2], + summary=summary, + contributor_attestation=contributor_attestation, + media_type=request.headers.get("content-type", ""), + byte_source=request.stream(), + ) + ) + except SubmissionBundlePreparationUnavailable as exc: + raise HTTPException(status_code=404, detail="Task not found") from exc + except SubmissionBundlePreparationRejected as exc: + code = str(exc) + if code == "submission_bundle_preparation_context_changed": + raise HTTPException(status_code=409, detail=code) from exc + raise HTTPException(status_code=422, detail=code) from exc + return SubmissionBundlePreparationResponse.model_validate(result, from_attributes=True) diff --git a/backend/app/interfaces/artifact_operations.py b/backend/app/interfaces/artifact_operations.py index eec9f243c..7abc3f0e5 100644 --- a/backend/app/interfaces/artifact_operations.py +++ b/backend/app/interfaces/artifact_operations.py @@ -7,19 +7,8 @@ from typing import Literal, Protocol from uuid import UUID -from app.modules.artifacts.sources import PreparedArtifact -from app.modules.artifacts.submission_archive import SubmissionArchiveInspectionResult -from app.modules.artifacts.submission_manifest import ( - SubmissionChangeGateResult, - SubmissionManifest, -) from app.modules.authorization.prepared import PreparedAuthorizationHandle from app.modules.authorization.runtime import AuthorizationContext -from app.modules.checkers.effective_plan import EffectivePreSubmissionExecutionPlan -from app.modules.checkers.pre_submit_execution import ( - PreSubmissionExecutionResult, - SubmissionPacketView, -) __all__ = ( "ArtifactAuditResourceType", @@ -47,9 +36,6 @@ "GuideSufficiencyMaterialUnavailable", "GuideSufficiencySourceItem", "GuideSufficiencyExtractionProvenance", - "PreparedBundleMaterializationRequest", - "SubmissionBundlePreparationPort", - "SubmissionBundlePreparationRequest", "SubmissionBindingRequest", ) @@ -255,38 +241,6 @@ class CheckerOutputBindingRequest: verified_content_ids: tuple[UUID, ...] -@dataclass(frozen=True, slots=True) -class SubmissionBundlePreparationRequest: - """One authorized continuous contributor ZIP request without local handles.""" - - authorization_context: AuthorizationContext - task_id: UUID - assignment_id: UUID - predecessor_submission_id: UUID | None - idempotency_key: UUID - summary: str - contributor_attestation: str - media_type: str - byte_source: AsyncIterable[bytes] - - -@dataclass(frozen=True, slots=True) -class PreparedBundleMaterializationRequest: - """Process-local prepared bytes and exact 04A/04B1 execution facts.""" - - prepared_authorization: PreparedAuthorizationHandle - task_id: UUID - assignment_id: UUID - submission_artifact_policy_id: UUID - checker_policy_id: UUID - prepared_artifact: PreparedArtifact - effective_plan: EffectivePreSubmissionExecutionPlan - inspection: SubmissionArchiveInspectionResult - manifest: SubmissionManifest - change_gate: SubmissionChangeGateResult - packet: SubmissionPacketView - - @dataclass(frozen=True, slots=True) class BindingMaterializationRequest: """Immutable bindings selected by exact execution context.""" @@ -348,13 +302,6 @@ async def ingest( """Prepare authority before delegating to durable byte ingestion.""" -class SubmissionBundlePreparationPort(Protocol): - """Prepare one continuous contributor bundle without upload sessions.""" - - async def prepare(self, request: SubmissionBundlePreparationRequest) -> object: - """Prepare one authorized outer ZIP in bounded private scratch.""" - - class ArtifactBindingPort(Protocol): """Create exact action-bound bindings from verified content.""" @@ -373,12 +320,6 @@ async def bind_checker_output(self, request: CheckerOutputBindingRequest) -> obj class ArtifactMaterializationPort(Protocol): """Materialize only canonical immutable source forms.""" - async def materialize_prepared_bundle( - self, - request: PreparedBundleMaterializationRequest, - ) -> PreSubmissionExecutionResult: - """Materialize one process-local prepared bundle generation.""" - async def materialize_guide_source( self, request: GuideSourceMaterializationRequest, diff --git a/backend/app/modules/artifacts/api/__init__.py b/backend/app/modules/artifacts/api/__init__.py new file mode 100644 index 000000000..5977c313f --- /dev/null +++ b/backend/app/modules/artifacts/api/__init__.py @@ -0,0 +1,17 @@ +"""Dependency-safe public API for the ARTIFACTS business module.""" + +from app.modules.artifacts.api.submission_preparation import ( + SubmissionBundlePreparationCommand, + SubmissionBundlePreparationRejected, + SubmissionBundlePreparationRequest, + SubmissionBundlePreparationResult, + SubmissionBundlePreparationUnavailable, +) + +__all__ = ( + "SubmissionBundlePreparationCommand", + "SubmissionBundlePreparationRejected", + "SubmissionBundlePreparationRequest", + "SubmissionBundlePreparationResult", + "SubmissionBundlePreparationUnavailable", +) diff --git a/backend/app/modules/artifacts/api/submission_preparation.py b/backend/app/modules/artifacts/api/submission_preparation.py new file mode 100644 index 000000000..6eb9b67e7 --- /dev/null +++ b/backend/app/modules/artifacts/api/submission_preparation.py @@ -0,0 +1,54 @@ +"""Public ART contracts for hidden contributor bundle preparation.""" + +from __future__ import annotations + +from collections.abc import AsyncIterable +from dataclasses import dataclass +from typing import Protocol +from uuid import UUID + +from app.modules.authorization.api import ActorIdentityFacts + + +class SubmissionBundlePreparationRejected(RuntimeError): + """Reject preparation with one stable ART-owned failure code.""" + + +class SubmissionBundlePreparationUnavailable(RuntimeError): + """Conceal unavailable or denied preparation authority.""" + + +@dataclass(frozen=True, slots=True) +class SubmissionBundlePreparationRequest: + """One continuous contributor ZIP request with server-owned actor facts.""" + + actor: ActorIdentityFacts + request_id: UUID + correlation_id: UUID + task_id: UUID + assignment_id: UUID + predecessor_submission_id: UUID | None + idempotency_key: UUID + summary: str + contributor_attestation: str + media_type: str + byte_source: AsyncIterable[bytes] + + +@dataclass(frozen=True, slots=True) +class SubmissionBundlePreparationResult: + """Bounded preparation result without custody or provider coordinates.""" + + put_attempt_id: UUID + admission_id: UUID | None + status: str + replayed: bool + + +class SubmissionBundlePreparationCommand(Protocol): + """Prepare one hidden contributor bundle through the sole ART command.""" + + async def prepare( + self, request: SubmissionBundlePreparationRequest + ) -> SubmissionBundlePreparationResult: + """Run the continuous fail-closed preparation workflow.""" diff --git a/backend/app/modules/artifacts/authorization.py b/backend/app/modules/artifacts/authorization.py index 453e1a31e..7061a7021 100644 --- a/backend/app/modules/artifacts/authorization.py +++ b/backend/app/modules/artifacts/authorization.py @@ -540,17 +540,10 @@ async def prepare( async def consume( self, *, - service_identity: ServiceIdentity, - action_id: ActionId, - prepared_authorization: PreparedAuthorizationHandle, + prepared_authorization: object, facts: PreSubmitMaterializationAuthorityFacts, ) -> None: """Consume authority only for the exact materializer and final facts.""" - if ( - service_identity is not ServiceIdentity.ARTIFACT_MATERIALIZER - or action_id is not ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE - ): - raise ArtifactAuthorityDeniedError("pre-submit materialization authority is invalid") await self._delegate.consume( prepared_authorization=prepared_authorization, facts=facts, diff --git a/backend/app/modules/artifacts/pre_submit_evidence.py b/backend/app/modules/artifacts/pre_submit_evidence.py index 1b0b0d948..5023a08d1 100644 --- a/backend/app/modules/artifacts/pre_submit_evidence.py +++ b/backend/app/modules/artifacts/pre_submit_evidence.py @@ -13,13 +13,57 @@ from app.core.hashing import canonical_json_hash from app.modules.artifacts.models import PreSubmitEvidenceResult, PreSubmitEvidenceSet from app.modules.artifacts.sources import ArtifactCommitment -from app.modules.checkers.pre_submit_execution import ( - ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, - PreSubmissionEntryResult, - PreSubmissionExecutionResult, - validate_pre_submission_execution_result, +from app.modules.checkers.api import ( + EffectivePreSubmissionExecutionPlan, + PreSubmissionExecutionEntryFacts, + PreSubmissionExecutionFacts, ) -from app.modules.checkers.effective_plan import EffectivePreSubmissionExecutionPlan +from app.modules.projects.api import ( + ProjectLockedPolicyContextPort, + ProjectLockedPolicyContextRequest, + ProjectLockedPolicyContextUnavailable, +) +from app.modules.tasks.api import ( + TaskSubmissionContextPort, + TaskSubmissionContextRequest, + TaskSubmissionContextUnavailable, +) + +ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) +_CHECKER_EXECUTION_STATUSES = frozenset( + {"passed", "warning", "advisory_disabled", "dependency_not_run", "failed"} +) + + +@dataclass(frozen=True, slots=True) +class PreSubmitExecutionCustody: + """ART-owned custody observed around one CHECKER execution.""" + + prepared_generation_id: UUID + archive_sha256: str + archive_byte_count: int + semantic_manifest_sha256: str + storage_scheme: str + + +@dataclass(frozen=True, slots=True) +class PreSubmitExecutionResult: + """ART custody combined with dependency-safe CHECKER result facts.""" + + custody: PreSubmitExecutionCustody + checker_facts: PreSubmissionExecutionFacts + + @property + def plan_sha256(self) -> str: + return self.checker_facts.plan_sha256 + + @property + def eligible(self) -> bool: + return self.checker_facts.eligible + + @property + def entries(self) -> tuple[PreSubmissionExecutionEntryFacts, ...]: + return self.checker_facts.entries class PreSubmitEvidenceConflict(RuntimeError): @@ -145,7 +189,7 @@ class PreSubmitEvidencePersistenceRequest: archive_byte_count: int semantic_manifest_sha256: str plan: EffectivePreSubmissionExecutionPlan - execution: PreSubmissionExecutionResult + execution: PreSubmitExecutionResult @dataclass(frozen=True, slots=True) @@ -164,7 +208,7 @@ def pre_submit_failure_audit_payload( task_id: UUID, prepared_generation_id: UUID, evidence: PersistedPreSubmitEvidence, - execution: PreSubmissionExecutionResult, + execution: PreSubmitExecutionResult, catalogue_id: str, catalogue_version: str, ) -> dict[str, object]: @@ -178,20 +222,20 @@ def pre_submit_failure_audit_payload( for result in execution.entries: result_outcomes.append( { - "definition_id": result.definition.definition_id, - "definition_version": result.definition.definition_version, - "status": result.status.value, + "definition_id": result.definition_id, + "definition_version": result.definition_version, + "status": result.checker_execution_status, "message_code": result.message_code, } ) - if result.status.value == "failed": + if result.checker_execution_status == "failed": counts["failed"] += 1 categories.add(result.classification) if result.failure_code is not None: failure_codes.add(result.failure_code) - elif result.status.value == "warning": + elif result.checker_execution_status == "warning": counts["warning"] += 1 - elif result.status.value == "dependency_not_run": + elif result.checker_execution_status == "dependency_not_run": counts["not_run"] += 1 return { "event_type": "pre_submission_check_failed", @@ -223,6 +267,59 @@ def semantic_manifest_identity(semantic_manifest_sha256: str) -> UUID: ) +def _validate_execution( + plan: EffectivePreSubmissionExecutionPlan, + execution: PreSubmitExecutionResult, +) -> None: + """Reject custody or CHECKER facts not bound to the exact immutable plan.""" + custody = execution.custody + try: + ArtifactCommitment.validate_sha256(custody.archive_sha256) + ArtifactCommitment.validate_sha256(custody.semantic_manifest_sha256) + except ValueError as exc: + raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") from exc + if ( + type(custody.prepared_generation_id) is not UUID + or type(custody.archive_byte_count) is not int + or custody.archive_byte_count < 0 + or custody.storage_scheme not in ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES + or type(execution.eligible) is not bool + or execution.plan_sha256 != plan.plan_sha256 + or len(execution.entries) != len(plan.entries) + ): + raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") + disqualified = False + for plan_entry, result in zip(plan.entries, execution.entries, strict=True): + expected_severity = ( + "warning" if plan_entry.classification == "advisory" else "blocking" + ) + status = result.checker_execution_status + if ( + result.dispatch_authority != "workstream.pre_submission_checker_catalogue" + or result.definition_id != plan_entry.definition_id + or result.definition_version != plan_entry.definition_version + or result.public_name != plan_entry.public_name + or result.policy_source != plan_entry.policy_trace_source + or result.effective_plan_sha256 != plan.plan_sha256 + or result.rule_instance_id != plan_entry.rule_instance_id + or result.locked_policy_sha256 != plan.lineage.effective_policy_hash + or result.phase != plan_entry.phase + or result.order != plan_entry.order + or result.classification != plan_entry.classification + or result.severity != expected_severity + or status not in _CHECKER_EXECUTION_STATUSES + or not result.message_code + or result.failure_code + != (plan_entry.failure_code if status == "failed" else None) + or len(result.metadata) != len({key for key, _ in result.metadata}) + or any(type(value) is not int or value < 0 for _, value in result.metadata) + ): + raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") + disqualified = disqualified or status in {"failed", "dependency_not_run"} + if execution.eligible == disqualified: + raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") + + _PRE_SUBMIT_PASS_CAPABILITY_SEAL = object() @@ -331,15 +428,23 @@ def _assert_live_prepared_custody( class _PreSubmitEvidenceRepository: """Insert or replay one immutable evidence set in the caller transaction.""" - def __init__(self, session: AsyncSession) -> None: + def __init__( + self, + session: AsyncSession, + *, + task_contexts: TaskSubmissionContextPort, + project_contexts: ProjectLockedPolicyContextPort, + ) -> None: self._session = session + self._task_contexts = task_contexts + self._project_contexts = project_contexts async def persist( self, *, context: PreSubmitEvidenceContext, plan: EffectivePreSubmissionExecutionPlan, - execution: PreSubmissionExecutionResult, + execution: PreSubmitExecutionResult, ) -> PersistedPreSubmitEvidence: """Persist exact results once; changed facts under the identity fail closed.""" transaction = self._session.sync_session.get_transaction() @@ -349,9 +454,9 @@ async def persist( or self._session.in_nested_transaction() ): raise RuntimeError("pre-submit evidence requires one root transaction") - validate_pre_submission_execution_result(plan, execution) + _validate_execution(plan, execution) operation_identity = context.operation_identity(effective_plan_sha256=execution.plan_sha256) - values = self._set_values(context, execution, operation_identity) + values = self._set_values(context, plan, execution, operation_identity) evidence_set_id = uuid4() inserted_id = await self._session.scalar( insert(PreSubmitEvidenceSet) @@ -379,8 +484,10 @@ async def persist( ).all() ) expected_results = tuple( - self._result_values(result_order, result) - for result_order, result in enumerate(execution.entries) + self._result_values(result_order, result, plan_entry.result_schema) + for result_order, (plan_entry, result) in enumerate( + zip(plan.entries, execution.entries, strict=True) + ) ) if len(persisted_results) != len(expected_results) or any( any(getattr(persisted, key) != value for key, value in expected.items()) @@ -392,12 +499,14 @@ async def persist( operation_identity=operation_identity, replayed=True, ) - for result_order, result in enumerate(execution.entries): + for result_order, (plan_entry, result) in enumerate( + zip(plan.entries, execution.entries, strict=True) + ): self._session.add( PreSubmitEvidenceResult( id=str(uuid4()), evidence_set_id=str(evidence_set_id), - **self._result_values(result_order, result), + **self._result_values(result_order, result, plan_entry.result_schema), ) ) await self._session.flush() @@ -408,30 +517,35 @@ async def persist( ) @staticmethod - def _result_values(result_order: int, result: PreSubmissionEntryResult) -> dict[str, object]: + def _result_values( + result_order: int, + result: PreSubmissionExecutionEntryFacts, + schema_version: str, + ) -> dict[str, object]: return { "result_order": result_order, - "schema_version": result.schema_version, - "dispatch_authority": result.definition.dispatch_authority, - "definition_id": result.definition.definition_id, - "definition_version": result.definition.definition_version, - "public_name": result.definition.public_name, - "source": result.definition.source, + "schema_version": schema_version, + "dispatch_authority": result.dispatch_authority, + "definition_id": result.definition_id, + "definition_version": result.definition_version, + "public_name": result.public_name, + "source": result.policy_source, "phase": result.phase, "classification": result.classification, "severity": result.severity, - "status": result.status.value, + "status": result.checker_execution_status, "failure_code": result.failure_code, "message_code": result.message_code, - "effective_plan_sha256": result.policy_trace.effective_plan_sha256, - "rule_instance_id": result.policy_trace.rule_instance_id, - "locked_policy_sha256": result.policy_trace.locked_policy_sha256, + "effective_plan_sha256": result.effective_plan_sha256, + "rule_instance_id": result.rule_instance_id, + "locked_policy_sha256": result.locked_policy_sha256, } @staticmethod def _set_values( context: PreSubmitEvidenceContext, - execution: PreSubmissionExecutionResult, + plan: EffectivePreSubmissionExecutionPlan, + execution: PreSubmitExecutionResult, operation_identity: str, ) -> dict[str, object]: values = { @@ -450,19 +564,27 @@ def _set_values( "schema_version": "pre_submit_evidence_result_manifest.v1", "entries": [ { - "schema_version": result.schema_version, - "definition": asdict(result.definition), - "policy_trace": asdict(result.policy_trace), + "schema_version": plan_entry.result_schema, + "dispatch_authority": result.dispatch_authority, + "definition_id": result.definition_id, + "definition_version": result.definition_version, + "public_name": result.public_name, + "policy_source": result.policy_source, + "effective_plan_sha256": result.effective_plan_sha256, + "rule_instance_id": result.rule_instance_id, + "locked_policy_sha256": result.locked_policy_sha256, "phase": result.phase, "order": result.order, "classification": result.classification, "severity": result.severity, - "status": result.status.value, + "status": result.checker_execution_status, "failure_code": result.failure_code, "message_code": result.message_code, "metadata": [list(item) for item in result.metadata], } - for result in execution.entries + for plan_entry, result in zip( + plan.entries, execution.entries, strict=True + ) ], } ), @@ -487,9 +609,21 @@ def _set_values( class PreSubmitEvidenceService: """Revalidate locked state and persist evidence in one caller transaction.""" - def __init__(self, session: AsyncSession) -> None: + def __init__( + self, + session: AsyncSession, + *, + task_contexts: TaskSubmissionContextPort, + project_contexts: ProjectLockedPolicyContextPort, + ) -> None: self._session = session - self._repository = _PreSubmitEvidenceRepository(session) + self._task_contexts = task_contexts + self._project_contexts = project_contexts + self._repository = _PreSubmitEvidenceRepository( + session, + task_contexts=task_contexts, + project_contexts=project_contexts, + ) self._live_pass_bindings: set[object] = set() def _claims_pass_binding(self, binding: object) -> bool: @@ -537,8 +671,6 @@ async def persist( self, request: PreSubmitEvidencePersistenceRequest ) -> PreSubmitEvidencePersistenceResult: """Persist only when the post-execution database lineage remains exact.""" - from app.modules.tasks.pre_submit_context import load_locked_pre_submit_context - transaction = self._session.sync_session.get_transaction() if ( transaction is None @@ -546,7 +678,7 @@ async def persist( or self._session.in_nested_transaction() ): raise RuntimeError("pre-submit evidence requires one root transaction") - validate_pre_submission_execution_result(request.plan, request.execution) + _validate_execution(request.plan, request.execution) custody = request.execution.custody if ( request.prepared_generation_id != custody.prepared_generation_id @@ -555,49 +687,90 @@ async def persist( or request.semantic_manifest_sha256 != custody.semantic_manifest_sha256 ): raise PreSubmitEvidenceConflict("pre_submit_execution_custody_changed") - locked = await load_locked_pre_submit_context( - self._session, - actor_profile_id=request.actor_profile_id, - identity_link_id=request.identity_link_id, - task_id=request.task_id, - assignment_id=request.assignment_id, - predecessor_submission_id=request.predecessor_submission_id, - ) + try: + task_context = await self._task_contexts.lock_submission_context( + TaskSubmissionContextRequest( + task_id=request.task_id, + assignment_id=request.assignment_id, + contributor_id=request.actor_profile_id, + predecessor_submission_id=request.predecessor_submission_id, + ) + ) + references = task_context.locked_project_context + project_context = await self._project_contexts.lock_locked_policy_context( + ProjectLockedPolicyContextRequest( + project_id=references.project_id, + guide_version=references.guide_version, + source_snapshot_id=references.source_snapshot_id, + source_snapshot_hash=references.source_snapshot_hash, + effective_policy_id=references.effective_policy_id, + effective_policy_hash=references.effective_policy_hash, + pre_submit_policy_id=references.pre_submit_policy_id, + pre_submit_policy_bundle_hash=references.pre_submit_policy_bundle_hash, + ) + ) + except (TaskSubmissionContextUnavailable, ProjectLockedPolicyContextUnavailable) as exc: + raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") from exc lineage = request.plan.lineage + guide_version = project_context.guide_version.removeprefix("v") + try: + numeric_guide_version = int(guide_version) + except ValueError as exc: + raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") from exc if ( - locked.project_id != lineage.project_id - or locked.guide_id != lineage.guide_id - or locked.guide_version != str(lineage.guide_version) - or locked.source_snapshot_id != lineage.source_snapshot_id - or locked.source_snapshot_sha256 != lineage.source_snapshot_hash - or locked.effective_policy_id != lineage.effective_policy_id - or locked.effective_policy_sha256 != lineage.effective_policy_hash - or locked.pre_submit_policy_id != lineage.pre_submit_policy_id - or locked.pre_submit_policy_sha256 != lineage.pre_submit_policy_bundle_hash + task_context.contributor_id != request.actor_profile_id + or task_context.predecessor is None + and request.predecessor_submission_id is not None + or task_context.predecessor is not None + and task_context.predecessor.submission_id != request.predecessor_submission_id + or project_context.project_id != lineage.project_id + or project_context.guide_id != lineage.guide_id + or numeric_guide_version != lineage.guide_version + or project_context.source_snapshot_id != lineage.source_snapshot_id + or project_context.source_snapshot_hash != lineage.source_snapshot_hash + or project_context.effective_policy_id != lineage.effective_policy_id + or project_context.effective_policy_hash != lineage.effective_policy_hash + or project_context.pre_submit_policy_id != lineage.pre_submit_policy_id + or project_context.pre_submit_policy_bundle_hash + != lineage.pre_submit_policy_bundle_hash ): raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") + locked_guide_sha256 = canonical_json_hash( + { + "domain": "workstream.locked_task_guide.v1", + "project_id": str(project_context.project_id), + "guide_id": str(project_context.guide_id), + "guide_version": project_context.guide_version, + "source_snapshot_id": str(project_context.source_snapshot_id), + "source_snapshot_sha256": project_context.source_snapshot_hash, + } + ) context = PreSubmitEvidenceContext( actor_profile_id=request.actor_profile_id, identity_link_id=request.identity_link_id, - project_id=locked.project_id, + project_id=project_context.project_id, task_id=request.task_id, assignment_id=request.assignment_id, predecessor_submission_id=request.predecessor_submission_id, - predecessor_submission_version=locked.predecessor_submission_version, + predecessor_submission_version=( + task_context.predecessor.version + if task_context.predecessor is not None + else None + ), prepared_generation_id=request.prepared_generation_id, archive_sha256=request.archive_sha256, archive_byte_count=request.archive_byte_count, semantic_manifest_id=semantic_manifest_identity(request.semantic_manifest_sha256), semantic_manifest_sha256=request.semantic_manifest_sha256, - guide_id=locked.guide_id, - guide_version=locked.guide_version, - source_snapshot_id=locked.source_snapshot_id, - source_snapshot_sha256=locked.source_snapshot_sha256, - locked_guide_sha256=locked.locked_guide_sha256, - effective_policy_id=locked.effective_policy_id, - locked_artifact_policy_sha256=locked.effective_policy_sha256, - pre_submit_policy_id=locked.pre_submit_policy_id, - locked_checker_policy_sha256=locked.pre_submit_policy_sha256, + guide_id=project_context.guide_id, + guide_version=project_context.guide_version, + source_snapshot_id=project_context.source_snapshot_id, + source_snapshot_sha256=project_context.source_snapshot_hash, + locked_guide_sha256=locked_guide_sha256, + effective_policy_id=project_context.effective_policy_id, + locked_artifact_policy_sha256=project_context.effective_policy_hash, + pre_submit_policy_id=project_context.pre_submit_policy_id, + locked_checker_policy_sha256=project_context.pre_submit_policy_bundle_hash, catalogue_id=request.plan.catalogue_id, catalogue_version=request.plan.catalogue_version, catalogue_manifest_sha256=request.plan.catalogue_manifest_sha256, @@ -626,7 +799,7 @@ async def persist( if request.execution.eligible else pre_submit_failure_audit_payload( actor_profile_id=request.actor_profile_id, - project_id=locked.project_id, + project_id=project_context.project_id, task_id=request.task_id, prepared_generation_id=request.prepared_generation_id, evidence=evidence, diff --git a/backend/app/modules/artifacts/submission_admission.py b/backend/app/modules/artifacts/submission_admission.py index 6c468d4ed..a9160e5f3 100644 --- a/backend/app/modules/artifacts/submission_admission.py +++ b/backend/app/modules/artifacts/submission_admission.py @@ -5,16 +5,17 @@ from collections.abc import Callable from contextlib import AbstractAsyncContextManager from dataclasses import dataclass -from typing import Protocol +import json from uuid import UUID, uuid4 from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from app.interfaces.artifact_operations import ( - PreparedBundleMaterializationRequest, - SubmissionBundlePreparationPort, +from app.modules.artifacts.api import ( + SubmissionBundlePreparationRejected, SubmissionBundlePreparationRequest, + SubmissionBundlePreparationResult, + SubmissionBundlePreparationUnavailable, ) from app.modules.artifacts.models import ( ArtifactContent, @@ -32,6 +33,7 @@ from app.modules.artifacts.preparation import ArtifactPreparationService from app.modules.artifacts.schemas import ( ArtifactAdmissionResult, + ArtifactAuthorityDeniedError, SubmissionBundleArtifactAdmissionRequest, ) from app.modules.artifacts.submission_authorization import ( @@ -53,18 +55,27 @@ evaluate_submission_change, ) from app.modules.artifacts.submission_materialization import ( + PreparedBundleMaterializationRequest, PreparedBundleMaterializationService, PreparedBundlePreSubmitEvidenceService, ) -from app.modules.authorization.prepared import PreparedAuthorizationHandle from app.modules.checkers.api import ( + EffectivePreSubmissionExecutionPlan, + EffectivePreSubmissionPlanLineage, EffectivePreSubmissionPlanningPort, SubmissionPacketView, ) -from app.modules.tasks.pre_submit_context import ( - compile_locked_pre_submit_plan, - load_canonical_submission_version, - load_locked_pre_submit_context, +from app.modules.projects.api import ( + ProjectLockedPolicyContextFacts, + ProjectLockedPolicyContextPort, + ProjectLockedPolicyContextRequest, + ProjectLockedPolicyContextUnavailable, +) +from app.modules.tasks.api import ( + TaskSubmissionContextFacts, + TaskSubmissionContextPort, + TaskSubmissionContextRequest, + TaskSubmissionContextUnavailable, ) @@ -72,7 +83,7 @@ class SubmissionBundleDurablePutRequest: """Exact live custody and opaque authority for one final durable handoff.""" - prepared_authorization: PreparedAuthorizationHandle + prepared_authorization: object prepared_artifact: PreparedArtifact pass_capability: PreSubmitPassCapability replay_durable_intent_id: UUID | None = None @@ -119,7 +130,6 @@ async def admit_in_transaction( transaction is None or not transaction.is_active or self._session.in_nested_transaction() - or type(request.prepared_authorization) is not PreparedAuthorizationHandle or type(prepared) is not PreparedArtifact or type(request.pass_capability) is not PreSubmitPassCapability ): @@ -403,18 +413,6 @@ async def current_submission_bundle_admission_id( return UUID(value) if value is not None else None -class SubmissionBundlePreparationRejected(RuntimeError): - """The complete effective pre-submit execution did not produce passing custody.""" - - -@dataclass(frozen=True, slots=True) -class SubmissionBundlePreparationResult: - put_attempt_id: UUID - admission_id: UUID | None - status: str - replayed: bool - - @dataclass(frozen=True, slots=True) class SubmissionBundlePreparationRuntime: preparation: ArtifactPreparationService @@ -425,12 +423,6 @@ class SubmissionBundlePreparationRuntime: durable_put: SubmissionBundleDurablePutService -class SubmissionBundlePreparationCommand(SubmissionBundlePreparationPort, Protocol): - async def prepare( - self, request: SubmissionBundlePreparationRequest - ) -> SubmissionBundlePreparationResult: ... - - class PreparedSubmissionBundlePreparationCommand: """Keep every process-local capability within one hidden request.""" @@ -439,12 +431,16 @@ def __init__( *, session: AsyncSession, authority: SubmissionBundlePreparationAuthorization, + task_contexts: TaskSubmissionContextPort, + project_contexts: ProjectLockedPolicyContextPort, runtime_factory: Callable[ [], AbstractAsyncContextManager[SubmissionBundlePreparationRuntime] ], ) -> None: self._session = session self._authority = authority + self._task_contexts = task_contexts + self._project_contexts = project_contexts self._runtime_factory = runtime_factory async def prepare( @@ -454,28 +450,18 @@ async def prepare( raise TypeError("invalid submission bundle preparation request") prepared = None try: - await self._authority.preflight( - authorization_context=request.authorization_context, - task_id=request.task_id, - assignment_id=request.assignment_id, - predecessor_submission_id=request.predecessor_submission_id, - idempotency_key=request.idempotency_key, - ) + await self._authority.preflight(request=request) if request.media_type.partition(";")[0].strip().lower() != "application/zip": raise SubmissionBundlePreparationRejected("submission_bundle_media_type_invalid") async with self._runtime_factory() as runtime: async with self._session.begin(): - locked = await load_locked_pre_submit_context( - self._session, - actor_profile_id=request.authorization_context.actor_profile_id, - identity_link_id=request.authorization_context.identity_link_id, - task_id=request.task_id, - assignment_id=request.assignment_id, - predecessor_submission_id=request.predecessor_submission_id, - include_actor_identity_locks=False, + task_context, project_context = await self._lock_context(request) + plan = self._compile_plan( + task_context, + project_context, + runtime.catalogue, ) - plan = compile_locked_pre_submit_plan(locked, runtime.catalogue) - predecessor = await self._load_predecessor(request.predecessor_submission_id) + predecessor = await self._load_predecessor(task_context) prepared = await runtime.preparation.prepare( request.byte_source, media_type="application/zip", @@ -484,8 +470,8 @@ async def prepare( materialization_handle = await runtime.materialization.prepare_authorization( task_id=request.task_id, assignment_id=request.assignment_id, - submission_artifact_policy_id=locked.effective_policy_id, - checker_policy_id=locked.pre_submit_policy_id, + submission_artifact_policy_id=project_context.effective_policy_id, + checker_policy_id=project_context.pre_submit_policy_id, prepared_artifact=prepared, effective_plan=plan, idempotency_key=request.idempotency_key, @@ -503,8 +489,8 @@ async def prepare( prepared_authorization=materialization_handle, task_id=request.task_id, assignment_id=request.assignment_id, - submission_artifact_policy_id=locked.effective_policy_id, - checker_policy_id=locked.pre_submit_policy_id, + submission_artifact_policy_id=project_context.effective_policy_id, + checker_policy_id=project_context.pre_submit_policy_id, prepared_artifact=prepared, effective_plan=plan, inspection=inspection, @@ -519,9 +505,7 @@ async def prepare( evidence = await runtime.evidence.persist( materialization_request, execution=execution, - actor_profile_id=request.authorization_context.actor_profile_id, - identity_link_id=request.authorization_context.identity_link_id, - predecessor_submission_id=request.predecessor_submission_id, + preparation_request=request, ) if evidence.pass_capability is None: replay = await self._existing_durable_result(evidence.evidence.evidence_set_id) @@ -536,13 +520,7 @@ async def prepare( evidence.evidence.evidence_set_id ) async with self._authority.transaction(): - final_handle = await self._authority.prepare_final( - authorization_context=request.authorization_context, - task_id=request.task_id, - assignment_id=request.assignment_id, - predecessor_submission_id=request.predecessor_submission_id, - idempotency_key=request.idempotency_key, - ) + final_handle = await self._authority.prepare_final(request=request) retained, _, durable = await runtime.durable_put.admit_in_transaction( SubmissionBundleDurablePutRequest( prepared_authorization=final_handle, @@ -558,6 +536,10 @@ async def prepare( durable, ) return self._result(result) + except ArtifactAuthorityDeniedError as exc: + raise SubmissionBundlePreparationUnavailable( + "submission bundle preparation is unavailable" + ) from exc finally: if prepared is not None: await prepared.close() @@ -633,6 +615,84 @@ async def _matching_replay_intent(self, evidence_id: UUID) -> UUID | None: value = await self._session.scalar(statement) return UUID(value) if value is not None else None + async def _lock_context( + self, + request: SubmissionBundlePreparationRequest, + ) -> tuple[TaskSubmissionContextFacts, ProjectLockedPolicyContextFacts]: + """Lock exact TASK then PROJECT facts through their public ports.""" + try: + task_context = await self._task_contexts.lock_submission_context( + TaskSubmissionContextRequest( + task_id=request.task_id, + assignment_id=request.assignment_id, + contributor_id=request.actor.actor_profile_id, + predecessor_submission_id=request.predecessor_submission_id, + ) + ) + references = task_context.locked_project_context + project_context = await self._project_contexts.lock_locked_policy_context( + ProjectLockedPolicyContextRequest( + project_id=references.project_id, + guide_version=references.guide_version, + source_snapshot_id=references.source_snapshot_id, + source_snapshot_hash=references.source_snapshot_hash, + effective_policy_id=references.effective_policy_id, + effective_policy_hash=references.effective_policy_hash, + pre_submit_policy_id=references.pre_submit_policy_id, + pre_submit_policy_bundle_hash=references.pre_submit_policy_bundle_hash, + ) + ) + except (TaskSubmissionContextUnavailable, ProjectLockedPolicyContextUnavailable) as exc: + raise SubmissionBundlePreparationRejected( + "submission_bundle_preparation_context_changed" + ) from exc + return task_context, project_context + + @staticmethod + def _compile_plan( + task_context: TaskSubmissionContextFacts, + project_context: ProjectLockedPolicyContextFacts, + planner: EffectivePreSubmissionPlanningPort, + ) -> EffectivePreSubmissionExecutionPlan: + """Compile the sole CHECKER plan from exact public PROJECT facts.""" + guide_version = project_context.guide_version.removeprefix("v") + try: + numeric_guide_version = int(guide_version) + effective_policy = json.loads(project_context.effective_policy.value) + compiled_bundle = json.loads( + project_context.compiled_pre_submit_bundle.value + ) + except (TypeError, ValueError) as exc: + raise SubmissionBundlePreparationRejected( + "submission_bundle_preparation_context_changed" + ) from exc + if ( + project_context.project_id + != task_context.locked_project_context.project_id + or not isinstance(effective_policy, dict) + or not isinstance(compiled_bundle, dict) + ): + raise SubmissionBundlePreparationRejected( + "submission_bundle_preparation_context_changed" + ) + return planner.compile_effective_plan( + lineage=EffectivePreSubmissionPlanLineage( + project_id=project_context.project_id, + guide_id=project_context.guide_id, + guide_version=numeric_guide_version, + source_snapshot_id=project_context.source_snapshot_id, + source_snapshot_hash=project_context.source_snapshot_hash, + effective_policy_id=project_context.effective_policy_id, + effective_policy_hash=project_context.effective_policy_hash, + pre_submit_policy_id=project_context.pre_submit_policy_id, + pre_submit_policy_bundle_hash=( + project_context.pre_submit_policy_bundle_hash + ), + ), + effective_policy=effective_policy, + compiled_bundle=compiled_bundle, + ) + async def _existing_durable_result( self, evidence_id: UUID ) -> SubmissionBundlePreparationResult | None: @@ -669,26 +729,26 @@ async def _existing_durable_result( ) async def _load_predecessor( - self, submission_id: UUID | None + self, + task_context: TaskSubmissionContextFacts, ) -> SubmissionCanonicalPredecessor | None: - if submission_id is None: + predecessor = task_context.predecessor + if predecessor is None: return None admission = await self._session.scalar( select(SubmissionBundleAdmission).where( - SubmissionBundleAdmission.consumed_by_submission_id == str(submission_id), + SubmissionBundleAdmission.consumed_by_submission_id + == str(predecessor.submission_id), SubmissionBundleAdmission.status == "consumed", ) ) - version = await load_canonical_submission_version( - self._session, submission_id=submission_id - ) - if admission is None or version is None: + if admission is None: raise SubmissionBundlePreparationRejected( "submission_canonical_predecessor_unavailable" ) return SubmissionCanonicalPredecessor( - submission_id=submission_id, - submission_version=version, + submission_id=predecessor.submission_id, + submission_version=predecessor.version, archive_sha256=admission.archive_sha256, semantic_manifest_sha256=admission.semantic_manifest_sha256, ) diff --git a/backend/app/modules/artifacts/submission_authorization.py b/backend/app/modules/artifacts/submission_authorization.py index 28e2560c9..51c967b97 100644 --- a/backend/app/modules/artifacts/submission_authorization.py +++ b/backend/app/modules/artifacts/submission_authorization.py @@ -4,14 +4,11 @@ from contextlib import AbstractAsyncContextManager from typing import Protocol -from uuid import UUID - +from app.modules.artifacts.api import SubmissionBundlePreparationRequest from app.modules.artifacts.schemas import ( ArtifactAuthorityDeniedError, SubmissionBundleDurableIntentAuthorityFacts, ) -from app.modules.authorization.prepared import PreparedAuthorizationHandle -from app.modules.authorization.runtime import AuthorizationContext class SubmissionBundlePreparedAuthorization(Protocol): @@ -20,7 +17,7 @@ class SubmissionBundlePreparedAuthorization(Protocol): async def consume( self, *, - prepared_authorization: PreparedAuthorizationHandle, + prepared_authorization: object, facts: SubmissionBundleDurableIntentAuthorityFacts, ) -> None: ... @@ -31,11 +28,13 @@ class SubmissionBundlePreparationAuthorization(SubmissionBundlePreparedAuthoriza async def preflight( self, *, - authorization_context: AuthorizationContext, - task_id: UUID, - assignment_id: UUID, - predecessor_submission_id: UUID | None, - idempotency_key: UUID, + request: SubmissionBundlePreparationRequest, + ) -> None: ... + + async def revalidate( + self, + *, + request: SubmissionBundlePreparationRequest, ) -> None: ... def transaction(self) -> AbstractAsyncContextManager[object]: ... @@ -43,12 +42,8 @@ def transaction(self) -> AbstractAsyncContextManager[object]: ... async def prepare_final( self, *, - authorization_context: AuthorizationContext, - task_id: UUID, - assignment_id: UUID, - predecessor_submission_id: UUID | None, - idempotency_key: UUID, - ) -> PreparedAuthorizationHandle: ... + request: SubmissionBundlePreparationRequest, + ) -> object: ... def close(self) -> None: ... @@ -59,7 +54,7 @@ class DenySubmissionBundlePreparedAuthorization: async def consume( self, *, - prepared_authorization: PreparedAuthorizationHandle, + prepared_authorization: object, facts: SubmissionBundleDurableIntentAuthorityFacts, ) -> None: del prepared_authorization, facts @@ -73,10 +68,14 @@ async def preflight(self, **values: object) -> None: del values raise ArtifactAuthorityDeniedError("submission bundle preparation is unavailable") + async def revalidate(self, **values: object) -> None: + del values + raise ArtifactAuthorityDeniedError("submission bundle preparation is unavailable") + def transaction(self) -> AbstractAsyncContextManager[object]: raise ArtifactAuthorityDeniedError("submission bundle preparation is unavailable") - async def prepare_final(self, **values: object) -> PreparedAuthorizationHandle: + async def prepare_final(self, **values: object) -> object: del values raise ArtifactAuthorityDeniedError("submission bundle preparation is unavailable") diff --git a/backend/app/modules/artifacts/submission_materialization.py b/backend/app/modules/artifacts/submission_materialization.py index 1fd78a8dd..108510b6b 100644 --- a/backend/app/modules/artifacts/submission_materialization.py +++ b/backend/app/modules/artifacts/submission_materialization.py @@ -10,29 +10,88 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.hashing import canonical_json_hash -from app.interfaces.artifact_operations import PreparedBundleMaterializationRequest -from app.modules.actors.service_identities import ServiceIdentity +from app.modules.artifacts.api import SubmissionBundlePreparationRequest from app.modules.artifacts.schemas import ArtifactAuthorityDeniedError from app.modules.artifacts.preparation import ArtifactPreparationService from app.modules.artifacts.sources import PreparedArtifact from app.modules.artifacts.pre_submit_evidence import ( + PreSubmitExecutionCustody, + PreSubmitExecutionResult, PreSubmitEvidencePersistenceRequest, PreSubmitEvidencePersistenceResult, PreSubmitEvidenceService, ) -from app.modules.artifacts.submission_archive import SubmissionArchiveInspector -from app.modules.artifacts.submission_manifest import build_submission_manifest -from app.modules.authorization.catalogue import ActionId -from app.modules.authorization.prepared import PreparedAuthorizationHandle -from app.modules.checkers.catalogue import PreSubmissionCheckerCatalogue -from app.modules.checkers.effective_plan import EffectivePreSubmissionExecutionPlan -from app.modules.checkers.pre_submit_execution import ( - ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, - DefaultPreSubmissionExecutionInput, - EffectivePreSubmissionProcessor, - PreSubmissionExecutionResult, - PreSubmissionInfrastructureUnavailable, +from app.modules.artifacts.submission_archive import ( + SubmissionArchiveInspectionResult, ) +from app.modules.artifacts.submission_manifest import ( + SubmissionChangeGateResult, + SubmissionManifest, + build_submission_manifest, +) +from app.modules.artifacts.submission_authorization import ( + SubmissionBundlePreparationAuthorization, +) +from app.modules.checkers.api import ( + EffectivePreSubmissionExecutionPlan, + PreSubmissionExecutionFacts, + PreSubmissionInfrastructureUnavailableError, + SubmissionPacketView, +) +from app.modules.projects.api import ProjectLockedPolicyContextPort +from app.modules.tasks.api import TaskSubmissionContextPort + +ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) + + +@dataclass(frozen=True, slots=True) +class PreparedBundleMaterializationRequest: + """ART-private process-local prepared bytes and exact execution facts.""" + + prepared_authorization: object + task_id: UUID + assignment_id: UUID + submission_artifact_policy_id: UUID + checker_policy_id: UUID + prepared_artifact: PreparedArtifact + effective_plan: EffectivePreSubmissionExecutionPlan + inspection: SubmissionArchiveInspectionResult + manifest: SubmissionManifest + change_gate: SubmissionChangeGateResult + packet: SubmissionPacketView + + +@dataclass(frozen=True, slots=True) +class PreSubmitCheckerExecutionRequest: + """Exact process-local input supplied to the composition CHECKER adapter.""" + + plan: EffectivePreSubmissionExecutionPlan + commitment: object + inspection: SubmissionArchiveInspectionResult + manifest: SubmissionManifest + change_gate: SubmissionChangeGateResult + packet: SubmissionPacketView + prepared_generation_id: UUID + storage_scheme: str + + +class PreSubmitCheckerProcessor(Protocol): + """Blocking processor built by the composition-root CHECKER adapter.""" + + def abort(self) -> None: ... + + def process_blocking( + self, reader: object, workspace: object + ) -> PreSubmissionExecutionFacts: ... + + +class PreSubmitCheckerExecutionFactory(Protocol): + """Build one process-local CHECKER processor without private imports in ART.""" + + @property + def catalogue_manifest_sha256(self) -> str: ... + + def build(self, request: PreSubmitCheckerExecutionRequest) -> PreSubmitCheckerProcessor: ... @dataclass(frozen=True, slots=True) @@ -97,16 +156,14 @@ async def prepare( *, facts: PreSubmitMaterializationPreparationFacts, idempotency_key: UUID, - ) -> PreparedAuthorizationHandle: + ) -> object: """Prepare an opaque capability before any submitted byte is inspected.""" ... async def consume( self, *, - service_identity: ServiceIdentity, - action_id: ActionId, - prepared_authorization: PreparedAuthorizationHandle, + prepared_authorization: object, facts: PreSubmitMaterializationAuthorityFacts, ) -> None: """Consume the capability against the server-computed final facts.""" @@ -121,7 +178,7 @@ async def prepare( *, facts: PreSubmitMaterializationPreparationFacts, idempotency_key: UUID, - ) -> PreparedAuthorizationHandle: + ) -> object: """Deny capability preparation while the production adapter is absent.""" del facts, idempotency_key raise ArtifactAuthorityDeniedError( @@ -131,13 +188,11 @@ async def prepare( async def consume( self, *, - service_identity: ServiceIdentity, - action_id: ActionId, - prepared_authorization: PreparedAuthorizationHandle, + prepared_authorization: object, facts: PreSubmitMaterializationAuthorityFacts, ) -> None: """Deny capability consumption while the production adapter is absent.""" - del service_identity, action_id, prepared_authorization, facts + del prepared_authorization, facts raise ArtifactAuthorityDeniedError( "pre-submit checker input materialization is unavailable" ) @@ -151,15 +206,13 @@ def __init__( *, authorization: PreSubmitMaterializationAuthorization, preparation: ArtifactPreparationService, - archive_inspector: SubmissionArchiveInspector, - catalogue: PreSubmissionCheckerCatalogue, + checker_execution: PreSubmitCheckerExecutionFactory, storage_scheme: str, ) -> None: """Compose the bounded materializer from its AUTH and ART dependencies.""" self._authorization = authorization self._preparation = preparation - self._archive_inspector = archive_inspector - self._catalogue = catalogue + self._checker_execution = checker_execution if storage_scheme not in ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES: raise ValueError("pre-submit materializer storage scheme is invalid") self._storage_scheme = storage_scheme @@ -167,19 +220,15 @@ def __init__( async def materialize_prepared_bundle( self, request: PreparedBundleMaterializationRequest, - ) -> PreSubmissionExecutionResult: + ) -> PreSubmitExecutionResult: """Consume fixed-service authority before any byte or workspace access.""" facts = self._authority_facts(request) await self._authorization.consume( - service_identity=ServiceIdentity.ARTIFACT_MATERIALIZER, - action_id=ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, prepared_authorization=request.prepared_authorization, facts=facts, ) - processor = EffectivePreSubmissionProcessor( - archive_inspector=self._archive_inspector, - catalogue=self._catalogue, - execution_input=DefaultPreSubmissionExecutionInput( + processor = self._checker_execution.build( + PreSubmitCheckerExecutionRequest( plan=request.effective_plan, commitment=request.prepared_artifact.commitment, inspection=request.inspection, @@ -192,12 +241,22 @@ async def materialize_prepared_bundle( ) # Intentional friend call: this is the sole authority-gated caller, and # the preparation byte-access surface must remain private. - return await self._preparation._process_prepared_submission( + checker_facts = await self._preparation._process_prepared_submission( request.prepared_artifact, processor, reserved_bytes=request.manifest.total_expanded_bytes, maximum_entries=request.manifest.entry_count, ) + return PreSubmitExecutionResult( + custody=PreSubmitExecutionCustody( + prepared_generation_id=request.prepared_artifact.generation_id, + archive_sha256=request.prepared_artifact.commitment.sha256, + archive_byte_count=request.prepared_artifact.commitment.byte_count, + semantic_manifest_sha256=request.manifest.sha256, + storage_scheme=self._storage_scheme, + ), + checker_facts=checker_facts, + ) async def prepare_authorization( self, @@ -209,7 +268,7 @@ async def prepare_authorization( prepared_artifact: PreparedArtifact, effective_plan: EffectivePreSubmissionExecutionPlan, idempotency_key: UUID, - ) -> PreparedAuthorizationHandle: + ) -> object: """Deny unavailable service authority before inspecting the ZIP.""" facts = self._preparation_facts( task_id=task_id, @@ -245,7 +304,7 @@ def _authority_facts( or request.prepared_artifact.commitment.byte_count != request.change_gate.archive_byte_count ): - raise PreSubmissionInfrastructureUnavailable( + raise PreSubmissionInfrastructureUnavailableError( "pre_submission_materialization_context_invalid" ) return PreSubmitMaterializationAuthorityFacts( @@ -266,13 +325,16 @@ def _preparation_facts( """Build pre-inspection facts from locked lineage and byte commitment.""" plan = effective_plan if plan.plan_sha256 != canonical_json_hash(plan.as_dict()): - raise PreSubmissionInfrastructureUnavailable("pre_submission_plan_identity_invalid") + raise PreSubmissionInfrastructureUnavailableError( + "pre_submission_plan_identity_invalid" + ) if ( submission_artifact_policy_id != plan.lineage.effective_policy_id or checker_policy_id != plan.lineage.pre_submit_policy_id - or plan.catalogue_manifest_sha256 != self._catalogue.manifest_sha256 + or plan.catalogue_manifest_sha256 + != self._checker_execution.catalogue_manifest_sha256 ): - raise PreSubmissionInfrastructureUnavailable( + raise PreSubmissionInfrastructureUnavailableError( "pre_submission_materialization_context_invalid" ) commitment = prepared_artifact.commitment @@ -305,18 +367,22 @@ def __init__( *, session: AsyncSession, materialization: PreparedBundleMaterializationService, + preparation_authorization: SubmissionBundlePreparationAuthorization, + task_contexts: TaskSubmissionContextPort, + project_contexts: ProjectLockedPolicyContextPort, ) -> None: """Bind execution to the transaction used for durable evidence.""" self._session = session self._materialization = materialization + self._preparation_authorization = preparation_authorization + self._task_contexts = task_contexts + self._project_contexts = project_contexts async def execute( self, request: PreparedBundleMaterializationRequest, *, - actor_profile_id: UUID, - identity_link_id: UUID, - predecessor_submission_id: UUID | None, + preparation_request: SubmissionBundlePreparationRequest, ) -> PreSubmitEvidencePersistenceResult: """Persist only after materialization has returned and cleaned its scratch lease.""" if self._session.in_transaction(): @@ -327,14 +393,12 @@ async def execute( return await self.persist( request, execution=execution, - actor_profile_id=actor_profile_id, - identity_link_id=identity_link_id, - predecessor_submission_id=predecessor_submission_id, + preparation_request=preparation_request, ) async def materialize( self, request: PreparedBundleMaterializationRequest - ) -> PreSubmissionExecutionResult: + ) -> PreSubmitExecutionResult: """Consume fixed-service authority while its owning transaction is active.""" return await self._materialization.materialize_prepared_bundle(request) @@ -342,10 +406,8 @@ async def persist( self, request: PreparedBundleMaterializationRequest, *, - execution: PreSubmissionExecutionResult, - actor_profile_id: UUID, - identity_link_id: UUID, - predecessor_submission_id: UUID | None, + execution: PreSubmitExecutionResult, + preparation_request: SubmissionBundlePreparationRequest, ) -> PreSubmitEvidencePersistenceResult: """Persist completed, cleaned execution evidence in a fresh transaction.""" if self._session.in_transaction(): @@ -356,13 +418,20 @@ async def persist( prepared_generation_id = request.prepared_artifact.generation_id async with self._session.begin(): await self._session.execute(text("set transaction isolation level read committed")) - return await PreSubmitEvidenceService(self._session).persist( + await self._preparation_authorization.revalidate(request=preparation_request) + return await PreSubmitEvidenceService( + self._session, + task_contexts=self._task_contexts, + project_contexts=self._project_contexts, + ).persist( PreSubmitEvidencePersistenceRequest( - actor_profile_id=actor_profile_id, - identity_link_id=identity_link_id, + actor_profile_id=preparation_request.actor.actor_profile_id, + identity_link_id=preparation_request.actor.identity_link_id, task_id=request.task_id, assignment_id=request.assignment_id, - predecessor_submission_id=predecessor_submission_id, + predecessor_submission_id=( + preparation_request.predecessor_submission_id + ), prepared_generation_id=prepared_generation_id, archive_sha256=commitment.sha256, archive_byte_count=commitment.byte_count, diff --git a/backend/app/modules/tasks/router.py b/backend/app/modules/tasks/router.py index 1f1e62d72..dac7b68ce 100644 --- a/backend/app/modules/tasks/router.py +++ b/backend/app/modules/tasks/router.py @@ -3,26 +3,14 @@ from __future__ import annotations from typing import Annotated -from uuid import UUID - -from fastapi import APIRouter, Depends, Header, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import JSONResponse -from pydantic import BaseModel, ConfigDict from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps.auth import actor_registry_http_error, get_registered_actor from app.core.api_controls import StructuredHTTPException, error_response from app.core.permissions import PermissionDenied from app.db.session import get_db_session -from app.adapters.artifacts import get_submission_bundle_preparation_command -from app.interfaces.artifact_operations import SubmissionBundlePreparationRequest -from app.modules.artifacts.authorization import get_artifact_authorization_context -from app.modules.artifacts.schemas import ArtifactAuthorityDeniedError -from app.modules.artifacts.submission_admission import ( - SubmissionBundlePreparationCommand, - SubmissionBundlePreparationRejected, -) -from app.modules.authorization.runtime import AuthorizationContext from app.modules.actors.schemas import ( LegacyWorkflowEligibilityActivationRequest, LegacyWorkflowEligibilityResponse, @@ -40,24 +28,12 @@ TaskWorkContextResponse, TaskWithAssignmentResponse, ) -from app.modules.tasks.pre_submit_context import PreSubmitLockedContextInvalid from app.modules.tasks.service import TaskService, TaskServiceError from app.schemas.auth import ActorContext router = APIRouter(tags=["tasks"]) -class SubmissionBundlePreparationResponse(BaseModel): - """Bounded hidden operation state without provider or scratch coordinates.""" - - model_config = ConfigDict(extra="forbid", from_attributes=True) - - put_attempt_id: UUID - admission_id: UUID | None - status: str - replayed: bool - - CANONICAL_ERROR_OBJECT_SCHEMA = {"$ref": "#/components/schemas/ApiError"} PRE_SUBMIT_DOMAIN_ERROR_RESPONSE_SCHEMA = { "oneOf": [ @@ -79,18 +55,6 @@ class SubmissionBundlePreparationResponse(BaseModel): } -def _require_ascii_submission_packet_headers(summary: str, attestation: str) -> None: - """Reject lossy HTTP-header decoding before immutable evidence hashing.""" - try: - summary.encode("ascii") - attestation.encode("ascii") - except UnicodeEncodeError as exc: - raise HTTPException( - status_code=422, - detail="submission_bundle_packet_header_encoding_invalid", - ) from exc - - TASK_LOCKED_CONTEXT_DOMAIN_ERROR_RESPONSE_SCHEMA = { "oneOf": [ { @@ -406,71 +370,6 @@ async def start_task( raise task_http_error(exc) from exc -@router.post( - "/tasks/{task_id}/submission-bundle-preparations", - response_model=SubmissionBundlePreparationResponse, - status_code=status.HTTP_202_ACCEPTED, - include_in_schema=False, -) -async def prepare_submission_bundle( - task_id: str, - request: Request, - context: Annotated[AuthorizationContext, Depends(get_artifact_authorization_context)], - command: Annotated[ - SubmissionBundlePreparationCommand, - Depends(get_submission_bundle_preparation_command), - ], - assignment_id: Annotated[str | None, Header(alias="X-Task-Assignment-Id")] = None, - idempotency_key: Annotated[str | None, Header(alias="Idempotency-Key")] = None, - summary: Annotated[str | None, Header(alias="X-Submission-Summary")] = None, - contributor_attestation: Annotated[ - str | None, Header(alias="X-Contributor-Attestation") - ] = None, - predecessor_submission_id: Annotated[ - str | None, Header(alias="X-Predecessor-Submission-Id") - ] = None, -) -> SubmissionBundlePreparationResponse: - """Run the hidden continuous ZIP preparation surface; AUTH remains fail closed.""" - if None in (assignment_id, idempotency_key, summary, contributor_attestation): - raise HTTPException(status_code=404, detail="Task not found") - assert assignment_id is not None and idempotency_key is not None - assert summary is not None and contributor_attestation is not None - _require_ascii_submission_packet_headers(summary, contributor_attestation) - try: - identifiers = ( - UUID(task_id), - UUID(assignment_id), - UUID(idempotency_key), - UUID(predecessor_submission_id) if predecessor_submission_id else None, - ) - except ValueError as exc: - raise HTTPException(status_code=404, detail="Task not found") from exc - try: - result = await command.prepare( - SubmissionBundlePreparationRequest( - authorization_context=context, - task_id=identifiers[0], - assignment_id=identifiers[1], - predecessor_submission_id=identifiers[3], - idempotency_key=identifiers[2], - summary=summary, - contributor_attestation=contributor_attestation, - media_type=request.headers.get("content-type", ""), - byte_source=request.stream(), - ) - ) - except ArtifactAuthorityDeniedError as exc: - raise HTTPException(status_code=404, detail="Task not found") from exc - except SubmissionBundlePreparationRejected as exc: - raise HTTPException(status_code=422, detail=str(exc)) from exc - except PreSubmitLockedContextInvalid as exc: - raise HTTPException( - status_code=409, - detail="submission_bundle_preparation_context_changed", - ) from exc - return SubmissionBundlePreparationResponse.model_validate(result, from_attributes=True) - - @router.post( "/tasks/{task_id}/submissions", response_model=SubmissionResponse, diff --git a/backend/scripts/module_boundaries.py b/backend/scripts/module_boundaries.py index 677f02430..f35480e92 100644 --- a/backend/scripts/module_boundaries.py +++ b/backend/scripts/module_boundaries.py @@ -122,6 +122,14 @@ def _source_module(source: str) -> str | None: return source.removeprefix(prefix).split("/", 1)[0] +def _source_owner_adapter(source: str) -> str | None: + """Return the module owned by one explicit adapter package.""" + prefix = "backend/app/adapters/" + if not source.startswith(prefix): + return None + return source.removeprefix(prefix).split("/", 1)[0] + + def _is_public_target(target: str, module: str) -> bool: public = f"{MODULE_PREFIX}{module}.api" return target == public or target.startswith(f"{public}.") @@ -187,6 +195,7 @@ def scan(root: Path, registry: Registry) -> tuple[set[PrivateEdge], dict[str, se for path in sorted(app_root.rglob("*.py")): source = path.relative_to(root).as_posix() source_module = _source_module(source) + source_owner_adapter = _source_owner_adapter(source) canonical_imports = authorization_boundary.source_imports(path, root) exact_imports = exact_source_imports(path, root, source_validated=True) for target in canonical_imports: @@ -197,6 +206,8 @@ def scan(root: Path, registry: Registry) -> tuple[set[PrivateEdge], dict[str, se raise ModuleBoundaryError("unknown_module") if source_module == target_module: continue + if source_owner_adapter == target_module: + continue edge = authorization_boundary.ImportEdge(source, target) if source_module == "authorization" or target_module == "authorization": if not _is_public_target(target, target_module): @@ -209,6 +220,8 @@ def scan(root: Path, registry: Registry) -> tuple[set[PrivateEdge], dict[str, se raise ModuleBoundaryError("unknown_module") if source_module == target_module: continue + if source_owner_adapter == target_module: + continue if source_module == "authorization" or target_module == "authorization": continue if _is_public_target(target, target_module): diff --git a/backend/tests/architecture/test_module_boundaries.py b/backend/tests/architecture/test_module_boundaries.py index 1fdad07df..17e7e97cd 100644 --- a/backend/tests/architecture/test_module_boundaries.py +++ b/backend/tests/architecture/test_module_boundaries.py @@ -85,6 +85,29 @@ def test_new_private_edge_fails_exact_ledger_comparison( boundary.validate(ROOT, REGISTRY, LEDGER, AUTH_LEDGER) +def test_owner_adapter_may_bind_only_its_own_private_implementation( + tmp_path: Path, +) -> None: + """Owner adapters are composition; cross-owner private imports remain debt.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/adapters/tasks/__init__.py", + "from app.modules.tasks.repository import TaskRepository\n" + "from app.modules.projects.repository import ProjectRepository\n", + ) + private, _, _ = boundary.scan( + tmp_path, boundary.load_registry(tmp_path / "registry.json") + ) + assert private == { + boundary.PrivateEdge( + "backend/app/adapters/tasks/__init__.py", + "projects", + "app.modules.projects.repository", + "WS-ARCH-001-03", + ) + } + + def test_import_from_modules_package_resolves_registered_alias(tmp_path: Path) -> None: """Package-level module aliases cannot disappear from dependency scanning.""" _registry(tmp_path / "registry.json") @@ -416,12 +439,16 @@ def test_initial_ledgers_capture_high_risk_application_edges() -> None: assert ( "backend/app/interfaces/artifact_operations.py", "app.modules.checkers.pre_submit_execution", - ) in actual - assert any( + ) not in actual + assert not any( source.startswith("backend/app/adapters/artifacts/") and target.startswith("app.modules.artifacts.") for source, target in actual ) + assert ( + "backend/app/adapters/artifacts/__init__.py", + "app.modules.actors.service_identities", + ) in actual assert any( source.startswith("backend/app/workers/") and target.startswith("app.modules.projects.") diff --git a/backend/tests/pre_submit_test_helpers.py b/backend/tests/pre_submit_test_helpers.py new file mode 100644 index 000000000..46ba1fc2f --- /dev/null +++ b/backend/tests/pre_submit_test_helpers.py @@ -0,0 +1,71 @@ +"""Shared dependency-safe construction for focused pre-submit tests.""" + +from uuid import UUID, uuid4 + +from app.adapters.checkers import PreSubmitCheckerExecutionAdapter +from app.modules.artifacts.api import SubmissionBundlePreparationRequest +from app.modules.artifacts.submission_materialization import ( + PreparedBundleMaterializationService, + PreparedBundlePreSubmitEvidenceService, +) +from app.modules.authorization.api import ActorIdentityFacts, ActorKind +from app.modules.projects.locked_policy_repository import ProjectLockedPolicyRepository +from app.modules.tasks.repository import TaskRepository + + +def checker_execution(inspector, catalogue) -> PreSubmitCheckerExecutionAdapter: + """Build the owner adapter used by ART materialization tests.""" + return PreSubmitCheckerExecutionAdapter( + archive_inspector=inspector, + catalogue=catalogue, + ) + + +def submission_preparation_request( + request, + *, + actor_profile_id: UUID, + identity_link_id: UUID, +) -> SubmissionBundlePreparationRequest: + """Project one private materialization fixture into the public ART request.""" + return SubmissionBundlePreparationRequest( + actor=ActorIdentityFacts( + actor_profile_id=actor_profile_id, + identity_link_id=identity_link_id, + actor_kind=ActorKind.HUMAN, + ), + request_id=uuid4(), + correlation_id=uuid4(), + task_id=request.task_id, + assignment_id=request.assignment_id, + predecessor_submission_id=None, + idempotency_key=uuid4(), + summary=request.packet.summary, + contributor_attestation=request.packet.contributor_attestation, + media_type="application/zip", + byte_source=_empty_bytes(), + ) + + +async def _empty_bytes(): + if False: # pragma: no cover - preserve the async-iterable request shape + yield b"" + + +def evidence_workflow( + *, session, preparation, inspector, catalogue, materialization_authorization, + preparation_authorization, +) -> PreparedBundlePreSubmitEvidenceService: + """Compose the exact public-port evidence workflow for database proof.""" + return PreparedBundlePreSubmitEvidenceService( + session=session, + materialization=PreparedBundleMaterializationService( + authorization=materialization_authorization, + preparation=preparation, + checker_execution=checker_execution(inspector, catalogue), + storage_scheme="s3", + ), + preparation_authorization=preparation_authorization, + task_contexts=TaskRepository(session), + project_contexts=ProjectLockedPolicyRepository(session), + ) diff --git a/backend/tests/test_artifact_architecture.py b/backend/tests/test_artifact_architecture.py index f1548906b..eeb940cad 100644 --- a/backend/tests/test_artifact_architecture.py +++ b/backend/tests/test_artifact_architecture.py @@ -13,12 +13,14 @@ BACKEND_ROOT = Path(__file__).parents[1] APP_ROOT = BACKEND_ROOT / "app" ARTIFACT_OPERATIONS = APP_ROOT / "interfaces" / "artifact_operations.py" +SUBMISSION_PREPARATION_API = ( + APP_ROOT / "modules" / "artifacts" / "api" / "submission_preparation.py" +) COMPOSITION_ROOT = APP_ROOT / "adapters" / "artifacts" / "__init__.py" S3_ADAPTER_MODULE = APP_ROOT / "adapters" / "artifacts" / "s3_compatible.py" CLOSED_PORTS = { "GuideArtifactIngestCommand", "GuideArtifactIngestPort", - "SubmissionBundlePreparationPort", "ArtifactBindingPort", "ArtifactMaterializationPort", "CheckerArtifactOutputPort", @@ -32,8 +34,6 @@ "GuideSourceMaterializationRequest", "SubmissionBindingRequest", "CheckerOutputBindingRequest", - "SubmissionBundlePreparationRequest", - "PreparedBundleMaterializationRequest", "BindingMaterializationRequest", "CheckerOutputArtifactRequest", "ArtifactRecoveryRequest", @@ -58,7 +58,6 @@ "ArtifactRecoveryRequest", "GuideSufficiencyMaterialRequest", "GuideSourceMaterializationRequest", - "SubmissionBundlePreparationRequest", } PREPARED_HANDLE_FORBIDDEN_ROOTS = ( APP_ROOT / "adapters", @@ -515,14 +514,12 @@ def test_durable_artifact_mutation_ports_require_process_local_prepared_authorit expected_methods = { "GuideArtifactIngestPort": {"ingest"}, - "SubmissionBundlePreparationPort": {"prepare"}, "ArtifactBindingPort": { "bind_guide_source", "bind_submission", "bind_checker_output", }, "ArtifactMaterializationPort": { - "materialize_prepared_bundle", "materialize_guide_source", "materialize_bindings", }, @@ -530,11 +527,9 @@ def test_durable_artifact_mutation_ports_require_process_local_prepared_authorit } expected_request_by_method = { "ingest": "GuideArtifactIngestRequest", - "prepare": "SubmissionBundlePreparationRequest", "bind_guide_source": "GuideSourceBindingRequest", "bind_submission": "SubmissionBindingRequest", "bind_checker_output": "CheckerOutputBindingRequest", - "materialize_prepared_bundle": "PreparedBundleMaterializationRequest", "materialize_guide_source": "GuideSourceMaterializationRequest", "materialize_bindings": "BindingMaterializationRequest", "store": "CheckerOutputArtifactRequest", @@ -567,7 +562,7 @@ def test_durable_artifact_mutation_ports_require_process_local_prepared_authorit def test_submission_preparation_http_request_never_carries_prepared_authority() -> None: - tree = _tree(ARTIFACT_OPERATIONS) + tree = _tree(SUBMISSION_PREPARATION_API) request_class = next( node for node in tree.body @@ -579,7 +574,9 @@ def test_submission_preparation_http_request_never_carries_prepared_authority() if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) } assert "prepared_authorization" not in fields - assert fields["authorization_context"] == {"AuthorizationContext"} + assert fields["actor"] == {"ActorIdentityFacts"} + assert fields["request_id"] == {"UUID"} + assert fields["correlation_id"] == {"UUID"} assert fields["idempotency_key"] == {"UUID"} diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 5396c1b8e..b1c9a3ab9 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -20,7 +20,6 @@ from app.core.hashing import canonical_json_hash from app.core.config import Settings -from app.interfaces.artifact_operations import PreparedBundleMaterializationRequest from app.modules.artifacts.preparation import ( HARD_MAXIMUM_ARTIFACT_BYTES, ArtifactPreparationLimits, @@ -38,6 +37,7 @@ ArtifactVerificationReceipt, SubmissionBundleDurableIntent, ) +from app.modules.artifacts.pre_submit_evidence import PreSubmitEvidenceConflict, _validate_execution from app.modules.artifacts.service import ( ArtifactAdmissionRelationshipError, ArtifactAdmissionService, @@ -64,6 +64,7 @@ ) from app.modules.artifacts.submission_materialization import ( DenyPreSubmitMaterializationAuthorization, + PreparedBundleMaterializationRequest, PreparedBundlePreSubmitEvidenceService, PreparedBundleMaterializationService, ) @@ -80,11 +81,15 @@ ) from app.modules.checkers.pre_submit_execution import ( PreSubmissionResultStatus, - PreSubmissionInfrastructureUnavailable, SubmissionPacketView, - validate_pre_submission_execution_result, ) +from app.modules.checkers.api import PreSubmissionInfrastructureUnavailableError from tests.artifact_store_helpers import artifact_admission_limit_settings +from tests.pre_submit_test_helpers import ( + checker_execution as _CheckerExecution, + evidence_workflow, + submission_preparation_request, +) async def _bytes(value: bytes): @@ -117,14 +122,15 @@ async def test_evidence_workflow_requires_transaction_free_session() -> None: workflow = PreparedBundlePreSubmitEvidenceService( session=cast(Any, SimpleNamespace(in_transaction=lambda: True)), materialization=cast(Any, materialization), + preparation_authorization=cast(Any, SimpleNamespace()), + task_contexts=cast(Any, SimpleNamespace()), + project_contexts=cast(Any, SimpleNamespace()), ) with pytest.raises(RuntimeError, match="requires a transaction-free session"): await workflow.execute( cast(Any, object()), - actor_profile_id=uuid4(), - identity_link_id=uuid4(), - predecessor_submission_id=None, + preparation_request=cast(Any, object()), ) @@ -233,8 +239,6 @@ async def prepare(self, *, facts, idempotency_key): async def consume(self, **values): self.facts = values["facts"] - self.action_id = values["action_id"] - self.service_identity = values["service_identity"] class _TestPreparedAuthorizationHandle: @@ -295,8 +299,7 @@ async def test_authority_denial_precedes_workspace_and_checker_access(tmp_path: service = PreparedBundleMaterializationService( authorization=DenyPreSubmitMaterializationAuthorization(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -329,8 +332,9 @@ async def forbidden_inspection(*_args, **_kwargs): service = PreparedBundleMaterializationService( authorization=DenyPreSubmitMaterializationAuthorization(), preparation=preparation, - archive_inspector=SubmissionArchiveInspector(SubmissionArchiveLimits()), - catalogue=catalogue, + checker_execution=_CheckerExecution( + SubmissionArchiveInspector(SubmissionArchiveLimits()), catalogue + ), storage_scheme="s3", ) @@ -373,13 +377,12 @@ async def test_manifest_drift_denies_before_authority_and_workspace(tmp_path: Pa service = PreparedBundleMaterializationService( authorization=authority, preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) with pytest.raises( - PreSubmissionInfrastructureUnavailable, + PreSubmissionInfrastructureUnavailableError, match="materialization_context_invalid", ): await service.materialize_prepared_bundle( @@ -401,8 +404,7 @@ async def test_two_stage_authority_uses_one_handle_and_exact_final_facts( service = PreparedBundleMaterializationService( authorization=authority, preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) handle = await service.prepare_authorization( @@ -642,15 +644,19 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( } session_factory = async_sessionmaker(engine, expire_on_commit=False) async with session_factory() as session: - workflow = PreparedBundlePreSubmitEvidenceService( + preparation_authority = cast(Any, SimpleNamespace(revalidate=AsyncMock())) + workflow = evidence_workflow( session=session, - materialization=PreparedBundleMaterializationService( - authorization=_AllowAuthority(), - preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, - storage_scheme="s3", - ), + preparation=preparation, + inspector=inspector, + catalogue=catalogue, + materialization_authorization=_AllowAuthority(), + preparation_authorization=preparation_authority, + ) + preparation_request = submission_preparation_request( + request, + actor_profile_id=actor_id, + identity_link_id=identity_link_id, ) async def fresh_checked_bundle(): @@ -674,24 +680,18 @@ async def fresh_checked_bundle(): ) result = await workflow.execute( fresh_request, - actor_profile_id=actor_id, - identity_link_id=identity_link_id, - predecessor_submission_id=None, + preparation_request=preparation_request, ) assert result.pass_capability is not None return prepared, result first = await workflow.execute( request, - actor_profile_id=actor_id, - identity_link_id=identity_link_id, - predecessor_submission_id=None, + preparation_request=preparation_request, ) replay = await workflow.execute( request, - actor_profile_id=actor_id, - identity_link_id=identity_link_id, - predecessor_submission_id=None, + preparation_request=preparation_request, ) assert first.pass_capability is not None namespace = ArtifactStorageNamespaceSpec( @@ -1018,9 +1018,7 @@ async def publish_ready() -> str: ) blocked = await workflow.execute( blocked_request, - actor_profile_id=actor_id, - identity_link_id=identity_link_id, - predecessor_submission_id=None, + preparation_request=preparation_request, ) async with engine.begin() as connection: evidence_count = int( @@ -1142,13 +1140,12 @@ async def test_materializer_rejects_policy_lineage_mismatch_before_authority( service = PreparedBundleMaterializationService( authorization=authority, preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) with pytest.raises( - PreSubmissionInfrastructureUnavailable, + PreSubmissionInfrastructureUnavailableError, match="pre_submission_materialization_context_invalid", ): await service.materialize_prepared_bundle( @@ -1170,8 +1167,7 @@ async def test_effective_executor_uses_plan_order_and_dispatches_project_rules( service = PreparedBundleMaterializationService( authorization=authority, preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -1182,13 +1178,14 @@ async def test_effective_executor_uses_plan_order_and_dispatches_project_rules( for entry in request.effective_plan.entries if entry.phase in set(PreSubmissionCheckerPhase) ] - assert [entry.definition.definition_id for entry in result.entries] == expected - assert any(entry.definition.definition_id.startswith("policy.") for entry in result.entries) - assert all(entry.status is PreSubmissionResultStatus.PASSED for entry in result.entries) + assert [entry.definition_id for entry in result.entries] == expected + assert any(entry.definition_id.startswith("policy.") for entry in result.entries) + assert all( + entry.checker_execution_status == PreSubmissionResultStatus.PASSED.value + for entry in result.entries + ) assert result.eligible is True assert authority.facts is not None - assert authority.action_id.value == "artifact.pre_submit.checker_input.materialize" - assert authority.service_identity.value == "workstream.artifact.materializer" assert authority.facts.task_id == request.task_id assert authority.facts.assignment_id == request.assignment_id assert authority.facts.project_id == request.effective_plan.lineage.project_id @@ -1217,25 +1214,26 @@ async def test_blocking_default_stops_later_dependency_without_review_decision( service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) result = await service.materialize_prepared_bundle(request) - by_id = {entry.definition.definition_id: entry for entry in result.entries} + by_id = {entry.definition_id: entry for entry in result.entries} - assert by_id["artifact.sensitive_paths.high_confidence"].status is ( - PreSubmissionResultStatus.FAILED + assert ( + by_id["artifact.sensitive_paths.high_confidence"].checker_execution_status + == PreSubmissionResultStatus.FAILED.value ) - assert by_id["artifact.quality.placeholder_signal"].status is ( - PreSubmissionResultStatus.DEPENDENCY_NOT_RUN + assert ( + by_id["artifact.quality.placeholder_signal"].checker_execution_status + == PreSubmissionResultStatus.DEPENDENCY_NOT_RUN.value ) assert result.eligible is False assert all( value not in {"accept", "needs_revision", "reject"} for entry in result.entries - for value in (entry.status.value, entry.message_code, entry.failure_code) + for value in (entry.checker_execution_status, entry.message_code, entry.failure_code) if value is not None ) await request.prepared_artifact.close() @@ -1251,8 +1249,7 @@ async def test_disabled_advisory_is_explicit_and_not_skipped_success(tmp_path: P service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -1260,10 +1257,13 @@ async def test_disabled_advisory_is_explicit_and_not_skipped_success(tmp_path: P advisory = next( entry for entry in result.entries - if entry.definition.definition_id == "artifact.quality.placeholder_signal" + if entry.definition_id == "artifact.quality.placeholder_signal" ) - assert advisory.status is PreSubmissionResultStatus.ADVISORY_DISABLED + assert ( + advisory.checker_execution_status + == PreSubmissionResultStatus.ADVISORY_DISABLED.value + ) assert result.eligible is True await request.prepared_artifact.close() manager.close() @@ -1279,8 +1279,7 @@ async def test_quality_warning_emits_only_a_bounded_category_count(tmp_path: Pat service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -1288,10 +1287,10 @@ async def test_quality_warning_emits_only_a_bounded_category_count(tmp_path: Pat warning = next( entry for entry in result.entries - if entry.definition.definition_id == "artifact.quality.placeholder_signal" + if entry.definition_id == "artifact.quality.placeholder_signal" ) - assert warning.status is PreSubmissionResultStatus.WARNING + assert warning.checker_execution_status == PreSubmissionResultStatus.WARNING.value assert warning.metadata == (("matched_category_count", 2),) assert result.eligible is True await request.prepared_artifact.close() @@ -1306,12 +1305,11 @@ async def test_forged_plan_identity_fails_closed_and_cleans_workspace(tmp_path: service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) - with pytest.raises(PreSubmissionInfrastructureUnavailable, match="plan_identity"): + with pytest.raises(PreSubmissionInfrastructureUnavailableError, match="plan_identity"): await service.materialize_prepared_bundle(request) assert list((tmp_path / "scratch" / "workspaces").iterdir()) == [] @@ -1374,12 +1372,11 @@ def __getattr__(self, name): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=selected_catalogue, + checker_execution=_CheckerExecution(inspector, selected_catalogue), storage_scheme="s3", ) - with pytest.raises(PreSubmissionInfrastructureUnavailable, match=expected_message): + with pytest.raises(PreSubmissionInfrastructureUnavailableError, match=expected_message): await service.materialize_prepared_bundle(request) assert list((tmp_path / "scratch" / "workspaces").iterdir()) == [] @@ -1410,12 +1407,11 @@ async def test_disabled_mandatory_executor_state_fails_closed(tmp_path: Path) -> service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) - with pytest.raises(PreSubmissionInfrastructureUnavailable): + with pytest.raises(PreSubmissionInfrastructureUnavailableError): await service.materialize_prepared_bundle(request) assert list((tmp_path / "scratch" / "workspaces").iterdir()) == [] @@ -1457,8 +1453,7 @@ async def test_effective_execution_enforces_project_only_forbidden_rule(tmp_path service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -1468,9 +1463,9 @@ async def test_effective_execution_enforces_project_only_forbidden_rule(tmp_path project_result = next( entry for entry in result.entries - if entry.definition.definition_id == "policy.artifact.forbid" + if entry.definition_id == "policy.artifact.forbid" ) - assert project_result.status is PreSubmissionResultStatus.FAILED + assert project_result.checker_execution_status == PreSubmissionResultStatus.FAILED.value await request.prepared_artifact.close() manager.close() @@ -1481,8 +1476,7 @@ async def test_effective_execution_enforces_server_owned_storage_scheme(tmp_path service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="local", ) @@ -1491,9 +1485,9 @@ async def test_effective_execution_enforces_server_owned_storage_scheme(tmp_path policy_result = next( entry for entry in result.entries - if entry.definition.definition_id == "policy.storage_scheme.enforce" + if entry.definition_id == "policy.storage_scheme.enforce" ) - assert policy_result.status is PreSubmissionResultStatus.FAILED + assert policy_result.checker_execution_status == PreSubmissionResultStatus.FAILED.value assert policy_result.message_code == "storage_scheme_not_allowed" await request.prepared_artifact.close() manager.close() @@ -1505,28 +1499,30 @@ async def test_canonical_result_validator_rejects_forged_definition(tmp_path: Pa service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) result = await service.materialize_prepared_bundle(request) first = result.entries[0] forged = replace( result, - entries=( - replace( - first, - definition=replace(first.definition, public_name="caller-selected"), + checker_facts=replace( + result.checker_facts, + entries=( + replace( + first, + public_name="caller-selected", + ), + *result.entries[1:], ), - *result.entries[1:], ), ) with pytest.raises( - PreSubmissionInfrastructureUnavailable, + PreSubmitEvidenceConflict, match="pre_submission_result_context_invalid", ): - validate_pre_submission_execution_result(request.effective_plan, forged) + _validate_execution(request.effective_plan, forged) await request.prepared_artifact.close() manager.close() @@ -1546,8 +1542,7 @@ def forbidden(*_args, **_kwargs): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) @@ -1578,8 +1573,7 @@ def blocking_process(self, reader, workspace): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) task = asyncio.create_task(service.materialize_prepared_bundle(request)) @@ -1612,8 +1606,9 @@ def _project_file(self, *args, **kwargs): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=_BlockingProjectionInspector(SubmissionArchiveLimits()), - catalogue=catalogue, + checker_execution=_CheckerExecution( + _BlockingProjectionInspector(SubmissionArchiveLimits()), catalogue + ), storage_scheme="s3", ) task = asyncio.create_task(service.materialize_prepared_bundle(request)) @@ -1653,8 +1648,7 @@ def blocking_process(self, reader, workspace): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=inspector, - catalogue=catalogue, + checker_execution=_CheckerExecution(inspector, catalogue), storage_scheme="s3", ) task = asyncio.create_task(service.materialize_prepared_bundle(request)) @@ -1705,8 +1699,9 @@ def observed_execute(self, tree): service = PreparedBundleMaterializationService( authorization=_AllowAuthority(), preparation=preparation, - archive_inspector=_BlockingSealInspector(SubmissionArchiveLimits()), - catalogue=catalogue, + checker_execution=_CheckerExecution( + _BlockingSealInspector(SubmissionArchiveLimits()), catalogue + ), storage_scheme="s3", ) task = asyncio.create_task(service.materialize_prepared_bundle(request)) diff --git a/backend/tests/test_effective_pre_submit_execution.py b/backend/tests/test_effective_pre_submit_execution.py index 924955a29..01c2e4688 100644 --- a/backend/tests/test_effective_pre_submit_execution.py +++ b/backend/tests/test_effective_pre_submit_execution.py @@ -13,6 +13,8 @@ load_locked_pre_submit_context, ) from app.modules.artifacts.pre_submit_evidence import ( + PreSubmitExecutionCustody, + PreSubmitExecutionResult, PreSubmitEvidenceConflict, PreSubmitEvidenceContext, PreSubmitEvidenceService, @@ -20,6 +22,10 @@ pre_submit_failure_audit_payload, semantic_manifest_identity, ) +from app.modules.checkers.api import ( + PreSubmissionExecutionEntryFacts, + PreSubmissionExecutionFacts, +) from app.modules.checkers.compiler import ( PreSubmitCheckerCompilerError, compile_effective_project_submission_artifact_policy, @@ -89,7 +95,11 @@ def test_semantic_manifest_identity_is_server_deterministic() -> None: def test_pass_capability_is_generation_bound_and_single_use() -> None: evidence_set_id = uuid4() generation_id = uuid4() - capability = PreSubmitEvidenceService(SimpleNamespace())._mint_pass_capability( + capability = PreSubmitEvidenceService( + SimpleNamespace(), + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), + )._mint_pass_capability( evidence_set_id=evidence_set_id, prepared_generation_id=generation_id, predecessor_submission_id=None, @@ -348,39 +358,36 @@ def test_failure_audit_projection_is_bounded_and_path_free() -> None: task_id = uuid4() generation_id = uuid4() evidence = PersistedPreSubmitEvidence(uuid4(), _sha("1"), False) - execution = PreSubmissionExecutionResult( - plan_sha256=_sha("2"), - custody=PreSubmissionExecutionCustody( + execution = PreSubmitExecutionResult( + custody=PreSubmitExecutionCustody( prepared_generation_id=generation_id, archive_sha256=_sha("5"), archive_byte_count=1, semantic_manifest_sha256=_sha("6"), storage_scheme="s3", ), - eligible=False, - entries=( - PreSubmissionEntryResult( - schema_version="pre_submission_checker_result.v1", - definition=PreSubmissionResultDefinition( + checker_facts=PreSubmissionExecutionFacts( + plan_sha256=_sha("2"), + eligible=False, + entries=( + PreSubmissionExecutionEntryFacts( dispatch_authority="workstream.pre_submission_checker_catalogue", definition_id="policy.file.require", definition_version="v1", public_name="check_required_files", - source="locked_effective_project_submission_artifact_policy", - ), - policy_trace=PreSubmissionResultPolicyTrace( + policy_source="locked_effective_project_submission_artifact_policy", effective_plan_sha256=_sha("2"), rule_instance_id=_sha("3"), locked_policy_sha256=_sha("4"), + phase="project_policy", + order=10, + classification="mandatory_accountability", + severity="blocking", + checker_execution_status="failed", + failure_code="pre_submission_checker_failed", + message_code="required_file_missing", + metadata=(("finding_count", 1),), ), - phase="project_policy", - order=10, - classification="mandatory_accountability", - severity="blocking", - status=PreSubmissionResultStatus.FAILED, - failure_code="pre_submission_checker_failed", - message_code="required_file_missing", - metadata=(("finding_count", 1),), ), ), ) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 164c70981..77043456f 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -19,6 +19,12 @@ PreSubmitEvidenceService, PreSubmitPassCapability, ) +from app.modules.artifacts.api import ( + SubmissionBundlePreparationRejected, + SubmissionBundlePreparationRequest, + SubmissionBundlePreparationResult, + SubmissionBundlePreparationUnavailable, +) from app.modules.artifacts.preparation import ArtifactPreparationService, ArtifactScratchManager from app.modules.artifacts.schemas import ( ArtifactAdmissionResult, @@ -42,25 +48,40 @@ from app.modules.artifacts.submission_custody import SubmissionBundlePreparedCustody from app.modules.artifacts.submission_admission import ( PreparedSubmissionBundlePreparationCommand, - SubmissionBundlePreparationRejected, - SubmissionBundlePreparationResult, -) -from app.interfaces.artifact_operations import SubmissionBundlePreparationRequest -from app.modules.authorization.runtime import ( - ActorKind, - ActorStatus, - HumanAuthorizationContext, - IdentityLinkStatus, ) +from app.modules.authorization.api import ActorIdentityFacts, ActorKind from app.modules.authorization.prepared import PreparedAuthorizationHandle from tests.artifact_store_helpers import artifact_byte_stream, artifact_preparation_limits from app.main import create_app -from app.modules.tasks.router import ( +from app.api.routes.artifact_submissions import ( _require_ascii_submission_packet_headers, prepare_submission_bundle, - router as tasks_router, + router as submission_router, ) -from app.modules.tasks.pre_submit_context import PreSubmitLockedContextInvalid + + +def _actor() -> ActorIdentityFacts: + return ActorIdentityFacts( + actor_profile_id=uuid4(), + identity_link_id=uuid4(), + actor_kind=ActorKind.HUMAN, + ) + + +def _preparation_request(*, byte_source, media_type: str = "application/zip"): + return SubmissionBundlePreparationRequest( + actor=_actor(), + request_id=uuid4(), + correlation_id=uuid4(), + task_id=uuid4(), + assignment_id=uuid4(), + predecessor_submission_id=None, + idempotency_key=uuid4(), + summary="summary", + contributor_attestation="attestation", + media_type=media_type, + byte_source=byte_source, + ) def _sha(character: str) -> str: @@ -71,7 +92,7 @@ def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next( route - for route in tasks_router.routes + for route in submission_router.routes if getattr(route, "name", None) == "prepare_submission_bundle" ) assert route.include_in_schema is False @@ -93,16 +114,11 @@ def test_submission_packet_headers_reject_non_ascii() -> None: @pytest.mark.asyncio async def test_hidden_preparation_maps_locked_context_race_to_bounded_conflict() -> None: command = SimpleNamespace( - prepare=AsyncMock(side_effect=PreSubmitLockedContextInvalid("changed")) - ) - context = HumanAuthorizationContext( - actor_profile_id=uuid4(), - actor_kind=ActorKind.HUMAN, - actor_status=ActorStatus.ACTIVE, - identity_link_id=uuid4(), - identity_link_status=IdentityLinkStatus.ACTIVE, - request_id=uuid4(), - correlation_id=uuid4(), + prepare=AsyncMock( + side_effect=SubmissionBundlePreparationRejected( + "submission_bundle_preparation_context_changed" + ) + ) ) request = Request( { @@ -117,7 +133,7 @@ async def test_hidden_preparation_maps_locked_context_race_to_bounded_conflict() await prepare_submission_bundle( task_id=str(uuid4()), request=request, - context=context, + actor=_actor(), command=command, assignment_id=str(uuid4()), idempotency_key=str(uuid4()), @@ -161,30 +177,12 @@ def runtime_factory(): command = PreparedSubmissionBundlePreparationCommand( session=SimpleNamespace(), authority=DenySubmissionBundlePreparationAuthorization(), + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), runtime_factory=runtime_factory, ) - with pytest.raises(ArtifactAuthorityDeniedError): - await command.prepare( - SubmissionBundlePreparationRequest( - authorization_context=HumanAuthorizationContext( - actor_profile_id=uuid4(), - actor_kind=ActorKind.HUMAN, - actor_status=ActorStatus.ACTIVE, - identity_link_id=uuid4(), - identity_link_status=IdentityLinkStatus.ACTIVE, - request_id=uuid4(), - correlation_id=uuid4(), - ), - task_id=uuid4(), - assignment_id=uuid4(), - predecessor_submission_id=None, - idempotency_key=uuid4(), - summary="summary", - contributor_attestation="attestation", - media_type="application/zip", - byte_source=bytes_source(), - ) - ) + with pytest.raises(SubmissionBundlePreparationUnavailable): + await command.prepare(_preparation_request(byte_source=bytes_source())) assert reads == 0 @@ -197,6 +195,8 @@ async def test_hidden_preparation_closes_authority_after_invalid_media_type() -> command = PreparedSubmissionBundlePreparationCommand( session=SimpleNamespace(), authority=authority, + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), runtime_factory=Mock(side_effect=AssertionError("runtime must stay closed")), ) with pytest.raises( @@ -204,24 +204,8 @@ async def test_hidden_preparation_closes_authority_after_invalid_media_type() -> match="submission_bundle_media_type_invalid", ): await command.prepare( - SubmissionBundlePreparationRequest( - authorization_context=HumanAuthorizationContext( - actor_profile_id=uuid4(), - actor_kind=ActorKind.HUMAN, - actor_status=ActorStatus.ACTIVE, - identity_link_id=uuid4(), - identity_link_status=IdentityLinkStatus.ACTIVE, - request_id=uuid4(), - correlation_id=uuid4(), - ), - task_id=uuid4(), - assignment_id=uuid4(), - predecessor_submission_id=None, - idempotency_key=uuid4(), - summary="summary", - contributor_attestation="attestation", - media_type="application/json", - byte_source=artifact_byte_stream(b"{}"), + _preparation_request( + byte_source=artifact_byte_stream(b"{}"), media_type="application/json" ) ) authority.close.assert_called_once_with() @@ -242,6 +226,8 @@ async def test_existing_durable_preparation_projects_exact_ready_admission() -> command = PreparedSubmissionBundlePreparationCommand( session=session, authority=SimpleNamespace(), + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), runtime_factory=Mock(), ) @@ -294,16 +280,6 @@ async def test_hidden_preparation_replays_persisted_checked_custody(monkeypatch) async def runtime_factory(): yield runtime - monkeypatch.setattr( - submission_admission_module, - "load_locked_pre_submit_context", - AsyncMock(return_value=locked), - ) - monkeypatch.setattr( - submission_admission_module, - "compile_locked_pre_submit_plan", - Mock(return_value=object()), - ) monkeypatch.setattr( submission_admission_module, "build_submission_manifest", @@ -318,22 +294,24 @@ async def runtime_factory(): command = PreparedSubmissionBundlePreparationCommand( session=SimpleNamespace(begin=_transaction), authority=authority, + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), runtime_factory=runtime_factory, ) + command._lock_context = AsyncMock(return_value=(object(), locked)) + command._compile_plan = Mock(return_value=object()) command._load_predecessor = AsyncMock(return_value=None) command._existing_durable_result = AsyncMock(return_value=expected) result = await command.prepare( SubmissionBundlePreparationRequest( - authorization_context=HumanAuthorizationContext( + actor=ActorIdentityFacts( actor_profile_id=actor_id, - actor_kind=ActorKind.HUMAN, - actor_status=ActorStatus.ACTIVE, identity_link_id=uuid4(), - identity_link_status=IdentityLinkStatus.ACTIVE, - request_id=uuid4(), - correlation_id=uuid4(), + actor_kind=ActorKind.HUMAN, ), + request_id=uuid4(), + correlation_id=uuid4(), task_id=task_id, assignment_id=assignment_id, predecessor_submission_id=None, @@ -374,7 +352,11 @@ def test_durable_put_result_projects_without_losing_replay_state() -> None: def _capability(prepared, evidence_set_id): - service = PreSubmitEvidenceService(SimpleNamespace()) + service = PreSubmitEvidenceService( + SimpleNamespace(), + task_contexts=SimpleNamespace(), + project_contexts=SimpleNamespace(), + ) return service._mint_pass_capability( evidence_set_id=evidence_set_id, prepared_generation_id=prepared.generation_id, From e6aa3abafeff7223ff5cfded2100f144d8272ecc Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 02:56:56 +0100 Subject: [PATCH 02/27] fix(architecture): complete ART preparation boundary --- .agent-loop/CURRENT_STATE.md | 2 +- .../CHUNK_MAP.md | 4 +- .../INTENT.md | 4 +- .../PLAN.md | 5 +- .../STATUS.md | 9 +- ...ARCH-001-02D-art-preparation-public-api.md | 32 ++++--- .../WS-ARCH-001-02D-resource-manifest.md | 74 ++++++++++++++++ .../TEST_STRUCTURE_DEBT.json | 84 ++++++++---------- .../policies/architecture-boundaries.md | 18 ++-- .../private-edge-debt.v1.json | 60 +++++++++++++ backend/app/adapters/README.md | 4 + backend/app/adapters/artifacts/__init__.py | 21 +++-- .../app/api/routes/artifact_submissions.py | 13 --- .../modules/artifacts/pre_submit_evidence.py | 74 ++++++++-------- .../modules/artifacts/submission_admission.py | 36 +++++++- .../artifacts/submission_materialization.py | 8 +- backend/app/modules/checkers/api/__init__.py | 6 ++ .../app/modules/checkers/api/pre_submit.py | 80 +++++++++++++++++ .../modules/checkers/pre_submit_execution.py | 67 ++------------- backend/scripts/module_boundaries.py | 7 +- .../architecture/test_module_boundaries.py | 28 ++++++ backend/tests/test_authorization.py | 13 --- .../test_default_pre_submit_execution.py | 3 + .../test_effective_pre_submit_execution.py | 38 +++++++++ .../tests/test_pre_submit_evidence_relock.py | 69 +++++++++++++++ .../tests/test_submission_bundle_admission.py | 85 +++++++++++++++---- docs/architecture_lockdown.md | 8 +- docs/operations_backend_testing.md | 5 ++ docs/spec_artifact_storage_service.md | 24 ++++-- 29 files changed, 653 insertions(+), 228 deletions(-) create mode 100644 .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md create mode 100644 backend/tests/test_pre_submit_evidence_relock.py diff --git a/.agent-loop/CURRENT_STATE.md b/.agent-loop/CURRENT_STATE.md index ed82504c6..83ba152ef 100644 --- a/.agent-loop/CURRENT_STATE.md +++ b/.agent-loop/CURRENT_STATE.md @@ -28,7 +28,7 @@ authority; these records do not grant or withhold it. | Initiative | Durable state on `main` | Remaining boundary | |---|---|---| -| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Boundary foundation and TASK, PROJECT, and CHECKER public facts are merged through PR #320; no product behavior was activated | `02D`, the ART hidden-preparation public API and private-edge migration, is the next durable boundary | +| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Boundary foundation and TASK, PROJECT, CHECKER, and ART hidden-preparation public facts are complete through `02D`; no product behavior was activated | `02E` is the ART ready-admission consumption and binding public capability | | [WS-ART-001](initiatives/WS-ART-001-immutable-artifact-storage/STATUS.md) | Active delivery initiative; verified ready-admission publication and hidden preparation are merged through `04C2` | Replace the non-executable 05-wave XINT/ART contracts with WS-ARCH-001 split public-API contracts before activation, Submission consumption, or live cutover | | [WS-AUTH-001](initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md) | Active delivery initiative; project-policy authority and unified compilation authorization are merged through `12I` | POL-03B consumes 12I next; remaining AUTH activation chunks wait for their exact hidden owner behavior | | [WS-CON-001](initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md) | Active delivery initiative; policy persistence and shared lifecycle audit are merged | Complete hidden services, contribution records, conditional awards, fulfillment, and reconciliation after named AUTH and REV gates | diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md index ac33f90db..c0345669a 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md @@ -8,8 +8,8 @@ | `WS-ARCH-001-02A` | TASKS task/assignment/predecessor and Submission public facts/ports | L1 | Merged PR #314 | | `WS-ARCH-001-02B` | PROJECTS locked guide and submission-policy public facts/ports | L1 | Merged PR #315 | | `WS-ARCH-001-02C` | CHECKERS effective pre-submit plan and bounded execution-result public facts/ports | L1 | Merged PR #320; no contributor preparation action or public route activated | -| `WS-ARCH-001-02D` | ART hidden preparation public API and private-edge migration | L1 | Next durable boundary | -| `WS-ARCH-001-02E` | ART ready-admission consumption and binding public capability | L1 | Proposed after 02D | +| `WS-ARCH-001-02D` | ART hidden preparation public API and private-edge migration | L1 | Complete; production remains deny-only | +| `WS-ARCH-001-02E` | ART ready-admission consumption and binding public capability | L1 | Next durable boundary | | `WS-ARCH-001-02F` | TASK-owned immutable Submission command and hidden composed transaction | L1 | Proposed after 02E | | `WS-ARCH-001-02G` | AUTH contributor preparation activation after the complete hidden path | L1 | Proposed after 02F | | `WS-ARCH-001-02H` | AUTH human/fixed-service consumption activation | L1 | Proposed after 02G | diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/INTENT.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/INTENT.md index 08f60245f..e916529ac 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/INTENT.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/INTENT.md @@ -12,7 +12,9 @@ module and every cross-module call must use a typed public API. - The nine business modules and three supporting modules have one canonical ownership map. - Cross-module runtime imports use only the target module's `api` package. -- Concrete implementations meet only in the application composition root. +- Concrete implementations meet only in the application composition root or + the exact same-owner `backend/app/adapters//__init__.py` composition + root; nested adapters and cross-owner private imports remain prohibited. - Existing private-import debt is frozen as exact source-to-target edges. - Every feature chunk repairs the capabilities and debt edges it touches. - No debt count grows, and final closure requires an empty private-import diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/PLAN.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/PLAN.md index f02cf961d..3e1f188e1 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/PLAN.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/PLAN.md @@ -22,7 +22,10 @@ canonical module map opaque capability Protocols, and ports—not implementation. 4. The caller supplies server-owned canonical facts; it never receives the target's ORM model or repository. -5. Concrete implementations meet only in the application composition root. +5. Concrete implementations meet only in the application composition root or + the exact same-owner `backend/app/adapters//__init__.py` composition + root. That adapter root constructs typed public ports; the exception never + extends to nested adapter files or cross-owner private imports. 6. The application composition root opens the SQLAlchemy transaction/unit of work and constructs transaction-bound public-port implementations. The owning application command coordinates those injected ports without diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md index 3e5e570e2..2ec6a75a4 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md @@ -18,8 +18,13 @@ - WS-ARCH-001-02C merged through PR #320. It exposes CHECKER-owned effective-plan and bounded execution-result contracts without activating the contributor preparation route. -- Next durable boundary: WS-ARCH-001-02D only, the ART hidden-preparation public - API and private-edge migration. Open pull requests show transient ownership. +- WS-ARCH-001-02D is complete in this change. It moves the hidden preparation + route to delivery composition, exposes the bounded ART request/result/command + API, consumes TASK/PROJECT/CHECKER public capabilities, keeps AUTH handles + opaque, and preserves deny-only availability. +- Next durable boundary: WS-ARCH-001-02E only, the ART + ready-admission consumption and binding public capability. Open pull requests + show transient ownership. - Repository housekeeping after PR #315 found no competing clean-up initiative: WS-ARCH-001 remains the general boundary owner, WS-AUTH-003 owns AUTH-specific debt, and test-structure repairs remain incremental with the diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index c9953e245..280953b5f 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -60,6 +60,9 @@ backend/scripts/module_boundaries.py backend/tests/test_artifact_architecture.py backend/tests/test_submission_bundle_admission.py backend/tests/test_default_pre_submit_execution.py +backend/tests/test_effective_pre_submit_execution.py +backend/tests/test_authorization.py +backend/tests/test_pre_submit_evidence_relock.py backend/tests/pre_submit_test_helpers.py .ci/module-boundaries/private-edge-debt.v1.json .agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md @@ -67,8 +70,17 @@ backend/tests/pre_submit_test_helpers.py .ci/behavior-ownership/** .agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md +.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/CHUNK_MAP.md +.agent-loop/CURRENT_STATE.md .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md docs/spec_artifact_storage_service.md +.agent-loop/policies/architecture-boundaries.md +.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/INTENT.md +.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/PLAN.md +docs/architecture_lockdown.md +docs/operations_backend_testing.md +backend/app/adapters/README.md ``` ## Public and private contract split @@ -95,33 +107,33 @@ cross-module imports; serialized prepared handles; compatibility facades. ## Acceptance criteria -- [ ] The route and composition code depend on `artifacts.api`; ART depends on +- [x] The route and composition code depend on `artifacts.api`; ART depends on owner public APIs only. The composition root may instantiate concrete implementations but may not hide a service locator or second factory behind the public API. -- [ ] The public port shape places preflight before byte acceptance and final +- [x] The public port shape places preflight before byte acceptance and final prepared-authority consumption in the durable-intent transaction before capacity, put attempt, or provider I/O, but production remains deny-only and no successful prepared handle is issued or consumed in this chunk. -- [ ] Planned-action denial, concealment, exact replay, stale lineage, and +- [x] Planned-action denial, concealment, exact replay, stale lineage, and cross-resource attempts preserve zero partial effect and zero provider I/O. -- [ ] `artifact.submission_bundle.prepare` remains planned/unavailable and the +- [x] `artifact.submission_bundle.prepare` remains planned/unavailable and the route remains hidden. -- [ ] Every touched private edge is removed. -- [ ] Route-facing submission-preparation types in +- [x] Every touched private edge is removed. +- [x] Route-facing submission-preparation types in `app.interfaces.artifact_operations` migrate to `artifacts.api`. Process-local sealed types become ART-private; no parallel legacy/public ART contract remains. -- [ ] After byte materialization and before evidence persistence, ART re-locks +- [x] After byte materialization and before evidence persistence, ART re-locks TASK assignment/predecessor facts and PROJECT locked-policy facts through their public ports and compares them with the original CHECKER plan lineage. Stale lineage fails before durable evidence or provider I/O. -- [ ] CHECKER execution is injected at the composition root and returns only +- [x] CHECKER execution is injected at the composition root and returns only public CHECKER result facts. ART constructs and retains its own custody facts; neither module imports the other's private implementation. -- [ ] `artifacts.api` imports no private module and exposes no ORM, session, +- [x] `artifacts.api` imports no private module and exposes no ORM, session, repository, provider, scratch path, byte buffer, or serialized handle. -- [ ] Record the exact preparation resource/port manifest in +- [x] Record the exact preparation resource/port manifest in `.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md`. ## Verification commands diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md new file mode 100644 index 000000000..238c4d097 --- /dev/null +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/evidence/WS-ARCH-001-02D-resource-manifest.md @@ -0,0 +1,74 @@ +# WS-ARCH-001-02D ART Preparation Resource Manifest + +## Public entry capability + +`app.modules.artifacts.api.SubmissionBundlePreparationCommand.prepare` accepts +one `SubmissionBundlePreparationRequest` and returns one +`SubmissionBundlePreparationResult`. + +The request carries only: + +- dependency-safe `ActorIdentityFacts`; +- request and correlation identifiers; +- task, assignment, predecessor, and idempotency identifiers; +- bounded packet text and media type; +- the request-local asynchronous byte source. + +It carries no authorization context, prepared handle, database session, ORM +row, provider coordinate, scratch path, inspection result, custody value, or +durable pass capability. The route remains hidden from OpenAPI and the +production contributor authority remains deny-only. + +## Owner ports consumed by ART + +| Owner | Public capability | Facts used by ART | +|---|---|---| +| TASK | `TaskSubmissionContextPort.lock_submission_context` | Exact task, assignment, contributor, lifecycle kind/status, predecessor version, and task-stamped project-policy references | +| PROJECT | `ProjectLockedPolicyContextPort.lock_locked_policy_context` | Exact guide/source lineage, effective artifact policy, compiled pre-submit policy, statuses, and canonical policy JSON | +| CHECKER | `EffectivePreSubmissionPlanningPort.compile_effective_plan` | Immutable effective plan and plan identity | +| CHECKER | `PreSubmissionExecutionFacts` | Bounded ordered result facts only; ART retains its own byte and scratch custody | +| AUTH | `ActorIdentityFacts` plus ART-private opaque authorization port | Active actor and identity-link references; process-local prepared handles remain opaque `object` values and are never serialized | + +## Lock and authority order + +1. Contributor preflight occurs before the runtime opens or request bytes are + read. +2. TASK and then PROJECT facts are locked through their public ports, and the + CHECKER plan is compiled from those exact facts. +3. ART prepares and inspects bytes in bounded scratch. +4. Fixed-service materialization authority is consumed before workspace byte + access or checker execution. +5. After execution and scratch cleanup, contributor authority is revalidated, + then TASK and PROJECT facts are locked again in the evidence transaction. +6. Evidence persists only if assignment, predecessor, guide, policy, plan, + byte, manifest, and result facts remain exact. +7. Fresh final contributor authority is prepared and consumed in the durable + put-intent transaction before capacity reservation, put-attempt creation, + or provider I/O. + +## Private custody retained by ART + +These values are deliberately not public contracts: + +- `PreparedBundleMaterializationRequest`; +- prepared artifact and generation binding; +- archive inspection and semantic manifest; +- scratch workspace and reader; +- `PreSubmitExecutionCustody`; +- `PreSubmitPassCapability`; +- durable put and ready-admission internals. + +## Composition + +Delivery code lives in `app.api.routes.artifact_submissions`. Owner adapter +packages bind TASK, PROJECT, CHECKER, and ART implementations to public ports. +The boundary gate permits an owner adapter to import only its own module's +private implementation; cross-owner private imports remain ledgered debt and +new debt remains prohibited against the protected base. + +## Denial guarantees + +Unavailable authority, changed TASK/PROJECT lineage, mismatched plans or +results, copied/replayed/wrong opaque handles, and invalid media types fail +without publishing a successful preparation. Authority handles do not enter +request schemas, Celery payloads, provider contracts, logs, or persistence. diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 1b45f5363..ac5e82349 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -218,11 +218,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "6c02f75335bb5ad6bb023b7d80b0c8801c849feae308fa4f5446d38d6058b1ed", - "end_line": 13434, + "content_sha256": "ebb3b0ede78653a373c17904bc29d40da7ed93e96e0416e0742079386ca7c7f5", + "end_line": 13421, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 13434, + "observed_lines": 13421, "path": "backend/tests/test_authorization.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "07f1e5ebe7404bb7cca904259434abb10abbd783808aee5e1e8d0a55b7e548d0", - "end_line": 1723, + "content_sha256": "815033627fcbf67b794b672a4c4de552626f08999bc76e4e6fafc10848d5ee5f", + "end_line": 1726, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1723, + "observed_lines": 1726, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -531,50 +531,50 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "951b44cc07e36002118fe93b7974e8d65851b0e2c3cec89a3031cbe42b014e2d", - "end_line": 8534, + "end_line": 8521, "hard_limit": 120, "kind": "test_function", "observed_lines": 140, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_actor_lifecycle_service_applies_success_and_guards_conflicts", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8395 + "start_line": 8382 }, { "capability": "unassigned_legacy_auth", "content_sha256": "569084d6de89ff9eae71d526fc6c157aea7638f55ca93ad128b721fbda339be6", - "end_line": 9024, + "end_line": 9011, "hard_limit": 120, "kind": "test_function", "observed_lines": 122, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_admin_resource_digest_alone_rejects_substituted_role_and_disposition", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8903 + "start_line": 8890 }, { "capability": "unassigned_legacy_auth", "content_sha256": "90b8b670b4d277209617cc1aa794188b4fb3cd9d894b69d5dc3d0adfc885f6ed", - "end_line": 9252, + "end_line": 9239, "hard_limit": 120, "kind": "test_function", "observed_lines": 132, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_admin_revoke_stages_complete_state_and_evidence", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9121 + "start_line": 9108 }, { "capability": "unassigned_legacy_auth", "content_sha256": "78d9bf3df08e5633e9a75720b1e4bf5b7d7b24019bc392b4cb7496a9ac5e5e8e", - "end_line": 11086, + "end_line": 11073, "hard_limit": 120, "kind": "test_function", "observed_lines": 122, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_authorization_locks_refresh_cached_actor_lifecycle_state", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10965 + "start_line": 10952 }, { "capability": "unassigned_legacy_auth", @@ -603,62 +603,50 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "05d1b020ecff0f9bc0a0567adc07f5b31a2f9dfb7828ae3ad34d4e1e7757797c", - "end_line": 8687, + "end_line": 8674, "hard_limit": 120, "kind": "test_function", "observed_lines": 151, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_identity_link_lifecycle_service_applies_success_and_guards_conflicts", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8537 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "a45270573154ce2f298221169a3d55530059598bc976263feafc349c77c6ea46", - "end_line": 6339, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 121, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_pre_submit_materializer_adapter_binds_every_fact_and_service", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6219 + "start_line": 8524 }, { "capability": "unassigned_legacy_auth", "content_sha256": "c5d6d0d480ced964354915614f15202c0159bfbb281658da0d44186dffd17848", - "end_line": 7482, + "end_line": 7469, "hard_limit": 120, "kind": "test_function", "observed_lines": 142, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_actor_authority_crossed_mutations_complete_in_both_orders", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7341 + "start_line": 7328 }, { "capability": "unassigned_legacy_auth", "content_sha256": "ebb76e63671195aa4d806ac602bfe58cb22bf82d00c8a70ab186785f3acd7f9b", - "end_line": 7823, + "end_line": 7810, "hard_limit": 120, "kind": "test_function", "observed_lines": 336, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_crosses_real_lifecycle_service_transactions", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7488 + "start_line": 7475 }, { "capability": "unassigned_legacy_auth", "content_sha256": "9ea4fc0ddbab4c7262a43bc3f498ee1afea3318e9a0b6c93c87763f9f22aae9c", - "end_line": 7313, + "end_line": 7300, "hard_limit": 120, "kind": "test_function", "observed_lines": 518, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_prepared_postgresql_failure_and_cancellation_are_atomic", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6796 + "start_line": 6783 }, { "capability": "unassigned_legacy_auth", @@ -687,38 +675,38 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "105667302ed6e8f2fd16ea7e95d642e72e41514673e1152503536e0520f9c362", - "end_line": 10961, + "end_line": 10948, "hard_limit": 120, "kind": "test_function", "observed_lines": 204, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_read_permissions_have_postgresql_role_scope_matrix", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10758 + "start_line": 10745 }, { "capability": "unassigned_legacy_auth", "content_sha256": "756b7f99f9a743284934b4d85ce263617710d9b8119792ccb4526a1de04070a1", - "end_line": 12463, + "end_line": 12450, "hard_limit": 120, "kind": "test_function", "observed_lines": 233, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_role_and_all_operation_mappings_commit_one_linked_pair", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12231 + "start_line": 12218 }, { "capability": "unassigned_legacy_auth", "content_sha256": "2e1d7db74ddb955b90a0ee12e4fb72b651a0f85d2746c76e09079c05b0b85252", - "end_line": 13434, + "end_line": 13421, "hard_limit": 120, "kind": "test_function", "observed_lines": 681, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_project_role_issue_postgresql_prep_binds_target_role_and_scope", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12754 + "start_line": 12741 }, { "capability": "unassigned_legacy_auth", @@ -747,26 +735,26 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "05621e885ed2f88d0ba1a072c1f263fc923939f7ce755a514e9872112aa830a1", - "end_line": 11980, + "end_line": 11967, "hard_limit": 120, "kind": "test_function", "observed_lines": 163, "path": "backend/tests/test_authorization.py", "qualified_symbol": "test_service_actor_replay_fails_closed_on_committed_state_drift", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11818 + "start_line": 11805 }, { "capability": "unassigned_legacy_auth", - "content_sha256": "4cf5ee426ef4eab663019f2315012ace5a5d04444ea2cc5f713254ce1f268fe1", - "end_line": 1130, + "content_sha256": "9f13a11de08e874e791cc9d625f473d77ef31a0c00b57e573b7256e0d453df85", + "end_line": 1133, "hard_limit": 120, "kind": "test_function", - "observed_lines": 697, + "observed_lines": 699, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 434 + "start_line": 435 }, { "capability": "unassigned_legacy_auth", @@ -1035,14 +1023,14 @@ { "capability": "unassigned_legacy_auth", "content_sha256": "1a0a9f3e2be6965e29f76aa74272ccfa2fe4b99e4e0c3bde5ec8ba2c374b369b", - "end_line": 11294, + "end_line": 11281, "hard_limit": 100, "kind": "test_helper", "observed_lines": 169, "path": "backend/tests/test_authorization.py", "qualified_symbol": "_operation_success", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11126 + "start_line": 11113 }, { "capability": "unassigned_legacy_auth", diff --git a/.agent-loop/policies/architecture-boundaries.md b/.agent-loop/policies/architecture-boundaries.md index 8c7e4f157..7e6f10be6 100644 --- a/.agent-loop/policies/architecture-boundaries.md +++ b/.agent-loop/policies/architecture-boundaries.md @@ -10,8 +10,9 @@ Cross-module runtime imports target only `app.modules..api`. A public API may expose immutable facts, commands/results, stable errors, opaque capability protocols, and ports. It may not expose ORM models, repositories, routers, database sessions, concrete services, kernels, registries, or other -private implementation surfaces. Concrete implementations meet only in the -application composition root. +private implementation surfaces. Concrete implementations meet only in an +explicit composition root: the application root or the exact owner adapter +root `backend/app/adapters//__init__.py`. Application-level paths are explicit rather than invisible exceptions: @@ -20,15 +21,22 @@ Application-level paths are explicit rather than invisible exceptions: public ports; - `backend/app/api/**` and `backend/app/workers/**` are delivery/composition entry code and must consume typed module APIs; -- `backend/app/adapters/**` implements infrastructure capabilities and is wired - at composition boundaries; +- the exact `backend/app/adapters//__init__.py` file is that owner's + adapter composition root and may import its own private implementation solely + to construct typed public ports; this exception does not apply to nested + adapter files, cross-owner private imports, product services, or delivery + code; +- other `backend/app/adapters/**` files implement infrastructure capabilities + and must consume module public APIs; their existing private imports remain + frozen debt; - `backend/app/interfaces/**` is legacy shared-contract debt, not a permanent public-contract namespace; - `backend/app/db/models.py` is the sole metadata-discovery path and may import only module model declarations for SQLAlchemy registration. It gains no runtime service, repository, command, or authorization capability. -All current private imports in the first four surfaces are frozen exact debt. +Except for the exact same-owner adapter-root rule above, all current private +imports in the first four surfaces are frozen exact debt. Every application path is scanned, including paths outside `modules/`. The one-time bootstrap is permitted only when the protected base contains neither registry nor ledger and the installing change touches no diff --git a/.ci/module-boundaries/private-edge-debt.v1.json b/.ci/module-boundaries/private-edge-debt.v1.json index 7d26608b0..1652cc348 100644 --- a/.ci/module-boundaries/private-edge-debt.v1.json +++ b/.ci/module-boundaries/private-edge-debt.v1.json @@ -13,6 +13,42 @@ "imported_private_path": "app.modules.actors.service_identities", "repair_owner": "WS-ARCH-001-03" }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.authorization", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.guide_setup", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.models", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.preparation", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.schemas", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.service", + "repair_owner": "WS-ARCH-001-04" + }, { "source_file": "backend/app/adapters/artifacts/internal_workers.py", "target_module": "projects", @@ -25,6 +61,30 @@ "imported_private_path": "app.modules.projects.models", "repair_owner": "WS-ARCH-001-03" }, + { + "source_file": "backend/app/adapters/artifacts/local.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.preparation", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/local.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.sources", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/s3_compatible.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.preparation", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/adapters/artifacts/s3_compatible.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.sources", + "repair_owner": "WS-ARCH-001-04" + }, { "source_file": "backend/app/api/deps/api_controls.py", "target_module": "api_controls", diff --git a/backend/app/adapters/README.md b/backend/app/adapters/README.md index 0f49d6fe7..e5fa7e7d7 100644 --- a/backend/app/adapters/README.md +++ b/backend/app/adapters/README.md @@ -2,3 +2,7 @@ Adapters implement interfaces for concrete providers such as Flow auth, local storage, object storage, and checker runners. +Each exact `backend/app/adapters//__init__.py` is an owner composition +root. It may import that owner's private implementation only to construct typed +public ports. Nested adapter files must consume module public APIs, and no +adapter may import another module's private implementation. diff --git a/backend/app/adapters/artifacts/__init__.py b/backend/app/adapters/artifacts/__init__.py index ad4d457a6..1de501e0b 100644 --- a/backend/app/adapters/artifacts/__init__.py +++ b/backend/app/adapters/artifacts/__init__.py @@ -270,12 +270,21 @@ def get_submission_bundle_preparation_command( @asynccontextmanager async def runtime(): bootstrap = create_artifact_store_bootstrap(settings) - manager = create_artifact_scratch_manager(settings) - materialization_authority = PreparedPreSubmitMaterializationAuthorization( - session, - request_id=request_id, - correlation_id=correlation_id, - ) + try: + manager = create_artifact_scratch_manager(settings) + except BaseException: + bootstrap.close() + raise + try: + materialization_authority = PreparedPreSubmitMaterializationAuthorization( + session, + request_id=request_id, + correlation_id=correlation_id, + ) + except BaseException: + manager.close() + bootstrap.close() + raise try: namespace = artifact_storage_namespace_spec(settings, bootstrap) store = bootstrap.initialize_after_namespace_claim( diff --git a/backend/app/api/routes/artifact_submissions.py b/backend/app/api/routes/artifact_submissions.py index 788dc7155..1bc5e80c5 100644 --- a/backend/app/api/routes/artifact_submissions.py +++ b/backend/app/api/routes/artifact_submissions.py @@ -33,18 +33,6 @@ class SubmissionBundlePreparationResponse(BaseModel): replayed: bool -def _require_ascii_submission_packet_headers(summary: str, attestation: str) -> None: - """Reject lossy HTTP-header decoding before immutable evidence hashing.""" - try: - summary.encode("ascii") - attestation.encode("ascii") - except UnicodeEncodeError as exc: - raise HTTPException( - status_code=422, - detail="submission_bundle_packet_header_encoding_invalid", - ) from exc - - @router.post( "/tasks/{task_id}/submission-bundle-preparations", response_model=SubmissionBundlePreparationResponse, @@ -74,7 +62,6 @@ async def prepare_submission_bundle( raise HTTPException(status_code=404, detail="Task not found") assert assignment_id is not None and idempotency_key is not None assert summary is not None and contributor_attestation is not None - _require_ascii_submission_packet_headers(summary, contributor_attestation) try: identifiers = ( UUID(task_id), diff --git a/backend/app/modules/artifacts/pre_submit_evidence.py b/backend/app/modules/artifacts/pre_submit_evidence.py index 5023a08d1..f8f226c04 100644 --- a/backend/app/modules/artifacts/pre_submit_evidence.py +++ b/backend/app/modules/artifacts/pre_submit_evidence.py @@ -14,9 +14,12 @@ from app.modules.artifacts.models import PreSubmitEvidenceResult, PreSubmitEvidenceSet from app.modules.artifacts.sources import ArtifactCommitment from app.modules.checkers.api import ( + ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, EffectivePreSubmissionExecutionPlan, PreSubmissionExecutionEntryFacts, PreSubmissionExecutionFacts, + PreSubmissionInfrastructureUnavailableError, + validate_pre_submission_execution_facts, ) from app.modules.projects.api import ( ProjectLockedPolicyContextPort, @@ -24,15 +27,29 @@ ProjectLockedPolicyContextUnavailable, ) from app.modules.tasks.api import ( + TaskSubmissionContextFacts, TaskSubmissionContextPort, TaskSubmissionContextRequest, TaskSubmissionContextUnavailable, ) -ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) -_CHECKER_EXECUTION_STATUSES = frozenset( - {"passed", "warning", "advisory_disabled", "dependency_not_run", "failed"} -) +def validate_predecessor_lineage( + task_context: TaskSubmissionContextFacts, + *, + predecessor_submission_id: UUID | None, + predecessor_submission_version: int | None, +) -> None: + """Require the post-byte predecessor to equal the initially checked version.""" + predecessor = task_context.predecessor + if ( + (predecessor is None) != (predecessor_submission_id is None) + or predecessor is not None + and ( + predecessor.submission_id != predecessor_submission_id + or predecessor.version != predecessor_submission_version + ) + ): + raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") @dataclass(frozen=True, slots=True) @@ -184,6 +201,7 @@ class PreSubmitEvidencePersistenceRequest: task_id: UUID assignment_id: UUID predecessor_submission_id: UUID | None + expected_predecessor_submission_version: int | None prepared_generation_id: UUID archive_sha256: str archive_byte_count: int @@ -288,35 +306,16 @@ def _validate_execution( or len(execution.entries) != len(plan.entries) ): raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") - disqualified = False - for plan_entry, result in zip(plan.entries, execution.entries, strict=True): - expected_severity = ( - "warning" if plan_entry.classification == "advisory" else "blocking" + try: + validate_pre_submission_execution_facts( + plan, + PreSubmissionExecutionFacts( + plan_sha256=execution.plan_sha256, + eligible=execution.eligible, + entries=execution.entries, + ), ) - status = result.checker_execution_status - if ( - result.dispatch_authority != "workstream.pre_submission_checker_catalogue" - or result.definition_id != plan_entry.definition_id - or result.definition_version != plan_entry.definition_version - or result.public_name != plan_entry.public_name - or result.policy_source != plan_entry.policy_trace_source - or result.effective_plan_sha256 != plan.plan_sha256 - or result.rule_instance_id != plan_entry.rule_instance_id - or result.locked_policy_sha256 != plan.lineage.effective_policy_hash - or result.phase != plan_entry.phase - or result.order != plan_entry.order - or result.classification != plan_entry.classification - or result.severity != expected_severity - or status not in _CHECKER_EXECUTION_STATUSES - or not result.message_code - or result.failure_code - != (plan_entry.failure_code if status == "failed" else None) - or len(result.metadata) != len({key for key, _ in result.metadata}) - or any(type(value) is not int or value < 0 for _, value in result.metadata) - ): - raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") - disqualified = disqualified or status in {"failed", "dependency_not_run"} - if execution.eligible == disqualified: + except PreSubmissionInfrastructureUnavailableError: raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") @@ -711,6 +710,13 @@ async def persist( ) except (TaskSubmissionContextUnavailable, ProjectLockedPolicyContextUnavailable) as exc: raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") from exc + validate_predecessor_lineage( + task_context, + predecessor_submission_id=request.predecessor_submission_id, + predecessor_submission_version=( + request.expected_predecessor_submission_version + ), + ) lineage = request.plan.lineage guide_version = project_context.guide_version.removeprefix("v") try: @@ -719,10 +725,6 @@ async def persist( raise PreSubmitEvidenceConflict("pre_submit_locked_context_changed") from exc if ( task_context.contributor_id != request.actor_profile_id - or task_context.predecessor is None - and request.predecessor_submission_id is not None - or task_context.predecessor is not None - and task_context.predecessor.submission_id != request.predecessor_submission_id or project_context.project_id != lineage.project_id or project_context.guide_id != lineage.guide_id or numeric_guide_version != lineage.guide_version diff --git a/backend/app/modules/artifacts/submission_admission.py b/backend/app/modules/artifacts/submission_admission.py index a9160e5f3..747fa9762 100644 --- a/backend/app/modules/artifacts/submission_admission.py +++ b/backend/app/modules/artifacts/submission_admission.py @@ -29,7 +29,10 @@ SubmissionBundleAdmission, SubmissionBundleDurableIntent, ) -from app.modules.artifacts.pre_submit_evidence import PreSubmitPassCapability +from app.modules.artifacts.pre_submit_evidence import ( + PreSubmitEvidenceConflict, + PreSubmitPassCapability, +) from app.modules.artifacts.preparation import ArtifactPreparationService from app.modules.artifacts.schemas import ( ArtifactAdmissionResult, @@ -79,6 +82,17 @@ ) +def validate_submission_packet_headers(summary: str, attestation: str) -> None: + """Reject lossy header decoding only after contributor preflight succeeds.""" + try: + summary.encode("ascii") + attestation.encode("ascii") + except UnicodeEncodeError as exc: + raise SubmissionBundlePreparationRejected( + "submission_bundle_packet_header_encoding_invalid" + ) from exc + + @dataclass(frozen=True, slots=True) class SubmissionBundleDurablePutRequest: """Exact live custody and opaque authority for one final durable handoff.""" @@ -451,6 +465,10 @@ async def prepare( prepared = None try: await self._authority.preflight(request=request) + validate_submission_packet_headers( + request.summary, + request.contributor_attestation, + ) if request.media_type.partition(";")[0].strip().lower() != "application/zip": raise SubmissionBundlePreparationRejected("submission_bundle_media_type_invalid") async with self._runtime_factory() as runtime: @@ -491,6 +509,11 @@ async def prepare( assignment_id=request.assignment_id, submission_artifact_policy_id=project_context.effective_policy_id, checker_policy_id=project_context.pre_submit_policy_id, + predecessor_submission_version=( + task_context.predecessor.version + if task_context.predecessor is not None + else None + ), prepared_artifact=prepared, effective_plan=plan, inspection=inspection, @@ -536,6 +559,10 @@ async def prepare( durable, ) return self._result(result) + except PreSubmitEvidenceConflict as exc: + raise SubmissionBundlePreparationRejected( + self._evidence_conflict_code(exc) + ) from exc except ArtifactAuthorityDeniedError as exc: raise SubmissionBundlePreparationUnavailable( "submission bundle preparation is unavailable" @@ -615,6 +642,13 @@ async def _matching_replay_intent(self, evidence_id: UUID) -> UUID | None: value = await self._session.scalar(statement) return UUID(value) if value is not None else None + @staticmethod + def _evidence_conflict_code(exc: PreSubmitEvidenceConflict) -> str: + """Map ART-private evidence failures to the bounded public vocabulary.""" + if str(exc) == "pre_submit_locked_context_changed": + return "submission_bundle_preparation_context_changed" + return "pre_submission_checked_custody_unavailable" + async def _lock_context( self, request: SubmissionBundlePreparationRequest, diff --git a/backend/app/modules/artifacts/submission_materialization.py b/backend/app/modules/artifacts/submission_materialization.py index 108510b6b..7cd5b9b25 100644 --- a/backend/app/modules/artifacts/submission_materialization.py +++ b/backend/app/modules/artifacts/submission_materialization.py @@ -33,6 +33,7 @@ SubmissionBundlePreparationAuthorization, ) from app.modules.checkers.api import ( + ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, EffectivePreSubmissionExecutionPlan, PreSubmissionExecutionFacts, PreSubmissionInfrastructureUnavailableError, @@ -41,9 +42,6 @@ from app.modules.projects.api import ProjectLockedPolicyContextPort from app.modules.tasks.api import TaskSubmissionContextPort -ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) - - @dataclass(frozen=True, slots=True) class PreparedBundleMaterializationRequest: """ART-private process-local prepared bytes and exact execution facts.""" @@ -53,6 +51,7 @@ class PreparedBundleMaterializationRequest: assignment_id: UUID submission_artifact_policy_id: UUID checker_policy_id: UUID + predecessor_submission_version: int | None prepared_artifact: PreparedArtifact effective_plan: EffectivePreSubmissionExecutionPlan inspection: SubmissionArchiveInspectionResult @@ -432,6 +431,9 @@ async def persist( predecessor_submission_id=( preparation_request.predecessor_submission_id ), + expected_predecessor_submission_version=( + request.predecessor_submission_version + ), prepared_generation_id=prepared_generation_id, archive_sha256=commitment.sha256, archive_byte_count=commitment.byte_count, diff --git a/backend/app/modules/checkers/api/__init__.py b/backend/app/modules/checkers/api/__init__.py index ee6b11fd9..93506d5b3 100644 --- a/backend/app/modules/checkers/api/__init__.py +++ b/backend/app/modules/checkers/api/__init__.py @@ -1,6 +1,7 @@ """Dependency-safe public API for the CHECKERS business module.""" from app.modules.checkers.api.pre_submit import ( + ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, EffectivePreSubmissionExecutionPlan, EffectivePreSubmissionPlanEntry, EffectivePreSubmissionPlanError, @@ -10,10 +11,13 @@ PreSubmissionExecutionEntryFacts, PreSubmissionExecutionFacts, PreSubmissionInfrastructureUnavailableError, + PRE_SUBMISSION_RESULT_METADATA_KEYS, SubmissionPacketView, + validate_pre_submission_execution_facts, ) __all__ = ( + "ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES", "EffectivePreSubmissionExecutionPlan", "EffectivePreSubmissionPlanEntry", "EffectivePreSubmissionPlanError", @@ -23,5 +27,7 @@ "PreSubmissionExecutionEntryFacts", "PreSubmissionExecutionFacts", "PreSubmissionInfrastructureUnavailableError", + "PRE_SUBMISSION_RESULT_METADATA_KEYS", "SubmissionPacketView", + "validate_pre_submission_execution_facts", ) diff --git a/backend/app/modules/checkers/api/pre_submit.py b/backend/app/modules/checkers/api/pre_submit.py index 5608e058e..e23455a0a 100644 --- a/backend/app/modules/checkers/api/pre_submit.py +++ b/backend/app/modules/checkers/api/pre_submit.py @@ -9,6 +9,32 @@ EFFECTIVE_PRE_SUBMISSION_PLAN_SCHEMA_VERSION = "effective_pre_submission_plan.v1" EFFECTIVE_PRE_SUBMISSION_PLAN_HASH_DOMAIN = "workstream.effective_pre_submission_plan.v1" +ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) +_RESULT_MESSAGE_CODES = frozenset( + { + "advisory_disabled", + "attestation_missing", + "dependency_not_run", + "file_size_limit_exceeded", + "forbidden_artifact_present", + "package_size_limit_exceeded", + "packaging_requirement_failed", + "passed", + "policy_attestation_missing", + "quality_signal_warning", + "required_evidence_missing", + "required_file_missing", + "sensitive_path_forbidden", + "storage_scheme_not_allowed", + "submission_packet_invalid", + } +) +PRE_SUBMISSION_RESULT_METADATA_KEYS = frozenset( + {"entry_count", "finding_count", "matched_category_count"} +) +_EXECUTION_STATUSES = frozenset( + {"passed", "warning", "advisory_disabled", "dependency_not_run", "failed"} +) class EffectivePreSubmissionPlanError(ValueError): @@ -206,6 +232,60 @@ class PreSubmissionExecutionFacts: entries: tuple[PreSubmissionExecutionEntryFacts, ...] +def validate_pre_submission_execution_facts( + plan: EffectivePreSubmissionExecutionPlan, + execution: PreSubmissionExecutionFacts, +) -> None: + """Validate one bounded CHECKER result against its exact immutable plan.""" + if ( + type(execution.eligible) is not bool + or execution.plan_sha256 != plan.plan_sha256 + or len(execution.entries) != len(plan.entries) + ): + raise PreSubmissionInfrastructureUnavailableError( + "pre_submission_result_context_invalid" + ) + disqualified = False + for plan_entry, result in zip(plan.entries, execution.entries, strict=True): + expected_severity = ( + "warning" if plan_entry.classification == "advisory" else "blocking" + ) + status = result.checker_execution_status + if ( + result.dispatch_authority != "workstream.pre_submission_checker_catalogue" + or result.definition_id != plan_entry.definition_id + or result.definition_version != plan_entry.definition_version + or result.public_name != plan_entry.public_name + or result.policy_source != plan_entry.policy_trace_source + or result.effective_plan_sha256 != plan.plan_sha256 + or result.rule_instance_id != plan_entry.rule_instance_id + or result.locked_policy_sha256 != plan.lineage.effective_policy_hash + or result.phase != plan_entry.phase + or result.order != plan_entry.order + or result.classification != plan_entry.classification + or result.severity != expected_severity + or status not in _EXECUTION_STATUSES + or result.message_code not in _RESULT_MESSAGE_CODES + or result.failure_code + != (plan_entry.failure_code if status == "failed" else None) + or len(result.metadata) != len({key for key, _ in result.metadata}) + or any( + key not in PRE_SUBMISSION_RESULT_METADATA_KEYS + or type(value) is not int + or value < 0 + for key, value in result.metadata + ) + ): + raise PreSubmissionInfrastructureUnavailableError( + "pre_submission_result_context_invalid" + ) + disqualified = disqualified or status in {"failed", "dependency_not_run"} + if execution.eligible == disqualified: + raise PreSubmissionInfrastructureUnavailableError( + "pre_submission_result_context_invalid" + ) + + def effective_plan_body( lineage: EffectivePreSubmissionPlanLineage, catalogue_id: str, diff --git a/backend/app/modules/checkers/pre_submit_execution.py b/backend/app/modules/checkers/pre_submit_execution.py index 46a3c4fcd..cdb6cd4da 100644 --- a/backend/app/modules/checkers/pre_submit_execution.py +++ b/backend/app/modules/checkers/pre_submit_execution.py @@ -13,11 +13,15 @@ from app.core.hashing import canonical_json_hash from app.modules.checkers.api import ( + ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES, EffectivePreSubmissionExecutionPlan, EffectivePreSubmissionPlanEntry, PreSubmissionExecutionEntryFacts, PreSubmissionExecutionFacts, + PreSubmissionInfrastructureUnavailableError, + PRE_SUBMISSION_RESULT_METADATA_KEYS, SubmissionPacketView, + validate_pre_submission_execution_facts, ) from app.modules.artifacts.sources import ArtifactCommitment from app.modules.artifacts.submission_archive import ( @@ -55,29 +59,6 @@ _FORBIDDEN_EXACT_NAMES = frozenset({".env", "id_rsa", "id_ed25519"}) _FORBIDDEN_DIRECTORY_NAMES = frozenset({".git"}) _FORBIDDEN_SUFFIXES = (".pem", ".key") -_RESULT_MESSAGE_CODES = frozenset( - { - "advisory_disabled", - "attestation_missing", - "dependency_not_run", - "file_size_limit_exceeded", - "forbidden_artifact_present", - "package_size_limit_exceeded", - "packaging_requirement_failed", - "passed", - "policy_attestation_missing", - "quality_signal_warning", - "required_evidence_missing", - "required_file_missing", - "sensitive_path_forbidden", - "storage_scheme_not_allowed", - "submission_packet_invalid", - } -) -_RESULT_METADATA_KEYS = frozenset({"entry_count", "finding_count", "matched_category_count"}) -ALLOWED_PRE_SUBMIT_STORAGE_SCHEMES = frozenset({"local", "s3"}) - - class DefaultPreSubmissionExecutionError(RuntimeError): """Fail hidden execution without creating a durable checker effect.""" @@ -178,7 +159,7 @@ def bounded_facts(self) -> PreSubmissionExecutionFacts: metadata=tuple( (key, value) for key, value in entry.metadata - if key in _RESULT_METADATA_KEYS + if key in PRE_SUBMISSION_RESULT_METADATA_KEYS and type(value) is int and value >= 0 ), @@ -604,46 +585,16 @@ def validate_pre_submission_execution_result( or len(execution.entries) != len(plan.entries) ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") - disqualified = False for plan_entry, result in zip(plan.entries, execution.entries, strict=True): - expected_severity = "warning" if plan_entry.classification == "advisory" else "blocking" if ( type(result.status) is not PreSubmissionResultStatus or result.schema_version != plan_entry.result_schema - or result.definition.dispatch_authority != "workstream.pre_submission_checker_catalogue" - or result.definition.definition_id != plan_entry.definition_id - or result.definition.definition_version != plan_entry.definition_version - or result.definition.public_name != plan_entry.public_name - or result.definition.source != plan_entry.policy_trace_source - or result.policy_trace.effective_plan_sha256 != plan.plan_sha256 - or result.policy_trace.rule_instance_id != plan_entry.rule_instance_id - or result.policy_trace.locked_policy_sha256 != plan.lineage.effective_policy_hash - or result.phase != plan_entry.phase - or result.order != plan_entry.order - or result.classification != plan_entry.classification - or result.severity != expected_severity - or result.message_code not in _RESULT_MESSAGE_CODES - or ( - result.failure_code - != ( - plan_entry.failure_code - if result.status is PreSubmissionResultStatus.FAILED - else None - ) - ) - or len(result.metadata) != len({key for key, _ in result.metadata}) - or any( - key not in _RESULT_METADATA_KEYS or type(value) is not int or value < 0 - for key, value in result.metadata - ) ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") - disqualified = disqualified or result.status in { - PreSubmissionResultStatus.FAILED, - PreSubmissionResultStatus.DEPENDENCY_NOT_RUN, - } - if execution.eligible == disqualified: - raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") + try: + validate_pre_submission_execution_facts(plan, execution.bounded_facts()) + except PreSubmissionInfrastructureUnavailableError as exc: + raise PreSubmissionInfrastructureUnavailable(str(exc)) from exc def _is_high_confidence_sensitive(normalized_path: str) -> bool: diff --git a/backend/scripts/module_boundaries.py b/backend/scripts/module_boundaries.py index f35480e92..75abd51d0 100644 --- a/backend/scripts/module_boundaries.py +++ b/backend/scripts/module_boundaries.py @@ -123,11 +123,14 @@ def _source_module(source: str) -> str | None: def _source_owner_adapter(source: str) -> str | None: - """Return the module owned by one explicit adapter package.""" + """Return the module owned by one exact adapter composition root.""" prefix = "backend/app/adapters/" if not source.startswith(prefix): return None - return source.removeprefix(prefix).split("/", 1)[0] + parts = source.removeprefix(prefix).split("/") + if len(parts) != 2 or parts[1] != "__init__.py": + return None + return parts[0] def _is_public_target(target: str, module: str) -> bool: diff --git a/backend/tests/architecture/test_module_boundaries.py b/backend/tests/architecture/test_module_boundaries.py index 17e7e97cd..d310d5db2 100644 --- a/backend/tests/architecture/test_module_boundaries.py +++ b/backend/tests/architecture/test_module_boundaries.py @@ -108,6 +108,28 @@ def test_owner_adapter_may_bind_only_its_own_private_implementation( } +def test_non_root_adapter_keeps_same_owner_private_debt_visible( + tmp_path: Path, +) -> None: + """Only the exact owner composition root receives the wiring exemption.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/adapters/tasks/worker.py", + "from app.modules.tasks.repository import TaskRepository\n", + ) + private, _, _ = boundary.scan( + tmp_path, boundary.load_registry(tmp_path / "registry.json") + ) + assert private == { + boundary.PrivateEdge( + "backend/app/adapters/tasks/worker.py", + "tasks", + "app.modules.tasks.repository", + "WS-ARCH-001-03", + ) + } + + def test_import_from_modules_package_resolves_registered_alias(tmp_path: Path) -> None: """Package-level module aliases cannot disappear from dependency scanning.""" _registry(tmp_path / "registry.json") @@ -441,7 +463,13 @@ def test_initial_ledgers_capture_high_risk_application_edges() -> None: "app.modules.checkers.pre_submit_execution", ) not in actual assert not any( + source == "backend/app/adapters/artifacts/__init__.py" + and target.startswith("app.modules.artifacts.") + for source, target in actual + ) + assert any( source.startswith("backend/app/adapters/artifacts/") + and source != "backend/app/adapters/artifacts/__init__.py" and target.startswith("app.modules.artifacts.") for source, target in actual ) diff --git a/backend/tests/test_authorization.py b/backend/tests/test_authorization.py index 9093a29fb..b63370aeb 100644 --- a/backend/tests/test_authorization.py +++ b/backend/tests/test_authorization.py @@ -6303,21 +6303,10 @@ def close(self) -> None: ) with pytest.raises(ArtifactAuthorityDeniedError, match="invalid"): await authority.consume( - service_identity=ServiceIdentity.ARTIFACT_MATERIALIZER, - action_id=ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, prepared_authorization=handle, facts=replace(facts, **{field_name: changed}), ) - with pytest.raises(ArtifactAuthorityDeniedError, match="invalid"): - await authority.consume( - service_identity=ServiceIdentity.ARTIFACT_GUIDE_READER, - action_id=ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, - prepared_authorization=handle, - facts=facts, - ) await authority.consume( - service_identity=ServiceIdentity.ARTIFACT_MATERIALIZER, - action_id=ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, prepared_authorization=handle, facts=facts, ) @@ -6332,8 +6321,6 @@ def close(self) -> None: ) with pytest.raises(ArtifactAuthorityDeniedError, match="invalid"): await authority.consume( - service_identity=ServiceIdentity.ARTIFACT_MATERIALIZER, - action_id=ActionId.ARTIFACT_PRE_SUBMIT_CHECKER_INPUT_MATERIALIZE, prepared_authorization=handle, facts=facts, ) diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index b1c9a3ab9..1c47a28cc 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -277,6 +277,7 @@ async def _request( assignment_id=uuid4(), submission_artifact_policy_id=plan.lineage.effective_policy_id, checker_policy_id=plan.lineage.pre_submit_policy_id, + predecessor_submission_version=None, prepared_artifact=prepared, effective_plan=plan, inspection=inspection, @@ -465,6 +466,8 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( "submissions", "checker_runs", "review_queue_entries", + "pre_submit_evidence_sets", + "pre_submit_evidence_results", ) try: async with engine.begin() as connection: diff --git a/backend/tests/test_effective_pre_submit_execution.py b/backend/tests/test_effective_pre_submit_execution.py index 01c2e4688..f6fc6f4ba 100644 --- a/backend/tests/test_effective_pre_submit_execution.py +++ b/backend/tests/test_effective_pre_submit_execution.py @@ -21,6 +21,12 @@ PersistedPreSubmitEvidence, pre_submit_failure_audit_payload, semantic_manifest_identity, + validate_predecessor_lineage, +) +from app.modules.tasks.api import ( + SubmissionPredecessorFacts, + TaskLockedProjectContextReferences, + TaskSubmissionContextFacts, ) from app.modules.checkers.api import ( PreSubmissionExecutionEntryFacts, @@ -87,6 +93,38 @@ def test_evidence_operation_identity_binds_every_custody_fact() -> None: assert identity != context.operation_identity(effective_plan_sha256=_sha("8")) +def test_post_byte_relock_rejects_advanced_predecessor_version() -> None: + predecessor_id = uuid4() + task_context = TaskSubmissionContextFacts( + task_id=uuid4(), + assignment_id=uuid4(), + contributor_id=uuid4(), + status="needs_revision", + kind="revision", + predecessor=SubmissionPredecessorFacts( + submission_id=predecessor_id, + version=2, + ), + locked_project_context=TaskLockedProjectContextReferences( + project_id=uuid4(), + guide_version="1", + source_snapshot_id=uuid4(), + source_snapshot_hash=_sha("1"), + effective_policy_id=uuid4(), + effective_policy_hash=_sha("2"), + pre_submit_policy_id=uuid4(), + pre_submit_policy_bundle_hash=_sha("3"), + ), + ) + + with pytest.raises(PreSubmitEvidenceConflict, match="pre_submit_locked_context_changed"): + validate_predecessor_lineage( + task_context, + predecessor_submission_id=predecessor_id, + predecessor_submission_version=1, + ) + + def test_semantic_manifest_identity_is_server_deterministic() -> None: assert semantic_manifest_identity(_sha("a")) == semantic_manifest_identity(_sha("a")) assert semantic_manifest_identity(_sha("a")) != semantic_manifest_identity(_sha("b")) diff --git a/backend/tests/test_pre_submit_evidence_relock.py b/backend/tests/test_pre_submit_evidence_relock.py new file mode 100644 index 000000000..dfe4fe48b --- /dev/null +++ b/backend/tests/test_pre_submit_evidence_relock.py @@ -0,0 +1,69 @@ +"""Focused proof for post-byte pre-submit evidence relocking.""" + +from types import SimpleNamespace +from unittest.mock import AsyncMock + +import pytest + +from app.modules.artifacts import pre_submit_evidence +from app.modules.artifacts.pre_submit_evidence import ( + PreSubmitEvidenceConflict, + PreSubmitEvidenceService, +) +from app.modules.tasks.api import TaskSubmissionContextUnavailable + + +@pytest.mark.asyncio +async def test_stale_task_relock_denies_before_evidence_or_pass_capability( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A post-byte TASK drift cannot persist evidence or mint continuation.""" + transaction = SimpleNamespace(is_active=True) + session = SimpleNamespace( + sync_session=SimpleNamespace(get_transaction=lambda: transaction), + in_nested_transaction=lambda: False, + ) + task_contexts = SimpleNamespace( + lock_submission_context=AsyncMock( + side_effect=TaskSubmissionContextUnavailable( + "task_submission_context_invalid" + ) + ) + ) + project_contexts = SimpleNamespace(lock_locked_policy_context=AsyncMock()) + service = PreSubmitEvidenceService( + session, # type: ignore[arg-type] + task_contexts=task_contexts, + project_contexts=project_contexts, + ) + service._repository.persist = AsyncMock() # type: ignore[method-assign] + monkeypatch.setattr(pre_submit_evidence, "_validate_execution", lambda *_: None) + custody = SimpleNamespace( + prepared_generation_id="generation", + archive_sha256="archive", + archive_byte_count=1, + semantic_manifest_sha256="manifest", + ) + request = SimpleNamespace( + plan=object(), + execution=SimpleNamespace(custody=custody), + prepared_generation_id="generation", + archive_sha256="archive", + archive_byte_count=1, + semantic_manifest_sha256="manifest", + task_id="task", + assignment_id="assignment", + actor_profile_id="actor", + predecessor_submission_id=None, + ) + + with pytest.raises( + PreSubmitEvidenceConflict, + match="pre_submit_locked_context_changed", + ): + await service.persist(request) # type: ignore[arg-type] + + task_contexts.lock_submission_context.assert_awaited_once() + project_contexts.lock_locked_policy_context.assert_not_awaited() + service._repository.persist.assert_not_awaited() # type: ignore[union-attr] + assert service._live_pass_bindings == set() diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 77043456f..97d8c7e66 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -40,6 +40,9 @@ SubmissionBundleDurablePutResult, SubmissionBundleDurablePutService, ) +from app.modules.artifacts.submission_materialization import ( + PreparedBundlePreSubmitEvidenceService, +) from app.modules.artifacts.submission_admission import ( SubmissionBundleAdmissionPublicationError, SubmissionBundleAdmissionPublisher, @@ -53,11 +56,8 @@ from app.modules.authorization.prepared import PreparedAuthorizationHandle from tests.artifact_store_helpers import artifact_byte_stream, artifact_preparation_limits from app.main import create_app -from app.api.routes.artifact_submissions import ( - _require_ascii_submission_packet_headers, - prepare_submission_bundle, - router as submission_router, -) +from app.api.routes.artifact_submissions import prepare_submission_bundle, router as submission_router +from app.modules.artifacts.submission_admission import validate_submission_packet_headers def _actor() -> ActorIdentityFacts: @@ -68,7 +68,9 @@ def _actor() -> ActorIdentityFacts: ) -def _preparation_request(*, byte_source, media_type: str = "application/zip"): +def _preparation_request( + *, byte_source, media_type: str = "application/zip", summary: str = "summary" +): return SubmissionBundlePreparationRequest( actor=_actor(), request_id=uuid4(), @@ -77,7 +79,7 @@ def _preparation_request(*, byte_source, media_type: str = "application/zip"): assignment_id=uuid4(), predecessor_submission_id=None, idempotency_key=uuid4(), - summary="summary", + summary=summary, contributor_attestation="attestation", media_type=media_type, byte_source=byte_source, @@ -104,11 +106,14 @@ def test_submission_bundle_preparation_route_is_hidden() -> None: def test_submission_packet_headers_reject_non_ascii() -> None: - _require_ascii_submission_packet_headers("plain summary", "plain attestation") - with pytest.raises(HTTPException) as failure: - _require_ascii_submission_packet_headers("caf\N{LATIN SMALL LETTER E WITH ACUTE}", "ok") - assert failure.value.status_code == 422 - assert failure.value.detail == "submission_bundle_packet_header_encoding_invalid" + validate_submission_packet_headers("plain summary", "plain attestation") + with pytest.raises( + SubmissionBundlePreparationRejected, + match="submission_bundle_packet_header_encoding_invalid", + ): + validate_submission_packet_headers( + "caf\N{LATIN SMALL LETTER E WITH ACUTE}", "ok" + ) @pytest.mark.asyncio @@ -182,7 +187,12 @@ def runtime_factory(): runtime_factory=runtime_factory, ) with pytest.raises(SubmissionBundlePreparationUnavailable): - await command.prepare(_preparation_request(byte_source=bytes_source())) + await command.prepare( + _preparation_request( + byte_source=bytes_source(), + summary="caf\N{LATIN SMALL LETTER E WITH ACUTE}", + ) + ) assert reads == 0 @@ -211,6 +221,43 @@ async def test_hidden_preparation_closes_authority_after_invalid_media_type() -> authority.close.assert_called_once_with() +@pytest.mark.asyncio +async def test_post_byte_authority_denial_precedes_evidence_relock() -> None: + denial = ArtifactAuthorityDeniedError("submission bundle preparation is unavailable") + authority = SimpleNamespace(revalidate=AsyncMock(side_effect=denial)) + task_contexts = SimpleNamespace(lock_submission_context=AsyncMock()) + project_contexts = SimpleNamespace(lock_locked_policy_context=AsyncMock()) + session = SimpleNamespace( + in_transaction=lambda: False, + begin=_transaction, + execute=AsyncMock(), + ) + workflow = PreparedBundlePreSubmitEvidenceService( + session=session, + materialization=SimpleNamespace(), + preparation_authorization=authority, + task_contexts=task_contexts, + project_contexts=project_contexts, + ) + materialization_request = SimpleNamespace( + prepared_artifact=SimpleNamespace( + commitment=SimpleNamespace(sha256=_sha("1"), byte_count=1), + generation_id=uuid4(), + ), + ) + + with pytest.raises(ArtifactAuthorityDeniedError): + await workflow.persist( + materialization_request, + execution=object(), + preparation_request=object(), + ) + + authority.revalidate.assert_awaited_once() + task_contexts.lock_submission_context.assert_not_awaited() + project_contexts.lock_locked_policy_context.assert_not_awaited() + + @pytest.mark.asyncio async def test_existing_durable_preparation_projects_exact_ready_admission() -> None: attempt_id = uuid4() @@ -298,7 +345,9 @@ async def runtime_factory(): project_contexts=SimpleNamespace(), runtime_factory=runtime_factory, ) - command._lock_context = AsyncMock(return_value=(object(), locked)) + command._lock_context = AsyncMock( + return_value=(SimpleNamespace(predecessor=None), locked) + ) command._compile_plan = Mock(return_value=object()) command._load_predecessor = AsyncMock(return_value=None) command._existing_durable_result = AsyncMock(return_value=expected) @@ -340,7 +389,6 @@ def test_durable_put_result_projects_without_losing_replay_state() -> None: status="prepared", replayed=True, ) - assert PreparedSubmissionBundlePreparationCommand._result(durable) == ( SubmissionBundlePreparationResult( put_attempt_id=durable.put_attempt_id, @@ -351,6 +399,13 @@ def test_durable_put_result_projects_without_losing_replay_state() -> None: ) +def test_post_byte_locked_context_conflict_maps_to_public_race_code() -> None: + conflict = PreSubmitEvidenceConflict("pre_submit_locked_context_changed") + assert ( + PreparedSubmissionBundlePreparationCommand._evidence_conflict_code(conflict) + == "submission_bundle_preparation_context_changed" + ) + def _capability(prepared, evidence_set_id): service = PreSubmitEvidenceService( SimpleNamespace(), diff --git a/docs/architecture_lockdown.md b/docs/architecture_lockdown.md index 3638d6901..2beb112c5 100644 --- a/docs/architecture_lockdown.md +++ b/docs/architecture_lockdown.md @@ -20,8 +20,12 @@ authentication adapter, not the product boundary. The backend is a modular monolith governed by the canonical module map and public-API dependency rules in [`../.agent-loop/policies/architecture-boundaries.md`](../.agent-loop/policies/architecture-boundaries.md). -Cross-module runtime imports use only the target module's typed `api` package; -the composition root alone wires concrete implementations. +Cross-module runtime imports use only the target module's typed `api` package. +Concrete implementations are wired only by the application composition root or +the exact same-owner adapter root `backend/app/adapters//__init__.py`. +That narrow adapter-root rule permits construction of the owner's public ports; +it does not permit nested adapter files or any adapter to import another +module's private implementation. The TASKS public boundary exposes immutable task, assignment, immediate Submission predecessor, and locked project-context reference selectors through diff --git a/docs/operations_backend_testing.md b/docs/operations_backend_testing.md index 5150be060..02842e463 100644 --- a/docs/operations_backend_testing.md +++ b/docs/operations_backend_testing.md @@ -270,3 +270,8 @@ edges, public API leaks, unknown modules, cyclic public dependencies, dynamic-import hiding, and agreement with the WS-AUTH-003 ledger. The edge inventory is temporary recovery evidence; do not add an edge to make a feature pass. Remove the dependency through the owning module's typed public API. +The sole non-ledgered owner-private composition exception is the exact +`backend/app/adapters//__init__.py` file importing that same owner's +private implementation to construct typed public ports. Nested adapter files +and cross-owner private imports remain scanned and must be repaired or remain +exact protected-base debt. diff --git a/docs/spec_artifact_storage_service.md b/docs/spec_artifact_storage_service.md index 042161cd5..5d95ba2d7 100644 --- a/docs/spec_artifact_storage_service.md +++ b/docs/spec_artifact_storage_service.md @@ -351,11 +351,10 @@ Product modules receive only these narrow asynchronous capabilities: ```text GuideArtifactIngestPort.ingest(GuideArtifactIngestRequest) -SubmissionBundlePreparationPort.prepare(SubmissionBundlePreparationRequest) +SubmissionBundlePreparationCommand.prepare(SubmissionBundlePreparationRequest) ArtifactBindingPort.bind_guide_source(GuideSourceBindingRequest) ArtifactBindingPort.bind_submission(SubmissionBindingRequest) ArtifactBindingPort.bind_checker_output(CheckerOutputBindingRequest) -ArtifactMaterializationPort.materialize_prepared_bundle(PreparedBundleMaterializationRequest) ArtifactMaterializationPort.materialize_bindings(BindingMaterializationRequest) CheckerArtifactOutputPort.store(CheckerOutputArtifactRequest) ArtifactOperatorReadPort.list_bindings/list_replicas/list_receipts/ @@ -365,8 +364,10 @@ ArtifactOperatorRecoveryPort.retry_verification(ArtifactRecoveryRequest) Every protected durable mutation in this port family consumes one opaque, process-local `PreparedAuthorizationHandle` at its transaction boundary. The -hidden HTTP submission-bundle preparation request carries authenticated request -context rather than a prepared handle; ART obtains separate prepared handles +hidden HTTP submission-bundle preparation request carries immutable +`ActorIdentityFacts`, request and correlation IDs, contributor selectors, +packet metadata, and an asynchronous byte source rather than a prepared handle; +ART obtains separate prepared handles immediately before materialization and durable put intent. The typed methods fix their expected actions, and handles never enter route schemas, outbox/Celery payloads, provider interfaces, or serialized contracts. @@ -378,12 +379,17 @@ reference. `GuideSourceMaterializationRequest` contains an idempotency key and the exact guide, source, setup-generation, and binding identifiers; it contains no prepared handle. The materializer creates and consumes fresh fixed-reader authority inside the same root transaction that locks the canonical read facts. -`SubmissionBundlePreparationRequest` contains authenticated request context, -contributor task/assignment selectors, and one outer ZIP byte source. There is -no upload-session compatibility port. +`SubmissionBundlePreparationRequest` is exported only from +`app.modules.artifacts.api`. It contains authenticated actor/identity-link +facts, request and correlation IDs, contributor task/assignment/predecessor and +idempotency selectors, packet metadata, and one outer ZIP byte source. It +exposes neither raw authorization context nor a prepared handle. There is no +upload-session compatibility port. `PreparedBundleMaterializationRequest` is internal and process-local; it wraps -only prepared authority, the exact task/assignment context, the current -`PreparedArtifact` generation, and exact policy/checker selectors. +only opaque prepared authority, the exact task/assignment context, the current +`PreparedArtifact` generation, and exact policy/checker selectors. It is not a +product capability port and may not cross a route, background execution, +provider, or module public boundary. `BindingMaterializationRequest` contains task/submission/checker-run context and immutable binding IDs. `CheckerOutputArtifactRequest` contains the fixed service's prepared authority, task/submission/checker-run IDs, logical role, From d53b25ac5669b82a4e87467e9a702921327c1ad3 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:00:43 +0100 Subject: [PATCH 03/27] docs(architecture): declare 02D merge outcome --- .agent-loop/CURRENT_STATE.md | 2 +- .../chunks/WS-ARCH-001-02D-art-preparation-public-api.md | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.agent-loop/CURRENT_STATE.md b/.agent-loop/CURRENT_STATE.md index 83ba152ef..89d7fbe32 100644 --- a/.agent-loop/CURRENT_STATE.md +++ b/.agent-loop/CURRENT_STATE.md @@ -28,7 +28,7 @@ authority; these records do not grant or withhold it. | Initiative | Durable state on `main` | Remaining boundary | |---|---|---| -| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Boundary foundation and TASK, PROJECT, CHECKER, and ART hidden-preparation public facts are complete through `02D`; no product behavior was activated | `02E` is the ART ready-admission consumption and binding public capability | +| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Boundary foundation and TASK, PROJECT, CHECKER, and ART hidden-preparation public facts are complete through `WS-ARCH-001-02D`; no product behavior was activated | `WS-ARCH-001-02E` is the ART ready-admission consumption and binding public capability | | [WS-ART-001](initiatives/WS-ART-001-immutable-artifact-storage/STATUS.md) | Active delivery initiative; verified ready-admission publication and hidden preparation are merged through `04C2` | Replace the non-executable 05-wave XINT/ART contracts with WS-ARCH-001 split public-API contracts before activation, Submission consumption, or live cutover | | [WS-AUTH-001](initiatives/WS-AUTH-001-workstream-authorization-service/STATUS.md) | Active delivery initiative; project-policy authority and unified compilation authorization are merged through `12I` | POL-03B consumes 12I next; remaining AUTH activation chunks wait for their exact hidden owner behavior | | [WS-CON-001](initiatives/WS-CON-001-contribution-compensation-boundary/STATUS.md) | Active delivery initiative; policy persistence and shared lifecycle audit are merged | Complete hidden services, contribution records, conditional awards, fulfillment, and reconciliation after named AUTH and REV gates | diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index 280953b5f..b2e7fe533 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -26,6 +26,10 @@ edges are removed and final authority remains transaction-bound. L1 +## Merge state + +- Outcome on merge: `complete` + ## SLA P1 From bb17594f8137cf250b40fec06e0ba45280f65e54 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:06:29 +0100 Subject: [PATCH 04/27] ci(architecture): register 02D behavior targets --- ...ARCH-001-02D-art-preparation-public-api.md | 2 ++ .ci/behavior-ownership/partition.v1.json | 10 ++++++++- backend/scripts/behavior_ownership.py | 2 ++ backend/tests/test_behavior_ownership.py | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index b2e7fe533..cffc42dda 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -61,6 +61,8 @@ backend/app/api/router.py backend/app/api/routes/artifact_submissions.py backend/tests/architecture/test_module_boundaries.py backend/scripts/module_boundaries.py +backend/scripts/behavior_ownership.py +backend/tests/test_behavior_ownership.py backend/tests/test_artifact_architecture.py backend/tests/test_submission_bundle_admission.py backend/tests/test_default_pre_submit_execution.py diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index c64cbebf7..853fb8615 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -48,6 +48,10 @@ "group": "shared", "target": "backend/app/api/router.py" }, + { + "group": "lifecycle", + "target": "backend/app/api/routes/artifact_submissions.py" + }, { "group": "auth", "target": "backend/app/api/routes/auth.py" @@ -168,6 +172,10 @@ "group": "shared", "target": "backend/app/modules/api_controls/service.py" }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/api/submission_preparation.py" + }, { "group": "artifacts", "target": "backend/app/modules/artifacts/authorization.py" @@ -801,7 +809,7 @@ "target": "backend/scripts/week2_api_e2e.py" } ], - "authority_digest": "ffaf4c71c39ad59d241f5526862800d870941a300d2f5810bb0ad33e30cb3b66", + "authority_digest": "ac50305ca09484ea683478e5a703056402892f61f0368b1a5ce5dee2e3d5ab15", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index 9781c67e0..af7cd8680 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -76,6 +76,8 @@ ) MODULE_PUBLIC_API_FOUNDATION_TARGETS = frozenset( { + "backend/app/api/routes/artifact_submissions.py", + "backend/app/modules/artifacts/api/submission_preparation.py", "backend/app/modules/checkers/api/pre_submit.py", "backend/app/modules/projects/api/locked_policy.py", "backend/app/modules/projects/locked_policy_repository.py", diff --git a/backend/tests/test_behavior_ownership.py b/backend/tests/test_behavior_ownership.py index af89c2633..18c2be49e 100644 --- a/backend/tests/test_behavior_ownership.py +++ b/backend/tests/test_behavior_ownership.py @@ -207,6 +207,27 @@ def test_partition_accepts_only_the_v01_migration_tool_removals() -> None: ) +def test_partition_accepts_only_exact_02d_behavior_targets() -> None: + """02D registers its route and public contract without wildcard authority.""" + retained = "backend/app/core/config.py" + additions = { + "backend/app/api/routes/artifact_submissions.py", + "backend/app/modules/artifacts/api/submission_preparation.py", + } + trusted = _partition([retained]) + current = _partition(sorted({retained, *additions})) + + ownership._validate_additive_partition_transition(current, trusted) + + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership._validate_additive_partition_transition( + _partition( + sorted({retained, *additions, "backend/app/api/routes/extra.py"}) + ), + trusted, + ) + + def test_partition_rejects_reordered_trusted_assignments( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: From 03c70ae851e7a5e038cfdd2282349454a199cc6b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:10:26 +0100 Subject: [PATCH 05/27] ci(backend): assign relock proof to shared lane --- .../chunks/WS-ARCH-001-02D-art-preparation-public-api.md | 1 + backend/scripts/run_test_lanes.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index cffc42dda..ae905a6d7 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -63,6 +63,7 @@ backend/tests/architecture/test_module_boundaries.py backend/scripts/module_boundaries.py backend/scripts/behavior_ownership.py backend/tests/test_behavior_ownership.py +backend/scripts/run_test_lanes.py backend/tests/test_artifact_architecture.py backend/tests/test_submission_bundle_admission.py backend/tests/test_default_pre_submit_execution.py diff --git a/backend/scripts/run_test_lanes.py b/backend/scripts/run_test_lanes.py index f0406fb3e..94ac8de6f 100644 --- a/backend/scripts/run_test_lanes.py +++ b/backend/scripts/run_test_lanes.py @@ -142,6 +142,7 @@ class TestLane: "tests/test_behavior_ownership.py", "tests/test_artifact_admission.py", "tests/test_submission_bundle_admission.py", + "tests/test_pre_submit_evidence_relock.py", "tests/test_artifact_operator_api.py", "tests/test_artifact_recovery.py", "tests/test_db_session.py", From 28bdec577780fe5b2dd80d2f7da3fa0362a77b31 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:21:31 +0100 Subject: [PATCH 06/27] test(artifacts): use valid locked project lineage --- .../TEST_STRUCTURE_DEBT.json | 4 ++-- backend/tests/test_default_pre_submit_execution.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index ac5e82349..1f502bae6 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,7 +230,7 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "815033627fcbf67b794b672a4c4de552626f08999bc76e4e6fafc10848d5ee5f", + "content_sha256": "4324d00330bbb0bbecb02c121c009d7aad075628d24b6e0a5dd0ce80f6cb5c47", "end_line": 1726, "hard_limit": 1200, "kind": "test_file", @@ -746,7 +746,7 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "9f13a11de08e874e791cc9d625f473d77ef31a0c00b57e573b7256e0d453df85", + "content_sha256": "dc96b79324a762d6998829fe0f6680c05af4500740ccfaebda8df9e649eaf528", "end_line": 1133, "hard_limit": 120, "kind": "test_function", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 1c47a28cc..e31fdbb4e 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -514,7 +514,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( await connection.execute( text( "insert into projects (id,name,slug,status) values " - "(:project,'Evidence project',:project,'draft')" + "(:project,'Evidence project',:project,'active')" ), params, ) @@ -522,7 +522,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( text( "insert into project_guides " "(id,project_id,version,status,content_markdown,created_by) values " - "(:guide,:project,'1','draft','# Guide','test')" + "(:guide,:project,'1','active','# Guide','test')" ), params, ) From b24bdef7f74eb58e916f53f510ebfc7107672f49 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:38:05 +0100 Subject: [PATCH 07/27] fix(architecture): address preparation boundary review --- backend/app/adapters/checkers/__init__.py | 12 ++++++++---- .../app/api/routes/artifact_submissions.py | 3 ++- backend/app/modules/artifacts/api/__init__.py | 2 ++ .../artifacts/api/submission_preparation.py | 13 ++++++++++++- .../modules/artifacts/pre_submit_evidence.py | 6 ++++-- backend/app/modules/artifacts/preparation.py | 6 +++--- .../modules/artifacts/submission_admission.py | 9 +++++++-- .../artifacts/submission_materialization.py | 4 ++-- backend/scripts/module_boundaries.py | 4 ++-- .../architecture/test_module_boundaries.py | 19 +++++++++++++++++++ .../tests/test_submission_bundle_admission.py | 6 +++--- docs/spec_artifact_storage_service.md | 5 +++-- 12 files changed, 67 insertions(+), 22 deletions(-) diff --git a/backend/app/adapters/checkers/__init__.py b/backend/app/adapters/checkers/__init__.py index ce8918f13..f9b650e0b 100644 --- a/backend/app/adapters/checkers/__init__.py +++ b/backend/app/adapters/checkers/__init__.py @@ -1,5 +1,6 @@ """CHECKER-owned composition adapters.""" +import asyncio from typing import Any, BinaryIO, Protocol from pathlib import Path @@ -9,9 +10,9 @@ ) from app.modules.checkers.catalogue import PreSubmissionCheckerCatalogue from app.modules.checkers.pre_submit_execution import ( + DefaultPreSubmissionExecutionError, DefaultPreSubmissionExecutionInput, EffectivePreSubmissionProcessor, - PreSubmissionInfrastructureUnavailable, ) @@ -35,12 +36,15 @@ def __init__(self, processor: EffectivePreSubmissionProcessor) -> None: def abort(self) -> None: self._processor.abort() - def process_blocking( + async def process( self, reader: BinaryIO, workspace: Path ) -> PreSubmissionExecutionFacts: try: - return self._processor.process_blocking(reader, workspace).bounded_facts() - except PreSubmissionInfrastructureUnavailable as exc: + result = await asyncio.to_thread( + self._processor.process_blocking, reader, workspace + ) + return result.bounded_facts() + except DefaultPreSubmissionExecutionError as exc: raise PreSubmissionInfrastructureUnavailableError(str(exc)) from exc diff --git a/backend/app/api/routes/artifact_submissions.py b/backend/app/api/routes/artifact_submissions.py index 1bc5e80c5..76f79508a 100644 --- a/backend/app/api/routes/artifact_submissions.py +++ b/backend/app/api/routes/artifact_submissions.py @@ -15,6 +15,7 @@ SubmissionBundlePreparationCommand, SubmissionBundlePreparationRejected, SubmissionBundlePreparationRequest, + SubmissionBundlePreparationStatus, SubmissionBundlePreparationUnavailable, ) from app.modules.authorization.api import ActorIdentityFacts @@ -29,7 +30,7 @@ class SubmissionBundlePreparationResponse(BaseModel): put_attempt_id: UUID admission_id: UUID | None - status: str + submission_bundle_preparation_status: SubmissionBundlePreparationStatus replayed: bool diff --git a/backend/app/modules/artifacts/api/__init__.py b/backend/app/modules/artifacts/api/__init__.py index 5977c313f..983cd5011 100644 --- a/backend/app/modules/artifacts/api/__init__.py +++ b/backend/app/modules/artifacts/api/__init__.py @@ -5,6 +5,7 @@ SubmissionBundlePreparationRejected, SubmissionBundlePreparationRequest, SubmissionBundlePreparationResult, + SubmissionBundlePreparationStatus, SubmissionBundlePreparationUnavailable, ) @@ -13,5 +14,6 @@ "SubmissionBundlePreparationRejected", "SubmissionBundlePreparationRequest", "SubmissionBundlePreparationResult", + "SubmissionBundlePreparationStatus", "SubmissionBundlePreparationUnavailable", ) diff --git a/backend/app/modules/artifacts/api/submission_preparation.py b/backend/app/modules/artifacts/api/submission_preparation.py index 6eb9b67e7..2118348d1 100644 --- a/backend/app/modules/artifacts/api/submission_preparation.py +++ b/backend/app/modules/artifacts/api/submission_preparation.py @@ -4,6 +4,7 @@ from collections.abc import AsyncIterable from dataclasses import dataclass +from enum import StrEnum from typing import Protocol from uuid import UUID @@ -18,6 +19,16 @@ class SubmissionBundlePreparationUnavailable(RuntimeError): """Conceal unavailable or denied preparation authority.""" +class SubmissionBundlePreparationStatus(StrEnum): + """Closed public state vocabulary for one ART preparation operation.""" + + PREPARED = "prepared" + PUT_IN_FLIGHT = "put_in_flight" + OBJECT_CONFIRMED = "object_confirmed" + ABSENT_REPLAY_REQUIRED = "absent_replay_required" + READY = "ready" + + @dataclass(frozen=True, slots=True) class SubmissionBundlePreparationRequest: """One continuous contributor ZIP request with server-owned actor facts.""" @@ -41,7 +52,7 @@ class SubmissionBundlePreparationResult: put_attempt_id: UUID admission_id: UUID | None - status: str + submission_bundle_preparation_status: SubmissionBundlePreparationStatus replayed: bool diff --git a/backend/app/modules/artifacts/pre_submit_evidence.py b/backend/app/modules/artifacts/pre_submit_evidence.py index f8f226c04..9c507ee31 100644 --- a/backend/app/modules/artifacts/pre_submit_evidence.py +++ b/backend/app/modules/artifacts/pre_submit_evidence.py @@ -315,8 +315,10 @@ def _validate_execution( entries=execution.entries, ), ) - except PreSubmissionInfrastructureUnavailableError: - raise PreSubmitEvidenceConflict("pre_submission_result_context_invalid") + except PreSubmissionInfrastructureUnavailableError as exc: + raise PreSubmitEvidenceConflict( + "pre_submission_result_context_invalid" + ) from exc _PRE_SUBMIT_PASS_CAPABILITY_SEAL = object() diff --git a/backend/app/modules/artifacts/preparation.py b/backend/app/modules/artifacts/preparation.py index 78150f30f..98a6dd2ac 100644 --- a/backend/app/modules/artifacts/preparation.py +++ b/backend/app/modules/artifacts/preparation.py @@ -1714,18 +1714,18 @@ async def _process_prepared_submission( if active is None or not active.handle_issued or active.stream_claimed: raise ArtifactScratchIntegrityError("prepared artifact source is unavailable") - def process_and_cleanup() -> _InspectionResult: + async def process_and_cleanup() -> _InspectionResult: with self._manager.extraction_workspace( reserved_bytes=reserved_bytes, maximum_entries=maximum_entries, ) as workspace: active.reader.seek(0) try: - return processor.process_blocking(active.reader, workspace) + return await processor.process(active.reader, workspace) finally: active.reader.seek(0) - operation = asyncio.create_task(self._run_io(process_and_cleanup)) + operation = asyncio.create_task(process_and_cleanup()) try: async with asyncio.timeout_at(active.deadline): return await asyncio.shield(operation) diff --git a/backend/app/modules/artifacts/submission_admission.py b/backend/app/modules/artifacts/submission_admission.py index 747fa9762..23291a64a 100644 --- a/backend/app/modules/artifacts/submission_admission.py +++ b/backend/app/modules/artifacts/submission_admission.py @@ -15,6 +15,7 @@ SubmissionBundlePreparationRejected, SubmissionBundlePreparationRequest, SubmissionBundlePreparationResult, + SubmissionBundlePreparationStatus, SubmissionBundlePreparationUnavailable, ) from app.modules.artifacts.models import ( @@ -758,7 +759,9 @@ async def _existing_durable_result( return SubmissionBundlePreparationResult( put_attempt_id=UUID(attempt.id), admission_id=UUID(admission.id) if admission is not None else None, - status="ready" if admission is not None else attempt.status, + submission_bundle_preparation_status=SubmissionBundlePreparationStatus( + "ready" if admission is not None else attempt.status + ), replayed=True, ) @@ -792,6 +795,8 @@ def _result(result: SubmissionBundleDurablePutResult) -> SubmissionBundlePrepara return SubmissionBundlePreparationResult( put_attempt_id=result.put_attempt_id, admission_id=result.admission_id, - status=result.status, + submission_bundle_preparation_status=SubmissionBundlePreparationStatus( + result.status + ), replayed=result.replayed, ) diff --git a/backend/app/modules/artifacts/submission_materialization.py b/backend/app/modules/artifacts/submission_materialization.py index 7cd5b9b25..7513617ec 100644 --- a/backend/app/modules/artifacts/submission_materialization.py +++ b/backend/app/modules/artifacts/submission_materialization.py @@ -75,11 +75,11 @@ class PreSubmitCheckerExecutionRequest: class PreSubmitCheckerProcessor(Protocol): - """Blocking processor built by the composition-root CHECKER adapter.""" + """Async processor built by the composition-root CHECKER adapter.""" def abort(self) -> None: ... - def process_blocking( + async def process( self, reader: object, workspace: object ) -> PreSubmissionExecutionFacts: ... diff --git a/backend/scripts/module_boundaries.py b/backend/scripts/module_boundaries.py index 75abd51d0..0136b1a01 100644 --- a/backend/scripts/module_boundaries.py +++ b/backend/scripts/module_boundaries.py @@ -209,12 +209,12 @@ def scan(root: Path, registry: Registry) -> tuple[set[PrivateEdge], dict[str, se raise ModuleBoundaryError("unknown_module") if source_module == target_module: continue - if source_owner_adapter == target_module: - continue edge = authorization_boundary.ImportEdge(source, target) if source_module == "authorization" or target_module == "authorization": if not _is_public_target(target, target_module): auth_edges.add(edge) + if source_owner_adapter == target_module: + continue for target in exact_imports: target_module = _module_from_target(target) if target_module is None: diff --git a/backend/tests/architecture/test_module_boundaries.py b/backend/tests/architecture/test_module_boundaries.py index d310d5db2..1ace4421c 100644 --- a/backend/tests/architecture/test_module_boundaries.py +++ b/backend/tests/architecture/test_module_boundaries.py @@ -530,6 +530,25 @@ def test_auth_ledger_and_general_view_divergence_fails_closed( boundary.validate(ROOT, REGISTRY, LEDGER, AUTH_LEDGER) +def test_authorization_adapter_root_is_present_in_canonical_auth_view( + tmp_path: Path, +) -> None: + """Both scanners retain AUTH-private imports from its exact adapter root.""" + _registry(tmp_path / "registry.json") + source = tmp_path / "backend/app/adapters/authorization/__init__.py" + _write(source, "import app.modules.authorization.runtime\n") + registry = boundary.load_registry(tmp_path / "registry.json") + + _, _, actual_auth = boundary.scan(tmp_path, registry) + + assert actual_auth == { + boundary.authorization_boundary.ImportEdge( + "backend/app/adapters/authorization/__init__.py", + "app.modules.authorization.runtime", + ) + } + + @pytest.mark.parametrize( "source", ( diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 97d8c7e66..133d6327f 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -283,7 +283,7 @@ async def test_existing_durable_preparation_projects_exact_ready_admission() -> assert result == SubmissionBundlePreparationResult( put_attempt_id=attempt_id, admission_id=admission_id, - status="ready", + submission_bundle_preparation_status="ready", replayed=True, ) @@ -297,7 +297,7 @@ async def test_hidden_preparation_replays_persisted_checked_custody(monkeypatch) expected = SubmissionBundlePreparationResult( put_attempt_id=uuid4(), admission_id=uuid4(), - status="ready", + submission_bundle_preparation_status="ready", replayed=True, ) locked = SimpleNamespace(effective_policy_id=uuid4(), pre_submit_policy_id=uuid4()) @@ -393,7 +393,7 @@ def test_durable_put_result_projects_without_losing_replay_state() -> None: SubmissionBundlePreparationResult( put_attempt_id=durable.put_attempt_id, admission_id=None, - status="prepared", + submission_bundle_preparation_status="prepared", replayed=True, ) ) diff --git a/docs/spec_artifact_storage_service.md b/docs/spec_artifact_storage_service.md index 5d95ba2d7..44560a3d2 100644 --- a/docs/spec_artifact_storage_service.md +++ b/docs/spec_artifact_storage_service.md @@ -386,8 +386,9 @@ idempotency selectors, packet metadata, and one outer ZIP byte source. It exposes neither raw authorization context nor a prepared handle. There is no upload-session compatibility port. `PreparedBundleMaterializationRequest` is internal and process-local; it wraps -only opaque prepared authority, the exact task/assignment context, the current -`PreparedArtifact` generation, and exact policy/checker selectors. It is not a +opaque prepared authority, the exact task/assignment context, the current +`PreparedArtifact` generation, the effective plan, inspection, manifest, +change gate, packet, and exact policy/checker selectors. It is not a product capability port and may not cross a route, background execution, provider, or module public boundary. `BindingMaterializationRequest` contains task/submission/checker-run context and From 1d9998a3716641df8115dcb58f0f8a21d500210b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:46:42 +0100 Subject: [PATCH 08/27] fix(checkers): preserve fail-closed async boundary --- backend/app/modules/artifacts/preparation.py | 6 +++--- .../modules/checkers/pre_submit_execution.py | 8 ++++++++ .../test_effective_pre_submit_execution.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/backend/app/modules/artifacts/preparation.py b/backend/app/modules/artifacts/preparation.py index 98a6dd2ac..fe01dba06 100644 --- a/backend/app/modules/artifacts/preparation.py +++ b/backend/app/modules/artifacts/preparation.py @@ -56,10 +56,10 @@ class PreparedSubmissionProcessor(Protocol[_InspectionResultCo]): - """Blocking projection capability used only by the bounded async adapter.""" + """Async projection capability used by bounded submission preparation.""" - def process_blocking(self, reader: BinaryIO, workspace: Path) -> _InspectionResultCo: - """Return bounded results while executing outside the event loop.""" + async def process(self, reader: BinaryIO, workspace: Path) -> _InspectionResultCo: + """Return bounded results without blocking the event loop.""" def abort(self) -> None: """Deny checker access after cancellation or deadline expiry.""" diff --git a/backend/app/modules/checkers/pre_submit_execution.py b/backend/app/modules/checkers/pre_submit_execution.py index cdb6cd4da..656750cc7 100644 --- a/backend/app/modules/checkers/pre_submit_execution.py +++ b/backend/app/modules/checkers/pre_submit_execution.py @@ -586,9 +586,17 @@ def validate_pre_submission_execution_result( ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") for plan_entry, result in zip(plan.entries, execution.entries, strict=True): + metadata_keys = tuple(key for key, _ in result.metadata) if ( type(result.status) is not PreSubmissionResultStatus or result.schema_version != plan_entry.result_schema + or len(metadata_keys) != len(set(metadata_keys)) + or any( + key not in PRE_SUBMISSION_RESULT_METADATA_KEYS + or type(value) is not int + or value < 0 + for key, value in result.metadata + ) ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") try: diff --git a/backend/tests/test_effective_pre_submit_execution.py b/backend/tests/test_effective_pre_submit_execution.py index f6fc6f4ba..29a871ee5 100644 --- a/backend/tests/test_effective_pre_submit_execution.py +++ b/backend/tests/test_effective_pre_submit_execution.py @@ -569,3 +569,20 @@ class _ForgedStatus: match="pre_submission_result_context_invalid", ): validate_pre_submission_execution_result(plan, replace(forged, eligible=1)) # type: ignore[arg-type] + + for metadata in ( + (("unknown_count", 1),), + (("finding_count", 1), ("finding_count", 2)), + (("finding_count", "1"),), + (("finding_count", -1),), + ): + malformed_entry = replace(forged.entries[0], metadata=metadata) # type: ignore[arg-type] + malformed = replace( + forged, + entries=(malformed_entry, *forged.entries[1:]), + ) + with pytest.raises( + PreSubmissionInfrastructureUnavailable, + match="pre_submission_result_context_invalid", + ): + validate_pre_submission_execution_result(plan, malformed) From 17452884d4c50dd2a489cd1ad3cb7dce38743854 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:47:58 +0100 Subject: [PATCH 09/27] fix(artifacts): close preparation outcome vocabulary --- .../artifacts/api/submission_preparation.py | 5 ++++ .../tests/test_submission_bundle_admission.py | 23 ++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/backend/app/modules/artifacts/api/submission_preparation.py b/backend/app/modules/artifacts/api/submission_preparation.py index 2118348d1..de7ff2652 100644 --- a/backend/app/modules/artifacts/api/submission_preparation.py +++ b/backend/app/modules/artifacts/api/submission_preparation.py @@ -25,7 +25,12 @@ class SubmissionBundlePreparationStatus(StrEnum): PREPARED = "prepared" PUT_IN_FLIGHT = "put_in_flight" OBJECT_CONFIRMED = "object_confirmed" + STORED_PENDING_VERIFICATION = "stored_pending_verification" + OBSERVED_CONFIRMED = "observed_confirmed" + ACKNOWLEDGEMENT_UNKNOWN = "acknowledgement_unknown" + PROVIDER_UNAVAILABLE = "provider_unavailable" ABSENT_REPLAY_REQUIRED = "absent_replay_required" + STALE = "stale" READY = "ready" diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 133d6327f..10149c61e 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -380,20 +380,37 @@ async def runtime_factory(): authority.close.assert_called_once_with() -def test_durable_put_result_projects_without_losing_replay_state() -> None: +@pytest.mark.parametrize( + "durable_status", + ( + "prepared", + "put_in_flight", + "object_confirmed", + "stored_pending_verification", + "observed_confirmed", + "acknowledgement_unknown", + "provider_unavailable", + "absent_replay_required", + "stale", + "ready", + ), +) +def test_durable_put_result_projects_every_closed_preparation_status( + durable_status: str, +) -> None: durable = SubmissionBundleDurablePutResult( put_attempt_id=uuid4(), pre_submit_evidence_set_id=uuid4(), operation_identity=_sha("9"), admission_id=None, - status="prepared", + status=durable_status, replayed=True, ) assert PreparedSubmissionBundlePreparationCommand._result(durable) == ( SubmissionBundlePreparationResult( put_attempt_id=durable.put_attempt_id, admission_id=None, - submission_bundle_preparation_status="prepared", + submission_bundle_preparation_status=durable_status, replayed=True, ) ) From 0b2924c770c92a67303bd40e60c61feb5586679e Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 03:52:11 +0100 Subject: [PATCH 10/27] test(checkers): isolate metadata validation proof --- backend/tests/test_effective_pre_submit_execution.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_effective_pre_submit_execution.py b/backend/tests/test_effective_pre_submit_execution.py index 29a871ee5..88ebd0bac 100644 --- a/backend/tests/test_effective_pre_submit_execution.py +++ b/backend/tests/test_effective_pre_submit_execution.py @@ -576,7 +576,11 @@ class _ForgedStatus: (("finding_count", "1"),), (("finding_count", -1),), ): - malformed_entry = replace(forged.entries[0], metadata=metadata) # type: ignore[arg-type] + malformed_entry = replace( + forged.entries[0], + failure_code=None, + metadata=metadata, # type: ignore[arg-type] + ) malformed = replace( forged, entries=(malformed_entry, *forged.entries[1:]), From 54c3d64367bcd88f6be8c52ce21d7b376830b0e0 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:00:12 +0100 Subject: [PATCH 11/27] fix(artifacts): cover terminal preparation outcomes --- .../chunks/WS-ARCH-001-02D-art-preparation-public-api.md | 4 ++++ backend/app/modules/artifacts/api/submission_preparation.py | 3 +++ backend/tests/test_submission_bundle_admission.py | 3 +++ 3 files changed, 10 insertions(+) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md index ae905a6d7..6e4ad5e4f 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-02D-art-preparation-public-api.md @@ -44,12 +44,16 @@ PROJECT, and CHECKER public fact manifests. ```text backend/app/modules/artifacts/api/** backend/app/modules/artifacts/authorization.py +backend/app/modules/artifacts/preparation.py backend/app/modules/artifacts/router.py backend/app/modules/artifacts/pre_submit_evidence.py backend/app/modules/artifacts/submission_admission.py backend/app/modules/artifacts/submission_authorization.py backend/app/modules/artifacts/submission_materialization.py backend/app/modules/artifacts/schemas.py +backend/app/modules/checkers/api/__init__.py +backend/app/modules/checkers/api/pre_submit.py +backend/app/modules/checkers/pre_submit_execution.py backend/app/adapters/artifacts/__init__.py backend/app/adapters/checkers/__init__.py backend/app/adapters/projects/__init__.py diff --git a/backend/app/modules/artifacts/api/submission_preparation.py b/backend/app/modules/artifacts/api/submission_preparation.py index de7ff2652..8d0ddddda 100644 --- a/backend/app/modules/artifacts/api/submission_preparation.py +++ b/backend/app/modules/artifacts/api/submission_preparation.py @@ -29,6 +29,9 @@ class SubmissionBundlePreparationStatus(StrEnum): OBSERVED_CONFIRMED = "observed_confirmed" ACKNOWLEDGEMENT_UNKNOWN = "acknowledgement_unknown" PROVIDER_UNAVAILABLE = "provider_unavailable" + CONFLICT = "conflict" + INTEGRITY_MISMATCH = "integrity_mismatch" + MISSING = "missing" ABSENT_REPLAY_REQUIRED = "absent_replay_required" STALE = "stale" READY = "ready" diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 10149c61e..291868899 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -390,6 +390,9 @@ async def runtime_factory(): "observed_confirmed", "acknowledgement_unknown", "provider_unavailable", + "conflict", + "integrity_mismatch", + "missing", "absent_replay_required", "stale", "ready", From 3a4b36b935f6841eede61ff6dec8238d46105e4b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:14:38 +0100 Subject: [PATCH 12/27] test(artifacts): build schema-valid active guide --- .../tests/test_default_pre_submit_execution.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index e31fdbb4e..7dfb2521e 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -522,7 +522,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( text( "insert into project_guides " "(id,project_id,version,status,content_markdown,created_by) values " - "(:guide,:project,'1','active','# Guide','test')" + "(:guide,:project,'1','draft','# Guide','test')" ), params, ) @@ -609,6 +609,19 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( ), params, ) + await connection.execute( + text( + "update project_guides set status='active'," + "selected_review_policy_id=:review_policy," + "selected_review_policy_generation=1," + "selected_review_policy_hash=:review_policy_hash," + "selected_revision_policy_id=:revision_policy," + "selected_revision_policy_generation=1," + "selected_revision_policy_hash=:revision_policy_hash " + "where id=:guide" + ), + params, + ) await connection.execute( text( "insert into workstream_tasks " From 34205945a2e480aa31047a32c9d16025e44c4547 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:21:26 +0100 Subject: [PATCH 13/27] test(artifacts): preserve fixture debt ceiling --- .../TEST_STRUCTURE_DEBT.json | 12 +++---- .../test_default_pre_submit_execution.py | 35 ++++--------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 1f502bae6..57a67a9e1 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "4324d00330bbb0bbecb02c121c009d7aad075628d24b6e0a5dd0ce80f6cb5c47", - "end_line": 1726, + "content_sha256": "e3bd675d166f363c3bd885efbdf46b6d779111a2ed716209809e7b8f819bbd9b", + "end_line": 1718, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1726, + "observed_lines": 1718, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -746,11 +746,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "dc96b79324a762d6998829fe0f6680c05af4500740ccfaebda8df9e649eaf528", - "end_line": 1133, + "content_sha256": "5cbea7c6a6933dc251cbe004bd47d45193f5ec5e0aa35e41f698729bf431ff82", + "end_line": 1132, "hard_limit": 120, "kind": "test_function", - "observed_lines": 699, + "observed_lines": 698, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 7dfb2521e..baead7e65 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -454,9 +454,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( ("review_policies", "review_policy_mutation_custody"), ("revision_policies", "revision_policy_mutation_custody"), ) - blocked_prepared = replay_prepared = None - drift_prepared = None - denied_prepared = None + blocked_prepared = replay_prepared = drift_prepared = denied_prepared = None original_prepared_closed = False tables = ( "artifact_contents", @@ -600,25 +598,13 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( ) await connection.execute( text( - "insert into revision_policies " + "with inserted_revision as (insert into revision_policies " "(id,project_id,guide_version,policy_generation,policy_hash," "semantics_status,max_revision_rounds,revision_deadline_hours," "allowed_resubmission_states) values " "(:revision_policy,:project,'1',1,:revision_policy_hash," - "'legacy_incomplete',1,24,'[]'::json)" - ), - params, - ) - await connection.execute( - text( - "update project_guides set status='active'," - "selected_review_policy_id=:review_policy," - "selected_review_policy_generation=1," - "selected_review_policy_hash=:review_policy_hash," - "selected_revision_policy_id=:revision_policy," - "selected_revision_policy_generation=1," - "selected_revision_policy_hash=:revision_policy_hash " - "where id=:guide" + "'legacy_incomplete',1,24,'[]'::json) returning id) " + "update project_guides set status='active',selected_review_policy_id=:review_policy,selected_review_policy_generation=1,selected_review_policy_hash=:review_policy_hash,selected_revision_policy_id=:revision_policy,selected_revision_policy_generation=1,selected_revision_policy_hash=:revision_policy_hash where id=:guide" ), params, ) @@ -1276,10 +1262,7 @@ async def test_disabled_advisory_is_explicit_and_not_skipped_success(tmp_path: P if entry.definition_id == "artifact.quality.placeholder_signal" ) - assert ( - advisory.checker_execution_status - == PreSubmissionResultStatus.ADVISORY_DISABLED.value - ) + assert advisory.checker_execution_status == PreSubmissionResultStatus.ADVISORY_DISABLED.value assert result.eligible is True await request.prepared_artifact.close() manager.close() @@ -1477,9 +1460,7 @@ async def test_effective_execution_enforces_project_only_forbidden_rule(tmp_path assert result.eligible is False project_result = next( - entry - for entry in result.entries - if entry.definition_id == "policy.artifact.forbid" + entry for entry in result.entries if entry.definition_id == "policy.artifact.forbid" ) assert project_result.checker_execution_status == PreSubmissionResultStatus.FAILED.value await request.prepared_artifact.close() @@ -1499,9 +1480,7 @@ async def test_effective_execution_enforces_server_owned_storage_scheme(tmp_path result = await service.materialize_prepared_bundle(request) policy_result = next( - entry - for entry in result.entries - if entry.definition_id == "policy.storage_scheme.enforce" + entry for entry in result.entries if entry.definition_id == "policy.storage_scheme.enforce" ) assert policy_result.checker_execution_status == PreSubmissionResultStatus.FAILED.value assert policy_result.message_code == "storage_scheme_not_allowed" From c486b4f696c743f134bde15da975391bb1264010 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:31:59 +0100 Subject: [PATCH 14/27] test(artifacts): permit canonical guide fixture activation --- .../TEST_STRUCTURE_DEBT.json | 12 ++++++------ backend/tests/test_default_pre_submit_execution.py | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 57a67a9e1..0c4560b4d 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "e3bd675d166f363c3bd885efbdf46b6d779111a2ed716209809e7b8f819bbd9b", - "end_line": 1718, + "content_sha256": "b42051772d38cbf9f580479c5fb9c6ddf1f8fc27052ed996944105c1888b4253", + "end_line": 1719, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1718, + "observed_lines": 1719, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -746,11 +746,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "5cbea7c6a6933dc251cbe004bd47d45193f5ec5e0aa35e41f698729bf431ff82", - "end_line": 1132, + "content_sha256": "af223da7c85903c7a468519fdbf2b304293383abb688679069ee96bf7c05bca8", + "end_line": 1133, "hard_limit": 120, "kind": "test_function", - "observed_lines": 698, + "observed_lines": 699, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index baead7e65..afd94da6d 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -444,6 +444,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( custody_triggers = ( ("projects", "project_creation_custody"), ("project_guides", "guide_mutation_product_custody"), + ("project_guides", "guide_lineage_lifecycle_guard"), ("guide_source_snapshots", "source_snapshot_product_custody"), ("submission_artifact_policies", "submission_policy_creation_custody"), ( From 8feb6efc36c38cae3cb396840e53ffbc97972d61 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:43:33 +0100 Subject: [PATCH 15/27] test(artifacts): use canonical snapshot lineage --- .../TEST_STRUCTURE_DEBT.json | 2 +- backend/tests/test_default_pre_submit_execution.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 0c4560b4d..255772113 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,7 +230,7 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "b42051772d38cbf9f580479c5fb9c6ddf1f8fc27052ed996944105c1888b4253", + "content_sha256": "f74818a4884957293423bdf2df1be8a5385cbd8e43782b5666311317c55230b9", "end_line": 1719, "hard_limit": 1200, "kind": "test_file", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index afd94da6d..cb2b6f2af 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -183,7 +183,7 @@ def _plan(catalogue): guide_id=uuid4(), guide_version=1, source_snapshot_id=uuid4(), - source_snapshot_hash="sha256:" + "1" * 64, + source_snapshot_hash=canonical_json_hash({}), effective_policy_id=uuid4(), effective_policy_hash=policy_hash, pre_submit_policy_id=uuid4(), From 2c925a20538c2f8175753c418299ffeb33804316 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 04:57:58 +0100 Subject: [PATCH 16/27] test(artifacts): persist canonical policy lineage --- .../TEST_STRUCTURE_DEBT.json | 12 ++++++------ backend/tests/test_default_pre_submit_execution.py | 11 ++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 255772113..e62eb2181 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "f74818a4884957293423bdf2df1be8a5385cbd8e43782b5666311317c55230b9", - "end_line": 1719, + "content_sha256": "7a00ad86da8526582f9149d87a0b4eb720b3ec427e511661bd359e624cd94aec", + "end_line": 1720, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1719, + "observed_lines": 1720, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -746,15 +746,15 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "af223da7c85903c7a468519fdbf2b304293383abb688679069ee96bf7c05bca8", - "end_line": 1133, + "content_sha256": "5addfeacd72d75c8e399f56de9609240ce31e89654e06d509da9f0d8fd7b1c75", + "end_line": 1134, "hard_limit": 120, "kind": "test_function", "observed_lines": 699, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 435 + "start_line": 436 }, { "capability": "unassigned_legacy_auth", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index cb2b6f2af..fea53b43e 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -5,6 +5,7 @@ import asyncio from io import BytesIO from dataclasses import replace +import json from pathlib import Path import threading from types import SimpleNamespace @@ -452,11 +453,10 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( "effective_submission_policy_custody", ), ("pre_submit_checker_policies", "pre_submit_policy_custody"), - ("review_policies", "review_policy_mutation_custody"), - ("revision_policies", "revision_policy_mutation_custody"), + ("review_policies", "review_policy_mutation_custody"), ("revision_policies", "revision_policy_mutation_custody"), # noqa: E501 ) blocked_prepared = replay_prepared = drift_prepared = denied_prepared = None - original_prepared_closed = False + original_prepared_closed = bool() tables = ( "artifact_contents", "artifact_replicas", @@ -480,6 +480,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( "submission_policy": str(uuid4()), "effective_policy": str(lineage.effective_policy_id), "effective_hash": lineage.effective_policy_hash, + "effective_body": json.dumps(_effective_policy()), "checker_body": json.dumps(compile_effective_project_submission_artifact_policy(_effective_policy(), lineage.effective_policy_hash).compiled_bundle), # noqa: E501 "checker_policy": str(lineage.pre_submit_policy_id), "checker_hash": lineage.pre_submit_policy_bundle_hash, "post_policy": str(uuid4()), @@ -553,7 +554,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( "submission_artifact_policy_hash,lifecycle_status,merge_algorithm_version," "effective_policy,effective_policy_hash,created_by) values " "(:effective_policy,:project,:guide,'1',:snapshot,:snapshot_hash," - ":submission_policy,:effective_hash,'approved','1','{}'::json," + ":submission_policy,:effective_hash,'approved','1',cast(:effective_body as json)," ":effective_hash,'test')" ), params, @@ -566,7 +567,7 @@ async def test_effective_evidence_workflow_persists_once_and_replays_exactly( "lifecycle_status,compiler_version,compiled_bundle,compiled_bundle_hash," "checker_names,checker_configs,created_by) values " "(:checker_policy,:project,:guide,'1',:snapshot,:snapshot_hash," - ":effective_policy,:effective_hash,'compiled','1','{}'::json," + ":effective_policy,:effective_hash,'compiled','1',cast(:checker_body as json)," ":checker_hash,'[]'::json,'{}'::json,'test')" ), params, From c9849d0a23c335c70a46a992e46eefcba46ca489 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:08:53 +0100 Subject: [PATCH 17/27] test(artifacts): assert durable evidence effects --- .../TEST_STRUCTURE_DEBT.json | 12 ++++++------ backend/tests/test_default_pre_submit_execution.py | 7 ++++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index e62eb2181..2b956205f 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "7a00ad86da8526582f9149d87a0b4eb720b3ec427e511661bd359e624cd94aec", - "end_line": 1720, + "content_sha256": "b3e5727e145afd27772ec35e656c6694a141eff683bef33d97aaf32a70d08b41", + "end_line": 1721, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1720, + "observed_lines": 1721, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -746,11 +746,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "5addfeacd72d75c8e399f56de9609240ce31e89654e06d509da9f0d8fd7b1c75", - "end_line": 1134, + "content_sha256": "132bfd3ca539369f5ba99718e6e14869a6f6b810f70c750f12df9adf03341949", + "end_line": 1135, "hard_limit": 120, "kind": "test_function", - "observed_lines": 699, + "observed_lines": 700, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index fea53b43e..9a76c7c68 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -1129,9 +1129,10 @@ async def publish_ready() -> str: **before, "artifact_contents": before["artifact_contents"] + 1, "artifact_replicas": before["artifact_replicas"] + 1, - "artifact_put_attempts": before["artifact_put_attempts"] + 1, - "submission_bundle_admissions": before["submission_bundle_admissions"] + 1, - } + "artifact_put_attempts": before["artifact_put_attempts"] + 1, + "submission_bundle_admissions": before["submission_bundle_admissions"] + 1, + "pre_submit_evidence_sets": evidence_count, "pre_submit_evidence_results": result_count, # noqa: E501 + } @pytest.mark.asyncio From 47a224640c9b9fda1c23826db56a16c06aa5b1dd Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:09:24 +0100 Subject: [PATCH 18/27] test(artifacts): retain evidence fixture ceiling --- .../TEST_STRUCTURE_DEBT.json | 1075 ----------------- .../test_default_pre_submit_execution.py | 3 +- 2 files changed, 1 insertion(+), 1077 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 2b956205f..e69de29bb 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -1,1075 +0,0 @@ -{ - "entries": [ - { - "capability": "unassigned_legacy_auth", - "content_sha256": "2a5e5c4a07996ca7e0a5be65197324602b1a6783440f3e143832aa6a9353659f", - "end_line": 1600, - "hard_limit": 1200, - "kind": "production_file", - "observed_lines": 1600, - "path": "backend/app/modules/authorization/kernel.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "5d8e8dd3970c9ec409d7864a9ebe9bef246aaff71f6e74dd4967a05773738ea7", - "end_line": 1642, - "hard_limit": 1200, - "kind": "production_file", - "observed_lines": 1642, - "path": "backend/app/modules/authorization/router.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "bf5ea8baf23e45ab71cd6a87d7fc48b3b8321b26fd616b2841d90fd90a18b1cc", - "end_line": 1694, - "hard_limit": 1200, - "kind": "production_file", - "observed_lines": 1694, - "path": "backend/app/modules/authorization/runtime.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "17251cc63f24b1cd34969fc192848459d4800ab2877e85d9244e468b4c82dfb7", - "end_line": 1113, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 126, - "path": "backend/app/modules/authorization/catalogue.py", - "qualified_symbol": "_index_service_actions", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 988 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "a7acb9f31de3a2b2e32c9e8419d1ea8546610f86dcd05f021c87ed340a134590", - "end_line": 604, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 217, - "path": "backend/app/modules/authorization/kernel.py", - "qualified_symbol": "AuthorizationService._prepare_prelocked", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 388 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "152e3da2ea2843ecfe2c0cfd30a5ba38fe87e4bc03308e1b3ee02351dda4b7c7", - "end_line": 1115, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 175, - "path": "backend/app/modules/authorization/kernel.py", - "qualified_symbol": "AuthorizationService._require_prelocked", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 941 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "8cb806aad12f5010d1e8875c4ba31eee43856a120e338d31a02d87e1ea30ad0c", - "end_line": 1600, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 125, - "path": "backend/app/modules/authorization/kernel.py", - "qualified_symbol": "AuthorizationService._stage_decision", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1476 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "cb697c0c6e970a22d59a7a7c93a8be3fdc7a79933c1e47f29466db64ae8e038c", - "end_line": 802, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 284, - "path": "backend/app/modules/authorization/prepared.py", - "qualified_symbol": "PreparedAuthorizationService._binding", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 519 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "b54b9e3027939480391b0bc1e8efd5db50c4a7a6d5aa22c78924e8a04246f1ff", - "end_line": 274, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 112, - "path": "backend/app/modules/authorization/project_role_service.py", - "qualified_symbol": "ProjectRoleGrantMutationService.complete_issue", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 163 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "f9e987c8d41ce4f4fcc7f2ba8a6a19406361f63dde8541af275824e461c35114", - "end_line": 1411, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 179, - "path": "backend/app/modules/authorization/router.py", - "qualified_symbol": "issue_project_role_grant", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1233 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "3bc6d1b229e30f03517079f80c7350e9ecb26a66162848019252756d0828918f", - "end_line": 615, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 118, - "path": "backend/app/modules/authorization/router.py", - "qualified_symbol": "provision_service_actor", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 498 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "3dd50feab3b1dd671bfce591f5ef337893807301a2248aed20c9e85865273e83", - "end_line": 1554, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 135, - "path": "backend/app/modules/authorization/router.py", - "qualified_symbol": "revoke_project_role_grant", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1420 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "2aa51a4d6baef4e2fdbce4bb8bba7a622270fec33614f6b8665a2f5a7240f36b", - "end_line": 317, - "hard_limit": 100, - "kind": "production_function", - "observed_lines": 146, - "path": "backend/app/modules/authorization/service.py", - "qualified_symbol": "AuthorityMutationService.complete", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 172 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "5c5364ad5a5387a6b56210901d9b07d4ab1f04d470056ae76bd1e5fd998548e5", - "end_line": 1594, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 1594, - "path": "backend/tests/test_actors.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "af8e6d93649b4715d9757f6ccf3aa6d24c260c494bfca076a3191d493cb514f9", - "end_line": 3293, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 3293, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "22a465ed4fce76ba8d37403942c0766783819bf0160fd9d2d2fa0614245ea3ec", - "end_line": 1209, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 1209, - "path": "backend/tests/test_artifact_internal_authorization.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "7717de94f55d517c03dbf8ca674cf4803f33d4d4412c5ea9b1fde3e3b57d199c", - "end_line": 1806, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 1806, - "path": "backend/tests/test_audit.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "16ebbaa1b66f08bf3bc395353f43505856aed1b937148f96b988377c028c4e39", - "end_line": 7424, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 7424, - "path": "backend/tests/test_auth.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "ebb3b0ede78653a373c17904bc29d40da7ed93e96e0416e0742079386ca7c7f5", - "end_line": 13421, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 13421, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "b3e5727e145afd27772ec35e656c6694a141eff683bef33d97aaf32a70d08b41", - "end_line": 1721, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 1721, - "path": "backend/tests/test_default_pre_submit_execution.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "8182eec08bb881fe6eed443e628bcf17710af112060ad3937a80c13040ccc5c1", - "end_line": 2526, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 2526, - "path": "backend/tests/test_guide_bindings.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "f1a9936e100e999fb6a2bad2b8306e1aefa5b594ad2759cd318182a35afefe87", - "end_line": 15700, - "hard_limit": 1200, - "kind": "test_file", - "observed_lines": 15700, - "path": "backend/tests/test_projects.py", - "qualified_symbol": null, - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "f7e72f9f5d32407bb995b955cfe7c15dfeb69e48a1877a9c861043663b4e35aa", - "end_line": 1594, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 122, - "path": "backend/tests/test_actors.py", - "qualified_symbol": "test_existing_actor_and_legacy_negative_states_fail_closed", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1473 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "0febc9420a1ee5b520e320ba511cda9f73c6469d1a63bf537a263ba5886f3ce3", - "end_line": 3251, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 123, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "test_checker_output_put_observation_terminal_outcomes", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 3129 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "fa7c9ad2acfa3b9810c43509966c61171d2234042ca32f7cb8e2d543f17cc252", - "end_line": 3028, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 187, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "test_checker_output_requires_exact_active_fixed_service_identity", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2842 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "e9f4d7d65b3794e1642401465a6f2c5a4ae179865c1a4113f97b8494ae0e1241", - "end_line": 2708, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 199, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "test_guide_admission_consumes_real_project_manager_prep_atomically", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2510 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "bf9f2a59d649d11892c2889665478cc06dc61f597692b403458c89a593538434", - "end_line": 2507, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 167, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "test_guide_admission_derives_three_scopes_without_provider_evidence", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2341 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "944c0487e49ed46976106404e83bcb8ad72ebb427eba10a34610a8de9697fb61", - "end_line": 1659, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 131, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "test_verification_claim_takeover_and_scanner_due_order_are_fenced", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1529 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "86e28f347925db968073550e8114c37650df51e65b65bb9cad9bdf2f78393de2", - "end_line": 1209, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 123, - "path": "backend/tests/test_artifact_internal_authorization.py", - "qualified_symbol": "test_post_provider_revocation_commits_denial_but_no_terminal_artifact_facts", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1087 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "54a4ac3c0018dbcee7aec6600f282471069261d6390872449f6cf1de9657ed2a", - "end_line": 788, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 153, - "path": "backend/tests/test_artifact_internal_authorization.py", - "qualified_symbol": "test_put_claim_and_terminal_injected_failures_roll_back_both_sides", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 636 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "4030b32ebcd38072e457a172184bb880348fa7b9039aeddac019ff972855a93a", - "end_line": 1083, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 181, - "path": "backend/tests/test_artifact_internal_authorization.py", - "qualified_symbol": "test_verification_claim_and_terminal_failures_roll_back_both_sides", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 903 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "c01dbfd93b66ed905f6f301b706e605e210fb467478b41d1b77fcd78f12986ac", - "end_line": 542, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 420, - "path": "backend/tests/test_artifact_operator_api.py", - "qualified_symbol": "test_real_http_operator_path_returns_redacted_lineage_and_recovery", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 123 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "02ed6da571dafee7f7b6f0051fb17620ad3058f92c4e283dc5cde24d098d4c31", - "end_line": 255, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 131, - "path": "backend/tests/test_audit.py", - "qualified_symbol": "test_action_aware_audit_input_enforces_mapping_and_action_availability", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 125 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "982f7c01fbc085454707e4b4491b9e79289af6203b7c2718635732d61ba8c9f1", - "end_line": 755, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 189, - "path": "backend/tests/test_audit.py", - "qualified_symbol": "test_authority_input_rejects_unbounded_or_inconsistent_evidence", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 567 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "36820d7d6c9db7dac729710d8d87ab233d4882febd049e862961449550b52e8e", - "end_line": 1316, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 210, - "path": "backend/tests/test_audit.py", - "qualified_symbol": "test_database_rejects_malformed_and_mutated_audit_rows", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1107 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "44b9858d132d384cd10c6b8b8fae096ade7c3fced6780cc2c142e59b4162d97d", - "end_line": 3542, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 280, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_actor_admin_reads_hold_caller_and_grant_locks_through_disclosure", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 3263 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "b0cb3554dd7f531648e4202302059b8416ca7ed2fdcc6529460f0acb092b82de", - "end_line": 7404, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 927, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_actor_identity_link_lifecycle_real_postgres_concurrency", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6478 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "15c7147c11fa1802f43480b38a3678ac1a2586320ae2f0013622f1d63b2a3806", - "end_line": 5961, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 535, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_actor_identity_link_lifecycle_real_postgres_matrix", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 5427 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "49243ef3a0b0b4b24e5033b3e0ad75eee17c7fc2abd2014224a595ed8c6c9ade", - "end_line": 6475, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 491, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_actor_profile_lifecycle_real_postgres_concurrency", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 5985 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "eca9eeff229ee37c103188f9a31e278773a69e96f2fd354662723f840cf8f16a", - "end_line": 5424, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 482, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_actor_profile_lifecycle_real_postgres_matrix", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 4943 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "e1fd679295947e120c6da520bd138a78a8e133e44f8dde91010de29354e88f5b", - "end_line": 3260, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 485, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_admin_bootstrap_replay_and_cross_revoke_are_concurrency_safe", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2776 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "8ea087b906726103295a019967f8c38e42b55deb4e912fda3a41289fda23f35d", - "end_line": 4268, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 647, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_controlled_service_actor_provisioning_includes_project_setup_and_is_atomic", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 3622 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "d26732dd125f8c2062b5a4b45b59fbccd283ee04939a9da7be1576235524292f", - "end_line": 4679, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 409, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_service_actor_provisioning_failure_and_authority_races_are_atomic", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 4271 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "a7e36b433ed775ab82e7ff92c013aab3a1cee14fbbed6f4ddace351ab936e24c", - "end_line": 2660, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 1041, - "path": "backend/tests/test_auth.py", - "qualified_symbol": "test_signed_tokens_bootstrap_and_admin_grant_lifecycle", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1620 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "951b44cc07e36002118fe93b7974e8d65851b0e2c3cec89a3031cbe42b014e2d", - "end_line": 8521, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 140, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_actor_lifecycle_service_applies_success_and_guards_conflicts", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8382 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "569084d6de89ff9eae71d526fc6c157aea7638f55ca93ad128b721fbda339be6", - "end_line": 9011, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 122, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_admin_resource_digest_alone_rejects_substituted_role_and_disposition", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8890 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "90b8b670b4d277209617cc1aa794188b4fb3cd9d894b69d5dc3d0adfc885f6ed", - "end_line": 9239, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 132, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_admin_revoke_stages_complete_state_and_evidence", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9108 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "78d9bf3df08e5633e9a75720b1e4bf5b7d7b24019bc392b4cb7496a9ac5e5e8e", - "end_line": 11073, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 122, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_authorization_locks_refresh_cached_actor_lifecycle_state", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10952 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "aab80cce1a72714c4ec2e95b0596031e59a463459c69a4079627070f79edf349", - "end_line": 2252, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 327, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_closed_permission_and_action_catalogue_is_exact_and_non_executable", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1926 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "df1df6ec2ba6aa55445e21cfcf4e4e3ad9664c9504313484a495b6a6df1e9047", - "end_line": 3481, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 126, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_identity_link_lifecycle_route_preserves_outcome_transaction_contract", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 3356 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "05d1b020ecff0f9bc0a0567adc07f5b31a2f9dfb7828ae3ad34d4e1e7757797c", - "end_line": 8674, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 151, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_identity_link_lifecycle_service_applies_success_and_guards_conflicts", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8524 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "c5d6d0d480ced964354915614f15202c0159bfbb281658da0d44186dffd17848", - "end_line": 7469, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 142, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_prepared_actor_authority_crossed_mutations_complete_in_both_orders", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7328 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "ebb76e63671195aa4d806ac602bfe58cb22bf82d00c8a70ab186785f3acd7f9b", - "end_line": 7810, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 336, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_prepared_crosses_real_lifecycle_service_transactions", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7475 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "9ea4fc0ddbab4c7262a43bc3f498ee1afea3318e9a0b6c93c87763f9f22aae9c", - "end_line": 7300, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 518, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_prepared_postgresql_failure_and_cancellation_are_atomic", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6783 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "46e0b031394da6fee856e48615732a17ff8d2276d9cedfb0ecaa3a6879410adb", - "end_line": 4686, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 157, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_11c2_reads_require_exact_admin_context_and_role_allowlist", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 4530 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "e95d2a03e3829df89c43210a9de92efca31a25905c9648c3179d17b078386b17", - "end_line": 2653, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 397, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_mutation_resources_and_prepared_scopes_are_closed", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2257 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "105667302ed6e8f2fd16ea7e95d642e72e41514673e1152503536e0520f9c362", - "end_line": 10948, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 204, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_read_permissions_have_postgresql_role_scope_matrix", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10745 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "756b7f99f9a743284934b4d85ce263617710d9b8119792ccb4526a1de04070a1", - "end_line": 12450, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 233, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_role_and_all_operation_mappings_commit_one_linked_pair", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12218 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "2e1d7db74ddb955b90a0ee12e4fb72b651a0f85d2746c76e09079c05b0b85252", - "end_line": 13421, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 681, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_role_issue_postgresql_prep_binds_target_role_and_scope", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 12741 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "cf7f0f09ac93a917c74fd66c0048a289f0a437ca89757f834c70afca273588b3", - "end_line": 1409, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 214, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_role_mutation_routes_conceal_denials_and_preserve_self_guards_atomically", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1196 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "2f01493754052fa841ca40a5600f6fc68cfac966e719323a9a89e4b8c38a5744", - "end_line": 1672, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 245, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_project_role_mutation_routes_enforce_project_lifecycle_without_disclosure", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1428 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "05621e885ed2f88d0ba1a072c1f263fc923939f7ce755a514e9872112aa830a1", - "end_line": 11967, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 163, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "test_service_actor_replay_fails_closed_on_committed_state_drift", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11805 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "132bfd3ca539369f5ba99718e6e14869a6f6b810f70c750f12df9adf03341949", - "end_line": 1135, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 700, - "path": "backend/tests/test_default_pre_submit_execution.py", - "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 436 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "3ee40b000ff508e0418aeb28a6dcb640dff4854436770343088e096c3f91c79a", - "end_line": 1575, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 135, - "path": "backend/tests/test_guide_bindings.py", - "qualified_symbol": "test_new_format_support_replaces_obsolete_policy_budget_without_replay", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 1441 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "9a0a149ec9d89468ec6a33e5b2bcaa825896a02f2aedf75f52aee3bb402ce5f7", - "end_line": 312, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 176, - "path": "backend/tests/test_guide_bindings.py", - "qualified_symbol": "test_sufficiency_material_uses_only_exact_current_extraction", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 137 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "14a28aa2e24af745acbfa5f2052873b893a42f4c27ad98325f193a9eff92dd08", - "end_line": 492, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 177, - "path": "backend/tests/test_guide_bindings.py", - "qualified_symbol": "test_verified_sufficiency_report_commits_exact_usage_provenance", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 316 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "26170dfbd986d2930d7f89357d59cdd17ae50b2ac1b936a3966e24225312e895", - "end_line": 666, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 132, - "path": "backend/tests/test_project_policy_mutations.py", - "qualified_symbol": "test_policy_router_dependencies_errors_and_transaction_outcomes", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 535 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "d964319e3c80e3caf0d12a5cb3bec366f99b9709b80a43fc36feb63adaec1f0f", - "end_line": 5832, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 133, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_corrected_submission_artifact_policy_resumes_post_submit_setup", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 5700 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "e594d392082b6a7b5f9c6f048361fd5d7d0495b72b84b313cba64f2041f104f4", - "end_line": 15338, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 121, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_guide_activation_and_active_guide_retrieval", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 15218 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "0c0725368231113bf21edfde138b0b55d64bc8a6f4382acc83e79c6d6e704fdf", - "end_line": 4428, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 143, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_guide_mutation_service_executes_all_three_authorized_happy_paths", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 4286 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "4ae7cc4865023adb90a531ebda235392d1168cd77f5d68b33543c0c38382bf61", - "end_line": 14757, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 261, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_post_submit_checker_policy_correction_preserves_audit_and_guides_rederivation", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 14497 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "b259f2053c3db0c26ebc7f0ab19b3c8acede85788ad97e4ea8af17dc4e9804dd", - "end_line": 6322, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 137, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_post_submit_derivation_unsupported_checker_gap_blocks_setup", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 6186 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "c18a567fb375d226c4edd636e133999e9f75ca454fec745e0a7ec81ee1aab0b0", - "end_line": 2517, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 161, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_project_role_grant_repository_filters_and_uses_strict_keyset", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 2357 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "102c97be36120910868ba02c0a6996767dc07430ed03585495e87b490ba7be12", - "end_line": 7347, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 197, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_submission_artifact_policy_agent_service_commits_and_replays_without_io", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 7151 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "754a750cfba33d5312c5f773086f01458497740023846016aa85a355375a5f14", - "end_line": 10048, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 143, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_submission_artifact_policy_authority_service_is_flush_only", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9906 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "bc9b037b8fe6f3450a203b8f691d4253ea1d3c3e40ae48c88ef16dd011eefdeb", - "end_line": 10505, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 182, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_submission_artifact_policy_manual_lineage_loads_exact_locked_context", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10324 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "9c74e06c2030e6fcad544df62f2950b8b1d62a797ce3b9328bbd3d1e02041b2a", - "end_line": 10321, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 271, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_submission_artifact_policy_manual_service_executes_authorized_create_and_update", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 10051 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "8a13e445413234d31c92e2ae133e2855633ce9d5b06ffba226c0ade58f51ee4d", - "end_line": 9903, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 153, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_submission_artifact_policy_replay_repository_idempotency_classifies_cross_action_states", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9751 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "58eb4394fb393de909f8b8f7ec879d5dbca99e60939d4a60d6bf31bb6e844568", - "end_line": 8941, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 217, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_sufficiency_mutation_fail_closed_internal_guards", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8725 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "414e9f8d8ff80ec5986db3d831635374e1592fab3fabdb152d3f92c6ec1081a1", - "end_line": 9174, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 231, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_sufficiency_mutation_manual_dispatch_commits_and_replays", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 8944 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "611ad24f47071fe9a9adc56bb91d68f53bd04be8fc54d97091043e394f577f15", - "end_line": 9531, - "hard_limit": 120, - "kind": "test_function", - "observed_lines": 355, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "test_sufficiency_mutation_services_commit_human_create_and_acknowledgement", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 9177 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "10292b6ed6f73baae351efbcb13102da0bd21eea2926587cef48e784baa95803", - "end_line": 376, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 104, - "path": "backend/tests/project_create_fixtures.py", - "qualified_symbol": "seed_authorized_project", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 273 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "0c7b21c804df09df7687641af563ea9808f7ce68fa29548b7c1c6f8b194e7d1b", - "end_line": 691, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 353, - "path": "backend/tests/test_artifact_admission.py", - "qualified_symbol": "_seed_checker_output_relationships", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 339 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "60c62ad8e998812943b4a61a2485e5454530abc8abf63fb45bf2401f86189b6d", - "end_line": 304, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 113, - "path": "backend/tests/test_artifact_recovery.py", - "qualified_symbol": "_exhausted_guide_job", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 192 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "efca18e30f3e8e9d7b16310a956b427262dc92bdf13165146b58523b25ea2687", - "end_line": 521, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 228, - "path": "backend/tests/test_audit.py", - "qualified_symbol": "_authority_event_matrix", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 294 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "1a0a9f3e2be6965e29f76aa74272ccfa2fe4b99e4e0c3bde5ec8ba2c374b369b", - "end_line": 11281, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 169, - "path": "backend/tests/test_authorization.py", - "qualified_symbol": "_operation_success", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 11113 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "19100482ea5552739e4ce0561c771da27b5ec4e99f108d0e7cbd24aae94dbaae", - "end_line": 959, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 221, - "path": "backend/tests/test_guide_bindings.py", - "qualified_symbol": "_seed_binding_lineage", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 739 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "e55632d4c6056e04f99d00104ca58677f354e604bbd279e9d0c14adc59bff900", - "end_line": 520, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 124, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "_PolicyReadRepository.__init__", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 397 - }, - { - "capability": "unassigned_legacy_auth", - "content_sha256": "addf8488c7fff4ec2ee4c2d70e65d208642f20169f13fe1ba76db6f6f725a0c7", - "end_line": 853, - "hard_limit": 100, - "kind": "test_helper", - "observed_lines": 118, - "path": "backend/tests/test_projects.py", - "qualified_symbol": "_activation_ready_bundle", - "removal_chunk": "WS-AUTH-003-CLOSE", - "start_line": 736 - } - ], - "exceptions": [], - "policy_sha256": "a6d2f60bdc8844bc3ee84b87b22be1b05bac55b39bfc1d31e3e70a71d61ebcb5", - "schema": "workstream.auth-test-structure-debt.v1" -} diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 9a76c7c68..9a29a129a 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -1123,8 +1123,7 @@ async def publish_ready() -> str: assert blocked.failure_audit["event_type"] == "pre_submission_check_failed" assert blocked.failure_audit["failed_count"] >= 1 assert "task.toml" not in repr(blocked.failure_audit) - assert evidence_count == 5 - assert result_count == 5 * len(request.effective_plan.entries) + assert (evidence_count, result_count) == (5, 5 * len(request.effective_plan.entries)) assert after == { **before, "artifact_contents": before["artifact_contents"] + 1, From d45843e8c22ea284b4ccf92795a4114d7676273b Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:09:39 +0100 Subject: [PATCH 19/27] Revert "test(artifacts): retain evidence fixture ceiling" This reverts commit 47a224640c9b9fda1c23826db56a16c06aa5b1dd. --- .../TEST_STRUCTURE_DEBT.json | 1075 +++++++++++++++++ .../test_default_pre_submit_execution.py | 3 +- 2 files changed, 1077 insertions(+), 1 deletion(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index e69de29bb..2b956205f 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -0,0 +1,1075 @@ +{ + "entries": [ + { + "capability": "unassigned_legacy_auth", + "content_sha256": "2a5e5c4a07996ca7e0a5be65197324602b1a6783440f3e143832aa6a9353659f", + "end_line": 1600, + "hard_limit": 1200, + "kind": "production_file", + "observed_lines": 1600, + "path": "backend/app/modules/authorization/kernel.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "5d8e8dd3970c9ec409d7864a9ebe9bef246aaff71f6e74dd4967a05773738ea7", + "end_line": 1642, + "hard_limit": 1200, + "kind": "production_file", + "observed_lines": 1642, + "path": "backend/app/modules/authorization/router.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "bf5ea8baf23e45ab71cd6a87d7fc48b3b8321b26fd616b2841d90fd90a18b1cc", + "end_line": 1694, + "hard_limit": 1200, + "kind": "production_file", + "observed_lines": 1694, + "path": "backend/app/modules/authorization/runtime.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "17251cc63f24b1cd34969fc192848459d4800ab2877e85d9244e468b4c82dfb7", + "end_line": 1113, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 126, + "path": "backend/app/modules/authorization/catalogue.py", + "qualified_symbol": "_index_service_actions", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 988 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "a7acb9f31de3a2b2e32c9e8419d1ea8546610f86dcd05f021c87ed340a134590", + "end_line": 604, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 217, + "path": "backend/app/modules/authorization/kernel.py", + "qualified_symbol": "AuthorizationService._prepare_prelocked", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 388 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "152e3da2ea2843ecfe2c0cfd30a5ba38fe87e4bc03308e1b3ee02351dda4b7c7", + "end_line": 1115, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 175, + "path": "backend/app/modules/authorization/kernel.py", + "qualified_symbol": "AuthorizationService._require_prelocked", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 941 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "8cb806aad12f5010d1e8875c4ba31eee43856a120e338d31a02d87e1ea30ad0c", + "end_line": 1600, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 125, + "path": "backend/app/modules/authorization/kernel.py", + "qualified_symbol": "AuthorizationService._stage_decision", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1476 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "cb697c0c6e970a22d59a7a7c93a8be3fdc7a79933c1e47f29466db64ae8e038c", + "end_line": 802, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 284, + "path": "backend/app/modules/authorization/prepared.py", + "qualified_symbol": "PreparedAuthorizationService._binding", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 519 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "b54b9e3027939480391b0bc1e8efd5db50c4a7a6d5aa22c78924e8a04246f1ff", + "end_line": 274, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 112, + "path": "backend/app/modules/authorization/project_role_service.py", + "qualified_symbol": "ProjectRoleGrantMutationService.complete_issue", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 163 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "f9e987c8d41ce4f4fcc7f2ba8a6a19406361f63dde8541af275824e461c35114", + "end_line": 1411, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 179, + "path": "backend/app/modules/authorization/router.py", + "qualified_symbol": "issue_project_role_grant", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1233 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "3bc6d1b229e30f03517079f80c7350e9ecb26a66162848019252756d0828918f", + "end_line": 615, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 118, + "path": "backend/app/modules/authorization/router.py", + "qualified_symbol": "provision_service_actor", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 498 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "3dd50feab3b1dd671bfce591f5ef337893807301a2248aed20c9e85865273e83", + "end_line": 1554, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 135, + "path": "backend/app/modules/authorization/router.py", + "qualified_symbol": "revoke_project_role_grant", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1420 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "2aa51a4d6baef4e2fdbce4bb8bba7a622270fec33614f6b8665a2f5a7240f36b", + "end_line": 317, + "hard_limit": 100, + "kind": "production_function", + "observed_lines": 146, + "path": "backend/app/modules/authorization/service.py", + "qualified_symbol": "AuthorityMutationService.complete", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 172 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "5c5364ad5a5387a6b56210901d9b07d4ab1f04d470056ae76bd1e5fd998548e5", + "end_line": 1594, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 1594, + "path": "backend/tests/test_actors.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "af8e6d93649b4715d9757f6ccf3aa6d24c260c494bfca076a3191d493cb514f9", + "end_line": 3293, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 3293, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "22a465ed4fce76ba8d37403942c0766783819bf0160fd9d2d2fa0614245ea3ec", + "end_line": 1209, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 1209, + "path": "backend/tests/test_artifact_internal_authorization.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "7717de94f55d517c03dbf8ca674cf4803f33d4d4412c5ea9b1fde3e3b57d199c", + "end_line": 1806, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 1806, + "path": "backend/tests/test_audit.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "16ebbaa1b66f08bf3bc395353f43505856aed1b937148f96b988377c028c4e39", + "end_line": 7424, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 7424, + "path": "backend/tests/test_auth.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "ebb3b0ede78653a373c17904bc29d40da7ed93e96e0416e0742079386ca7c7f5", + "end_line": 13421, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 13421, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "b3e5727e145afd27772ec35e656c6694a141eff683bef33d97aaf32a70d08b41", + "end_line": 1721, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 1721, + "path": "backend/tests/test_default_pre_submit_execution.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "8182eec08bb881fe6eed443e628bcf17710af112060ad3937a80c13040ccc5c1", + "end_line": 2526, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 2526, + "path": "backend/tests/test_guide_bindings.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "f1a9936e100e999fb6a2bad2b8306e1aefa5b594ad2759cd318182a35afefe87", + "end_line": 15700, + "hard_limit": 1200, + "kind": "test_file", + "observed_lines": 15700, + "path": "backend/tests/test_projects.py", + "qualified_symbol": null, + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "f7e72f9f5d32407bb995b955cfe7c15dfeb69e48a1877a9c861043663b4e35aa", + "end_line": 1594, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 122, + "path": "backend/tests/test_actors.py", + "qualified_symbol": "test_existing_actor_and_legacy_negative_states_fail_closed", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1473 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "0febc9420a1ee5b520e320ba511cda9f73c6469d1a63bf537a263ba5886f3ce3", + "end_line": 3251, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 123, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "test_checker_output_put_observation_terminal_outcomes", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 3129 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "fa7c9ad2acfa3b9810c43509966c61171d2234042ca32f7cb8e2d543f17cc252", + "end_line": 3028, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 187, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "test_checker_output_requires_exact_active_fixed_service_identity", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2842 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "e9f4d7d65b3794e1642401465a6f2c5a4ae179865c1a4113f97b8494ae0e1241", + "end_line": 2708, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 199, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "test_guide_admission_consumes_real_project_manager_prep_atomically", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2510 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "bf9f2a59d649d11892c2889665478cc06dc61f597692b403458c89a593538434", + "end_line": 2507, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 167, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "test_guide_admission_derives_three_scopes_without_provider_evidence", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2341 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "944c0487e49ed46976106404e83bcb8ad72ebb427eba10a34610a8de9697fb61", + "end_line": 1659, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 131, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "test_verification_claim_takeover_and_scanner_due_order_are_fenced", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1529 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "86e28f347925db968073550e8114c37650df51e65b65bb9cad9bdf2f78393de2", + "end_line": 1209, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 123, + "path": "backend/tests/test_artifact_internal_authorization.py", + "qualified_symbol": "test_post_provider_revocation_commits_denial_but_no_terminal_artifact_facts", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1087 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "54a4ac3c0018dbcee7aec6600f282471069261d6390872449f6cf1de9657ed2a", + "end_line": 788, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 153, + "path": "backend/tests/test_artifact_internal_authorization.py", + "qualified_symbol": "test_put_claim_and_terminal_injected_failures_roll_back_both_sides", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 636 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "4030b32ebcd38072e457a172184bb880348fa7b9039aeddac019ff972855a93a", + "end_line": 1083, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 181, + "path": "backend/tests/test_artifact_internal_authorization.py", + "qualified_symbol": "test_verification_claim_and_terminal_failures_roll_back_both_sides", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 903 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "c01dbfd93b66ed905f6f301b706e605e210fb467478b41d1b77fcd78f12986ac", + "end_line": 542, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 420, + "path": "backend/tests/test_artifact_operator_api.py", + "qualified_symbol": "test_real_http_operator_path_returns_redacted_lineage_and_recovery", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 123 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "02ed6da571dafee7f7b6f0051fb17620ad3058f92c4e283dc5cde24d098d4c31", + "end_line": 255, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 131, + "path": "backend/tests/test_audit.py", + "qualified_symbol": "test_action_aware_audit_input_enforces_mapping_and_action_availability", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 125 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "982f7c01fbc085454707e4b4491b9e79289af6203b7c2718635732d61ba8c9f1", + "end_line": 755, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 189, + "path": "backend/tests/test_audit.py", + "qualified_symbol": "test_authority_input_rejects_unbounded_or_inconsistent_evidence", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 567 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "36820d7d6c9db7dac729710d8d87ab233d4882febd049e862961449550b52e8e", + "end_line": 1316, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 210, + "path": "backend/tests/test_audit.py", + "qualified_symbol": "test_database_rejects_malformed_and_mutated_audit_rows", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1107 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "44b9858d132d384cd10c6b8b8fae096ade7c3fced6780cc2c142e59b4162d97d", + "end_line": 3542, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 280, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_actor_admin_reads_hold_caller_and_grant_locks_through_disclosure", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 3263 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "b0cb3554dd7f531648e4202302059b8416ca7ed2fdcc6529460f0acb092b82de", + "end_line": 7404, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 927, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_actor_identity_link_lifecycle_real_postgres_concurrency", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 6478 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "15c7147c11fa1802f43480b38a3678ac1a2586320ae2f0013622f1d63b2a3806", + "end_line": 5961, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 535, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_actor_identity_link_lifecycle_real_postgres_matrix", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 5427 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "49243ef3a0b0b4b24e5033b3e0ad75eee17c7fc2abd2014224a595ed8c6c9ade", + "end_line": 6475, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 491, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_actor_profile_lifecycle_real_postgres_concurrency", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 5985 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "eca9eeff229ee37c103188f9a31e278773a69e96f2fd354662723f840cf8f16a", + "end_line": 5424, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 482, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_actor_profile_lifecycle_real_postgres_matrix", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 4943 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "e1fd679295947e120c6da520bd138a78a8e133e44f8dde91010de29354e88f5b", + "end_line": 3260, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 485, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_admin_bootstrap_replay_and_cross_revoke_are_concurrency_safe", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2776 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "8ea087b906726103295a019967f8c38e42b55deb4e912fda3a41289fda23f35d", + "end_line": 4268, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 647, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_controlled_service_actor_provisioning_includes_project_setup_and_is_atomic", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 3622 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "d26732dd125f8c2062b5a4b45b59fbccd283ee04939a9da7be1576235524292f", + "end_line": 4679, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 409, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_service_actor_provisioning_failure_and_authority_races_are_atomic", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 4271 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "a7e36b433ed775ab82e7ff92c013aab3a1cee14fbbed6f4ddace351ab936e24c", + "end_line": 2660, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 1041, + "path": "backend/tests/test_auth.py", + "qualified_symbol": "test_signed_tokens_bootstrap_and_admin_grant_lifecycle", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1620 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "951b44cc07e36002118fe93b7974e8d65851b0e2c3cec89a3031cbe42b014e2d", + "end_line": 8521, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 140, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_actor_lifecycle_service_applies_success_and_guards_conflicts", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 8382 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "569084d6de89ff9eae71d526fc6c157aea7638f55ca93ad128b721fbda339be6", + "end_line": 9011, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 122, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_admin_resource_digest_alone_rejects_substituted_role_and_disposition", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 8890 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "90b8b670b4d277209617cc1aa794188b4fb3cd9d894b69d5dc3d0adfc885f6ed", + "end_line": 9239, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 132, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_admin_revoke_stages_complete_state_and_evidence", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 9108 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "78d9bf3df08e5633e9a75720b1e4bf5b7d7b24019bc392b4cb7496a9ac5e5e8e", + "end_line": 11073, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 122, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_authorization_locks_refresh_cached_actor_lifecycle_state", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 10952 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "aab80cce1a72714c4ec2e95b0596031e59a463459c69a4079627070f79edf349", + "end_line": 2252, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 327, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_closed_permission_and_action_catalogue_is_exact_and_non_executable", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1926 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "df1df6ec2ba6aa55445e21cfcf4e4e3ad9664c9504313484a495b6a6df1e9047", + "end_line": 3481, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 126, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_identity_link_lifecycle_route_preserves_outcome_transaction_contract", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 3356 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "05d1b020ecff0f9bc0a0567adc07f5b31a2f9dfb7828ae3ad34d4e1e7757797c", + "end_line": 8674, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 151, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_identity_link_lifecycle_service_applies_success_and_guards_conflicts", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 8524 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "c5d6d0d480ced964354915614f15202c0159bfbb281658da0d44186dffd17848", + "end_line": 7469, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 142, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_prepared_actor_authority_crossed_mutations_complete_in_both_orders", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 7328 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "ebb76e63671195aa4d806ac602bfe58cb22bf82d00c8a70ab186785f3acd7f9b", + "end_line": 7810, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 336, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_prepared_crosses_real_lifecycle_service_transactions", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 7475 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "9ea4fc0ddbab4c7262a43bc3f498ee1afea3318e9a0b6c93c87763f9f22aae9c", + "end_line": 7300, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 518, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_prepared_postgresql_failure_and_cancellation_are_atomic", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 6783 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "46e0b031394da6fee856e48615732a17ff8d2276d9cedfb0ecaa3a6879410adb", + "end_line": 4686, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 157, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_11c2_reads_require_exact_admin_context_and_role_allowlist", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 4530 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "e95d2a03e3829df89c43210a9de92efca31a25905c9648c3179d17b078386b17", + "end_line": 2653, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 397, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_mutation_resources_and_prepared_scopes_are_closed", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2257 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "105667302ed6e8f2fd16ea7e95d642e72e41514673e1152503536e0520f9c362", + "end_line": 10948, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 204, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_read_permissions_have_postgresql_role_scope_matrix", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 10745 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "756b7f99f9a743284934b4d85ce263617710d9b8119792ccb4526a1de04070a1", + "end_line": 12450, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 233, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_role_and_all_operation_mappings_commit_one_linked_pair", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 12218 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "2e1d7db74ddb955b90a0ee12e4fb72b651a0f85d2746c76e09079c05b0b85252", + "end_line": 13421, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 681, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_role_issue_postgresql_prep_binds_target_role_and_scope", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 12741 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "cf7f0f09ac93a917c74fd66c0048a289f0a437ca89757f834c70afca273588b3", + "end_line": 1409, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 214, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_role_mutation_routes_conceal_denials_and_preserve_self_guards_atomically", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1196 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "2f01493754052fa841ca40a5600f6fc68cfac966e719323a9a89e4b8c38a5744", + "end_line": 1672, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 245, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_project_role_mutation_routes_enforce_project_lifecycle_without_disclosure", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1428 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "05621e885ed2f88d0ba1a072c1f263fc923939f7ce755a514e9872112aa830a1", + "end_line": 11967, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 163, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "test_service_actor_replay_fails_closed_on_committed_state_drift", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 11805 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "132bfd3ca539369f5ba99718e6e14869a6f6b810f70c750f12df9adf03341949", + "end_line": 1135, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 700, + "path": "backend/tests/test_default_pre_submit_execution.py", + "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 436 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "3ee40b000ff508e0418aeb28a6dcb640dff4854436770343088e096c3f91c79a", + "end_line": 1575, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 135, + "path": "backend/tests/test_guide_bindings.py", + "qualified_symbol": "test_new_format_support_replaces_obsolete_policy_budget_without_replay", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 1441 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "9a0a149ec9d89468ec6a33e5b2bcaa825896a02f2aedf75f52aee3bb402ce5f7", + "end_line": 312, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 176, + "path": "backend/tests/test_guide_bindings.py", + "qualified_symbol": "test_sufficiency_material_uses_only_exact_current_extraction", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 137 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "14a28aa2e24af745acbfa5f2052873b893a42f4c27ad98325f193a9eff92dd08", + "end_line": 492, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 177, + "path": "backend/tests/test_guide_bindings.py", + "qualified_symbol": "test_verified_sufficiency_report_commits_exact_usage_provenance", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 316 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "26170dfbd986d2930d7f89357d59cdd17ae50b2ac1b936a3966e24225312e895", + "end_line": 666, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 132, + "path": "backend/tests/test_project_policy_mutations.py", + "qualified_symbol": "test_policy_router_dependencies_errors_and_transaction_outcomes", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 535 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "d964319e3c80e3caf0d12a5cb3bec366f99b9709b80a43fc36feb63adaec1f0f", + "end_line": 5832, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 133, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_corrected_submission_artifact_policy_resumes_post_submit_setup", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 5700 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "e594d392082b6a7b5f9c6f048361fd5d7d0495b72b84b313cba64f2041f104f4", + "end_line": 15338, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 121, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_guide_activation_and_active_guide_retrieval", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 15218 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "0c0725368231113bf21edfde138b0b55d64bc8a6f4382acc83e79c6d6e704fdf", + "end_line": 4428, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 143, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_guide_mutation_service_executes_all_three_authorized_happy_paths", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 4286 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "4ae7cc4865023adb90a531ebda235392d1168cd77f5d68b33543c0c38382bf61", + "end_line": 14757, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 261, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_post_submit_checker_policy_correction_preserves_audit_and_guides_rederivation", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 14497 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "b259f2053c3db0c26ebc7f0ab19b3c8acede85788ad97e4ea8af17dc4e9804dd", + "end_line": 6322, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 137, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_post_submit_derivation_unsupported_checker_gap_blocks_setup", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 6186 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "c18a567fb375d226c4edd636e133999e9f75ca454fec745e0a7ec81ee1aab0b0", + "end_line": 2517, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 161, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_project_role_grant_repository_filters_and_uses_strict_keyset", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 2357 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "102c97be36120910868ba02c0a6996767dc07430ed03585495e87b490ba7be12", + "end_line": 7347, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 197, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_submission_artifact_policy_agent_service_commits_and_replays_without_io", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 7151 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "754a750cfba33d5312c5f773086f01458497740023846016aa85a355375a5f14", + "end_line": 10048, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 143, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_submission_artifact_policy_authority_service_is_flush_only", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 9906 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "bc9b037b8fe6f3450a203b8f691d4253ea1d3c3e40ae48c88ef16dd011eefdeb", + "end_line": 10505, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 182, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_submission_artifact_policy_manual_lineage_loads_exact_locked_context", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 10324 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "9c74e06c2030e6fcad544df62f2950b8b1d62a797ce3b9328bbd3d1e02041b2a", + "end_line": 10321, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 271, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_submission_artifact_policy_manual_service_executes_authorized_create_and_update", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 10051 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "8a13e445413234d31c92e2ae133e2855633ce9d5b06ffba226c0ade58f51ee4d", + "end_line": 9903, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 153, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_submission_artifact_policy_replay_repository_idempotency_classifies_cross_action_states", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 9751 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "58eb4394fb393de909f8b8f7ec879d5dbca99e60939d4a60d6bf31bb6e844568", + "end_line": 8941, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 217, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_sufficiency_mutation_fail_closed_internal_guards", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 8725 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "414e9f8d8ff80ec5986db3d831635374e1592fab3fabdb152d3f92c6ec1081a1", + "end_line": 9174, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 231, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_sufficiency_mutation_manual_dispatch_commits_and_replays", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 8944 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "611ad24f47071fe9a9adc56bb91d68f53bd04be8fc54d97091043e394f577f15", + "end_line": 9531, + "hard_limit": 120, + "kind": "test_function", + "observed_lines": 355, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "test_sufficiency_mutation_services_commit_human_create_and_acknowledgement", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 9177 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "10292b6ed6f73baae351efbcb13102da0bd21eea2926587cef48e784baa95803", + "end_line": 376, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 104, + "path": "backend/tests/project_create_fixtures.py", + "qualified_symbol": "seed_authorized_project", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 273 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "0c7b21c804df09df7687641af563ea9808f7ce68fa29548b7c1c6f8b194e7d1b", + "end_line": 691, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 353, + "path": "backend/tests/test_artifact_admission.py", + "qualified_symbol": "_seed_checker_output_relationships", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 339 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "60c62ad8e998812943b4a61a2485e5454530abc8abf63fb45bf2401f86189b6d", + "end_line": 304, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 113, + "path": "backend/tests/test_artifact_recovery.py", + "qualified_symbol": "_exhausted_guide_job", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 192 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "efca18e30f3e8e9d7b16310a956b427262dc92bdf13165146b58523b25ea2687", + "end_line": 521, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 228, + "path": "backend/tests/test_audit.py", + "qualified_symbol": "_authority_event_matrix", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 294 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "1a0a9f3e2be6965e29f76aa74272ccfa2fe4b99e4e0c3bde5ec8ba2c374b369b", + "end_line": 11281, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 169, + "path": "backend/tests/test_authorization.py", + "qualified_symbol": "_operation_success", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 11113 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "19100482ea5552739e4ce0561c771da27b5ec4e99f108d0e7cbd24aae94dbaae", + "end_line": 959, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 221, + "path": "backend/tests/test_guide_bindings.py", + "qualified_symbol": "_seed_binding_lineage", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 739 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "e55632d4c6056e04f99d00104ca58677f354e604bbd279e9d0c14adc59bff900", + "end_line": 520, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 124, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "_PolicyReadRepository.__init__", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 397 + }, + { + "capability": "unassigned_legacy_auth", + "content_sha256": "addf8488c7fff4ec2ee4c2d70e65d208642f20169f13fe1ba76db6f6f725a0c7", + "end_line": 853, + "hard_limit": 100, + "kind": "test_helper", + "observed_lines": 118, + "path": "backend/tests/test_projects.py", + "qualified_symbol": "_activation_ready_bundle", + "removal_chunk": "WS-AUTH-003-CLOSE", + "start_line": 736 + } + ], + "exceptions": [], + "policy_sha256": "a6d2f60bdc8844bc3ee84b87b22be1b05bac55b39bfc1d31e3e70a71d61ebcb5", + "schema": "workstream.auth-test-structure-debt.v1" +} diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 9a29a129a..9a76c7c68 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -1123,7 +1123,8 @@ async def publish_ready() -> str: assert blocked.failure_audit["event_type"] == "pre_submission_check_failed" assert blocked.failure_audit["failed_count"] >= 1 assert "task.toml" not in repr(blocked.failure_audit) - assert (evidence_count, result_count) == (5, 5 * len(request.effective_plan.entries)) + assert evidence_count == 5 + assert result_count == 5 * len(request.effective_plan.entries) assert after == { **before, "artifact_contents": before["artifact_contents"] + 1, From af96f8ae697eaf9fc6de37d417ccbc7690cb70f9 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:10:30 +0100 Subject: [PATCH 20/27] test(artifacts): retain evidence fixture ceiling --- .../TEST_STRUCTURE_DEBT.json | 12 ++++++------ backend/tests/test_default_pre_submit_execution.py | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json index 2b956205f..be14dfaa6 100644 --- a/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json +++ b/.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/TEST_STRUCTURE_DEBT.json @@ -230,11 +230,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "b3e5727e145afd27772ec35e656c6694a141eff683bef33d97aaf32a70d08b41", - "end_line": 1721, + "content_sha256": "ce68513010bfa3af071aa5b73289a1d793a2c20593e7099726787b03e058b5a7", + "end_line": 1720, "hard_limit": 1200, "kind": "test_file", - "observed_lines": 1721, + "observed_lines": 1720, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": null, "removal_chunk": "WS-AUTH-003-CLOSE", @@ -746,11 +746,11 @@ }, { "capability": "unassigned_legacy_auth", - "content_sha256": "132bfd3ca539369f5ba99718e6e14869a6f6b810f70c750f12df9adf03341949", - "end_line": 1135, + "content_sha256": "be162594febdd56816d1a6a947b3d120ca35538f3fae5864ff0cac7a168493c3", + "end_line": 1134, "hard_limit": 120, "kind": "test_function", - "observed_lines": 700, + "observed_lines": 699, "path": "backend/tests/test_default_pre_submit_execution.py", "qualified_symbol": "test_effective_evidence_workflow_persists_once_and_replays_exactly", "removal_chunk": "WS-AUTH-003-CLOSE", diff --git a/backend/tests/test_default_pre_submit_execution.py b/backend/tests/test_default_pre_submit_execution.py index 9a76c7c68..7d4be09be 100644 --- a/backend/tests/test_default_pre_submit_execution.py +++ b/backend/tests/test_default_pre_submit_execution.py @@ -1123,16 +1123,15 @@ async def publish_ready() -> str: assert blocked.failure_audit["event_type"] == "pre_submission_check_failed" assert blocked.failure_audit["failed_count"] >= 1 assert "task.toml" not in repr(blocked.failure_audit) - assert evidence_count == 5 - assert result_count == 5 * len(request.effective_plan.entries) + assert (evidence_count, result_count) == (5, 5 * len(request.effective_plan.entries)) assert after == { **before, "artifact_contents": before["artifact_contents"] + 1, "artifact_replicas": before["artifact_replicas"] + 1, - "artifact_put_attempts": before["artifact_put_attempts"] + 1, - "submission_bundle_admissions": before["submission_bundle_admissions"] + 1, - "pre_submit_evidence_sets": evidence_count, "pre_submit_evidence_results": result_count, # noqa: E501 - } + "artifact_put_attempts": before["artifact_put_attempts"] + 1, + "submission_bundle_admissions": before["submission_bundle_admissions"] + 1, + "pre_submit_evidence_sets": evidence_count, "pre_submit_evidence_results": result_count, # noqa: E501 + } @pytest.mark.asyncio From a3c371cd20e4c05d44a847a2bc470c4cfc136c8a Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:34:56 +0100 Subject: [PATCH 21/27] test(artifacts): cover preparation adapter defaults --- .../tests/test_submission_bundle_admission.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 291868899..a5a70c890 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -13,7 +13,9 @@ from fastapi import HTTPException from starlette.requests import Request +import app.adapters.artifacts as artifact_adapters import app.modules.artifacts.submission_admission as submission_admission_module +from app.core.config import Settings from app.modules.artifacts.pre_submit_evidence import ( PreSubmitEvidenceConflict, PreSubmitEvidenceService, @@ -90,6 +92,37 @@ def _sha(character: str) -> str: return "sha256:" + character * 64 +@pytest.mark.asyncio +async def test_artifact_adapter_preserves_public_actor_facts() -> None: + actor = _actor() + + assert await artifact_adapters.get_submission_bundle_preparation_actor(actor) is actor + + +@pytest.mark.asyncio +async def test_artifact_adapter_default_preparation_authority_denies() -> None: + authority = artifact_adapters.get_submission_bundle_preparation_authorization() + + with pytest.raises(ArtifactAuthorityDeniedError): + await authority.preflight(request=_preparation_request(byte_source=object())) + + +@pytest.mark.asyncio +async def test_artifact_adapter_stale_scratch_cleanup_closes_manager( + monkeypatch: pytest.MonkeyPatch, +) -> None: + manager = SimpleNamespace(cleanup_stale=AsyncMock(return_value=3), close=Mock()) + monkeypatch.setattr( + artifact_adapters, + "create_artifact_scratch_manager", + lambda _settings: manager, + ) + + assert await artifact_adapters.cleanup_stale_artifact_scratch(Settings()) == 3 + manager.cleanup_stale.assert_awaited_once_with() + manager.close.assert_called_once_with() + + def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next( From 8f4c891d49faa9aff664a05ad5363f1ebbe15753 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 05:47:36 +0100 Subject: [PATCH 22/27] test(artifacts): cover preparation limit mapping --- .../tests/test_submission_bundle_admission.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index a5a70c890..36bb43a59 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -123,6 +123,23 @@ async def test_artifact_adapter_stale_scratch_cleanup_closes_manager( manager.close.assert_called_once_with() +def test_artifact_adapter_maps_preparation_limits_from_settings() -> None: + settings = Settings() + + preparation = artifact_adapters.artifact_preparation_limits(settings) + archive = artifact_adapters.submission_archive_limits(settings) + + assert preparation.maximum_source_bytes == settings.artifact_maximum_bytes + assert preparation.maximum_files == settings.artifact_scratch_maximum_files + assert preparation.stream_buffer_bytes == settings.artifact_stream_buffer_bytes + assert archive.maximum_entries == settings.artifact_submission_zip_maximum_entries + assert archive.maximum_entry_bytes == settings.artifact_submission_zip_maximum_entry_bytes + assert ( + archive.maximum_compression_ratio + == settings.artifact_submission_zip_maximum_compression_ratio + ) + + def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next( From cd3b186639021f8a48254479e02b7105b7df4911 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 06:01:20 +0100 Subject: [PATCH 23/27] test(artifacts): cover adapter safety failures --- .../tests/test_submission_bundle_admission.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 36bb43a59..8c17e45b2 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -33,6 +33,10 @@ ArtifactAuthorityDeniedError, SubmissionBundleArtifactAdmissionRequest, ) +from app.interfaces.artifacts import ( + ArtifactConfigurationError, + ArtifactProviderLiveProofRequiredError, +) from app.modules.artifacts.submission_authorization import ( DenySubmissionBundlePreparedAuthorization, DenySubmissionBundlePreparationAuthorization, @@ -140,6 +144,23 @@ def test_artifact_adapter_maps_preparation_limits_from_settings() -> None: ) +def test_artifact_adapter_rejects_unproven_aws_runtime() -> None: + settings = SimpleNamespace( + artifact_store_backend="s3_compatible", artifact_s3_provider_profile="aws_s3" + ) + + with pytest.raises(ArtifactProviderLiveProofRequiredError): + artifact_adapters.require_artifact_runtime_eligible(settings) # type: ignore[arg-type] + + +def test_artifact_adapter_requires_configured_scratch_root() -> None: + settings = Settings() + settings.artifact_scratch_root = None + + with pytest.raises(ArtifactConfigurationError): + artifact_adapters.create_artifact_scratch_manager(settings) + + def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next( From c04935b8afde7cd13fc23188a73415040c7e7144 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 06:11:59 +0100 Subject: [PATCH 24/27] test(artifacts): cover local adapter composition --- .../tests/test_submission_bundle_admission.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 8c17e45b2..885c2d730 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -61,6 +61,7 @@ from app.modules.authorization.api import ActorIdentityFacts, ActorKind from app.modules.authorization.prepared import PreparedAuthorizationHandle from tests.artifact_store_helpers import artifact_byte_stream, artifact_preparation_limits +from tests.artifact_store_helpers import artifact_admission_limit_settings from app.main import create_app from app.api.routes.artifact_submissions import prepare_submission_bundle, router as submission_router from app.modules.artifacts.submission_admission import validate_submission_packet_headers @@ -161,6 +162,27 @@ def test_artifact_adapter_requires_configured_scratch_root() -> None: artifact_adapters.create_artifact_scratch_manager(settings) +def test_artifact_adapter_builds_configured_local_bootstrap(tmp_path) -> None: + root = tmp_path / "objects" + root.mkdir(mode=0o700) + settings = Settings( + **artifact_admission_limit_settings(), + environment="test", + artifact_store_backend="local", + artifact_local_root=root, + artifact_scratch_root=tmp_path / "scratch", + artifact_scratch_minimum_free_bytes=0, + ) + + bootstrap = artifact_adapters.create_artifact_store_bootstrap(settings) + + try: + assert bootstrap.identity.provider_key == "local" + assert bootstrap.namespace_identity.provider_profile == "local-v2" + finally: + bootstrap.close() + + def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next( From a9323227a789156563953b2d1be3725336b27190 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 06:25:59 +0100 Subject: [PATCH 25/27] fix(checkers): reject malformed result metadata --- .../app/modules/checkers/pre_submit_execution.py | 16 +++++++++++++--- .../tests/test_effective_pre_submit_execution.py | 2 ++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/app/modules/checkers/pre_submit_execution.py b/backend/app/modules/checkers/pre_submit_execution.py index 656750cc7..f75d8e265 100644 --- a/backend/app/modules/checkers/pre_submit_execution.py +++ b/backend/app/modules/checkers/pre_submit_execution.py @@ -586,16 +586,26 @@ def validate_pre_submission_execution_result( ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") for plan_entry, result in zip(plan.entries, execution.entries, strict=True): - metadata_keys = tuple(key for key, _ in result.metadata) + metadata = result.metadata + if type(metadata) is not tuple or any( + type(item) is not tuple + or len(item) != 2 + or type(item[0]) is not str + or type(item[1]) is not int + for item in metadata + ): + raise PreSubmissionInfrastructureUnavailable( + "pre_submission_result_context_invalid" + ) + metadata_keys = tuple(item[0] for item in metadata) if ( type(result.status) is not PreSubmissionResultStatus or result.schema_version != plan_entry.result_schema or len(metadata_keys) != len(set(metadata_keys)) or any( key not in PRE_SUBMISSION_RESULT_METADATA_KEYS - or type(value) is not int or value < 0 - for key, value in result.metadata + for key, value in metadata ) ): raise PreSubmissionInfrastructureUnavailable("pre_submission_result_context_invalid") diff --git a/backend/tests/test_effective_pre_submit_execution.py b/backend/tests/test_effective_pre_submit_execution.py index 88ebd0bac..1ab53c78a 100644 --- a/backend/tests/test_effective_pre_submit_execution.py +++ b/backend/tests/test_effective_pre_submit_execution.py @@ -571,6 +571,8 @@ class _ForgedStatus: validate_pre_submission_execution_result(plan, replace(forged, eligible=1)) # type: ignore[arg-type] for metadata in ( + (("finding_count",),), + ((["finding_count"], 1),), (("unknown_count", 1),), (("finding_count", 1), ("finding_count", 2)), (("finding_count", "1"),), From 0d982f43d3d88fd25f926e1d65f43db4d374be5f Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 06:32:10 +0100 Subject: [PATCH 26/27] test(artifacts): cover unavailable authority contract --- backend/tests/test_submission_bundle_admission.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 885c2d730..4f28f3c93 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -107,9 +107,17 @@ async def test_artifact_adapter_preserves_public_actor_facts() -> None: @pytest.mark.asyncio async def test_artifact_adapter_default_preparation_authority_denies() -> None: authority = artifact_adapters.get_submission_bundle_preparation_authorization() + request = _preparation_request(byte_source=object()) with pytest.raises(ArtifactAuthorityDeniedError): - await authority.preflight(request=_preparation_request(byte_source=object())) + await authority.preflight(request=request) + with pytest.raises(ArtifactAuthorityDeniedError): + await authority.revalidate(request=request) + with pytest.raises(ArtifactAuthorityDeniedError): + authority.transaction() + with pytest.raises(ArtifactAuthorityDeniedError): + await authority.prepare_final(request=request) + authority.close() @pytest.mark.asyncio From bfe0ea347b18e997cfea3c631fbe05317bd2b99d Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Wed, 12 Aug 2026 06:49:56 +0100 Subject: [PATCH 27/27] test(artifacts): prove preparation command composition --- .../tests/test_submission_bundle_admission.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/backend/tests/test_submission_bundle_admission.py b/backend/tests/test_submission_bundle_admission.py index 4f28f3c93..f88f2b023 100644 --- a/backend/tests/test_submission_bundle_admission.py +++ b/backend/tests/test_submission_bundle_admission.py @@ -191,6 +191,58 @@ def test_artifact_adapter_builds_configured_local_bootstrap(tmp_path) -> None: bootstrap.close() +def test_artifact_adapter_composes_submission_command_from_owner_ports( + monkeypatch: pytest.MonkeyPatch, +) -> None: + request_id = uuid4() + correlation_id = uuid4() + request = Request( + { + "type": "http", + "method": "POST", + "path": "/hidden", + "headers": [], + "state": { + "request_id": str(request_id), + "correlation_id": str(correlation_id), + }, + } + ) + request.scope["app"] = SimpleNamespace( + state=SimpleNamespace( + settings=Settings(), + pre_submission_checker_catalogue=object(), + ) + ) + session = object() + internal_authority = object() + authority = artifact_adapters.get_submission_bundle_preparation_authorization() + task_contexts = object() + project_contexts = object() + monkeypatch.setattr( + artifact_adapters, "task_submission_context_port", Mock(return_value=task_contexts) + ) + monkeypatch.setattr( + artifact_adapters, + "project_locked_policy_context_port", + Mock(return_value=project_contexts), + ) + + command = artifact_adapters.get_submission_bundle_preparation_command( + request, + session, # type: ignore[arg-type] + internal_authority, # type: ignore[arg-type] + authority, + ) + + assert command._session is session + assert command._authority is authority + assert command._task_contexts is task_contexts + assert command._project_contexts is project_contexts + artifact_adapters.task_submission_context_port.assert_called_once_with(session) + artifact_adapters.project_locked_policy_context_port.assert_called_once_with(session) + + def test_submission_bundle_preparation_route_is_hidden() -> None: app = create_app() route = next(