diff --git a/yuqei-infra/deploy/README.md b/yuqei-infra/deploy/README.md index efde2732..138f1ce4 100644 --- a/yuqei-infra/deploy/README.md +++ b/yuqei-infra/deploy/README.md @@ -21,7 +21,12 @@ The current RC-05 through RC-08 release must follow the repository [mandatory de - `../compose/.env.production.example`: reviewed variable inventory. The real file belongs at `/etc/yuqei/production.env`, mode `0600`, outside the repository. - `preflight-production.py`: read-only production configuration gate. - `verify-production-compose.py`: read-only merged-model gate. It renders Docker Compose JSON for online and offline modes across the default, `backend-hosted-web`, `ops`, and combined profile sets. -- `release-package.py`: backend-only release-package verifier and planner. Its production apply entry point remains deliberately closed until the complete infrastructure-image import plan and externally trusted backup/compatibility approvals are available. +- `release-package.py`: backend-only release-package verifier, planner, and guarded executor. Package v1 is read-only compatibility input; package v2 can reach apply only after all eleven image imports, third-party materials, the selected recovery strategy, external evidence, and exact authorization pass validation. +- `plan-first-release-snapshot-restore.py`: verifies first-release pre-install snapshot/backup/restore-test evidence and can mint a short-lived install-readiness receipt. It emits plans and receipts only; it does not restore data. +- `plan-rollback-rehearsal.py`: verifies the frozen package and source-bound release tools before emitting a non-writing rollback rehearsal plan. +- `../scripts/build-infrastructure-image-bundle.py`: file-only validator for the four application and seven infrastructure image archives, security reports, content-addressed layers, and offline import plan. +- `../scripts/build-third-party-materials.py`: deterministic third-party notice, license-text, and corresponding-source material builder/verifier. +- `../release-trust/third-party-materials-baseline.json`: source-frozen technical trust baseline. The shipped baseline intentionally remains `NOT_PROVISIONED` until reviewed real catalog, license, and source identities are supplied. ## Prepare without changing traffic @@ -78,17 +83,20 @@ a registry. A companion overlay that introduces another image-bearing service ne an explicit offline pull policy of its own before it is approved for this path. Do not add the offline override to the online `pull` command. -`release-package.py` validates the current four-application Docker import plan, -including archive size/SHA-256 and the expected config-based image IDs used for -offline Compose overrides. That is not yet a clean-host offline installation -closure: PostgreSQL, the PostgreSQL client, Redis, MinIO, mc, Nginx, and the -backup helper do not all have approved import archives, and the packaged backup -and rollback-compatibility records are only self-hashed rather than externally -signed. Consequently `verify` reports structural validity without apply -eligibility, plans expose the blocking reasons, and the normal `--apply` entry -point fails before creating state or loading an image. Contract Web remains an -identity-bound, separately deployed artifact; this backend executor never claims -that the full cross-host release has been installed or rolled back. +`release-package.py` keeps package v1 available for read-only historical +verification. Package v2 binds four application images and seven infrastructure +images, their offline archives and import order, exact Compose image IDs, +third-party materials, and one explicit recovery strategy. A v2 apply is still +fail-closed unless the fixed technical trust baseline is provisioned, the raw +external recovery evidence is independently revalidated, all dynamic blockers +are empty, and the exact operation authorization is present. The executor then +loads, tags, and inspects each image before Compose and runs its built-in +post-apply checks; a failure leaves reconciliation state instead of being treated +as success. The first-release recovery planner never performs a restore, and a +successful technical verification is not legal approval, distribution +clearance, UAT evidence, production authorization, or public-release approval. +Contract Web remains an identity-bound, separately deployed artifact, so this +backend executor also does not claim that the cross-host release was installed. The production override also replaces `ai-platform-prompt-assets-import` with the importer's default dry-run command. That Compose service may validate and report the diff --git a/yuqei-infra/deploy/plan-first-release-snapshot-restore.py b/yuqei-infra/deploy/plan-first-release-snapshot-restore.py index 259690e6..cc646f36 100644 --- a/yuqei-infra/deploy/plan-first-release-snapshot-restore.py +++ b/yuqei-infra/deploy/plan-first-release-snapshot-restore.py @@ -79,6 +79,7 @@ PACKAGE_VALIDATOR_SOURCE_CLOSURE = ( "yuqei-infra/scripts/build-installable-release.py", "yuqei-infra/scripts/build-infrastructure-image-bundle.py", "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/release-trust/third-party-materials-baseline.json", "yuqei-infra/scripts/linux_release_common.py", ) MAX_EVIDENCE_AGE = timedelta(hours=24) @@ -1453,10 +1454,9 @@ def verify_and_build_output( ) -> dict[str, Any]: """Revalidate every raw input and return one non-writing output document. - ``package_validator_for_tests`` exists only to exercise the v2 contract - before the official validator lands in the integration branch. The CLI - never exposes or supplies it and therefore always loads the source-bound - official validator. + ``package_validator_for_tests`` exists only for isolated unit fixtures. + The CLI never exposes or supplies it and therefore always loads the + source-bound official validator and its complete trust-input closure. """ if output_kind not in {"plan", "install-readiness-receipt"}: diff --git a/yuqei-infra/deploy/release-package.py b/yuqei-infra/deploy/release-package.py index 7777f28c..c0774fa9 100644 --- a/yuqei-infra/deploy/release-package.py +++ b/yuqei-infra/deploy/release-package.py @@ -1710,6 +1710,10 @@ def validate_release_package( "yuqei-infra/scripts/build-third-party-materials.py", Path(third_party_module.__file__), ), + ( + "yuqei-infra/release-trust/third-party-materials-baseline.json", + Path(third_party_module.REPOSITORY_TRUST_BASELINE_PATH), + ), ( "yuqei-infra/deploy/plan-first-release-snapshot-restore.py", Path(snapshot_module.__file__), diff --git a/yuqei-infra/scripts/build-release-candidate-manifest.py b/yuqei-infra/scripts/build-release-candidate-manifest.py index 3feb5df8..a3cd587e 100644 --- a/yuqei-infra/scripts/build-release-candidate-manifest.py +++ b/yuqei-infra/scripts/build-release-candidate-manifest.py @@ -55,15 +55,24 @@ RELEASE_CONTROL_PATHS = ( "yuqei-infra/deploy/preflight-production.py", "yuqei-infra/deploy/verify-production-compose.py", "yuqei-infra/deploy/release-package.py", + "yuqei-infra/deploy/plan-first-release-snapshot-restore.py", + "yuqei-infra/deploy/plan-rollback-rehearsal.py", + "yuqei-infra/release-trust/third-party-materials-baseline.json", "yuqei-infra/release/linux-release-security-policy.v1.json", "yuqei-infra/scripts/linux_release_common.py", "yuqei-infra/scripts/run-linux-release-build.py", "yuqei-infra/scripts/build-installable-release.py", + "yuqei-infra/scripts/build-infrastructure-image-bundle.py", + "yuqei-infra/scripts/build-third-party-materials.py", "yuqei-infra/tests/test_production_compose_preflight.py", "yuqei-infra/tests/test_production_compose_merge_gate.py", "yuqei-infra/tests/test_release_package_executor.py", "yuqei-infra/tests/test_linux_release_build_tools.py", "yuqei-infra/tests/test_installable_release_assembler_hardening.py", + "yuqei-infra/tests/test_infrastructure_image_bundle.py", + "yuqei-infra/tests/test_first_release_snapshot_restore_planner.py", + "yuqei-infra/tests/test_rollback_rehearsal_planner.py", + "yuqei-infra/tests/test_third_party_materials.py", "yuqei-infra/tests/test_release_candidate_manifest_tools.py", ) CONTRACT_LOCK_PATH = REQUIREMENT_LOCK_PATHS[0] diff --git a/yuqei-infra/scripts/build-release-worktree-ownership.py b/yuqei-infra/scripts/build-release-worktree-ownership.py index 852f6591..1f503d54 100644 --- a/yuqei-infra/scripts/build-release-worktree-ownership.py +++ b/yuqei-infra/scripts/build-release-worktree-ownership.py @@ -86,12 +86,24 @@ RUNTIME_COMPONENTS = { "yuqei-infra/requirements/", "yuqei-infra/deploy/", "yuqei-infra/release/", + "yuqei-infra/release-trust/third-party-materials-baseline.json", "yuqei-infra/scripts/linux_release_common.py", "yuqei-infra/scripts/run-linux-release-build.py", "yuqei-infra/scripts/build-installable-release.py", + "yuqei-infra/scripts/build-infrastructure-image-bundle.py", + "yuqei-infra/scripts/build-third-party-materials.py", "yuqei-infra/scripts/build-release-candidate-manifest.py", "yuqei-infra/scripts/build-release-worktree-ownership.py", "yuqei-infra/tests/test_installable_release_assembler_hardening.py", + "yuqei-infra/tests/test_infrastructure_image_bundle.py", + "yuqei-infra/tests/test_first_release_snapshot_restore_planner.py", + "yuqei-infra/tests/test_rollback_rehearsal_planner.py", + "yuqei-infra/tests/test_third_party_materials.py", + "yuqei-infra/tests/test_release_package_executor.py", + "yuqei-infra/tests/test_release_candidate_manifest_tools.py", + "yuqei-infra/tests/test_production_compose_preflight.py", + "yuqei-infra/tests/test_production_compose_merge_gate.py", + "yuqei-infra/tests/test_linux_release_build_tools.py", ), } REQUIRED_WORKTREE_ANCHORS = ( @@ -125,15 +137,24 @@ REQUIRED_WORKTREE_ANCHORS = ( "yuqei-infra/deploy/preflight-production.py", "yuqei-infra/deploy/verify-production-compose.py", "yuqei-infra/deploy/release-package.py", + "yuqei-infra/deploy/plan-first-release-snapshot-restore.py", + "yuqei-infra/deploy/plan-rollback-rehearsal.py", + "yuqei-infra/release-trust/third-party-materials-baseline.json", "yuqei-infra/release/linux-release-security-policy.v1.json", "yuqei-infra/scripts/linux_release_common.py", "yuqei-infra/scripts/run-linux-release-build.py", "yuqei-infra/scripts/build-installable-release.py", + "yuqei-infra/scripts/build-infrastructure-image-bundle.py", + "yuqei-infra/scripts/build-third-party-materials.py", "yuqei-infra/tests/test_production_compose_preflight.py", "yuqei-infra/tests/test_production_compose_merge_gate.py", "yuqei-infra/tests/test_release_package_executor.py", "yuqei-infra/tests/test_linux_release_build_tools.py", "yuqei-infra/tests/test_installable_release_assembler_hardening.py", + "yuqei-infra/tests/test_infrastructure_image_bundle.py", + "yuqei-infra/tests/test_first_release_snapshot_restore_planner.py", + "yuqei-infra/tests/test_rollback_rehearsal_planner.py", + "yuqei-infra/tests/test_third_party_materials.py", "yuqei-infra/tests/test_release_candidate_manifest_tools.py", ) @@ -394,12 +415,15 @@ def closure_audit(repo: Path, entries: list[dict[str, Any]]) -> dict[str, Any]: "yuqei-infra/nginx/", "yuqei-infra/deploy/", "yuqei-infra/release/", + "yuqei-infra/release-trust/", ) ) or path in { "yuqei-infra/scripts/linux_release_common.py", "yuqei-infra/scripts/run-linux-release-build.py", "yuqei-infra/scripts/build-installable-release.py", + "yuqei-infra/scripts/build-infrastructure-image-bundle.py", + "yuqei-infra/scripts/build-third-party-materials.py", } ] ownership_blockers = [ diff --git a/yuqei-infra/tests/test_first_release_snapshot_restore_planner.py b/yuqei-infra/tests/test_first_release_snapshot_restore_planner.py index 61bfebfc..c10d572a 100644 --- a/yuqei-infra/tests/test_first_release_snapshot_restore_planner.py +++ b/yuqei-infra/tests/test_first_release_snapshot_restore_planner.py @@ -759,17 +759,27 @@ def test_package_rejects_wrong_trusted_build_receipt_from_official_validator( @pytest.mark.parametrize("mutation", ("missing", "drifted")) +@pytest.mark.parametrize( + "relative", + ( + "yuqei-infra/scripts/build-infrastructure-image-bundle.py", + "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/release-trust/third-party-materials-baseline.json", + ), +) def test_package_source_closure_binds_dynamic_bundle_builders( tmp_path: Path, mutation: str, + relative: str, ) -> None: case = _case(tmp_path) - for relative in ( + for required in ( "yuqei-infra/scripts/build-infrastructure-image-bundle.py", "yuqei-infra/scripts/build-third-party-materials.py", + "yuqei-infra/release-trust/third-party-materials-baseline.json", ): - assert relative in planner.PACKAGE_VALIDATOR_SOURCE_CLOSURE - target = case["repo_root"] / "yuqei-infra/scripts/build-third-party-materials.py" + assert required in planner.PACKAGE_VALIDATOR_SOURCE_CLOSURE + target = case["repo_root"].joinpath(*relative.split("/")) if mutation == "missing": target.unlink() match = "validator source .* is missing" diff --git a/yuqei-infra/tests/test_release_candidate_manifest_tools.py b/yuqei-infra/tests/test_release_candidate_manifest_tools.py index e46464c8..f8cef94c 100644 --- a/yuqei-infra/tests/test_release_candidate_manifest_tools.py +++ b/yuqei-infra/tests/test_release_candidate_manifest_tools.py @@ -302,6 +302,10 @@ RUN python -c "import platform; name, version = platform.libc_ver(); minimum=(2, ("project-docs/上线收口-执行约束-2026-09-10.md", "release_evidence_compact_include"), ("project-docs/unreviewed-plan.md", "needs_manual_review"), ("yuqei-infra/config/.env.example", "release_source_include"), + ( + "yuqei-infra/release-trust/third-party-materials-baseline.json", + "release_source_include", + ), ), ) def test_worktree_classifier_has_explicit_release_ownership(path: str, category: str) -> None: @@ -312,13 +316,28 @@ def test_worktree_classifier_has_explicit_release_ownership(path: str, category: def test_manifest_is_deterministic_and_summarizes_complete_committed_graph(tmp_path: Path) -> None: - assembler_hardening_test = ( - "yuqei-infra/tests/test_installable_release_assembler_hardening.py" + release_control_tests = ( + "yuqei-infra/tests/test_production_compose_preflight.py", + "yuqei-infra/tests/test_production_compose_merge_gate.py", + "yuqei-infra/tests/test_release_package_executor.py", + "yuqei-infra/tests/test_linux_release_build_tools.py", + "yuqei-infra/tests/test_installable_release_assembler_hardening.py", + "yuqei-infra/tests/test_infrastructure_image_bundle.py", + "yuqei-infra/tests/test_first_release_snapshot_restore_planner.py", + "yuqei-infra/tests/test_rollback_rehearsal_planner.py", + "yuqei-infra/tests/test_third_party_materials.py", + "yuqei-infra/tests/test_release_candidate_manifest_tools.py", ) - assert assembler_hardening_test in manifest.RELEASE_CONTROL_PATHS - assert assembler_hardening_test in ownership.REQUIRED_WORKTREE_ANCHORS - assert assembler_hardening_test in ownership.RUNTIME_COMPONENTS["deployment"] - assert ownership.classify_path(assembler_hardening_test)[0] == "release_test_include" + for release_control_test in release_control_tests: + assert release_control_test in manifest.RELEASE_CONTROL_PATHS + assert release_control_test in ownership.REQUIRED_WORKTREE_ANCHORS + assert release_control_test in ownership.RUNTIME_COMPONENTS["deployment"] + assert ownership.classify_path(release_control_test)[0] == "release_test_include" + for release_control_path in manifest.RELEASE_CONTROL_PATHS: + assert ownership._path_matches( + release_control_path, + ownership.RUNTIME_COMPONENTS["deployment"], + ) pinned_bases = { "yuqei-infra/docker/contract-api.Dockerfile": ( "ARG PYTHON_BASE_IMAGE=python:3.12-slim@sha256:44ff437bba879d4941b710a369a8f19266aea34b29002807f0c487fabc9eec9b", diff --git a/yuqei-infra/tests/test_release_package_executor.py b/yuqei-infra/tests/test_release_package_executor.py index 5ef1b2a5..59cbf0b3 100644 --- a/yuqei-infra/tests/test_release_package_executor.py +++ b/yuqei-infra/tests/test_release_package_executor.py @@ -89,6 +89,92 @@ class PackageFixture: _STRICT_BUILD_TEMPLATE: dict[str, Any] | None = None _REAL_INFRA_TOOL: Any | None = None _INFRA_SYNTHETIC_APPLICATION: dict[str, Any] | None = None +_FIXTURE_MIT_TEXT = b"MIT fixture license text\n" +_FIXTURE_GPL_TEXT = b"GPL-2.0-only fixture license text\n" +_FIXTURE_SOURCE_BYTES = b"def copyleft_fixture() -> str:\n return 'source'\n" + + +def _fixture_third_party_trust_baseline(third_tool: Any) -> dict[str, Any]: + licenses_payload = { + "licenseListVersion": "3.29.0", + "licenses": [ + {"licenseId": "Apache-2.0", "isDeprecatedLicenseId": False}, + {"licenseId": "GPL-2.0-only", "isDeprecatedLicenseId": False}, + {"licenseId": "MIT", "isDeprecatedLicenseId": False}, + ], + } + exceptions_payload = {"licenseListVersion": "3.29.0", "exceptions": []} + + def record(raw: bytes) -> dict[str, Any]: + return {"sha256": hashlib.sha256(raw).hexdigest(), "size_bytes": len(raw)} + + licenses_raw = release_package.canonical_json_bytes(licenses_payload) + b"\n" + exceptions_raw = release_package.canonical_json_bytes(exceptions_payload) + b"\n" + return { + "schema_version": third_tool.TRUST_BASELINE_SCHEMA, + "baseline_id": "release-package-isolated-test-baseline", + "technical_approval": { + "status": third_tool.ACTIVE_TECHNICAL_TRUST_STATUS, + "reviewer": "isolated-release-package-test-fixture", + "reviewed_at": "2026-09-27T00:00:00+00:00", + "scope": third_tool.TRUST_SCOPE_STATEMENT, + }, + "candidate_contract": { + "input_schema": third_tool.INPUT_SCHEMA, + "source_archive_schema": third_tool.SOURCE_ARCHIVE_SCHEMA, + "profiles": sorted(third_tool.PROFILES), + "image_roles": sorted(third_tool.EXPECTED_ROLES), + }, + "spdx_catalog": { + "license_list_version": "3.29.0", + "licenses": { + "origin": "https://spdx.org/licenses/licenses.json", + **record(licenses_raw), + }, + "exceptions": { + "origin": "https://spdx.org/licenses/exceptions.json", + **record(exceptions_raw), + }, + }, + "license_texts": [ + { + "id": "MIT", + "kind": "license", + "spdx_expression": "MIT", + "aliases": [], + **record(_FIXTURE_MIT_TEXT), + }, + { + "id": "GPL-2.0-only", + "kind": "license", + "spdx_expression": "GPL-2.0-only", + "aliases": [], + **record(_FIXTURE_GPL_TEXT), + }, + ], + "source_materials": [ + { + "id": "fixture-copyleft-source", + "kind": "source-archive", + "package_name": "fixture-copyleft", + "package_version": "1.0.0", + "binary_coordinate": "pkg:generic/fixture-copyleft@1.0.0", + "source_coordinate": "pkg:generic/fixture-copyleft-source@1.0.0", + "files": [ + { + "path": "src/main.py", + "kind": "source", + **record(_FIXTURE_SOURCE_BYTES), + } + ], + } + ], + "clearance_claims": { + "legal_approval": False, + "distribution_clearance": False, + "production_authorization": False, + }, + } def _fixture_infrastructure_build_bundle( @@ -145,6 +231,9 @@ def _extend_source_fixture_with_release_validators(helpers, original, tmp_path: destination = repo.joinpath(*Path(relative).parts) destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes((REPO_ROOT / relative).read_bytes()) + third_tool = release_package._load_third_party_materials_module() + trust_path = repo / "yuqei-infra/release-trust/third-party-materials-baseline.json" + _write_json(trust_path, _fixture_third_party_trust_baseline(third_tool)) helpers._git(repo, "add", ".") commit_env = os.environ.copy() commit_env.update( @@ -258,7 +347,12 @@ def _record_for(path: Path, root: Path) -> dict[str, Any]: } -def _add_v2_release_artifacts(bundle_root: Path, v2_root: Path) -> dict[str, Any]: +def _add_v2_release_artifacts( + bundle_root: Path, + v2_root: Path, + *, + repo_root: Path, +) -> dict[str, Any]: global _INFRA_SYNTHETIC_APPLICATION, _REAL_INFRA_TOOL infra_helper = _load_test_helper( @@ -271,6 +365,9 @@ def _add_v2_release_artifacts(bundle_root: Path, v2_root: Path) -> dict[str, Any ) real_infra_tool = release_package._load_infrastructure_bundle_module() third_tool = release_package._load_third_party_materials_module() + third_tool.REPOSITORY_TRUST_BASELINE_PATH = ( + repo_root / "yuqei-infra/release-trust/third-party-materials-baseline.json" + ) installable_path = bundle_root / "installable-release-manifest.json" installable = json.loads(installable_path.read_text(encoding="utf-8")) commit = installable["candidate"]["commit"] @@ -414,8 +511,8 @@ def _add_v2_release_artifacts(bundle_root: Path, v2_root: Path) -> dict[str, Any text_dir.mkdir() mit = text_dir / "MIT.txt" gpl = text_dir / "GPL-2.0-only.txt" - mit.write_text("MIT fixture license text\n", encoding="utf-8") - gpl.write_text("GPL-2.0-only fixture license text\n", encoding="utf-8") + mit.write_bytes(_FIXTURE_MIT_TEXT) + gpl.write_bytes(_FIXTURE_GPL_TEXT) spdx_licenses = materials_root / "spdx/licenses.json" spdx_exceptions = materials_root / "spdx/exceptions.json" _write_json( @@ -490,7 +587,7 @@ def _add_v2_release_artifacts(bundle_root: Path, v2_root: Path) -> dict[str, Any contract_api_image = next( item for item in release_images if item["role"] == "contract-api" ) - source_bytes = b"def copyleft_fixture() -> str:\n return 'source'\n" + source_bytes = _FIXTURE_SOURCE_BYTES source_file_record = { "path": "src/main.py", "sha256": third_tool._sha256_bytes(source_bytes), @@ -674,7 +771,12 @@ def _strict_build_template(tmp_path: Path) -> dict[str, Any]: helpers, original_source_fixture, path ) try: - bundle = helpers._bundle(tmp_path.parent / "release-package-strict-template-source") + template_source = tmp_path.parent / "release-package-strict-template-source" + # A failed first construction must not poison every later case in the + # module with a half-created shared fixture directory. + if template_source.exists(): + shutil.rmtree(template_source) + bundle = helpers._bundle(template_source) finally: helpers._fixture_source = original_source_fixture receipt_sha256 = _sha(bundle["receipt_path"]) @@ -688,7 +790,11 @@ def _strict_build_template(tmp_path: Path) -> dict[str, Any]: output=installable, ) v2_root = bundle["root"].parent / "v2-release-artifacts" - v2_artifacts = _add_v2_release_artifacts(bundle["root"], v2_root) + v2_artifacts = _add_v2_release_artifacts( + bundle["root"], + v2_root, + repo_root=bundle["repo"], + ) _STRICT_BUILD_TEMPLATE = { "bundle_root": bundle["root"], "repo": bundle["repo"], @@ -1488,6 +1594,34 @@ def test_default_verify_fully_validates_without_writes_or_docker(tmp_path: Path) assert not state.exists() +def test_v2_source_bound_third_party_trust_baseline_drift_is_rejected( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + fixture = _make_package(tmp_path) + frozen_baseline_path = ( + fixture.repo + / "yuqei-infra/release-trust/third-party-materials-baseline.json" + ) + baseline = json.loads(frozen_baseline_path.read_text(encoding="utf-8")) + baseline["technical_approval"]["reviewer"] = "unfrozen-drift" + loaded_baseline_path = tmp_path / "ambient-drifted-trust-baseline.json" + _write_json(loaded_baseline_path, baseline) + third_party_module = release_package._load_third_party_materials_module() + monkeypatch.setattr( + third_party_module, + "REPOSITORY_TRUST_BASELINE_PATH", + loaded_baseline_path, + ) + + with pytest.raises(release_package.ReleasePackageError, match="frozen source manifest"): + release_package.validate_release_package( + fixture.root, + expected_package_sha256=fixture.expected_sha256, + repo_root=fixture.repo, + ) + + def test_v2_predecessor_recovery_preserves_legacy_plan_semantics(tmp_path: Path) -> None: fixture = _make_package(tmp_path) _use_v2_predecessor_recovery(fixture)