From 63d188e9a2d769ca02806292b788bba2839f71b5 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Sat, 8 Aug 2026 04:15:43 +0100 Subject: [PATCH 1/2] feat(quality): add behavior ownership catalogue foundation --- .../STATUS.md | 21 +- .../WS-QUAL-002-01-catalogue-foundation.md | 5 + .../reviews/WS-QUAL-002-01-pr-trust-bundle.md | 108 +++ .ci/behavior-ownership/README.md | 40 + .../examples/reviewed.example.json | 20 + .ci/behavior-ownership/partition.v1.json | 699 +++++++++++++++ CONTRIBUTING.md | 21 +- backend/scripts/behavior_ownership.py | 520 +++++++++++ backend/scripts/run_test_lanes.py | 1 + backend/tests/test_behavior_ownership.py | 809 ++++++++++++++++++ docs/operations_backend_testing.md | 28 + scripts/behavior-ownership.schema.json | 70 ++ 12 files changed, 2335 insertions(+), 7 deletions(-) create mode 100644 .agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md create mode 100644 .ci/behavior-ownership/README.md create mode 100644 .ci/behavior-ownership/examples/reviewed.example.json create mode 100644 .ci/behavior-ownership/partition.v1.json create mode 100644 backend/scripts/behavior_ownership.py create mode 100644 backend/tests/test_behavior_ownership.py create mode 100644 scripts/behavior-ownership.schema.json diff --git a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/STATUS.md b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/STATUS.md index 5522592a..0e16b7f1 100644 --- a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/STATUS.md +++ b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/STATUS.md @@ -1,11 +1,24 @@ # Status: WS-QUAL-002 Behavior Ownership Catalogue Planning is complete and internally reviewed. PR #289 retired the 05M hosted -mutation workflow; Backend lanes and coverage remain authoritative. No -catalogue implementation or mutation reactivation has started. +mutation workflow; Backend lanes and coverage remain authoritative. PR #290 +merged the catalogue plan. -The first proposed chunk is `WS-QUAL-002-01`. Stop after planning review and -human approval; do not start implementation automatically. +`WS-QUAL-002-01` is in implementation. The current branch adds the schema, +canonical exact target partition, deterministic inventory/candidate generator, +fail-closed validator, examples, focused tests, and contributor documentation. +It does not reactivate mutation or change any workflow, product behavior, +coverage floor, lane, skip, or deselection. + +Current focused evidence: + +- 49 focused tests pass. +- `scripts.behavior_ownership` coverage is 91.30 percent. +- 33 semantic-lane contract tests pass, including exact assignment of the new + focused test module to `shared_foundations` under the human-approved scope + correction. +- The initial catalogue is explicitly incomplete and candidate output remains + non-authoritative while subsystem population has not started. ## Plan review diff --git a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/chunks/WS-QUAL-002-01-catalogue-foundation.md b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/chunks/WS-QUAL-002-01-catalogue-foundation.md index 51f1ee22..7d5dc5f3 100644 --- a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/chunks/WS-QUAL-002-01-catalogue-foundation.md +++ b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/chunks/WS-QUAL-002-01-catalogue-foundation.md @@ -36,6 +36,7 @@ P2. .ci/behavior-ownership/examples/** scripts/behavior-ownership.schema.json backend/scripts/behavior_ownership.py +backend/scripts/run_test_lanes.py backend/tests/test_behavior_ownership.py CONTRIBUTING.md docs/operations_backend_testing.md @@ -56,6 +57,10 @@ coverage thresholds, lane membership, skips, deselection, survivor exemptions authoritative inferred ownership without reviewed catalogue state ``` +The only approved lane-membership change is assigning +`tests/test_behavior_ownership.py` to the existing `shared_foundations` lane. +No other lane membership or execution behavior may change. + ## Acceptance criteria - [ ] Schema distinguishes reviewed ownership, candidates, and strict structural-only records. diff --git a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md new file mode 100644 index 00000000..b3351011 --- /dev/null +++ b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md @@ -0,0 +1,108 @@ +# PR Trust Bundle: WS-QUAL-002-01 + +## Chunk + +`WS-QUAL-002-01` — Behavior Ownership Catalogue Foundation. + +## Goal And Human-Approved Intent + +Add one versioned catalogue contract, exact eligible-target partition, and +deterministic read-only generator/validator without activating mutation CI or +changing Workstream product behavior. The human separately approved the narrow +contract correction that assigns the new focused test module to the existing +`shared_foundations` lane. + +## What Changed And Why + +- Added the schema separating `candidate`, `reviewed`, and strict + `structural_only` records. +- Added the canonical digest-bound partition assigning every eligible target to + exactly one population group. +- Added deterministic inventory, non-authoritative candidate generation, + validation, exact pytest collection, and optional exact owned-test execution. +- Added fail-closed custody, symlink, path, schema, callable, test, remap, + carry-forward, structural-side-effect, identity, and effective-owner checks. +- Added contributor and backend-testing documentation plus a real reviewed + example. + +## Design Chosen + +The tooling delegates eligibility, safe paths, callable spans, changed-callable +derivation, outcomes, boundaries, and test-node syntax to +`backend/scripts/mutation_policy.py`. Candidate inference is structurally +non-authoritative. Protected records remain byte-identical or resolve through +exactly one reviewed, evidence-preserving remap. The initial empty catalogue is +reported as incomplete rather than promoted or blocked. + +## Alternatives Rejected + +No wildcard/group inference authority, branch-local partition replacement, +callable-wide mutation activation, inferred reviewed ownership, free-form +structural exemption, parallel AST implementation, or workflow change. + +## Scope Control And Product Behavior + +All files are within the approved contract plus the human-approved single lane +assignment. No `.github/workflows/**`, backend application module, migration, +coverage floor, timeout, skip, deselection, product review decision, +authorization rule, payment, reputation, or `ContributionRecord` behavior +changed. + +## Acceptance Proof And Tests + +- 49 focused tests pass. +- `scripts.behavior_ownership` focused coverage is 91.30 percent, above 90. +- 33 semantic-lane contract tests pass. +- Ruff passes for all touched backend Python files. +- Real validation reports 173 unresolved targets and `complete: false`. +- Candidate generation remains `authoritative: false`, emits no empty-callable + candidate, and separates structural-review targets. +- Markdown links pass; stale wording passed during product/operations review. + +## Test Delta And CI Integrity + +One focused test module was added and assigned to `shared_foundations`. No test +was removed, skipped, deselected, weakened, or moved between existing lanes. No +workflow, coverage threshold, package configuration, or required-check behavior +changed. + +## Reviewer Results + +- Architecture: PASS. +- Senior engineering: PASS after typed non-reviewed supersession and physical + group-directory enforcement. +- QA: PASS after current callable-owner uniqueness. +- Security: PASS after protected deletion and multiple-effective-owner repairs. +- Product/operations: PASS. +- CI integrity: PASS. +- Documentation: PASS after using a real example and documenting strict + structural exclusions. +- Reuse/deduplication: PASS. +- Test delta: PASS. + +## External Review + +CodeRabbit and exact-head GitHub checks are pending after PR creation. They +supplement, but do not replace, the internal reviews above. + +## Remaining Risks And Follow-Up + +The catalogue is intentionally incomplete until population chunks `03A` through +`03D` merge. Context evidence (`02`), completeness integration (`04`), and any +future changed-line mutation reactivation (`05`) remain separate approved +chunks. Mutation enforcement remains retired. + +## Human Review Focus + +Review candidate-versus-reviewed authority, protected partition bootstrap and +future trusted-base custody, remap carry-forward/effective-owner rules, strict +structural-only behavior, and the single lane assignment. + +## Human Merge Ownership + +- [x] Intent, scope, and non-goals are explicit. +- [x] Deterministic local evidence passes. +- [x] Required internal reviewers pass. +- [ ] External review findings are addressed. +- [ ] Exact-head GitHub checks pass. +- [ ] The user explicitly approves this PR for merge. diff --git a/.ci/behavior-ownership/README.md b/.ci/behavior-ownership/README.md new file mode 100644 index 00000000..a72ee14f --- /dev/null +++ b/.ci/behavior-ownership/README.md @@ -0,0 +1,40 @@ +# Behavior Ownership Catalogue + +This directory holds reviewed engineering evidence about which exact tests own +which executable behavior. It does not grant Workstream product authority and +does not currently activate mutation testing. + +`partition.v1.json` is the sole target-to-population-group partition. It lists +every eligible non-`__init__` Python target exactly once, binds the protected +base commit used to create it, and carries a digest over that authority data. +Population work must load this artifact from protected `main` or the approved +foundation commit. A branch-local replacement, relocation, duplicate, wildcard, +or digest mismatch fails validation. + +Catalogue records belong under one group directory: `auth/`, `artifacts/`, +`lifecycle/`, or `shared/`. The JSON Schema is +[`scripts/behavior-ownership.schema.json`](../../scripts/behavior-ownership.schema.json). +Examples are illustrative only and never count as reviewed ownership. + +Statuses are deliberately separate: + +- `candidate` is deterministic discovery output awaiting human review. +- `reviewed` binds exact AST callables, collected pytest nodes, observable + outcomes, real boundaries, and reviewers. +- `structural_only` requires a reviewed reason and is valid only when the target + has no executable callable or module-level runtime behavior. Calls, branches, + loops, raises, awaits, mutation, I/O, SQL, validators, and other runtime side + effects fail validation. Structural records cannot contain callable or test + fields. + +Run the tooling from `backend/`: + +```bash +.venv/bin/python -m scripts.behavior_ownership inventory +.venv/bin/python -m scripts.behavior_ownership generate --group auth +.venv/bin/python -m scripts.behavior_ownership validate +``` + +Validation reports unresolved targets while the catalogue is being populated. +An empty catalogue is therefore explicit and non-authoritative, not silently +complete. diff --git a/.ci/behavior-ownership/examples/reviewed.example.json b/.ci/behavior-ownership/examples/reviewed.example.json new file mode 100644 index 00000000..0d6433c4 --- /dev/null +++ b/.ci/behavior-ownership/examples/reviewed.example.json @@ -0,0 +1,20 @@ +{ + "behavior_id": "example.hashing.sha256", + "boundaries": [], + "callables": [ + "app.core.hashing.canonical_json_hash" + ], + "group": "shared", + "outcomes": [ + "return" + ], + "reviewed_by": [ + "example-reviewer" + ], + "schema": "workstream.behavior-ownership.v1", + "status": "reviewed", + "target": "backend/app/core/hashing.py", + "tests": [ + "backend/tests/test_checkers.py::test_canonical_json_hash_rejects_non_finite_numbers" + ] +} diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json new file mode 100644 index 00000000..25f2b7f9 --- /dev/null +++ b/.ci/behavior-ownership/partition.v1.json @@ -0,0 +1,699 @@ +{ + "assignments": [ + { + "group": "artifacts", + "target": "backend/app/adapters/artifacts/internal_workers.py" + }, + { + "group": "artifacts", + "target": "backend/app/adapters/artifacts/local.py" + }, + { + "group": "artifacts", + "target": "backend/app/adapters/artifacts/s3_compatible.py" + }, + { + "group": "shared", + "target": "backend/app/adapters/auth/dev.py" + }, + { + "group": "shared", + "target": "backend/app/adapters/auth/flow.py" + }, + { + "group": "shared", + "target": "backend/app/adapters/auth/metrics.py" + }, + { + "group": "lifecycle", + "target": "backend/app/adapters/project_agents/openai_agent_sdk.py" + }, + { + "group": "shared", + "target": "backend/app/api/deps/api_controls.py" + }, + { + "group": "auth", + "target": "backend/app/api/deps/auth.py" + }, + { + "group": "shared", + "target": "backend/app/api/deps/authorization.py" + }, + { + "group": "shared", + "target": "backend/app/api/deps/rate_controls.py" + }, + { + "group": "shared", + "target": "backend/app/api/router.py" + }, + { + "group": "auth", + "target": "backend/app/api/routes/auth.py" + }, + { + "group": "shared", + "target": "backend/app/api/routes/health.py" + }, + { + "group": "shared", + "target": "backend/app/core/api_controls.py" + }, + { + "group": "auth", + "target": "backend/app/core/auth.py" + }, + { + "group": "shared", + "target": "backend/app/core/cancellation.py" + }, + { + "group": "shared", + "target": "backend/app/core/config.py" + }, + { + "group": "shared", + "target": "backend/app/core/file_locks.py" + }, + { + "group": "shared", + "target": "backend/app/core/hashing.py" + }, + { + "group": "shared", + "target": "backend/app/core/permissions.py" + }, + { + "group": "lifecycle", + "target": "backend/app/core/project_agents.py" + }, + { + "group": "shared", + "target": "backend/app/core/s3_validation.py" + }, + { + "group": "shared", + "target": "backend/app/db/base.py" + }, + { + "group": "shared", + "target": "backend/app/db/errors.py" + }, + { + "group": "shared", + "target": "backend/app/db/models.py" + }, + { + "group": "shared", + "target": "backend/app/db/session.py" + }, + { + "group": "shared", + "target": "backend/app/interfaces/artifact_operations.py" + }, + { + "group": "shared", + "target": "backend/app/interfaces/artifacts.py" + }, + { + "group": "auth", + "target": "backend/app/interfaces/auth.py" + }, + { + "group": "artifacts", + "target": "backend/app/interfaces/external_services.py" + }, + { + "group": "lifecycle", + "target": "backend/app/interfaces/project_agents.py" + }, + { + "group": "shared", + "target": "backend/app/main.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/legacy_classification.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/models.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/repository.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/schemas.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/service.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/service_identities.py" + }, + { + "group": "shared", + "target": "backend/app/modules/actors/service_identity_migration.py" + }, + { + "group": "shared", + "target": "backend/app/modules/api_controls/models.py" + }, + { + "group": "shared", + "target": "backend/app/modules/api_controls/repository.py" + }, + { + "group": "shared", + "target": "backend/app/modules/api_controls/service.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/authorization.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_bindings.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_docx.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_extraction.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_extraction_service.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_extraction_worker.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_formats.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_images.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_materialization.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_ooxml.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_pdf.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_pptx.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_setup.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_sufficiency_material.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/guide_xlsx.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/metrics.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/models.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/operator.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/pre_submit_evidence.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/preparation.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/repository.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/router.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/schemas.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/service.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/sources.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_archive.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_manifest.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_materialization.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/zip_safety.py" + }, + { + "group": "shared", + "target": "backend/app/modules/audit/repository.py" + }, + { + "group": "shared", + "target": "backend/app/modules/audit/schemas.py" + }, + { + "group": "shared", + "target": "backend/app/modules/audit/service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/admin_schemas.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/admin_service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/catalogue.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/kernel.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/lifecycle_schemas.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/lifecycle_service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/models.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/pagination.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/policy.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/prepared.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/project_role_schemas.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/project_role_service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/read_service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/repository.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/review_contracts.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/router.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/runtime.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/schemas.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/service.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/service_actor_schemas.py" + }, + { + "group": "auth", + "target": "backend/app/modules/authorization/service_actor_service.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/catalogue.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/compiler.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/effective_plan.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/gate_queue.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/models.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/pre_review_gate.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/pre_submit_defaults.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/pre_submit_execution.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/repository.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/router.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/runner.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/schemas.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/checkers/service.py" + }, + { + "group": "shared", + "target": "backend/app/modules/compensation/models.py" + }, + { + "group": "shared", + "target": "backend/app/modules/compensation/schemas.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/contributions/models.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/contributions/schemas.py" + }, + { + "group": "shared", + "target": "backend/app/modules/outbox/models.py" + }, + { + "group": "shared", + "target": "backend/app/modules/outbox/repository.py" + }, + { + "group": "shared", + "target": "backend/app/modules/outbox/schemas.py" + }, + { + "group": "shared", + "target": "backend/app/modules/outbox/service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/authorization_reads.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/create_repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/create_router.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/create_service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/guide_mutation_repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/guide_mutation_router.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/guide_mutation_service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/guide_setup_continuation.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/models.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/policy_lineage.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/policy_mutation_replay_repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/policy_mutation_router.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/policy_mutation_service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/post_submit_policy.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/router.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/schemas.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/setup_queue.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/submission_policy_mutation_repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/submission_policy_mutation_service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/sufficiency_mutation_repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/projects/sufficiency_mutation_service.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/reviews/models.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/reviews/repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/reviews/schemas.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/authorization.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/lifecycle.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/models.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/pre_submit_context.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/repository.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/router.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/schemas.py" + }, + { + "group": "lifecycle", + "target": "backend/app/modules/tasks/service.py" + }, + { + "group": "auth", + "target": "backend/app/schemas/auth.py" + }, + { + "group": "shared", + "target": "backend/app/schemas/health.py" + }, + { + "group": "shared", + "target": "backend/app/workers/artifacts.py" + }, + { + "group": "shared", + "target": "backend/app/workers/async_runner.py" + }, + { + "group": "shared", + "target": "backend/app/workers/celery_app.py" + }, + { + "group": "artifacts", + "target": "backend/app/workers/checkers.py" + }, + { + "group": "shared", + "target": "backend/app/workers/errors.py" + }, + { + "group": "lifecycle", + "target": "backend/app/workers/project_setup.py" + }, + { + "group": "lifecycle", + "target": "backend/app/workers/task_settings.py" + }, + { + "group": "shared", + "target": "backend/scripts/api_contract_e2e.py" + }, + { + "group": "shared", + "target": "backend/scripts/behavior_ownership.py" + }, + { + "group": "shared", + "target": "backend/scripts/bootstrap_access_administrator.py" + }, + { + "group": "shared", + "target": "backend/scripts/check_guide_extractor_dependencies.py" + }, + { + "group": "shared", + "target": "backend/scripts/coverage_policy.py" + }, + { + "group": "shared", + "target": "backend/scripts/legacy_actor_classification.py" + }, + { + "group": "shared", + "target": "backend/scripts/merge_test_lane_evidence.py" + }, + { + "group": "shared", + "target": "backend/scripts/mutation_policy.py" + }, + { + "group": "shared", + "target": "backend/scripts/run_isolated_tests.py" + }, + { + "group": "shared", + "target": "backend/scripts/run_test_lanes.py" + }, + { + "group": "shared", + "target": "backend/scripts/service_actor_identity_mapping.py" + }, + { + "group": "shared", + "target": "backend/scripts/validate_test_lane_evidence.py" + }, + { + "group": "shared", + "target": "backend/scripts/week2_api_e2e.py" + } + ], + "authority_digest": "9897223853b489bc7fa998910e84a9fc260849d1ea7292d796e95438094137e3", + "protected_base_commit": "99c0aaf04efd36c7ac4af4aeec2e9d810f012305", + "schema": "workstream.behavior-ownership-partition.v1" +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2cc7d7a..5578aca4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,12 +70,27 @@ Different initiatives may proceed concurrently in separate branches or worktrees. If another pull request changes the base, inspect the new delta and rerun affected checks; unchanged evidence does not need ceremonial repetition. -## Behavior Mutation Claims +## Behavior Ownership Catalogue The hosted behavior-mutation check is temporarily retired because its callable-wide survivor policy blocked declaration-only changes by mutating -unchanged executable lines. Do not treat a behavior claim as a required PR -gate while the replacement is being designed. +unchanged executable lines. Do not treat a historical behavior claim or a +catalogue candidate as a required PR gate. + +The versioned catalogue foundation lives under `.ci/behavior-ownership/`. +Contributors can run its read-only inventory, candidate generator, and validator +from `backend/`: + +```bash +.venv/bin/python -m scripts.behavior_ownership inventory +.venv/bin/python -m scripts.behavior_ownership generate +.venv/bin/python -m scripts.behavior_ownership validate +``` + +Candidate output is discovery assistance only. It cannot become reviewed +ownership without an explicit catalogue record and the required human and +internal review. The validator reports unresolved targets while population is +in progress; that report does not block ordinary contributions. Existing claim, schema, policy, dependency, and evidence files remain as historical design input. They do not replace focused tests, hosted Backend diff --git a/backend/scripts/behavior_ownership.py b/backend/scripts/behavior_ownership.py new file mode 100644 index 00000000..17649461 --- /dev/null +++ b/backend/scripts/behavior_ownership.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python3 +"""Generate and validate repository behavior-ownership catalogue data.""" + +from __future__ import annotations + +import argparse +import ast +import hashlib +import json +from pathlib import Path +import subprocess +import sys +from typing import Any, Iterable + +from jsonschema import Draft202012Validator + +from scripts.mutation_policy import CALLABLE_RE +from scripts.mutation_policy import OBSERVABLE_OUTCOMES +from scripts.mutation_policy import REAL_BOUNDARIES +from scripts.mutation_policy import TEST_NODE_RE +from scripts.mutation_policy import _callable_spans +from scripts.mutation_policy import _eligible_target +from scripts.mutation_policy import _safe_path +from scripts.mutation_policy import _regular_repository_file +from scripts.mutation_policy import changed_callables + + +ROOT = Path(__file__).resolve().parents[2] +SCHEMA_PATH = "scripts/behavior-ownership.schema.json" +PARTITION_PATH = ".ci/behavior-ownership/partition.v1.json" +PARTITION_SCHEMA = "workstream.behavior-ownership-partition.v1" +CATALOGUE_SCHEMA = "workstream.behavior-ownership.v1" +GROUPS = ("auth", "artifacts", "lifecycle", "shared") + + +class BehaviorOwnershipError(RuntimeError): + """Catalogue input is unsafe, stale, incomplete, or ambiguous.""" + + +def _json_bytes(value: Any) -> bytes: + return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode() + + +def _digest(value: Any) -> str: + return hashlib.sha256(_json_bytes(value)).hexdigest() + + +def _git(root: Path, *arguments: str) -> str: + result = subprocess.run( + ["git", *arguments], cwd=root, text=True, capture_output=True, check=False + ) + if result.returncode: + raise BehaviorOwnershipError(f"git_command_failed:{arguments[0]}") + return result.stdout.strip() + + +def _git_show_optional(root: Path, revision: str, path: str) -> str | None: + result = subprocess.run( + ["git", "show", f"{revision}:{path}"], + cwd=root, + text=True, + capture_output=True, + check=False, + ) + return result.stdout if result.returncode == 0 else None + + +def eligible_targets(root: Path = ROOT) -> list[str]: + """Return every tracked eligible module using mutation policy eligibility.""" + paths = _git(root, "ls-files", "backend/app", "backend/scripts").splitlines() + return sorted(path for path in paths if _eligible_target(path)) + + +def module_name(target: str) -> str: + """Convert an eligible repository path to its import-qualified module.""" + _safe_path(target) + if not _eligible_target(target): + raise BehaviorOwnershipError("ineligible_target") + return target.removeprefix("backend/").removesuffix(".py").replace("/", ".") + + +def callable_names(root: Path, target: str) -> list[str]: + """Read exact callable names through mutation policy's AST implementation.""" + if not _regular_repository_file(root, target): + raise BehaviorOwnershipError("unsafe_or_missing_target") + try: + source = (root / _safe_path(target)).read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError) as exc: + raise BehaviorOwnershipError("unsafe_or_missing_target") from exc + return sorted(item[2] for item in _callable_spans(source, module_name(target))[0]) + + +def changed_callable_names(root: Path, base_sha: str, head_sha: str, target: str) -> list[str]: + """Delegate exact changed-callable derivation to the mutation policy.""" + return changed_callables(root, base_sha, head_sha, target) + + +def group_for_target(target: str) -> str: + """Assign one exact population group without wildcard authority.""" + if "/authorization/" in target or target.endswith("/auth.py"): + return "auth" + if "/artifacts/" in target or any( + token in target for token in ("storage", "archive", "checker", "external_service") + ): + return "artifacts" + if any( + token in target + for token in ("project", "task", "submission", "review", "contribution", "payment") + ): + return "lifecycle" + return "shared" + + +def build_partition(root: Path = ROOT, *, base_commit: str | None = None) -> dict[str, Any]: + """Build the canonical deterministic path partition.""" + resolved_base = base_commit or _git(root, "rev-parse", "HEAD") + assignments = [ + {"group": group_for_target(target), "target": target} + for target in eligible_targets(root) + ] + authority = { + "schema": PARTITION_SCHEMA, + "protected_base_commit": resolved_base, + "assignments": assignments, + } + return {**authority, "authority_digest": _digest(authority)} + + +def _read_json(path: Path, error: str) -> Any: + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise BehaviorOwnershipError(error) from exc + + +def validate_partition( + root: Path = ROOT, + *, + partition_path: Path | None = None, + trusted_revision: str | None = "origin/main", +) -> dict[str, str]: + """Validate exact partition completeness, digest, location, and custody.""" + expected_path = root / PARTITION_PATH + path = partition_path or expected_path + if not _regular_repository_file(root, PARTITION_PATH): + raise BehaviorOwnershipError("unsafe_or_missing_partition") + try: + if path.resolve() != expected_path.resolve(): + raise BehaviorOwnershipError("relocated_partition") + except OSError as exc: + raise BehaviorOwnershipError("missing_partition") from exc + value = _read_json(path, "invalid_partition_json") + if not isinstance(value, dict) or set(value) != { + "schema", + "protected_base_commit", + "assignments", + "authority_digest", + }: + raise BehaviorOwnershipError("invalid_partition_shape") + if value["schema"] != PARTITION_SCHEMA: + raise BehaviorOwnershipError("unsupported_partition_schema") + authority = {key: value[key] for key in value if key != "authority_digest"} + if value["authority_digest"] != _digest(authority): + raise BehaviorOwnershipError("partition_digest_mismatch") + protected_base = value["protected_base_commit"] + if not isinstance(protected_base, str) or len(protected_base) != 40 or any( + character not in "0123456789abcdef" for character in protected_base + ): + raise BehaviorOwnershipError("invalid_partition_base_commit") + if _git(root, "rev-parse", "--verify", f"{protected_base}^{{commit}}") != protected_base: + raise BehaviorOwnershipError("missing_partition_base_commit") + assignments = value["assignments"] + if not isinstance(assignments, list): + raise BehaviorOwnershipError("invalid_partition_assignments") + targets: list[str] = [] + for item in assignments: + if ( + not isinstance(item, dict) + or set(item) != {"group", "target"} + or item["group"] not in GROUPS + or not isinstance(item["target"], str) + or not _eligible_target(item["target"]) + ): + raise BehaviorOwnershipError("invalid_partition_assignment") + targets.append(item["target"]) + if len(targets) != len(set(targets)): + raise BehaviorOwnershipError("duplicate_partition_target") + expected = eligible_targets(root) + if sorted(targets) != expected: + raise BehaviorOwnershipError("partition_target_mismatch") + expected_assignments = [ + {"group": group_for_target(target), "target": target} for target in expected + ] + if assignments != expected_assignments: + raise BehaviorOwnershipError("partition_assignment_mismatch") + if trusted_revision is not None: + trusted = _git_show_optional(root, trusted_revision, PARTITION_PATH) + if trusted is not None: + try: + trusted_value = json.loads(trusted) + except json.JSONDecodeError as exc: + raise BehaviorOwnershipError("invalid_trusted_partition") from exc + if trusted_value != value: + raise BehaviorOwnershipError("untrusted_partition_change") + else: + try: + _git(root, "merge-base", "--is-ancestor", protected_base, "HEAD") + except BehaviorOwnershipError as exc: + raise BehaviorOwnershipError("invalid_partition_ancestry") from exc + return {item["target"]: item["group"] for item in assignments} + + +def load_schema(root: Path = ROOT) -> dict[str, Any]: + """Load and verify the catalogue JSON Schema.""" + if not _regular_repository_file(root, SCHEMA_PATH): + raise BehaviorOwnershipError("unsafe_or_missing_catalogue_schema") + value = _read_json(root / SCHEMA_PATH, "invalid_catalogue_schema_json") + try: + Draft202012Validator.check_schema(value) + except Exception as exc: # jsonschema exposes version-specific subclasses + raise BehaviorOwnershipError("invalid_catalogue_schema") from exc + return value + + +def _catalogue_files(root: Path) -> list[Path]: + base = root / ".ci/behavior-ownership" + return sorted( + path + for path in base.glob("*/*.json") + if path.is_file() and "examples" not in path.parts + ) + + +def _is_strictly_structural(root: Path, target: str) -> bool: + if not _regular_repository_file(root, target): + raise BehaviorOwnershipError("unsafe_or_missing_target") + try: + tree = ast.parse((root / target).read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, SyntaxError) as exc: + raise BehaviorOwnershipError("invalid_target_syntax") from exc + forbidden = ( + ast.FunctionDef, + ast.AsyncFunctionDef, + ast.Call, + ast.Await, + ast.Yield, + ast.YieldFrom, + ast.If, + ast.For, + ast.AsyncFor, + ast.While, + ast.With, + ast.AsyncWith, + ast.Try, + ast.Raise, + ast.Match, + ast.NamedExpr, + ast.AugAssign, + ast.Delete, + ) + return not any(isinstance(node, forbidden) for node in ast.walk(tree)) + + +def _validate_record_semantics(root: Path, record: dict[str, Any]) -> None: + target = record["target"] + actual_callables = set(callable_names(root, target)) + status = record["status"] + if status != "reviewed" and "supersedes_behavior_id" in record: + raise BehaviorOwnershipError("invalid_non_reviewed_supersession") + if status == "structural_only": + if actual_callables or not _is_strictly_structural(root, target): + raise BehaviorOwnershipError("executable_structural_only") + return + for callable_name in record["callables"]: + if CALLABLE_RE.fullmatch(callable_name) is None or callable_name not in actual_callables: + raise BehaviorOwnershipError("missing_catalogue_callable") + if len(record["callables"]) != len(set(record["callables"])): + raise BehaviorOwnershipError("duplicate_catalogue_callable") + if status == "reviewed": + if not set(record["outcomes"]).issubset(OBSERVABLE_OUTCOMES): + raise BehaviorOwnershipError("invalid_catalogue_outcome") + if not set(record["boundaries"]).issubset(REAL_BOUNDARIES): + raise BehaviorOwnershipError("invalid_catalogue_boundary") + if any(TEST_NODE_RE.fullmatch(node) is None for node in record["tests"]): + raise BehaviorOwnershipError("invalid_catalogue_test") + for node in record["tests"]: + test_path = node.split("::", 1)[0] + if not _regular_repository_file(root, test_path): + raise BehaviorOwnershipError("missing_catalogue_test") + + +def _catalogue_at_revision(root: Path, revision: str) -> list[dict[str, Any]]: + output = _git(root, "ls-tree", "-r", "--name-only", revision, "--", ".ci/behavior-ownership") + records: list[dict[str, Any]] = [] + for path in output.splitlines(): + if not path.endswith(".json") or "/examples/" in path or path == PARTITION_PATH: + continue + source = _git_show_optional(root, revision, path) + if source is None: + raise BehaviorOwnershipError("missing_protected_record") + try: + value = json.loads(source) + except json.JSONDecodeError as exc: + raise BehaviorOwnershipError("invalid_protected_record") from exc + if isinstance(value, dict): + records.append(value) + return records + + +def _validate_remaps( + root: Path, records: list[dict[str, Any]], *, base_sha: str, head_sha: str +) -> None: + protected = _catalogue_at_revision(root, base_sha) + protected_by_id: dict[str, list[dict[str, Any]]] = {} + for item in protected: + protected_by_id.setdefault(item.get("behavior_id", ""), []).append(item) + current_by_id = {item["behavior_id"]: item for item in records} + for behavior_id, owners in protected_by_id.items(): + if len(owners) != 1: + raise BehaviorOwnershipError("multiple_protected_owners") + current = current_by_id.get(behavior_id) + if current is not None and current != owners[0]: + raise BehaviorOwnershipError("protected_owner_replacement") + for record in records: + superseded_id = record.get("supersedes_behavior_id") + if superseded_id is None: + continue + owners = protected_by_id.get(superseded_id, []) + if len(owners) != 1 or owners[0].get("status") != "reviewed": + raise BehaviorOwnershipError("invalid_remap_ancestry") + old = owners[0] + if _git_show_optional(root, head_sha, old["target"]) is not None: + raise BehaviorOwnershipError("protected_location_still_exists") + if _git_show_optional(root, head_sha, record["target"]) is None: + raise BehaviorOwnershipError("missing_remap_location") + for field in ("tests", "outcomes", "boundaries"): + if not set(old[field]).issubset(record[field]): + raise BehaviorOwnershipError("narrowed_remap_evidence") + for behavior_id, owners in protected_by_id.items(): + current = current_by_id.get(behavior_id) + replacements = [ + item for item in records if item.get("supersedes_behavior_id") == behavior_id + ] + effective_count = int(current == owners[0]) + len(replacements) + if effective_count == 0: + raise BehaviorOwnershipError("missing_effective_owner") + if effective_count > 1: + raise BehaviorOwnershipError("multiple_effective_owners") + + +def validate_catalogue( + root: Path = ROOT, + *, + group: str | None = None, + trusted_revision: str = "origin/main", + head_revision: str = "HEAD", + run_tests: bool = False, +) -> dict[str, Any]: + """Validate catalogue shape, exact bindings, identities, and completeness.""" + partition = validate_partition(root, trusted_revision=trusted_revision) + if group is not None and group not in GROUPS: + raise BehaviorOwnershipError("invalid_group") + validator = Draft202012Validator(load_schema(root)) + all_records: list[dict[str, Any]] = [] + for path in _catalogue_files(root): + relative = path.relative_to(root).as_posix() + if not _regular_repository_file(root, relative): + raise BehaviorOwnershipError("unsafe_catalogue_record") + value = _read_json(path, "invalid_catalogue_json") + errors = sorted(validator.iter_errors(value), key=lambda item: list(item.path)) + if errors: + raise BehaviorOwnershipError(f"invalid_catalogue_record:{errors[0].json_path}") + if partition.get(value["target"]) != value["group"]: + raise BehaviorOwnershipError("wrong_catalogue_group") + if path.parent.name != value["group"]: + raise BehaviorOwnershipError("misplaced_catalogue_record") + _validate_record_semantics(root, value) + all_records.append(value) + identities = [item["behavior_id"] for item in all_records] + if len(identities) != len(set(identities)): + raise BehaviorOwnershipError("duplicate_behavior_id") + superseded = [ + item["supersedes_behavior_id"] + for item in all_records + if "supersedes_behavior_id" in item + ] + if len(superseded) != len(set(superseded)): + raise BehaviorOwnershipError("duplicate_supersession") + callable_owners = [ + (item["target"], callable_name) + for item in all_records + if item["status"] == "reviewed" + for callable_name in item["callables"] + ] + if len(callable_owners) != len(set(callable_owners)): + raise BehaviorOwnershipError("multiple_effective_callable_owners") + partition_value = _read_json(root / PARTITION_PATH, "invalid_partition_json") + _validate_remaps( + root, + all_records, + base_sha=partition_value["protected_base_commit"], + head_sha=head_revision, + ) + records = [item for item in all_records if group in (None, item["group"])] + reviewed_records = [item for item in records if item["status"] == "reviewed"] + if reviewed_records: + collection_code = _run_test_nodes(root, reviewed_records, collect_only=True) + if collection_code: + raise BehaviorOwnershipError("stale_catalogue_test") + if run_tests and _run_test_nodes(root, reviewed_records, collect_only=False): + raise BehaviorOwnershipError("owned_test_failure") + covered = {item["target"] for item in records} + expected = {target for target, assigned in partition.items() if group in (None, assigned)} + return { + "schema": CATALOGUE_SCHEMA, + "group": group, + "reviewed": sum(item["status"] == "reviewed" for item in records), + "candidates": sum(item["status"] == "candidate" for item in records), + "structural_only": sum(item["status"] == "structural_only" for item in records), + "unresolved": sorted(expected - covered), + "complete": expected == covered and all(item["status"] != "candidate" for item in records), + } + + +def generate_candidates(root: Path = ROOT, *, group: str | None = None) -> dict[str, Any]: + """Generate deterministic non-authoritative candidates and unresolved paths.""" + partition = validate_partition(root) + if group is not None and group not in GROUPS: + raise BehaviorOwnershipError("invalid_group") + candidates = [] + unresolved = [] + for target, assigned in sorted(partition.items()): + if group not in (None, assigned): + continue + names = callable_names(root, target) + if not names: + unresolved.append( + {"group": assigned, "target": target, "reason": "structural review required"} + ) + continue + candidates.append( + { + "schema": CATALOGUE_SCHEMA, + "behavior_id": "candidate:" + target.removeprefix("backend/").removesuffix(".py").replace("/", "."), + "status": "candidate", + "group": assigned, + "target": target, + "callables": names, + "unresolved_reason": "reviewed tests, outcomes, and boundaries required", + } + ) + return { + "schema": CATALOGUE_SCHEMA, + "authoritative": False, + "candidates": candidates, + "unresolved": unresolved, + } + + +def _run_test_nodes( + root: Path, records: Iterable[dict[str, Any]], *, collect_only: bool +) -> int: + nodes = sorted( + {node for item in records if item.get("status") == "reviewed" for node in item["tests"]} + ) + if not nodes: + return 0 + arguments = [sys.executable, "-m", "pytest", "-q"] + if collect_only: + arguments.append("--collect-only") + result = subprocess.run( + [*arguments, *[node.removeprefix("backend/") for node in nodes]], + cwd=root / "backend", + check=False, + ) + return result.returncode + + +def run_owned_tests(root: Path, records: Iterable[dict[str, Any]]) -> int: + """Run exact reviewed test nodes; candidates and structural records are excluded.""" + return _run_test_nodes(root, records, collect_only=False) + + +def _main() -> int: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + subparsers.add_parser("inventory") + candidate_parser = subparsers.add_parser("generate") + candidate_parser.add_argument("--group", choices=GROUPS) + validate_parser = subparsers.add_parser("validate") + validate_parser.add_argument("--group", choices=GROUPS) + validate_parser.add_argument("--trusted-revision") + validate_parser.add_argument("--head-revision", default="HEAD") + validate_parser.add_argument("--run-owned-tests", action="store_true") + partition_parser = subparsers.add_parser("partition") + partition_parser.add_argument("--base-commit") + args = parser.parse_args() + try: + if args.command == "inventory": + result: Any = eligible_targets() + elif args.command == "generate": + result = generate_candidates(group=args.group) + elif args.command == "partition": + result = build_partition(base_commit=args.base_commit) + else: + result = validate_catalogue( + group=args.group, + trusted_revision=args.trusted_revision or "origin/main", + head_revision=args.head_revision, + run_tests=args.run_owned_tests, + ) + except BehaviorOwnershipError as exc: + print(f"behavior_ownership_error:{exc}", file=sys.stderr) + return 2 + print(_json_bytes(result).decode(), end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(_main()) diff --git a/backend/scripts/run_test_lanes.py b/backend/scripts/run_test_lanes.py index 2b95833c..9bc03d18 100644 --- a/backend/scripts/run_test_lanes.py +++ b/backend/scripts/run_test_lanes.py @@ -132,6 +132,7 @@ class TestLane: "tests/test_audit.py", "tests/test_auth.py", "tests/test_authorization.py", + "tests/test_behavior_ownership.py", "tests/test_artifact_admission.py", "tests/test_artifact_operator_api.py", "tests/test_artifact_recovery.py", diff --git a/backend/tests/test_behavior_ownership.py b/backend/tests/test_behavior_ownership.py new file mode 100644 index 00000000..4a065f2f --- /dev/null +++ b/backend/tests/test_behavior_ownership.py @@ -0,0 +1,809 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +import subprocess +import sys + +import pytest +from jsonschema import Draft202012Validator + +from scripts import behavior_ownership as ownership + + +def _write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def _partition(targets: list[str]) -> dict[str, object]: + authority = { + "schema": ownership.PARTITION_SCHEMA, + "protected_base_commit": "a" * 40, + "assignments": [ + {"group": ownership.group_for_target(target), "target": target} + for target in targets + ], + } + return {**authority, "authority_digest": ownership._digest(authority)} + + +def _mock_partition_git(monkeypatch: pytest.MonkeyPatch) -> None: + 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: None) + + +def test_repository_partition_is_exact_deterministic_and_digest_bound() -> None: + value = json.loads((ownership.ROOT / ownership.PARTITION_PATH).read_text()) + mapping = ownership.validate_partition() + assert len(mapping) == len(ownership.eligible_targets()) + assert list(mapping) == ownership.eligible_targets() + authority = {key: value[key] for key in value if key != "authority_digest"} + assert value["authority_digest"] == hashlib.sha256(ownership._json_bytes(authority)).hexdigest() + assert ownership.build_partition(base_commit=value["protected_base_commit"]) == value + + +def test_inventory_reuses_mutation_policy_eligibility() -> None: + targets = ownership.eligible_targets() + assert targets == sorted(set(targets)) + assert "backend/scripts/mutation_policy.py" in targets + assert all(target.endswith(".py") and not target.endswith("/__init__.py") for target in targets) + + +@pytest.mark.parametrize( + ("target", "group"), + [ + ("backend/app/modules/authorization/kernel.py", "auth"), + ("backend/app/modules/artifacts/service.py", "artifacts"), + ("backend/app/modules/projects/service.py", "lifecycle"), + ("backend/app/core/config.py", "shared"), + ], +) +def test_partition_group_assignment_is_single_and_deterministic(target: str, group: str) -> None: + assert ownership.group_for_target(target) == group + + +def test_module_and_callable_inventory_delegate_to_policy(tmp_path: Path) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def outer():\n def inner():\n return 1\n return inner()\n") + assert ownership.module_name(target) == "scripts.example" + assert ownership.callable_names(tmp_path, target) == [ + "scripts.example.outer", + "scripts.example.outer.inner", + ] + with pytest.raises(ownership.BehaviorOwnershipError, match="ineligible_target"): + ownership.module_name("README.md") + with pytest.raises(ownership.BehaviorOwnershipError, match="unsafe_or_missing_target"): + ownership.callable_names(tmp_path, "backend/scripts/missing.py") + + +@pytest.mark.parametrize( + ("mutator", "error"), + [ + (lambda value: value.update(schema="wrong"), "unsupported_partition_schema"), + (lambda value: value.update(authority_digest="0" * 64), "partition_digest_mismatch"), + (lambda value: value.update(assignments="wrong"), "partition_digest_mismatch"), + (lambda value: value["assignments"].append(value["assignments"][0]), "partition_digest_mismatch"), + ], +) +def test_partition_tampering_fails_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, mutator, error: str +) -> None: + target = "backend/scripts/example.py" + value = _partition([target]) + mutator(value) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: [target]) + _mock_partition_git(monkeypatch) + with pytest.raises(ownership.BehaviorOwnershipError, match=error): + ownership.validate_partition(tmp_path, trusted_revision=None) + + +def test_partition_rejects_duplicate_missing_wrong_group_and_relocation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = "backend/scripts/example.py" + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: [target]) + _mock_partition_git(monkeypatch) + value = _partition([target]) + value["assignments"].append(value["assignments"][0]) + authority = {key: value[key] for key in value if key != "authority_digest"} + value["authority_digest"] = ownership._digest(authority) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + with pytest.raises(ownership.BehaviorOwnershipError, match="duplicate_partition_target"): + ownership.validate_partition(tmp_path, trusted_revision=None) + value = _partition([]) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + with pytest.raises(ownership.BehaviorOwnershipError, match="partition_target_mismatch"): + ownership.validate_partition(tmp_path, trusted_revision=None) + with pytest.raises(ownership.BehaviorOwnershipError, match="relocated_partition"): + ownership.validate_partition( + tmp_path, partition_path=tmp_path / "copy.json", trusted_revision=None + ) + + +def test_partition_rejects_untrusted_branch_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + target = "backend/scripts/example.py" + value = _partition([target]) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: [target]) + 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({**value, "authority_digest": "0" * 64}), + ) + with pytest.raises(ownership.BehaviorOwnershipError, match="untrusted_partition_change"): + ownership.validate_partition(tmp_path, trusted_revision="main") + + +def test_schema_separates_reviewed_candidate_and_structural_records() -> None: + validator = Draft202012Validator(ownership.load_schema()) + reviewed = json.loads( + (ownership.ROOT / ".ci/behavior-ownership/examples/reviewed.example.json").read_text() + ) + assert not list(validator.iter_errors(reviewed)) + candidate = { + "schema": ownership.CATALOGUE_SCHEMA, + "behavior_id": "candidate:example", + "status": "candidate", + "group": "shared", + "target": "backend/scripts/example.py", + "callables": ["scripts.example.run"], + "unresolved_reason": "review required", + } + assert not list(validator.iter_errors(candidate)) + structural = { + "schema": ownership.CATALOGUE_SCHEMA, + "behavior_id": "structural:example", + "status": "structural_only", + "group": "shared", + "target": "backend/scripts/example.py", + "reason": "constants only", + "reviewed_by": ["reviewer"], + } + assert not list(validator.iter_errors(structural)) + structural["tests"] = ["backend/tests/test_example.py::test_value"] + assert list(validator.iter_errors(structural)) + + +def test_generator_is_deterministic_candidate_only(monkeypatch: pytest.MonkeyPatch) -> None: + mapping = { + "backend/scripts/a.py": "shared", + "backend/scripts/b.py": "auth", + } + monkeypatch.setattr(ownership, "validate_partition", lambda root=ownership.ROOT, **kwargs: mapping) + monkeypatch.setattr(ownership, "callable_names", lambda root, target: [target + ":run"]) + first = ownership.generate_candidates(group="shared") + assert first == ownership.generate_candidates(group="shared") + assert first["authoritative"] is False + assert [item["target"] for item in first["candidates"]] == ["backend/scripts/a.py"] + assert all(item["status"] == "candidate" for item in first["candidates"]) + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_group"): + ownership.generate_candidates(group="unknown") + + +def test_record_semantics_reject_missing_callable_and_executable_structural( + tmp_path: Path, +) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n") + reviewed = { + "status": "reviewed", + "target": target, + "callables": ["scripts.example.missing"], + "tests": ["backend/tests/test_example.py::test_run"], + "outcomes": ["return"], + "boundaries": [], + } + with pytest.raises(ownership.BehaviorOwnershipError, match="missing_catalogue_callable"): + ownership._validate_record_semantics(tmp_path, reviewed) + with pytest.raises(ownership.BehaviorOwnershipError, match="executable_structural_only"): + ownership._validate_record_semantics(tmp_path, {"status": "structural_only", "target": target}) + + +def test_structural_target_has_no_owned_tests(tmp_path: Path) -> None: + assert ownership.run_owned_tests( + tmp_path, + [{"status": "candidate"}, {"status": "structural_only"}], + ) == 0 + + +def test_catalogue_empty_state_is_explicitly_incomplete(monkeypatch: pytest.MonkeyPatch) -> None: + target = "backend/scripts/example.py" + monkeypatch.setattr( + ownership, "validate_partition", lambda root=ownership.ROOT, **kwargs: {target: "shared"} + ) + monkeypatch.setattr(ownership, "_catalogue_files", lambda root: []) + result = ownership.validate_catalogue() + assert result["complete"] is False + assert result["unresolved"] == [target] + + +def test_changed_callable_parity_delegates_to_policy(monkeypatch: pytest.MonkeyPatch) -> None: + captured: list[object] = [] + + def fake_changed(root: Path, base: str, head: str, target: str) -> list[str]: + captured.extend([root, base, head, target]) + return ["scripts.example.run"] + + monkeypatch.setattr(ownership, "changed_callables", fake_changed) + assert ownership.changed_callable_names(Path("/repo"), "base", "head", "target") == [ + "scripts.example.run" + ] + assert captured == [Path("/repo"), "base", "head", "target"] + assert ownership.OBSERVABLE_OUTCOMES + assert ownership.REAL_BOUNDARIES + + +def _catalogue_record(status: str, target: str, behavior_id: str) -> dict[str, object]: + base: dict[str, object] = { + "schema": ownership.CATALOGUE_SCHEMA, + "behavior_id": behavior_id, + "status": status, + "group": "shared", + "target": target, + } + if status == "reviewed": + base.update( + callables=[ownership.module_name(target) + ".run"], + tests=["backend/tests/test_example.py::test_run"], + outcomes=["return"], + boundaries=[], + reviewed_by=["reviewer"], + ) + elif status == "candidate": + base.update( + callables=[ownership.module_name(target) + ".run"], + unresolved_reason="review required", + ) + else: + base.update(reason="constants only", reviewed_by=["reviewer"]) + return base + + +def test_catalogue_validation_reports_reviewed_candidate_and_structural( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + targets = [ + "backend/scripts/reviewed.py", + "backend/scripts/candidate.py", + "backend/scripts/structural.py", + ] + for target in targets[:2]: + path = tmp_path / target + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + structural = tmp_path / targets[2] + structural.parent.mkdir(parents=True, exist_ok=True) + structural.write_text("VALUE = 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + for index, (status, target) in enumerate(zip(("reviewed", "candidate", "structural_only"), targets)): + _write_json( + tmp_path / f".ci/behavior-ownership/shared/{index}.json", + _catalogue_record(status, target, f"behavior:{index}"), + ) + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared" for target in targets}, + ) + monkeypatch.setattr(ownership, "_validate_remaps", lambda *args, **kwargs: None) + monkeypatch.setattr(ownership, "_run_test_nodes", lambda *args, **kwargs: 0) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition(targets)) + result = ownership.validate_catalogue(tmp_path, group="shared") + assert result == { + "schema": ownership.CATALOGUE_SCHEMA, + "group": "shared", + "reviewed": 1, + "candidates": 1, + "structural_only": 1, + "unresolved": [], + "complete": False, + } + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_group"): + ownership.validate_catalogue(tmp_path, group="wrong") + + +@pytest.mark.parametrize( + ("field", "value", "error"), + [ + ("outcomes", ["unknown"], "invalid_catalogue_outcome"), + ("boundaries", ["mock"], "invalid_catalogue_boundary"), + ("tests", ["not-a-node"], "invalid_catalogue_test"), + ("callables", ["scripts.example.run", "scripts.example.run"], "duplicate_catalogue_callable"), + ], +) +def test_reviewed_semantics_fail_closed( + tmp_path: Path, field: str, value: object, error: str +) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + record = _catalogue_record("reviewed", target, "behavior:example") + record[field] = value + with pytest.raises(ownership.BehaviorOwnershipError, match=error): + ownership._validate_record_semantics(tmp_path, record) + + +def test_catalogue_rejects_schema_and_group_errors( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + record = _catalogue_record("reviewed", target, "behavior:example") + record["unexpected"] = True + record_path = tmp_path / ".ci/behavior-ownership/shared/example.json" + _write_json(record_path, record) + monkeypatch.setattr( + ownership, "validate_partition", lambda root=ownership.ROOT, **kwargs: {target: "shared"} + ) + monkeypatch.setattr(ownership, "_validate_remaps", lambda *args, **kwargs: None) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition([target])) + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_catalogue_record"): + ownership.validate_catalogue(tmp_path) + record.pop("unexpected") + record["group"] = "auth" + _write_json(record_path, record) + with pytest.raises(ownership.BehaviorOwnershipError, match="wrong_catalogue_group"): + ownership.validate_catalogue(tmp_path) + + +def test_catalogue_rejects_duplicate_identity_and_supersession( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + targets = ["backend/scripts/a.py", "backend/scripts/b.py"] + for target in targets: + path = tmp_path / target + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + records = [_catalogue_record("reviewed", target, "same:id") for target in targets] + for index, record in enumerate(records): + _write_json(tmp_path / f".ci/behavior-ownership/shared/{index}.json", record) + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared" for target in targets}, + ) + monkeypatch.setattr(ownership, "_validate_remaps", lambda *args, **kwargs: None) + monkeypatch.setattr(ownership, "_run_test_nodes", lambda *args, **kwargs: 0) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition(targets)) + with pytest.raises(ownership.BehaviorOwnershipError, match="duplicate_behavior_id"): + ownership.validate_catalogue(tmp_path) + for index, record in enumerate(records): + record["behavior_id"] = f"behavior:{index}" + record["supersedes_behavior_id"] = "protected:id" + _write_json(tmp_path / f".ci/behavior-ownership/shared/{index}.json", record) + with pytest.raises(ownership.BehaviorOwnershipError, match="duplicate_supersession"): + ownership.validate_catalogue(tmp_path) + + +def test_catalogue_rejects_multiple_reviewed_owners_for_one_callable( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + for index in range(2): + _write_json( + tmp_path / f".ci/behavior-ownership/shared/{index}.json", + _catalogue_record("reviewed", target, f"behavior:{index}"), + ) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition([target])) + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared"}, + ) + monkeypatch.setattr(ownership, "_validate_remaps", lambda *args, **kwargs: None) + with pytest.raises( + ownership.BehaviorOwnershipError, match="multiple_effective_callable_owners" + ): + ownership.validate_catalogue(tmp_path) + + +def test_owned_tests_runs_only_reviewed_nodes( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + captured: dict[str, object] = {} + + def fake_run(arguments, **kwargs): + captured["arguments"] = arguments + captured["kwargs"] = kwargs + return subprocess.CompletedProcess(arguments, 7) + + monkeypatch.setattr(ownership.subprocess, "run", fake_run) + code = ownership.run_owned_tests( + tmp_path, + [ + {"status": "reviewed", "tests": ["backend/tests/test_example.py::test_run"]}, + {"status": "candidate", "tests": ["backend/tests/test_example.py::test_ignored"]}, + ], + ) + assert code == 7 + assert captured["arguments"][-1] == "tests/test_example.py::test_run" + + +def test_cli_inventory_generate_validate_and_errors( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: ["target"]) + monkeypatch.setattr(sys, "argv", ["behavior_ownership.py", "inventory"]) + assert ownership._main() == 0 + assert json.loads(capsys.readouterr().out) == ["target"] + monkeypatch.setattr(ownership, "generate_candidates", lambda **kwargs: {"generated": kwargs}) + monkeypatch.setattr(sys, "argv", ["behavior_ownership.py", "generate", "--group", "auth"]) + assert ownership._main() == 0 + assert json.loads(capsys.readouterr().out)["generated"] == {"group": "auth"} + monkeypatch.setattr(ownership, "validate_catalogue", lambda **kwargs: {"validated": kwargs}) + monkeypatch.setattr(ownership, "validate_partition", lambda **kwargs: {}) + monkeypatch.setattr( + sys, + "argv", + [ + "behavior_ownership.py", + "validate", + "--group", + "shared", + "--trusted-revision", + "main", + "--run-owned-tests", + ], + ) + assert ownership._main() == 0 + assert json.loads(capsys.readouterr().out)["validated"] == { + "group": "shared", + "trusted_revision": "main", + "head_revision": "HEAD", + "run_tests": True, + } + monkeypatch.setattr( + ownership, + "build_partition", + lambda **kwargs: {"base": kwargs["base_commit"]}, + ) + monkeypatch.setattr(sys, "argv", ["behavior_ownership.py", "partition", "--base-commit", "abc"]) + assert ownership._main() == 0 + assert json.loads(capsys.readouterr().out) == {"base": "abc"} + monkeypatch.setattr( + ownership, + "eligible_targets", + lambda root=ownership.ROOT: (_ for _ in ()).throw(ownership.BehaviorOwnershipError("bad")), + ) + monkeypatch.setattr(sys, "argv", ["behavior_ownership.py", "inventory"]) + assert ownership._main() == 2 + assert "behavior_ownership_error:bad" in capsys.readouterr().err + + +@pytest.mark.parametrize( + "source", + [ + "open('value.txt')\n", + "if FLAG:\n VALUE = 1\n", + "VALUES = []\nVALUES.append(1)\n", + "for item in VALUES:\n VALUE = item\n", + "raise RuntimeError('side effect')\n", + ], +) +def test_structural_only_rejects_runtime_side_effects(tmp_path: Path, source: str) -> None: + target = "backend/scripts/structural.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text(source, encoding="utf-8") + with pytest.raises(ownership.BehaviorOwnershipError, match="executable_structural_only"): + ownership._validate_record_semantics( + tmp_path, {"status": "structural_only", "target": target} + ) + + +def test_candidate_generator_reports_no_callable_target_as_unresolved( + monkeypatch: pytest.MonkeyPatch, +) -> None: + target = "backend/scripts/constants.py" + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared"}, + ) + monkeypatch.setattr(ownership, "callable_names", lambda root, value: []) + result = ownership.generate_candidates() + assert result["candidates"] == [] + assert result["unresolved"] == [ + {"group": "shared", "target": target, "reason": "structural review required"} + ] + + +def test_record_rejects_validly_shaped_missing_test_node(tmp_path: Path) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + record = _catalogue_record("reviewed", target, "behavior:example") + record["tests"] = ["backend/tests/test_missing.py::test_missing"] + with pytest.raises(ownership.BehaviorOwnershipError, match="missing_catalogue_test"): + ownership._validate_record_semantics(tmp_path, record) + + +def test_callable_inventory_rejects_symlink_target(tmp_path: Path) -> None: + outside = tmp_path / "outside.py" + outside.write_text("def leaked():\n return 1\n", encoding="utf-8") + target = tmp_path / "backend/scripts/example.py" + target.parent.mkdir(parents=True) + target.symlink_to(outside) + with pytest.raises(ownership.BehaviorOwnershipError, match="unsafe_or_missing_target"): + ownership.callable_names(tmp_path, "backend/scripts/example.py") + + +def test_partition_rejects_recomputed_group_reassignment( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = "backend/scripts/example.py" + value = _partition([target]) + value["assignments"][0]["group"] = "auth" + authority = {key: value[key] for key in value if key != "authority_digest"} + value["authority_digest"] = ownership._digest(authority) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: [target]) + _mock_partition_git(monkeypatch) + with pytest.raises(ownership.BehaviorOwnershipError, match="partition_assignment_mismatch"): + ownership.validate_partition(tmp_path, trusted_revision=None) + + +def test_partition_rejects_missing_invalid_shape_and_invalid_base( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + with pytest.raises(ownership.BehaviorOwnershipError, match="unsafe_or_missing_partition"): + ownership.validate_partition(tmp_path, trusted_revision=None) + _write_json(tmp_path / ownership.PARTITION_PATH, {}) + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_partition_shape"): + ownership.validate_partition(tmp_path, trusted_revision=None) + target = "backend/scripts/example.py" + value = _partition([target]) + value["protected_base_commit"] = "invalid" + authority = {key: value[key] for key in value if key != "authority_digest"} + value["authority_digest"] = ownership._digest(authority) + _write_json(tmp_path / ownership.PARTITION_PATH, value) + monkeypatch.setattr(ownership, "eligible_targets", lambda root=ownership.ROOT: [target]) + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_partition_base_commit"): + ownership.validate_partition(tmp_path, trusted_revision=None) + + +def _reviewed_remap(target: str, behavior_id: str, supersedes: str | None = None): + record = { + "status": "reviewed", + "behavior_id": behavior_id, + "target": target, + "tests": ["backend/tests/test_example.py::test_run"], + "outcomes": ["return"], + "boundaries": [], + } + if supersedes is not None: + record["supersedes_behavior_id"] = supersedes + return record + + +def test_remap_requires_protected_ancestry_location_and_carried_evidence( + monkeypatch: pytest.MonkeyPatch, +) -> None: + old = _reviewed_remap("backend/scripts/old.py", "protected:id") + new = _reviewed_remap("backend/scripts/new.py", "new:id", "protected:id") + monkeypatch.setattr(ownership, "_catalogue_at_revision", lambda root, revision: [old]) + monkeypatch.setattr( + ownership, + "_git_show_optional", + lambda root, revision, path: "source" if path == new["target"] else None, + ) + ownership._validate_remaps(Path("/repo"), [new], base_sha="base", head_sha="head") + missing = dict(new, supersedes_behavior_id="missing:id") + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_remap_ancestry"): + ownership._validate_remaps(Path("/repo"), [missing], base_sha="base", head_sha="head") + narrowed = dict(new, tests=[]) + with pytest.raises(ownership.BehaviorOwnershipError, match="narrowed_remap_evidence"): + ownership._validate_remaps(Path("/repo"), [narrowed], base_sha="base", head_sha="head") + monkeypatch.setattr(ownership, "_git_show_optional", lambda root, revision, path: "source") + with pytest.raises(ownership.BehaviorOwnershipError, match="protected_location_still_exists"): + ownership._validate_remaps(Path("/repo"), [new], base_sha="base", head_sha="head") + + +def test_remap_rejects_protected_replacement_and_multiple_owners( + monkeypatch: pytest.MonkeyPatch, +) -> None: + old = _reviewed_remap("backend/scripts/old.py", "protected:id") + replacement = dict(old, tests=["backend/tests/test_example.py::test_weaker"]) + monkeypatch.setattr(ownership, "_catalogue_at_revision", lambda root, revision: [old]) + with pytest.raises(ownership.BehaviorOwnershipError, match="protected_owner_replacement"): + ownership._validate_remaps( + Path("/repo"), [replacement], base_sha="base", head_sha="head" + ) + monkeypatch.setattr(ownership, "_catalogue_at_revision", lambda root, revision: [old, old]) + with pytest.raises(ownership.BehaviorOwnershipError, match="multiple_protected_owners"): + ownership._validate_remaps(Path("/repo"), [], base_sha="base", head_sha="head") + + +def test_remap_requires_exactly_one_effective_owner(monkeypatch: pytest.MonkeyPatch) -> None: + old = _reviewed_remap("backend/scripts/old.py", "protected:id") + new = _reviewed_remap("backend/scripts/new.py", "new:id", "protected:id") + monkeypatch.setattr(ownership, "_catalogue_at_revision", lambda root, revision: [old]) + monkeypatch.setattr( + ownership, + "_git_show_optional", + lambda root, revision, path: "source" if path == new["target"] else None, + ) + with pytest.raises(ownership.BehaviorOwnershipError, match="missing_effective_owner"): + ownership._validate_remaps(Path("/repo"), [], base_sha="base", head_sha="head") + with pytest.raises(ownership.BehaviorOwnershipError, match="multiple_effective_owners"): + ownership._validate_remaps(Path("/repo"), [old, new], base_sha="base", head_sha="head") + + +def test_catalogue_fails_when_exact_test_collection_fails( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + _write_json( + tmp_path / ".ci/behavior-ownership/shared/example.json", + _catalogue_record("reviewed", target, "behavior:example"), + ) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition([target])) + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared"}, + ) + monkeypatch.setattr(ownership, "_validate_remaps", lambda *args, **kwargs: None) + monkeypatch.setattr(ownership, "_run_test_nodes", lambda *args, **kwargs: 1) + with pytest.raises(ownership.BehaviorOwnershipError, match="stale_catalogue_test"): + ownership.validate_catalogue(tmp_path) + + +def test_low_level_invalid_inputs_fail_closed(tmp_path: Path) -> None: + invalid = tmp_path / "invalid.json" + invalid.write_text("not json", encoding="utf-8") + with pytest.raises(ownership.BehaviorOwnershipError, match="bad_json"): + ownership._read_json(invalid, "bad_json") + with pytest.raises( + ownership.BehaviorOwnershipError, match="unsafe_or_missing_catalogue_schema" + ): + ownership.load_schema(tmp_path) + target = tmp_path / "backend/scripts/broken.py" + target.parent.mkdir(parents=True) + target.write_text("if:\n", encoding="utf-8") + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_target_syntax"): + ownership._is_strictly_structural(tmp_path, "backend/scripts/broken.py") + + +def test_candidate_supersession_and_invalid_group_fail_closed( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + record = _catalogue_record("candidate", target, "candidate:id") + record["supersedes_behavior_id"] = "protected:id" + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_non_reviewed_supersession"): + ownership._validate_record_semantics(tmp_path, record) + monkeypatch.setattr(ownership, "validate_partition", lambda root=ownership.ROOT: {}) + with pytest.raises(ownership.BehaviorOwnershipError, match="invalid_group"): + ownership.generate_candidates(group="wrong") + + +def test_structural_supersession_fails_with_typed_error(tmp_path: Path) -> None: + target = "backend/scripts/constants.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("VALUE = 1\n", encoding="utf-8") + record = _catalogue_record("structural_only", target, "structural:id") + record["supersedes_behavior_id"] = "protected:id" + with pytest.raises( + ownership.BehaviorOwnershipError, match="invalid_non_reviewed_supersession" + ): + ownership._validate_record_semantics(tmp_path, record) + + +def test_catalogue_record_must_reside_in_declared_group( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + (tmp_path / "scripts").mkdir() + (tmp_path / ownership.SCHEMA_PATH).write_text( + (ownership.ROOT / ownership.SCHEMA_PATH).read_text(), encoding="utf-8" + ) + target = "backend/scripts/example.py" + path = tmp_path / target + path.parent.mkdir(parents=True) + path.write_text("def run():\n return 1\n", encoding="utf-8") + test_file = tmp_path / "backend/tests/test_example.py" + test_file.parent.mkdir(parents=True) + test_file.write_text("def test_run():\n pass\n", encoding="utf-8") + _write_json( + tmp_path / ".ci/behavior-ownership/auth/misplaced.json", + _catalogue_record("reviewed", target, "behavior:example"), + ) + _write_json(tmp_path / ownership.PARTITION_PATH, _partition([target])) + monkeypatch.setattr( + ownership, + "validate_partition", + lambda root=ownership.ROOT, **kwargs: {target: "shared"}, + ) + with pytest.raises(ownership.BehaviorOwnershipError, match="misplaced_catalogue_record"): + ownership.validate_catalogue(tmp_path) + + +def test_remap_rejects_missing_new_location(monkeypatch: pytest.MonkeyPatch) -> None: + old = _reviewed_remap("backend/scripts/old.py", "protected:id") + new = _reviewed_remap("backend/scripts/new.py", "new:id", "protected:id") + monkeypatch.setattr(ownership, "_catalogue_at_revision", lambda root, revision: [old]) + monkeypatch.setattr(ownership, "_git_show_optional", lambda root, revision, path: None) + with pytest.raises(ownership.BehaviorOwnershipError, match="missing_remap_location"): + ownership._validate_remaps(Path("/repo"), [new], base_sha="base", head_sha="head") + + +def test_collect_only_runner_adds_collection_flag( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + captured: list[str] = [] + + def fake_run(arguments, **kwargs): + captured.extend(arguments) + return subprocess.CompletedProcess(arguments, 0) + + monkeypatch.setattr(ownership.subprocess, "run", fake_run) + assert ownership._run_test_nodes( + tmp_path, + [{"status": "reviewed", "tests": ["backend/tests/test_example.py::test_run"]}], + collect_only=True, + ) == 0 + assert "--collect-only" in captured diff --git a/docs/operations_backend_testing.md b/docs/operations_backend_testing.md index 0adc6c4a..d31811ed 100644 --- a/docs/operations_backend_testing.md +++ b/docs/operations_backend_testing.md @@ -186,3 +186,31 @@ remain in the repository as design input for a future changed-line-aware gate. They are not active contribution requirements. Behavior-mutation enforcement must not resume until a fresh changed-line-aware plan is approved and proves that unchanged executable lines cannot block a declaration-only change. + +### Behavior ownership catalogue foundation + +The replacement foundation is a read-only engineering QA catalogue under +`.ci/behavior-ownership/`. Its canonical `partition.v1.json` assigns every +eligible non-`__init__` module to exactly one population group and binds the +protected-base commit plus a digest over the assignment authority. Validation +rejects missing, relocated, duplicated, modified, branch-local, or incomplete +partitions. + +Run its commands from `backend/`: + +```bash +.venv/bin/python -m scripts.behavior_ownership inventory +.venv/bin/python -m scripts.behavior_ownership generate --group auth +.venv/bin/python -m scripts.behavior_ownership validate +``` + +The generator is deterministic and emits only `candidate` records. Candidates +never satisfy reviewed ownership or activate a gate. Reviewed executable +records bind exact AST callables, pytest nodes, outcomes, boundaries, and +reviewers. `structural_only` records require a reviewed reason, cannot contain +callable or test fields, and fail validation when the target contains an +executable callable or module-level runtime behavior such as calls, branches, +loops, raises, awaits, mutation, I/O, SQL, validators, or other side effects. +Until population and a separately approved changed-line reactivation chunk are +complete, Backend lanes and existing coverage floors remain the only hosted +test authority. diff --git a/scripts/behavior-ownership.schema.json b/scripts/behavior-ownership.schema.json new file mode 100644 index 00000000..9ec5cc61 --- /dev/null +++ b/scripts/behavior-ownership.schema.json @@ -0,0 +1,70 @@ +{ + "$id": "https://workstream.dev/schemas/behavior-ownership-v1.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { "$ref": "#/$defs/reviewed" }, + { "$ref": "#/$defs/candidate" }, + { "$ref": "#/$defs/structural" } + ], + "$defs": { + "base": { + "type": "object", + "required": ["schema", "behavior_id", "status", "group", "target"], + "properties": { + "schema": { "const": "workstream.behavior-ownership.v1" }, + "behavior_id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._:-]+$" }, + "group": { "enum": ["auth", "artifacts", "lifecycle", "shared"] }, + "target": { "type": "string", "pattern": "^backend/(app|scripts)/.+\\.py$" }, + "supersedes_behavior_id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9._:-]+$" } + } + }, + "reviewed": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "type": "object", + "required": ["callables", "tests", "outcomes", "boundaries", "reviewed_by"], + "properties": { + "status": { "const": "reviewed" }, + "callables": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string" } }, + "tests": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string" } }, + "outcomes": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string" } }, + "boundaries": { "type": "array", "uniqueItems": true, "items": { "type": "string" } }, + "reviewed_by": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } } + } + } + ], + "unevaluatedProperties": false + }, + "candidate": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "type": "object", + "required": ["callables", "unresolved_reason"], + "properties": { + "status": { "const": "candidate" }, + "callables": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string" } }, + "unresolved_reason": { "type": "string", "minLength": 1 } + } + } + ], + "unevaluatedProperties": false + }, + "structural": { + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "type": "object", + "required": ["reason", "reviewed_by"], + "properties": { + "status": { "const": "structural_only" }, + "reason": { "type": "string", "minLength": 1 }, + "reviewed_by": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "type": "string", "minLength": 1 } } + } + } + ], + "unevaluatedProperties": false + } + } +} From e9a2c7d9c5730bf8d8301bee67aba12adfeffed0 Mon Sep 17 00:00:00 2001 From: Abiorh001 Date: Sat, 8 Aug 2026 11:08:10 +0100 Subject: [PATCH 2/2] fix(quality): refresh catalogue partition after main updates --- .../reviews/WS-QUAL-002-01-pr-trust-bundle.md | 2 +- .ci/behavior-ownership/partition.v1.json | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md index b3351011..5ae13b03 100644 --- a/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md +++ b/.agent-loop/initiatives/WS-QUAL-002-behavior-ownership-catalogue/reviews/WS-QUAL-002-01-pr-trust-bundle.md @@ -54,7 +54,7 @@ changed. - `scripts.behavior_ownership` focused coverage is 91.30 percent, above 90. - 33 semantic-lane contract tests pass. - Ruff passes for all touched backend Python files. -- Real validation reports 173 unresolved targets and `complete: false`. +- Real validation reports 176 unresolved targets and `complete: false`. - Candidate generation remains `authoritative: false`, emits no empty-callable candidate, and separates structural-review targets. - Markdown links pass; stale wording passed during product/operations review. diff --git a/.ci/behavior-ownership/partition.v1.json b/.ci/behavior-ownership/partition.v1.json index 25f2b7f9..24cd55a2 100644 --- a/.ci/behavior-ownership/partition.v1.json +++ b/.ci/behavior-ownership/partition.v1.json @@ -272,10 +272,22 @@ "group": "artifacts", "target": "backend/app/modules/artifacts/sources.py" }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_admission.py" + }, { "group": "artifacts", "target": "backend/app/modules/artifacts/submission_archive.py" }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_authorization.py" + }, + { + "group": "artifacts", + "target": "backend/app/modules/artifacts/submission_custody.py" + }, { "group": "artifacts", "target": "backend/app/modules/artifacts/submission_manifest.py" @@ -693,7 +705,7 @@ "target": "backend/scripts/week2_api_e2e.py" } ], - "authority_digest": "9897223853b489bc7fa998910e84a9fc260849d1ea7292d796e95438094137e3", - "protected_base_commit": "99c0aaf04efd36c7ac4af4aeec2e9d810f012305", + "authority_digest": "c05cdc4b7a4c6c016000475035e47ef4437767ce5152b322cba9dceb6e992e7f", + "protected_base_commit": "7676ce4347db0c9694962a9b587a20765e16eac6", "schema": "workstream.behavior-ownership-partition.v1" }