From a05aa0714e1c02780afba8ea55a9fec1001b4a25 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Mon, 10 Aug 2026 15:45:05 +0100 Subject: [PATCH 1/4] feat(arch): enforce modular monolith boundaries --- .agent-loop/CURRENT_STATE.md | 2 +- .../CHUNK_MAP.md | 2 +- .../DECISIONS.md | 9 + .../STATUS.md | 9 +- .../WS-ARCH-001-01-boundary-foundation.md | 30 +- .../AUTH_HANDOFF.md | 18 +- .../policies/architecture-boundaries.md | 67 +- .../policies/repository-engineering-policy.md | 5 + .ci/behavior-ownership/partition.v1.json | 6 +- .../shared/module-boundary-validator.json | 56 ++ .../private-edge-debt.v1.json | 863 ++++++++++++++++++ .ci/module-boundaries/registry.v1.json | 27 + .github/workflows/backend.yml | 10 +- backend/scripts/behavior_ownership.py | 9 +- backend/scripts/module_boundaries.py | 407 +++++++++ .../architecture/test_module_boundaries.py | 295 ++++++ backend/tests/test_behavior_ownership.py | 33 + docs/architecture_lockdown.md | 6 + docs/operations_backend_testing.md | 14 + 19 files changed, 1827 insertions(+), 41 deletions(-) create mode 100644 .ci/behavior-ownership/shared/module-boundary-validator.json create mode 100644 .ci/module-boundaries/private-edge-debt.v1.json create mode 100644 .ci/module-boundaries/registry.v1.json create mode 100644 backend/scripts/module_boundaries.py create mode 100644 backend/tests/architecture/test_module_boundaries.py diff --git a/.agent-loop/CURRENT_STATE.md b/.agent-loop/CURRENT_STATE.md index c043eb078..b2123847f 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) | Planning proposed; no runtime behavior changed | Review the canonical twelve-module map and incremental no-new-private-edge recovery before starting the boundary foundation | +| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Canonical twelve-module registry, exact protected debt ledgers, public-API checks, and CI no-growth enforcement are established; no product behavior changed | Split WS-ARCH-001-02 into exact AUTH, ART, TASK, and composition-root submission capability contracts before implementation | | [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; foundations and project-policy authority are merged through `12F3` | Remaining activation chunks depend on exact merged POL, ART, and REV behavior; refresh each executable contract on current `main` | | [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 9d88f5683..731590d0a 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 @@ -2,7 +2,7 @@ | Chunk | Goal | Risk | State | |---|---|---:|---| -| `WS-ARCH-001-01` | Canonical module map, exact general edge ledger, public-API validator, and CI foundation | L1 | Proposed | +| `WS-ARCH-001-01` | Canonical module map, exact general edge ledger, public-API validator, and CI foundation | L1 | Complete | | `WS-ARCH-001-02` | AUTH/ART/TASK submission preparation and ready-admission consumption public capabilities | L1 | Proposed after 01 and XINT-05A reconciliation | | `WS-ARCH-001-03` | PROJECT/TASK guide, locked-context, task and assignment capability repairs | L1 | Non-executable placeholder; requires split contract | | `WS-ARCH-001-04` | ART/CHECKER materialization, run and result capability repairs | L1 | Non-executable placeholder; requires split contract | diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/DECISIONS.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/DECISIONS.md index 6ba55531b..05bfc2b44 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/DECISIONS.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/DECISIONS.md @@ -39,3 +39,12 @@ sequence. AUTH edges are never copied into a WS-ARCH ledger. The general validator loads WS-AUTH-003's canonical ledger through its existing parser and fails if the AUTH-specific and general views diverge. + +## D7: Application wiring is classified, not exempted + +API delivery, adapters, durable workers, and legacy shared interfaces are all +scanned. Their current private product imports are protected debt and may not +grow. `backend/app/interfaces/**` is not a permanent public contract surface. +Database metadata discovery is a distinct infrastructure concern: only the +exact registered discovery path may import module model declarations, and that +exception grants no runtime command, repository, service, or authority access. 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 17c216f82..f078b5779 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 @@ -1,6 +1,6 @@ # Status: WS-ARCH-001 Modular Monolith Boundaries -- Initiative state: planning proposed +- Initiative state: active; boundary foundation complete - Runtime behavior changed: no - Canonical target: nine business modules and three supporting modules - Recovery model: freeze exact debt, prohibit growth, repair touched @@ -8,5 +8,8 @@ - Existing dependency: WS-AUTH-003 boundary foundation merged - Reconciled merged overlap: POL-03A PR #307 is the first public AUTH-capability proof and owns migration `0062_guide_compilation` -- First implementation chunk: WS-ARCH-001-01, only after human approval and - plan review +- Completed foundation: WS-ARCH-001-01 installs the canonical registry, exact + general debt ledger, AUTH-ledger composition, protected-base validator, + public-API checks, behavior-ownership record, and hosted CI enforcement. +- Next boundary: split WS-ARCH-001-02 into exact AUTH, ART, TASK, and + composition-root contracts after this foundation is human-merged. diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md index 9598668f5..4dba0122a 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md @@ -1,6 +1,6 @@ # Chunk Contract: WS-ARCH-001-01 Boundary Foundation -Status: Proposed. Risk: L1. +Status: Complete. Risk: L1. ## Goal @@ -14,9 +14,14 @@ enforcement that composes with WS-AUTH-003. ```text .agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/** .agent-loop/CURRENT_STATE.md +.agent-loop/initiatives/WS-ART-001-immutable-artifact-storage/AUTH_HANDOFF.md .agent-loop/policies/architecture-boundaries.md .agent-loop/policies/repository-engineering-policy.md .ci/module-boundaries/** +.ci/behavior-ownership/partition.v1.json +.ci/behavior-ownership/shared/module-boundary-validator.json +backend/scripts/behavior_ownership.py +backend/tests/test_behavior_ownership.py backend/scripts/module_boundaries.py backend/tests/architecture/test_module_boundaries.py .github/workflows/backend.yml @@ -37,6 +42,23 @@ docs/operations_backend_testing.md ## Acceptance criteria - Registry names exactly nine business and three supporting modules. +- Registry classifies application-level paths explicitly: + - `backend/app/main.py` is the exact application composition root and its + existing private implementation imports remain frozen debt; + - `backend/app/api/**` is delivery/composition entry code and may consume + module public APIs; every existing private import remains frozen debt; + - `backend/app/adapters/**` implements typed infrastructure capabilities and + may be wired by composition, but every existing product-private import is + frozen debt; + - `backend/app/workers/**` is durable delivery/composition entry code and may + consume public APIs; every existing private import is frozen debt; + - `backend/app/interfaces/**` is legacy shared-contract debt, not a permanent + public-contract namespace, and all product-private imports are frozen; + - database metadata discovery is limited to the exact registered discovery + path and model-only imports; it grants no runtime capability authority. +- The validator scans all application paths, not only `backend/app/modules/**`. + New private edges from API, adapters, workers, interfaces, composition, or + metadata discovery fail closed. - Repository engineering policy uses the same canonical ownership map and contains no nonexistent `modules/submissions` boundary. - Ledger records exact source file, target module, imported private path, and @@ -47,6 +69,10 @@ docs/operations_backend_testing.md - Validator rejects a new edge, expanded existing edge, public API private leak, unknown module, and cyclic public dependency. - Existing exact debt passes only through protected ledger reconciliation. +- The first ledger includes ART-to-AUTH/TASK/CHECKER debt through the combined + general and AUTH views, TASK route and pre-submit-context debt, legacy shared + interfaces, and worker/adapter private assembly. AUTH-affecting edges remain + exclusively in the canonical AUTH ledger. - AUTH-003 is the sole canonical source for every inbound/outbound AUTH edge. The general validator loads that ledger through the existing AUTH boundary parser and combines it by reference; the general ledger contains no copied @@ -57,7 +83,7 @@ docs/operations_backend_testing.md ## Verification commands ```text -PYTHONPATH=backend backend/.venv/bin/python backend/scripts/module_boundaries.py validate +PYTHONPATH=backend backend/.venv/bin/python backend/scripts/module_boundaries.py validate --protected-base origin/main PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 PYTHONPATH=backend backend/.venv/bin/python -m pytest -q -p pytest_asyncio.plugin backend/tests/architecture/test_module_boundaries.py backend/tests/architecture/test_authorization_boundary.py PYTHONPATH=backend backend/.venv/bin/ruff check backend/scripts/module_boundaries.py backend/tests/architecture/test_module_boundaries.py git diff --check diff --git a/.agent-loop/initiatives/WS-ART-001-immutable-artifact-storage/AUTH_HANDOFF.md b/.agent-loop/initiatives/WS-ART-001-immutable-artifact-storage/AUTH_HANDOFF.md index f537e363f..5f1e83cd8 100644 --- a/.agent-loop/initiatives/WS-ART-001-immutable-artifact-storage/AUTH_HANDOFF.md +++ b/.agent-loop/initiatives/WS-ART-001-immutable-artifact-storage/AUTH_HANDOFF.md @@ -99,13 +99,17 @@ immutable historical records and the deterministic deletion proof. They are not an active design, grant, route, compatibility alias, or permission to implement a second intake path. -ART-04A1 through 04C2, with 04B split into 04B1-04B3, implement one hidden -continuous surface and publish -its exact route/resource/guard manifest. After 04C, a separate reviewed AUTH -activation contract may integrate the evaluator and change only -`artifact.submission_bundle.prepare` to active. ART-05 cannot start until that -activation merges. Before XINT-05A, XINT-06A must separately activate the fixed -pre-submit materializer required by the locked-guide checker boundary. +ART-04A1 through 04C2, with 04B split into 04B1-04B3, implemented one hidden +continuous surface and published its exact route/resource/guard manifest. That +history does not authorize immediate activation. Contributor preparation stays +planned and unavailable until `WS-ARCH-001-02` splits and establishes the exact +AUTH preparation capability, ART admission/binding ports, TASK-owned +Submission command, and composition-owned transaction wiring. Only the +replacement reviewed cross-module activation contract may then change +`artifact.submission_bundle.prepare` to active. ART-05 remains non-executable +until that sequence merges. XINT-06A separately activated the fixed pre-submit +materializer required by the locked-guide checker boundary; it does not grant +contributor preparation authority. The preparation surface authorizes before scratch intake, but the initial decision cannot authorize the later durable mutation. Immediately before diff --git a/.agent-loop/policies/architecture-boundaries.md b/.agent-loop/policies/architecture-boundaries.md index 37c252a64..add10ae52 100644 --- a/.agent-loop/policies/architecture-boundaries.md +++ b/.agent-loop/policies/architecture-boundaries.md @@ -1,29 +1,48 @@ -# Architecture Boundaries +# Modular Monolith Architecture Boundaries -Workstream is a modular monolith. Boundaries exist so product rules remain -auditable and later adapters can be added without rewriting the core loop. +Workstream has nine business modules: `actors`, `authorization`, `projects`, +`tasks`, `artifacts`, `checkers`, `reviews`, `contributions`, and +`compensation`. It has three supporting modules: `audit`, `outbox`, and +`api_controls`. The machine-readable canonical registry is +`.ci/module-boundaries/registry.v1.json`. -## Non-Negotiable Boundaries +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. -- Routers translate HTTP to service calls; they do not own business rules. -- Services own lifecycle rules, permission checks, and domain decisions. -- Repositories own persistence queries and do not make policy decisions. -- Adapters isolate external systems such as Flow auth and future object storage. -- Workstream product contracts remain distinct: project guide, submission - artifact policy, checker policy, review policy, revision policy, and - contribution policy are not engineering-loop policy files. -- `.agent-loop/policies/repository-engineering-policy.md` governs this - repository's engineering loop; it does not replace or rename the product - project guide. -- Pre-submit checks and post-submit/internal checker runs remain separate phases. -- Workstream engineering loop artifacts under `.agent-loop/` do not define - Workstream product runtime behavior. +Application-level paths are explicit rather than invisible exceptions: -## Review Questions +- `backend/app/main.py` is the application composition root; its current + product-private imports are frozen debt while future wiring consumes typed + 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; +- `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. -- Did this mix engineering process state with product runtime state? -- Did this bypass service-layer lifecycle rules? -- Did this hide a policy decision inside persistence, tests, or UI copy? -- Did this create vague naming that could confuse operators, contributors, - reviewers, or engineering reviewers? -- Did this preserve locked v0.1 scope? +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 +`backend/app/**` runtime source. After bootstrap, the current ledger must be a +subset of the protected-base ledger, so code and ledger cannot grow together. + +The JSON private-edge ledger is an exact, temporary recovery inventory—not an +allowlist. Every entry binds a source file, target module, imported private +path, and repair owner. New or expanded edges fail CI. An unresolved ordinary +edge uses `owner-unresolved`; an authorization-affecting edge without ownership +requires `security-triage-required` and blocks the touched capability. + +AUTH private edges are never copied into the general ledger. WS-AUTH-003 is +their sole canonical source; the general validator directly loads and verifies +that ledger through the existing AUTH parser and fails closed on disagreement. +Recovery is complete only when both protected debt ledgers are empty while the +validators remain enabled. diff --git a/.agent-loop/policies/repository-engineering-policy.md b/.agent-loop/policies/repository-engineering-policy.md index a281daf47..91ced4d36 100644 --- a/.agent-loop/policies/repository-engineering-policy.md +++ b/.agent-loop/policies/repository-engineering-policy.md @@ -54,6 +54,11 @@ capabilities, and Protocol ports—not ORM models, repositories, sessions, or concrete services. The application composition root alone wires concrete cross-module implementations. +The canonical registry and temporary exact private-edge recovery rules live in +`architecture-boundaries.md`. CI rejects unknown modules, new or expanded +private edges, public API private leaks, cyclic public dependencies, and any +divergence from the sole WS-AUTH-003 AUTH ledger. + ## Dependency Policy - Production dependencies require explicit human approval. diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index adba0afe3..1b2729349 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -728,6 +728,10 @@ "group": "shared", "target": "backend/scripts/merge_test_lane_evidence.py" }, + { + "group": "shared", + "target": "backend/scripts/module_boundaries.py" + }, { "group": "shared", "target": "backend/scripts/mutation_policy.py" @@ -757,7 +761,7 @@ "target": "backend/scripts/week2_api_e2e.py" } ], - "authority_digest": "225faaad98e3c22d6306b240aa933e5a58e879450d0f9ea67497ab698e5ecda7", + "authority_digest": "a5eda4a48b564b685080c6a387a3563c638b0e46bda0ac1b5b06d28f2f4b13ea", "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" } diff --git a/.ci/behavior-ownership/shared/module-boundary-validator.json b/.ci/behavior-ownership/shared/module-boundary-validator.json new file mode 100644 index 000000000..649f3c949 --- /dev/null +++ b/.ci/behavior-ownership/shared/module-boundary-validator.json @@ -0,0 +1,56 @@ +{ + "behavior_id": "shared.module_boundary.validator", + "boundaries": [], + "callables": [ + "scripts.module_boundaries.Registry.names", + "scripts.module_boundaries._git_changed_paths", + "scripts.module_boundaries._git_document", + "scripts.module_boundaries._is_public_target", + "scripts.module_boundaries._load_json", + "scripts.module_boundaries._metadata_discovery_import", + "scripts.module_boundaries._module_from_target", + "scripts.module_boundaries._parse_ledger", + "scripts.module_boundaries._repair_owner", + "scripts.module_boundaries._source_module", + "scripts.module_boundaries._validate_acyclic", + "scripts.module_boundaries._validate_acyclic.visit", + "scripts.module_boundaries._validate_public_apis", + "scripts.module_boundaries.exact_source_imports", + "scripts.module_boundaries.inventory", + "scripts.module_boundaries.load_ledger", + "scripts.module_boundaries.load_registry", + "scripts.module_boundaries.main", + "scripts.module_boundaries.scan", + "scripts.module_boundaries.validate", + "scripts.module_boundaries.validate_protected_base" + ], + "group": "shared", + "outcomes": [ + "return", + "mapped_error" + ], + "reviewed_by": [ + "WS-ARCH-001-01 required reviewers" + ], + "schema": "workstream.behavior-ownership.v1", + "status": "reviewed", + "target": "backend/scripts/module_boundaries.py", + "tests": [ + "backend/tests/architecture/test_module_boundaries.py::test_repository_matches_the_protected_module_boundary", + "backend/tests/architecture/test_module_boundaries.py::test_registry_names_exactly_nine_business_and_three_supporting_modules", + "backend/tests/architecture/test_module_boundaries.py::test_new_private_edge_fails_exact_ledger_comparison", + "backend/tests/architecture/test_module_boundaries.py::test_protected_base_rejects_new_or_expanded_debt", + "backend/tests/architecture/test_module_boundaries.py::test_bootstrap_rejects_runtime_source_changes", + "backend/tests/architecture/test_module_boundaries.py::test_bootstrap_allows_enforcement_only_install", + "backend/tests/architecture/test_module_boundaries.py::test_unknown_module_fails_closed", + "backend/tests/architecture/test_module_boundaries.py::test_auth_view_contains_only_cross_boundary_private_edges", + "backend/tests/architecture/test_module_boundaries.py::test_public_api_private_reexport_fails_closed", + "backend/tests/architecture/test_module_boundaries.py::test_cyclic_public_dependencies_fail_closed", + "backend/tests/architecture/test_module_boundaries.py::test_general_ledger_rejects_copied_authorization_edges", + "backend/tests/architecture/test_module_boundaries.py::test_general_ledger_allows_non_auth_security_triage_owner", + "backend/tests/architecture/test_module_boundaries.py::test_initial_ledgers_capture_high_risk_application_edges", + "backend/tests/architecture/test_module_boundaries.py::test_metadata_discovery_exception_is_exact_and_model_only", + "backend/tests/architecture/test_module_boundaries.py::test_auth_ledger_and_general_view_divergence_fails_closed", + "backend/tests/architecture/test_module_boundaries.py::test_dynamic_and_wildcard_imports_fail_repository_wide" + ] +} diff --git a/.ci/module-boundaries/private-edge-debt.v1.json b/.ci/module-boundaries/private-edge-debt.v1.json new file mode 100644 index 000000000..ecbd2e5bf --- /dev/null +++ b/.ci/module-boundaries/private-edge-debt.v1.json @@ -0,0 +1,863 @@ +{ + "schema_version": 1, + "edges": [ + { + "source_file": "backend/app/adapters/artifacts/__init__.py", + "target_module": "actors", + "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", + "imported_private_path": "app.modules.projects.guide_setup_continuation", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/adapters/artifacts/internal_workers.py", + "target_module": "projects", + "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", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/api/deps/auth.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/deps/auth.py", + "target_module": "api_controls", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/api/deps/authorization.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/deps/authorization.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/deps/authorization.py", + "target_module": "api_controls", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/api/deps/rate_controls.py", + "target_module": "api_controls", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.router", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.router", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.create_router", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.guide_mutation_router", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.policy_mutation_router", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.router", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/router.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.router", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/routes/auth.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.schemas", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/routes/auth.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/api/routes/auth.py", + "target_module": "projects", + "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", + "imported_private_path": "app.modules.artifacts.sources", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/main.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.service", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/main.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.catalogue", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/actors/service.py", + "target_module": "audit", + "imported_private_path": "app.modules.audit.repository", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/actors/service.py", + "target_module": "audit", + "imported_private_path": "app.modules.audit.schemas", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/actors/service.py", + "target_module": "audit", + "imported_private_path": "app.modules.audit.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/actors/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/authorization.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/authorization.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_bindings.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_bindings.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_extraction_service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_materialization.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_setup.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/guide_sufficiency_material.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/operator.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.models", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/artifacts/operator.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/operator.py", + "target_module": "tasks", + "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", + "imported_private_path": "app.modules.checkers.models", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/artifacts/repository.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/repository.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/schemas.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/service.py", + "target_module": "audit", + "imported_private_path": "app.modules.audit.repository", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/artifacts/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/artifacts/service.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_admission.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.catalogue", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/artifacts/submission_admission.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/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", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/audit/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/pre_review_gate.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/pre_submit_execution.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.sources", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/checkers/pre_submit_execution.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.submission_archive", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/checkers/pre_submit_execution.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.submission_manifest", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/checkers/runner.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/runner.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.schemas", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/schemas.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.schemas", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.post_submit_policy", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.repository", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.authorization", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.lifecycle", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.repository", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/checkers/service.py", + "target_module": "tasks", + "imported_private_path": "app.modules.tasks.schemas", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/contributions/schemas.py", + "target_module": "compensation", + "imported_private_path": "app.modules.compensation.schemas", + "repair_owner": "WS-ARCH-001-06" + }, + { + "source_file": "backend/app/modules/projects/create_router.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/create_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/guide_mutation_router.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/guide_mutation_router.py", + "target_module": "api_controls", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/projects/guide_mutation_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/policy_mutation_router.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/policy_mutation_router.py", + "target_module": "api_controls", + "imported_private_path": "app.modules.api_controls.service", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/projects/policy_mutation_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/post_submit_policy.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.runner", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/router.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/router.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.authorization", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/router.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.schemas", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/router.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.service", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.compiler", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.runner", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/projects/submission_policy_mutation_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/submission_policy_mutation_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/projects/sufficiency_mutation_service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/pre_submit_context.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/pre_submit_context.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.catalogue", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/pre_submit_context.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.effective_plan", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/pre_submit_context.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/repository.py", + "target_module": "audit", + "imported_private_path": "app.modules.audit.repository", + "repair_owner": "WS-ARCH-001-07" + }, + { + "source_file": "backend/app/modules/tasks/router.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.schemas", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/router.py", + "target_module": "actors", + "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", + "imported_private_path": "app.modules.actors.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.compiler", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.gate_queue", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.pre_review_gate", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.service", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.models", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.post_submit_policy", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.repository", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/workers/checkers.py", + "target_module": "checkers", + "imported_private_path": "app.modules.checkers.service", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "artifacts", + "imported_private_path": "app.modules.artifacts.guide_sufficiency_material", + "repair_owner": "WS-ARCH-001-04" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.setup_queue", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.submission_policy_mutation_service", + "repair_owner": "WS-ARCH-001-03" + }, + { + "source_file": "backend/app/workers/project_setup.py", + "target_module": "projects", + "imported_private_path": "app.modules.projects.sufficiency_mutation_service", + "repair_owner": "WS-ARCH-001-03" + } + ] +} diff --git a/.ci/module-boundaries/registry.v1.json b/.ci/module-boundaries/registry.v1.json new file mode 100644 index 000000000..67468eac7 --- /dev/null +++ b/.ci/module-boundaries/registry.v1.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "business_modules": [ + "actors", + "authorization", + "projects", + "tasks", + "artifacts", + "checkers", + "reviews", + "contributions", + "compensation" + ], + "supporting_modules": [ + "audit", + "outbox", + "api_controls" + ], + "application_surfaces": [ + {"path": "backend/app/main.py", "classification": "application-composition-root-debt-tracked"}, + {"path": "backend/app/api", "classification": "delivery-composition-debt-tracked"}, + {"path": "backend/app/adapters", "classification": "adapter-debt-tracked"}, + {"path": "backend/app/workers", "classification": "durable-delivery-debt-tracked"}, + {"path": "backend/app/interfaces", "classification": "legacy-interface-debt-tracked"}, + {"path": "backend/app/db/models.py", "classification": "metadata-discovery-model-only"} + ] +} diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 834df7981..9c15145bd 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -49,7 +49,7 @@ jobs: python -m pip install ruff==0.15.22 test "$(ruff --version)" = "ruff 0.15.22" - - name: Validate AUTH module and test boundaries + - name: Validate module, AUTH, and test boundaries working-directory: backend shell: bash run: | @@ -57,9 +57,17 @@ jobs: ruff check \ app/modules/authorization/api \ scripts/authorization_boundary.py \ + scripts/module_boundaries.py \ scripts/test_structure_boundary.py \ tests/architecture/test_authorization_boundary.py \ + tests/architecture/test_module_boundaries.py \ tests/architecture/test_test_structure_boundary.py + python -m scripts.module_boundaries validate \ + --protected-base "${{ github.event.pull_request.base.sha || github.event.before }}" + PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest -q \ + -p pytest_asyncio.plugin \ + tests/architecture/test_module_boundaries.py \ + tests/architecture/test_authorization_boundary.py python -m scripts.authorization_boundary validate \ --ledger ../.agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md python -m scripts.test_structure_boundary validate \ diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py index a7df0f5c7..b281d2cad 100644 --- a/backend/scripts/behavior_ownership.py +++ b/backend/scripts/behavior_ownership.py @@ -71,6 +71,9 @@ "backend/scripts/test_structure_boundary.py", } ) +MODULE_BOUNDARY_FOUNDATION_TARGETS = frozenset( + {"backend/scripts/module_boundaries.py"} +) POL_03A_CALLABLE_TARGETS = frozenset( { "backend/app/modules/authorization/api/project_guide_compilation.py", @@ -223,7 +226,11 @@ def _validate_additive_partition_transition( ): raise BehaviorOwnershipError("untrusted_partition_change") additions = set(current_by_target) - set(trusted_targets) - approved_additions = AUTH_BOUNDARY_FOUNDATION_TARGETS | POL_03A_CALLABLE_TARGETS + approved_additions = ( + AUTH_BOUNDARY_FOUNDATION_TARGETS + | MODULE_BOUNDARY_FOUNDATION_TARGETS + | POL_03A_CALLABLE_TARGETS + ) expected_additions = (approved_additions & additions) - set(trusted_targets) if POL_03A_DECLARATIVE_MODEL_TARGET in additions: expected_additions = expected_additions | {POL_03A_DECLARATIVE_MODEL_TARGET} diff --git a/backend/scripts/module_boundaries.py b/backend/scripts/module_boundaries.py new file mode 100644 index 000000000..53057f0cf --- /dev/null +++ b/backend/scripts/module_boundaries.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +"""Fail-closed modular-monolith dependency validation.""" + +from __future__ import annotations + +import argparse +import ast +from dataclasses import asdict, dataclass +import json +from pathlib import Path +import subprocess +import sys +from typing import Any + +from scripts import authorization_boundary + +REPOSITORY_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_REGISTRY = REPOSITORY_ROOT / ".ci/module-boundaries/registry.v1.json" +DEFAULT_LEDGER = REPOSITORY_ROOT / ".ci/module-boundaries/private-edge-debt.v1.json" +DEFAULT_AUTH_LEDGER = ( + REPOSITORY_ROOT + / ".agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md" +) +MODULE_PREFIX = "app.modules." +BUSINESS_MODULES = 9 +SUPPORTING_MODULES = 3 + + +class ModuleBoundaryError(RuntimeError): + """The source tree or protected boundary configuration is invalid.""" + + +@dataclass(frozen=True, order=True, slots=True) +class PrivateEdge: + """One exact non-AUTH private cross-module dependency.""" + + source_file: str + target_module: str + imported_private_path: str + repair_owner: str + + +@dataclass(frozen=True, slots=True) +class Registry: + """Canonical modular-monolith module classification.""" + + business: tuple[str, ...] + supporting: tuple[str, ...] + application_surfaces: tuple[tuple[str, str], ...] + + @property + def names(self) -> frozenset[str]: + """Return every registered module name.""" + return frozenset((*self.business, *self.supporting)) + + +def _load_json(path: Path, error: str) -> dict[str, Any]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ModuleBoundaryError(error) from exc + if not isinstance(value, dict): + raise ModuleBoundaryError(error) + return value + + +def load_registry(path: Path) -> Registry: + """Load and strictly validate the canonical module registry.""" + value = _load_json(path, "invalid_registry") + if value.get("schema_version") != 1 or set(value) != { + "schema_version", + "business_modules", + "supporting_modules", + "application_surfaces", + }: + raise ModuleBoundaryError("invalid_registry") + business = value["business_modules"] + supporting = value["supporting_modules"] + surfaces = value["application_surfaces"] + if not isinstance(business, list) or not isinstance(supporting, list): + raise ModuleBoundaryError("invalid_registry") + if len(business) != BUSINESS_MODULES or len(supporting) != SUPPORTING_MODULES: + raise ModuleBoundaryError("invalid_module_count") + values = business + supporting + if any(not isinstance(name, str) or not name or not name.isidentifier() for name in values): + raise ModuleBoundaryError("invalid_module_name") + if len(set(values)) != len(values): + raise ModuleBoundaryError("duplicate_module") + expected_surfaces = { + ("backend/app/main.py", "application-composition-root-debt-tracked"), + ("backend/app/api", "delivery-composition-debt-tracked"), + ("backend/app/adapters", "adapter-debt-tracked"), + ("backend/app/workers", "durable-delivery-debt-tracked"), + ("backend/app/interfaces", "legacy-interface-debt-tracked"), + ("backend/app/db/models.py", "metadata-discovery-model-only"), + } + if not isinstance(surfaces, list) or any( + not isinstance(row, dict) + or set(row) != {"path", "classification"} + or not isinstance(row["path"], str) + or not isinstance(row["classification"], str) + for row in surfaces + ): + raise ModuleBoundaryError("invalid_application_surfaces") + surface_pairs = {(row["path"], row["classification"]) for row in surfaces} + if surface_pairs != expected_surfaces or len(surface_pairs) != len(surfaces): + raise ModuleBoundaryError("invalid_application_surfaces") + return Registry(tuple(business), tuple(supporting), tuple(sorted(surface_pairs))) + + +def _module_from_target(target: str) -> str | None: + if not target.startswith(MODULE_PREFIX): + return None + remainder = target.removeprefix(MODULE_PREFIX) + return remainder.split(".", 1)[0] or None + + +def _source_module(source: str) -> str | None: + prefix = "backend/app/modules/" + 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}.") + + +def _repair_owner(target_module: str) -> str: + if target_module in {"projects", "tasks", "actors"}: + return "WS-ARCH-001-03" + if target_module in {"artifacts", "checkers"}: + return "WS-ARCH-001-04" + if target_module == "reviews": + return "WS-ARCH-001-05" + if target_module in {"contributions", "compensation"}: + return "WS-ARCH-001-06" + if target_module in {"audit", "outbox", "api_controls"}: + return "WS-ARCH-001-07" + return "owner-unresolved" + + +def _metadata_discovery_import(source: str, target: str, registry: Registry) -> bool: + """Recognize the sole model-registration infrastructure exception.""" + metadata_paths = { + path for path, classification in registry.application_surfaces + if classification == "metadata-discovery-model-only" + } + return source in metadata_paths and target.split(".")[-1] == "models" + + +def exact_source_imports(path: Path, root: Path, *, source_validated: bool = False) -> set[str]: + """Return alias-expanded imports after AUTH's fail-closed source validation.""" + if not source_validated: + authorization_boundary.source_imports(path, root) + try: + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + except (OSError, UnicodeDecodeError, SyntaxError) as exc: + raise ModuleBoundaryError("invalid_python_source") from exc + current_module = authorization_boundary._module_name(path, root) + targets: set[str] = set() + for node in ast.walk(tree): + if isinstance(node, ast.Import): + targets.update(alias.name for alias in node.names) + elif isinstance(node, ast.ImportFrom): + base = authorization_boundary._resolve_from( + node, current_module, is_package=path.name == "__init__.py" + ) + parts = base.split(".") + expand = len(parts) == 3 and parts[:2] == ["app", "modules"] + for alias in node.names: + targets.add(f"{base}.{alias.name}" if expand else base) + return targets + + +def scan(root: Path, registry: Registry) -> tuple[set[PrivateEdge], dict[str, set[str]], set[authorization_boundary.ImportEdge]]: + """Scan exact private edges, public dependencies, and AUTH-related edges.""" + app_root = root / "backend/app" + if not app_root.is_dir(): + raise ModuleBoundaryError("missing_application_root") + private: set[PrivateEdge] = set() + public_graph = {name: set() for name in registry.names} + auth_edges: set[authorization_boundary.ImportEdge] = set() + for path in sorted(app_root.rglob("*.py")): + source = path.relative_to(root).as_posix() + source_module = _source_module(source) + canonical_imports = authorization_boundary.source_imports(path, root) + exact_imports = exact_source_imports(path, root, source_validated=True) + for target in canonical_imports: + target_module = _module_from_target(target) + if target_module is None: + continue + if target_module not in registry.names: + raise ModuleBoundaryError("unknown_module") + if source_module == 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) + for target in exact_imports: + target_module = _module_from_target(target) + if target_module is None: + continue + if target_module not in registry.names: + raise ModuleBoundaryError("unknown_module") + if source_module == target_module: + continue + if source_module == "authorization" or target_module == "authorization": + continue + if _is_public_target(target, target_module): + if source_module is not None: + public_graph[source_module].add(target_module) + else: + if _metadata_discovery_import(source, target, registry): + continue + private.add( + PrivateEdge(source, target_module, target, _repair_owner(target_module)) + ) + return private, public_graph, auth_edges + + +def _validate_public_apis(root: Path, registry: Registry) -> None: + """Reject public packages that reach into any private module surface.""" + modules_root = root / "backend/app/modules" + for module in registry.names: + api_root = modules_root / module / "api" + if not api_root.exists(): + continue + for path in sorted(api_root.rglob("*.py")): + for target in exact_source_imports(path, root): + target_module = _module_from_target(target) + if target_module is None: + continue + if target_module not in registry.names: + raise ModuleBoundaryError("unknown_module") + if not _is_public_target(target, target_module): + raise ModuleBoundaryError("public_api_private_leak") + + +def _validate_acyclic(graph: dict[str, set[str]]) -> None: + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str) -> None: + if node in visiting: + raise ModuleBoundaryError("cyclic_public_dependency") + if node in visited: + return + visiting.add(node) + for target in sorted(graph[node]): + visit(target) + visiting.remove(node) + visited.add(node) + + for node in sorted(graph): + visit(node) + + +def load_ledger(path: Path, registry: Registry) -> set[PrivateEdge]: + """Load the exact protected-base non-AUTH private-edge ledger.""" + value = _load_json(path, "invalid_ledger") + return _parse_ledger(value, registry) + + +def _parse_ledger(value: dict[str, Any], registry: Registry) -> set[PrivateEdge]: + """Parse one already-loaded protected ledger document.""" + if value.get("schema_version") != 1 or set(value) != {"schema_version", "edges"}: + raise ModuleBoundaryError("invalid_ledger") + rows = value["edges"] + if not isinstance(rows, list): + raise ModuleBoundaryError("invalid_ledger") + edges: set[PrivateEdge] = set() + required = {"source_file", "target_module", "imported_private_path", "repair_owner"} + for row in rows: + if not isinstance(row, dict) or set(row) != required: + raise ModuleBoundaryError("invalid_ledger_edge") + if any(not isinstance(row[key], str) or not row[key] for key in required): + raise ModuleBoundaryError("invalid_ledger_edge") + edge = PrivateEdge(**row) + if edge.target_module not in registry.names or edge.target_module == "authorization": + raise ModuleBoundaryError("invalid_ledger_target") + if edge in edges: + raise ModuleBoundaryError("duplicate_ledger_edge") + edges.add(edge) + return edges + + +def _git_document(root: Path, ref: str, path: str) -> dict[str, Any] | None: + """Read one JSON document from an exact Git tree without checkout.""" + result = subprocess.run( + ["git", "show", f"{ref}:{path}"], + cwd=root, + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + return None + try: + value = json.loads(result.stdout) + except json.JSONDecodeError as exc: + raise ModuleBoundaryError("invalid_protected_base_ledger") from exc + if not isinstance(value, dict): + raise ModuleBoundaryError("invalid_protected_base_ledger") + return value + + +def _git_changed_paths(root: Path, ref: str) -> set[str]: + """Return exact paths changed from the protected base to current HEAD.""" + result = subprocess.run( + ["git", "diff", "--name-only", f"{ref}...HEAD"], + cwd=root, + check=False, + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise ModuleBoundaryError("invalid_protected_base") + return {line for line in result.stdout.splitlines() if line} + + +def validate_protected_base( + root: Path, ref: str, registry: Registry, current: set[PrivateEdge] +) -> None: + """Allow debt removal while rejecting additions against an exact base tree.""" + ledger_path = ".ci/module-boundaries/private-edge-debt.v1.json" + document = _git_document(root, ref, ledger_path) + if document is None: + if _git_document(root, ref, ".ci/module-boundaries/registry.v1.json") is not None: + raise ModuleBoundaryError("missing_protected_base_ledger") + if any(path.startswith("backend/app/") for path in _git_changed_paths(root, ref)): + raise ModuleBoundaryError("unsafe_bootstrap_runtime_change") + return # Bootstrap is enforcement-only and cannot alter runtime sources. + protected = _parse_ledger(document, registry) + if not current.issubset(protected): + raise ModuleBoundaryError("protected_base_edge_growth") + + +def validate( + root: Path, + registry_path: Path, + ledger_path: Path, + auth_ledger: Path, + protected_base: str | None = None, +) -> None: + """Validate registry, exact debt, AUTH composition, leaks, and cycles.""" + registry = load_registry(registry_path) + modules_root = root / "backend/app/modules" + discovered_modules = { + path.name for path in modules_root.iterdir() + if path.is_dir() and path.name != "__pycache__" + } + if discovered_modules != registry.names: + raise ModuleBoundaryError("module_directory_mismatch") + authorization_boundary.validate(root, auth_ledger) + expected_auth = set().union(*authorization_boundary.load_ledger(auth_ledger)) + actual_private, public_graph, actual_auth = scan(root, registry) + if actual_auth != expected_auth: + raise ModuleBoundaryError("authorization_edge_divergence") + current_ledger = load_ledger(ledger_path, registry) + if actual_private != current_ledger: + raise ModuleBoundaryError("private_edge_mismatch") + if protected_base: + validate_protected_base(root, protected_base, registry, current_ledger) + _validate_public_apis(root, registry) + _validate_acyclic(public_graph) + + +def inventory(root: Path, registry_path: Path) -> dict[str, Any]: + """Return the deterministic current non-AUTH ledger document.""" + registry = load_registry(registry_path) + edges, _, _ = scan(root, registry) + return {"schema_version": 1, "edges": [asdict(edge) for edge in sorted(edges)]} + + +def main() -> int: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + validate_parser = subparsers.add_parser("validate") + validate_parser.add_argument("--registry", type=Path, default=DEFAULT_REGISTRY) + validate_parser.add_argument("--ledger", type=Path, default=DEFAULT_LEDGER) + validate_parser.add_argument("--auth-ledger", type=Path, default=DEFAULT_AUTH_LEDGER) + validate_parser.add_argument("--protected-base") + inventory_parser = subparsers.add_parser("inventory") + inventory_parser.add_argument("--registry", type=Path, default=DEFAULT_REGISTRY) + args = parser.parse_args() + try: + if args.command == "inventory": + print(json.dumps(inventory(REPOSITORY_ROOT, args.registry), indent=2)) + else: + validate( + REPOSITORY_ROOT, + args.registry, + args.ledger, + args.auth_ledger, + args.protected_base, + ) + except (ModuleBoundaryError, authorization_boundary.AuthorizationBoundaryError) as exc: + print(f"module-boundaries: {exc}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/tests/architecture/test_module_boundaries.py b/backend/tests/architecture/test_module_boundaries.py new file mode 100644 index 000000000..65ab71296 --- /dev/null +++ b/backend/tests/architecture/test_module_boundaries.py @@ -0,0 +1,295 @@ +"""Architecture proof for the repository-wide public module boundary.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from scripts import module_boundaries as boundary +from scripts.authorization_boundary import AuthorizationBoundaryError + +ROOT = Path(__file__).resolve().parents[3] +REGISTRY = ROOT / ".ci/module-boundaries/registry.v1.json" +LEDGER = ROOT / ".ci/module-boundaries/private-edge-debt.v1.json" +AUTH_LEDGER = ( + ROOT / ".agent-loop/initiatives/WS-AUTH-003-module-boundary-recovery/IMPORT_LEDGER.md" +) + + +def _write(path: Path, value: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(value, encoding="utf-8") + + +def _registry(path: Path) -> None: + path.write_text(REGISTRY.read_text(encoding="utf-8"), encoding="utf-8") + + +def test_repository_matches_the_protected_module_boundary() -> None: + """Current exact debt, AUTH composition, APIs, and graph are valid.""" + boundary.validate(ROOT, REGISTRY, LEDGER, AUTH_LEDGER) + + +def test_registry_names_exactly_nine_business_and_three_supporting_modules() -> None: + """The canonical ownership map cannot silently grow or collapse.""" + registry = boundary.load_registry(REGISTRY) + assert len(registry.business) == 9 + assert len(registry.supporting) == 3 + assert registry.names == { + "actors", "authorization", "projects", "tasks", "artifacts", "checkers", + "reviews", "contributions", "compensation", "audit", "outbox", "api_controls", + } + assert dict(registry.application_surfaces)["backend/app/interfaces"] == ( + "legacy-interface-debt-tracked" + ) + + +def test_new_private_edge_fails_exact_ledger_comparison(tmp_path: Path) -> None: + """A second import in an already indebted source is still new debt.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/modules/tasks/service.py", + "from app.modules.projects import models, repository\n", + ) + registry = boundary.load_registry(tmp_path / "registry.json") + edges, _, _ = boundary.scan(tmp_path, registry) + assert {edge.imported_private_path for edge in edges} == { + "app.modules.projects.models", "app.modules.projects.repository" + } + + +def test_protected_base_rejects_new_or_expanded_debt( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Editing code and its ledger together cannot authorize boundary growth.""" + registry = boundary.load_registry(REGISTRY) + current = boundary.load_ledger(LEDGER, registry) + new_edge = boundary.PrivateEdge( + "backend/app/modules/tasks/new.py", + "projects", + "app.modules.projects.repository", + "WS-ARCH-001-03", + ) + protected_document = { + "schema_version": 1, + "edges": [boundary.asdict(edge) for edge in sorted(current)], + } + monkeypatch.setattr(boundary, "_git_document", lambda *_: protected_document) + with pytest.raises(boundary.ModuleBoundaryError, match="protected_base_edge_growth"): + boundary.validate_protected_base(ROOT, "base", registry, current | {new_edge}) + + +def test_bootstrap_rejects_runtime_source_changes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The first ledger install cannot hide a product edge added by its own PR.""" + registry = boundary.load_registry(REGISTRY) + monkeypatch.setattr(boundary, "_git_document", lambda *_: None) + monkeypatch.setattr( + boundary, + "_git_changed_paths", + lambda *_: {"backend/app/modules/tasks/service.py"}, + ) + with pytest.raises(boundary.ModuleBoundaryError, match="unsafe_bootstrap_runtime_change"): + boundary.validate_protected_base(ROOT, "base", registry, set()) + + +def test_bootstrap_allows_enforcement_only_install( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """WS-ARCH-001-01 may install the absent gate without runtime changes.""" + registry = boundary.load_registry(REGISTRY) + monkeypatch.setattr(boundary, "_git_document", lambda *_: None) + monkeypatch.setattr( + boundary, + "_git_changed_paths", + lambda *_: {"backend/scripts/module_boundaries.py"}, + ) + boundary.validate_protected_base(ROOT, "base", registry, set()) + + +def test_unknown_module_fails_closed(tmp_path: Path) -> None: + """Unregistered product packages cannot bypass module ownership.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/modules/tasks/service.py", + "from app.modules.unknown import repository\n", + ) + with pytest.raises(boundary.ModuleBoundaryError, match="unknown_module"): + boundary.scan(tmp_path, boundary.load_registry(tmp_path / "registry.json")) + + +def test_auth_view_contains_only_cross_boundary_private_edges(tmp_path: Path) -> None: + """AUTH internal topology never pollutes its canonical boundary ledger.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/modules/authorization/service.py", + "from app.modules.authorization import repository\n" + "from app.modules.tasks import models\n", + ) + _write( + tmp_path / "backend/app/modules/tasks/service.py", + "from app.modules.authorization import runtime\n", + ) + _, _, auth_edges = boundary.scan( + tmp_path, boundary.load_registry(tmp_path / "registry.json") + ) + assert auth_edges == { + boundary.authorization_boundary.ImportEdge( + "backend/app/modules/authorization/service.py", "app.modules.tasks" + ), + boundary.authorization_boundary.ImportEdge( + "backend/app/modules/tasks/service.py", "app.modules.authorization.runtime" + ), + } + + +def test_public_api_private_reexport_fails_closed(tmp_path: Path) -> None: + """An api package cannot disguise another module's private implementation.""" + _registry(tmp_path / "registry.json") + _write( + tmp_path / "backend/app/modules/tasks/api/__init__.py", + "from app.modules.projects.repository import ProjectRepository\n", + ) + with pytest.raises(boundary.ModuleBoundaryError, match="public_api_private_leak"): + boundary._validate_public_apis( # noqa: SLF001 - architecture proof + tmp_path, boundary.load_registry(tmp_path / "registry.json") + ) + + +def test_cyclic_public_dependencies_fail_closed() -> None: + """Typed public facades cannot form an architectural dependency cycle.""" + graph = {name: set() for name in boundary.load_registry(REGISTRY).names} + graph["tasks"].add("artifacts") + graph["artifacts"].add("tasks") + with pytest.raises(boundary.ModuleBoundaryError, match="cyclic_public_dependency"): + boundary._validate_acyclic(graph) # noqa: SLF001 - architecture proof + + +def test_general_ledger_rejects_copied_authorization_edges(tmp_path: Path) -> None: + """AUTH debt remains exclusively owned by the AUTH-003 ledger.""" + document = { + "schema_version": 1, + "edges": [{ + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "authorization", + "imported_private_path": "app.modules.authorization.runtime", + "repair_owner": "security-triage-required", + }], + } + path = tmp_path / "ledger.json" + path.write_text(json.dumps(document), encoding="utf-8") + with pytest.raises(boundary.ModuleBoundaryError, match="invalid_ledger_target"): + boundary.load_ledger(path, boundary.load_registry(REGISTRY)) + + +def test_general_ledger_allows_non_auth_security_triage_owner(tmp_path: Path) -> None: + """Unresolved authorization-affecting non-AUTH debt can block its capability.""" + document = { + "schema_version": 1, + "edges": [{ + "source_file": "backend/app/modules/tasks/service.py", + "target_module": "actors", + "imported_private_path": "app.modules.actors.service_identities", + "repair_owner": "security-triage-required", + }], + } + path = tmp_path / "ledger.json" + path.write_text(json.dumps(document), encoding="utf-8") + assert boundary.load_ledger(path, boundary.load_registry(REGISTRY)) == { + boundary.PrivateEdge( + "backend/app/modules/tasks/service.py", + "actors", + "app.modules.actors.service_identities", + "security-triage-required", + ) + } + + +def test_initial_ledgers_capture_high_risk_application_edges() -> None: + """The protected base includes known ART/TASK/wiring boundary debt.""" + general = boundary.load_ledger(LEDGER, boundary.load_registry(REGISTRY)) + actual = {(edge.source_file, edge.imported_private_path) for edge in general} + assert ("backend/app/api/router.py", "app.modules.tasks.router") in actual + assert ( + "backend/app/interfaces/artifact_operations.py", + "app.modules.checkers.pre_submit_execution", + ) in actual + assert any( + source.startswith("backend/app/adapters/artifacts/") + and target.startswith("app.modules.artifacts.") + for source, target in actual + ) + assert any( + source.startswith("backend/app/workers/") + and target.startswith("app.modules.projects.") + for source, target in actual + ) + auth_inbound, _ = boundary.authorization_boundary.load_ledger(AUTH_LEDGER) + assert boundary.authorization_boundary.ImportEdge( + "backend/app/interfaces/artifact_operations.py", + "app.modules.authorization.prepared", + ) in auth_inbound + + +def test_metadata_discovery_exception_is_exact_and_model_only(tmp_path: Path) -> None: + """Metadata registration cannot become a generic private capability path.""" + _registry(tmp_path / "registry.json") + path = tmp_path / "backend/app/db/models.py" + _write(path, "from app.modules.tasks import models\n") + registry = boundary.load_registry(tmp_path / "registry.json") + edges, _, _ = boundary.scan(tmp_path, registry) + assert not edges + _write(path, "from app.modules.tasks import repository\n") + edges, _, _ = boundary.scan(tmp_path, registry) + assert {edge.imported_private_path for edge in edges} == { + "app.modules.tasks.repository" + } + + +@pytest.mark.parametrize("mutation", ("missing", "additional")) +def test_auth_ledger_and_general_view_divergence_fails_closed( + monkeypatch: pytest.MonkeyPatch, mutation: str +) -> None: + """A missing or additional AUTH edge cannot disappear between validators.""" + registry = boundary.load_registry(REGISTRY) + _, _, actual_auth = boundary.scan(ROOT, registry) + expected = set(actual_auth) + if mutation == "missing": + expected.remove(next(iter(expected))) + else: + expected.add( + boundary.authorization_boundary.ImportEdge( + "backend/app/example.py", "app.modules.authorization.runtime" + ) + ) + monkeypatch.setattr(boundary.authorization_boundary, "validate", lambda *_: None) + monkeypatch.setattr( + boundary.authorization_boundary, + "load_ledger", + lambda *_: (expected, set()), + ) + with pytest.raises(boundary.ModuleBoundaryError, match="authorization_edge_divergence"): + boundary.validate(ROOT, REGISTRY, LEDGER, AUTH_LEDGER) + + +@pytest.mark.parametrize( + "source", + ( + "value = __import__('app.modules.projects.repository')\n", + "import importlib\nimportlib.import_module('app.modules.projects.repository')\n", + "exec('from app.modules.projects import repository')\n", + "import sys\nsys.modules['app.modules.projects.repository']\n", + "from app.modules.projects.repository import *\n", + ), +) +def test_dynamic_and_wildcard_imports_fail_repository_wide( + tmp_path: Path, source: str +) -> None: + """The general scanner inherits AUTH's fail-closed AST parser.""" + _registry(tmp_path / "registry.json") + _write(tmp_path / "backend/app/modules/tasks/service.py", source) + with pytest.raises(AuthorizationBoundaryError): + boundary.scan(tmp_path, boundary.load_registry(tmp_path / "registry.json")) diff --git a/backend/tests/test_behavior_ownership.py b/backend/tests/test_behavior_ownership.py index 515dfea62..4e28bbe06 100644 --- a/backend/tests/test_behavior_ownership.py +++ b/backend/tests/test_behavior_ownership.py @@ -219,6 +219,39 @@ def test_partition_rejects_reordered_trusted_assignments( ownership.validate_partition(tmp_path, trusted_revision="main") +def test_partition_allows_the_module_boundary_foundation_target( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The reviewed general boundary validator has one exact additive slot.""" + existing = "backend/scripts/existing.py" + addition = "backend/scripts/module_boundaries.py" + trusted = _partition([existing]) + current = _partition([existing, addition]) + _write_json(tmp_path / ownership.PARTITION_PATH, current) + monkeypatch.setattr(ownership, "AUTH_BOUNDARY_FOUNDATION_TARGETS", frozenset()) + monkeypatch.setattr( + ownership, "MODULE_BOUNDARY_FOUNDATION_TARGETS", frozenset({addition}) + ) + monkeypatch.setattr(ownership, "POL_03A_CALLABLE_TARGETS", frozenset()) + monkeypatch.setattr( + ownership, "eligible_targets", lambda root=ownership.ROOT: [existing, addition] + ) + monkeypatch.setattr( + ownership, + "_git", + lambda root, *arguments: "a" * 40 if arguments[0] == "rev-parse" else "", + ) + monkeypatch.setattr( + ownership, + "_git_show_optional", + lambda root, revision, path: json.dumps(trusted), + ) + assert ownership.validate_partition(tmp_path, trusted_revision="main") == { + existing: "shared", + addition: "shared", + } + + @pytest.mark.parametrize("case", ("extra", "removal", "reassignment", "trusted_digest", "base")) def test_partition_additive_transition_rejects_custody_drift( tmp_path: Path, diff --git a/docs/architecture_lockdown.md b/docs/architecture_lockdown.md index eef75f54c..cd320f45e 100644 --- a/docs/architecture_lockdown.md +++ b/docs/architecture_lockdown.md @@ -17,6 +17,12 @@ or revises Workstream identity, authority, submission, Review, or `ContributionRecord` truth. Flow Identity remains the current v0.1 external 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. + The ADR files under `docs/decision_*.md` are the decision record for this lockdown. When a locked rule changes, update or add an ADR before changing implementation specs. Repository changes use the Repository-Native Human-Agent SDLC documented in diff --git a/docs/operations_backend_testing.md b/docs/operations_backend_testing.md index 3cc6362ca..30b0746c1 100644 --- a/docs/operations_backend_testing.md +++ b/docs/operations_backend_testing.md @@ -254,3 +254,17 @@ required check invokes the command. Only pytest `|run` coverage contexts count as callable-execution evidence. Fixture setup and teardown coverage is intentionally excluded because it does not prove that the test body exercised the callable. + +## Module-boundary validation + +Run the modular-monolith boundary gate from the repository root: + +```bash +PYTHONPATH=backend backend/.venv/bin/python backend/scripts/module_boundaries.py validate --protected-base origin/main +``` + +It checks the canonical 12-module registry, exact protected non-AUTH private +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. From 8971393f4106c527ff4526e814c4a43faa3d66fe Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Mon, 10 Aug 2026 15:48:06 +0100 Subject: [PATCH 2/4] fix(arch): close boundary gate review gaps --- .../chunks/WS-ARCH-001-01-boundary-foundation.md | 6 +++--- .agent-loop/policies/architecture-boundaries.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md index 4dba0122a..b1acbb801 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md @@ -50,14 +50,14 @@ docs/operations_backend_testing.md - `backend/app/adapters/**` implements typed infrastructure capabilities and may be wired by composition, but every existing product-private import is frozen debt; - - `backend/app/workers/**` is durable delivery/composition entry code and may + - `backend/app/workers/**` is durable delivery/composition entry code and may consume public APIs; every existing private import is frozen debt; - `backend/app/interfaces/**` is legacy shared-contract debt, not a permanent public-contract namespace, and all product-private imports are frozen; - database metadata discovery is limited to the exact registered discovery path and model-only imports; it grants no runtime capability authority. - The validator scans all application paths, not only `backend/app/modules/**`. - New private edges from API, adapters, workers, interfaces, composition, or + New private edges from API, adapters, background execution, interfaces, composition, or metadata discovery fail closed. - Repository engineering policy uses the same canonical ownership map and contains no nonexistent `modules/submissions` boundary. @@ -71,7 +71,7 @@ docs/operations_backend_testing.md - Existing exact debt passes only through protected ledger reconciliation. - The first ledger includes ART-to-AUTH/TASK/CHECKER debt through the combined general and AUTH views, TASK route and pre-submit-context debt, legacy shared - interfaces, and worker/adapter private assembly. AUTH-affecting edges remain + interfaces, and background-execution/adapter private assembly. AUTH-affecting edges remain exclusively in the canonical AUTH ledger. - AUTH-003 is the sole canonical source for every inbound/outbound AUTH edge. The general validator loads that ledger through the existing AUTH boundary diff --git a/.agent-loop/policies/architecture-boundaries.md b/.agent-loop/policies/architecture-boundaries.md index add10ae52..8c7e4f157 100644 --- a/.agent-loop/policies/architecture-boundaries.md +++ b/.agent-loop/policies/architecture-boundaries.md @@ -18,7 +18,7 @@ Application-level paths are explicit rather than invisible exceptions: - `backend/app/main.py` is the application composition root; its current product-private imports are frozen debt while future wiring consumes typed public ports; -- `backend/app/api/**` and `backend/app/workers/**` are delivery/composition +- `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; From 360e55a77682d3d7cc9ed7882ad5632ac2ad2558 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Mon, 10 Aug 2026 15:55:01 +0100 Subject: [PATCH 3/4] fix(ci): register module boundary tests --- .../chunks/WS-ARCH-001-01-boundary-foundation.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-01-boundary-foundation.md b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md index b1acbb801..d1f679fa8 100644 --- a/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md +++ b/.agent-loop/initiatives/WS-ARCH-001-modular-monolith-boundaries/chunks/WS-ARCH-001-01-boundary-foundation.md @@ -21,6 +21,7 @@ enforcement that composes with WS-AUTH-003. .ci/behavior-ownership/partition.v1.json .ci/behavior-ownership/shared/module-boundary-validator.json backend/scripts/behavior_ownership.py +backend/scripts/run_test_lanes.py backend/tests/test_behavior_ownership.py backend/scripts/module_boundaries.py backend/tests/architecture/test_module_boundaries.py diff --git a/backend/scripts/run_test_lanes.py b/backend/scripts/run_test_lanes.py index 88eaf3292..09567eea4 100644 --- a/backend/scripts/run_test_lanes.py +++ b/backend/scripts/run_test_lanes.py @@ -92,6 +92,7 @@ class TestLane: "tests/test_app.py", "tests/test_artifact_architecture.py", "tests/architecture/test_authorization_boundary.py", + "tests/architecture/test_module_boundaries.py", "tests/architecture/test_test_structure_boundary.py", "tests/test_artifact_authorization.py", "tests/test_artifact_internal_authorization.py", From 4bb526686dbd7bfd3782aadd758d9fcba5d1a57d Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Mon, 10 Aug 2026 16:18:47 +0100 Subject: [PATCH 4/4] fix(arch): close import scanner gaps --- .agent-loop/CURRENT_STATE.md | 2 +- .../shared/module-boundary-validator.json | 2 + backend/scripts/module_boundaries.py | 11 +++- .../architecture/test_module_boundaries.py | 54 +++++++++++++++---- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/.agent-loop/CURRENT_STATE.md b/.agent-loop/CURRENT_STATE.md index b2123847f..3943a208e 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) | Canonical twelve-module registry, exact protected debt ledgers, public-API checks, and CI no-growth enforcement are established; no product behavior changed | Split WS-ARCH-001-02 into exact AUTH, ART, TASK, and composition-root submission capability contracts before implementation | +| [WS-ARCH-001](initiatives/WS-ARCH-001-modular-monolith-boundaries/STATUS.md) | Boundary foundation is implemented in PR #310 but is not durable on `main` until human merge; no product behavior changed | After merge, split WS-ARCH-001-02 into exact AUTH, ART, TASK, and composition-root submission capability contracts before implementation | | [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; foundations and project-policy authority are merged through `12F3` | Remaining activation chunks depend on exact merged POL, ART, and REV behavior; refresh each executable contract on current `main` | | [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/.ci/behavior-ownership/shared/module-boundary-validator.json b/.ci/behavior-ownership/shared/module-boundary-validator.json index 649f3c949..322401c55 100644 --- a/.ci/behavior-ownership/shared/module-boundary-validator.json +++ b/.ci/behavior-ownership/shared/module-boundary-validator.json @@ -39,6 +39,8 @@ "backend/tests/architecture/test_module_boundaries.py::test_repository_matches_the_protected_module_boundary", "backend/tests/architecture/test_module_boundaries.py::test_registry_names_exactly_nine_business_and_three_supporting_modules", "backend/tests/architecture/test_module_boundaries.py::test_new_private_edge_fails_exact_ledger_comparison", + "backend/tests/architecture/test_module_boundaries.py::test_import_from_modules_package_resolves_registered_alias", + "backend/tests/architecture/test_module_boundaries.py::test_unregistered_top_level_module_file_fails_closed", "backend/tests/architecture/test_module_boundaries.py::test_protected_base_rejects_new_or_expanded_debt", "backend/tests/architecture/test_module_boundaries.py::test_bootstrap_rejects_runtime_source_changes", "backend/tests/architecture/test_module_boundaries.py::test_bootstrap_allows_enforcement_only_install", diff --git a/backend/scripts/module_boundaries.py b/backend/scripts/module_boundaries.py index 53057f0cf..677f02430 100644 --- a/backend/scripts/module_boundaries.py +++ b/backend/scripts/module_boundaries.py @@ -168,7 +168,9 @@ def exact_source_imports(path: Path, root: Path, *, source_validated: bool = Fal node, current_module, is_package=path.name == "__init__.py" ) parts = base.split(".") - expand = len(parts) == 3 and parts[:2] == ["app", "modules"] + expand = base == "app.modules" or ( + len(parts) == 3 and parts[:2] == ["app", "modules"] + ) for alias in node.names: targets.add(f"{base}.{alias.name}" if expand else base) return targets @@ -348,11 +350,16 @@ def validate( """Validate registry, exact debt, AUTH composition, leaks, and cycles.""" registry = load_registry(registry_path) modules_root = root / "backend/app/modules" + if not modules_root.is_dir(): + raise ModuleBoundaryError("module_directory_mismatch") discovered_modules = { path.name for path in modules_root.iterdir() if path.is_dir() and path.name != "__pycache__" } - if discovered_modules != registry.names: + unexpected_module_files = { + path.name for path in modules_root.glob("*.py") + } - {"__init__.py"} + if discovered_modules != registry.names or unexpected_module_files: raise ModuleBoundaryError("module_directory_mismatch") authorization_boundary.validate(root, auth_ledger) expected_auth = set().union(*authorization_boundary.load_ledger(auth_ledger)) diff --git a/backend/tests/architecture/test_module_boundaries.py b/backend/tests/architecture/test_module_boundaries.py index 65ab71296..54023486b 100644 --- a/backend/tests/architecture/test_module_boundaries.py +++ b/backend/tests/architecture/test_module_boundaries.py @@ -46,18 +46,52 @@ def test_registry_names_exactly_nine_business_and_three_supporting_modules() -> ) -def test_new_private_edge_fails_exact_ledger_comparison(tmp_path: Path) -> None: +def test_new_private_edge_fails_exact_ledger_comparison( + monkeypatch: pytest.MonkeyPatch, +) -> None: """A second import in an already indebted source is still new debt.""" - _registry(tmp_path / "registry.json") - _write( - tmp_path / "backend/app/modules/tasks/service.py", - "from app.modules.projects import models, repository\n", + edge = boundary.PrivateEdge( + "backend/app/modules/tasks/service.py", + "projects", + "app.modules.projects.repository", + "WS-ARCH-001-03", ) - registry = boundary.load_registry(tmp_path / "registry.json") - edges, _, _ = boundary.scan(tmp_path, registry) - assert {edge.imported_private_path for edge in edges} == { - "app.modules.projects.models", "app.modules.projects.repository" - } + graph = {name: set() for name in boundary.load_registry(REGISTRY).names} + monkeypatch.setattr(boundary.authorization_boundary, "validate", lambda *_: None) + monkeypatch.setattr( + boundary.authorization_boundary, "load_ledger", lambda *_: (set(), set()) + ) + monkeypatch.setattr(boundary, "scan", lambda *_: ({edge}, graph, set())) + monkeypatch.setattr(boundary, "load_ledger", lambda *_: set()) + monkeypatch.setattr(boundary, "_validate_public_apis", lambda *_: None) + with pytest.raises(boundary.ModuleBoundaryError, match="private_edge_mismatch"): + boundary.validate(ROOT, REGISTRY, LEDGER, AUTH_LEDGER) + + +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") + path = tmp_path / "backend/app/modules/tasks/service.py" + _write(path, "from app.modules import projects\n") + assert boundary.exact_source_imports(path, tmp_path) == {"app.modules.projects"} + + +def test_unregistered_top_level_module_file_fails_closed(tmp_path: Path) -> None: + """A file beside module packages cannot create an invisible pseudo-module.""" + registry_path = tmp_path / "registry.json" + _registry(registry_path) + registry = boundary.load_registry(registry_path) + modules_root = tmp_path / "backend/app/modules" + for name in registry.names: + (modules_root / name).mkdir(parents=True) + _write(modules_root / "rogue.py", "value = 1\n") + with pytest.raises(boundary.ModuleBoundaryError, match="module_directory_mismatch"): + boundary.validate( + tmp_path, + registry_path, + tmp_path / "ledger.json", + tmp_path / "auth-ledger.md", + ) def test_protected_base_rejects_new_or_expanded_debt(