diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index 95db0a1..37dd396 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -46,7 +46,18 @@ plus history-preserving promotion; implementation remains PLANNED. scaffolder. This design neither starts a Mission nor changes Mission 3, live policies, credentials, runtime schemas or package versions. -## Current pickup checkpoint — 17 September 2026 +## Criterion completion source checkpoint — 18 September 2026 + +[Implementation PR #149](https://github.com/pcvantol/forge/pull/149) is +protected-merged at `6199a7645c15f078aa69d6246023b078069a9742`. +Read the [separate source finalization](docs/operations/CRITERION_COMPLETION_SOURCE_FINALIZATION.md) +and [qualification boundaries](docs/operations/CRITERION_COMPLETION_QUALIFICATION.md) +for criterion-bound assessment, durable successor planning and the distinct +source, provider, peer and installed-composition evidence. Publication and +installed activation remain required under the same repair assignment; +this checkpoint does not authorize a reset or Mission 3. + +## Previous pickup checkpoint — 17 September 2026 Implementation PR #128 merged the deterministic aggregate-health timeout-boundary correction at `1726f774cb42894d3d3386e80adee9444c7a7914`. The evaluated candidate diff --git a/docs/operations/CRITERION_COMPLETION_SOURCE_FINALIZATION.md b/docs/operations/CRITERION_COMPLETION_SOURCE_FINALIZATION.md new file mode 100644 index 0000000..9abf3db --- /dev/null +++ b/docs/operations/CRITERION_COMPLETION_SOURCE_FINALIZATION.md @@ -0,0 +1,80 @@ +# Criterion completion source finalization + +Assignment: `L1-CRITERION-PLANNING-REPAIR-20260918`, 18 September 2026. +This is the separate source finalization for +[implementation PR #149](https://github.com/pcvantol/forge/pull/149), merged +through the protected route at +`6199a7645c15f078aa69d6246023b078069a9742`. +It is an intermediate delivery checkpoint: publication, exact registry +artifacts, safe installed activation and the final completion record remain +required within this same assignment. + +## Immutable source and assurance + +The independently reviewed implementation candidate is +`b368052ed59b18b19182ee724802f5d67d264340`, tree +`085c7b419efc10b782002167642a7462ec03958a`. +Separate Quality and Security reviewers did not author the implementation. +Both returned PASS with no open findings on this exact candidate. Quality +reran 171 focused tests with one opt-in skip; Security reran 50 focused tests. +The earlier three Quality findings and their corrections remain documented in +the [qualification record](CRITERION_COMPLETION_QUALIFICATION.md). + +Owning validation passed 817 tests with one existing optional exact-artifact +test skipped, plus compilation, version policy, projection, JSON and whitespace +checks. The hosted required +[Test and static validation run](https://github.com/pcvantol/forge/actions/runs/35336005960) +passed; CodeQL, version validation and advisory TDE checks also passed before +merge. No branch protection or check requirement was bypassed. + +A clean Git archive of that candidate produced qualified wheel and sdist +payloads. The noneditable wheel passed all eight normal installed composition +scenarios across separate processes. Candidate artifact digests are: + +| Candidate artifact | SHA-256 | +| --- | --- | +| `forge_autonomy-2.7.25-py3-none-any.whl` | `3e288c76ec0b53a4aba795103eb72a53a7bb98667c7c9584ba87c02267762d45` | +| `forge_autonomy-2.7.25.tar.gz` | `9cf5f07cf16464cf531cfe89cf6d5133f14562b94ff3c7b61d57acaf717d91d9` | + +These are candidate-only bytes, not the later published release artifacts. +The real provider result, retained failed attempt, EP HTTP qualification and +actual schema-38 migration fixture are separately scoped in the qualification +record. None represents a production Mission run. + +## Delivered behavior and remaining delivery gates + +Finalization review caught a producer/consumer mismatch before publication: +the release workflow now retains installed-composition summaries, while the +updater's original strict receipt shape rejected those additional fields. +This finalization binds both qualification and registry-readback summaries +to the exact 2.7.25 wheel and requires all eight expected outcomes, counts and +history/reopen guarantees. Captured summary-shape tests reject missing, +malformed, mismatched or incomplete evidence. Older release receipt shapes +remain unchanged. This is a bounded release-to-updater correction within the +same version and assignment; no published receipt is rewritten. + +The [criterion assessment contract](../architecture/criterion-completion-v2.md) +binds substantive evidence to approved criterion semantics and preserves +original observations. Partial completion enters the durable successor route; +fully proven criteria can finish after one Action. Current-revision properties +are reobserved on later Repository Truth. Safe legacy readback, finite planning +and replay retain their explicit failure boundaries. + +The release version is selected once as **2.7.25**, with schema **39** and the +bounded supported updater transition from **2.7.24/schema 38**. The normal +production workflow must build from the final protected source, retain exact +qualification/publication/completion receipts, verify downloaded PyPI bytes +and repeat the installed composition qualification. The existing updater must +then qualify and activate those exact bytes with preservation checks and a +new concrete peer-coordinated activation window. + +The repository JSON route proves only its approved structural assertion; +`host_control` remains unsupported until authoritative EP control receipts +are exported. Provider usage is checked after generation, not guaranteed by a +hard token preflight. Those limitations remain prerequisites to evaluate for +any later Mission-3 preflight. + +No production reset, Mission allocation/intake/planning or historical Mission +requalification is part of this finalization. The #148 results remain +`AUTONOMY_E2E_ACCEPTANCE = NIET_GEHAALD` and +`MISSION_EXECUTION_STATE = NIET_GESTART`. diff --git a/scripts/update_installed_forge.py b/scripts/update_installed_forge.py index 4bfaa90..856ad21 100644 --- a/scripts/update_installed_forge.py +++ b/scripts/update_installed_forge.py @@ -403,6 +403,73 @@ def _validated_wheel(request: UpdateRequest) -> tuple[bytes, dict[str, str]]: return wheel_bytes, manifest +def _validate_criterion_qualification(report: object, request: UpdateRequest) -> None: + """Require the exact installed composition evidence added by release 2.7.25.""" + expected = { + "partial": ("COMPLETED", 2), "single": ("COMPLETED", 1), + "misleading": ("COMPLETED", 2), "invalid": ("BLOCKED", 1), + "missing": ("BLOCKED", 1), "no-progress": ("BLOCKED", 1), + "limit": ("BLOCKED", 1), "regression": ("BLOCKED", 2), + } + row_keys = {"scenario", "status", "waiting_reason", "criteria", "actions", "assessments", + "planner_invocations", "submissions", "original_observations_preserved", + "same_mission_and_approval", "separate_process_reopen"} + if ( + not isinstance(report, Mapping) + or set(report) != {"qualification", "artifact", "scenarios", "limitations"} + or report.get("qualification") != "INSTALLED_COMPOSITION_WITH_EXTERNAL_FIXTURES" + or report.get("artifact") != { + "version": request.version, + "wheel_sha256": request.wheel_sha256.removeprefix("sha256:"), + } + or not isinstance(report.get("scenarios"), list) + or len(report["scenarios"]) != len(expected) + or not isinstance(report.get("limitations"), list) + or not report["limitations"] + or any(not isinstance(item, str) or not item for item in report["limitations"]) + ): + raise InstalledForgeUpdateError("installed criterion qualification is noncanonical") + seen = set() + for row in report["scenarios"]: + if (not isinstance(row, Mapping) or set(row) != row_keys + or not isinstance(row.get("scenario"), str)): + raise InstalledForgeUpdateError("installed criterion scenario is noncanonical") + name = row["scenario"] + if name not in expected or name in seen: + raise InstalledForgeUpdateError("installed criterion scenarios are missing or duplicated") + seen.add(name) + status, count = expected[name] + expected_proven = ({"SYNTHETIC-K1", "SYNTHETIC-K2"} if status == "COMPLETED" else + {"SYNTHETIC-K1"} if name == "limit" else + {"SYNTHETIC-K2"} if name == "regression" else set()) + expected_reason = (None if status == "COMPLETED" else + "MISSION_ACTION_LIMIT_REACHED" if name in {"limit", "regression"} else + "MISSION_NO_PROGRESS_LIMIT_REACHED") + criteria = row.get("criteria") + if ( + row.get("status") != status + or any(type(row.get(key)) is not int or row[key] != count + for key in ("actions", "assessments", "planner_invocations", "submissions")) + or any(row.get(key) is not True for key in ( + "original_observations_preserved", "same_mission_and_approval", "separate_process_reopen", + )) + or not isinstance(criteria, list) or len(criteria) != 2 + or any(not isinstance(item, Mapping) or set(item) != {"criterion", "status", "reason"} + for item in criteria) + or any(not isinstance(item.get("criterion"), str) + or not isinstance(item.get("status"), str) for item in criteria) + or {item.get("criterion") for item in criteria} != {"SYNTHETIC-K1", "SYNTHETIC-K2"} + or any(item.get("status") not in {"PROVEN", "UNSATISFIED"} for item in criteria) + or {item["criterion"] for item in criteria if item["status"] == "PROVEN"} != expected_proven + or row.get("waiting_reason") != expected_reason + or any(item.get("reason") != ( + "ALL_APPROVED_REQUIREMENTS_PROVEN" if item["status"] == "PROVEN" + else "REQUIRED_OBSERVATIONS_UNPROVEN" + ) for item in criteria) + ): + raise InstalledForgeUpdateError("installed criterion outcome does not qualify the release") + + def _normal_release_evidence( request: UpdateRequest, receipt: Mapping[str, Any], manifest: Mapping[str, str], receipt_path: Path, @@ -425,6 +492,7 @@ def _normal_release_evidence( f"dist/{sdist_name}": sdist_digest, } exact_observed = {expected_name: request.wheel_sha256, sdist_name: sdist_digest} + composition_keys = {"criterion_completion"} if request.version == "2.7.25" else set() if ( (request.existing_version, request.version) not in NORMAL_RELEASE_TRANSITIONS or set(receipt) != expected_top @@ -441,12 +509,12 @@ def _normal_release_evidence( or not isinstance(sdist_digest, str) or re.fullmatch(r"sha256:[0-9a-f]{64}", sdist_digest) is None or not isinstance(qualification, Mapping) - or set(qualification) != {"artifact_digests", "exact_main_sha", "qualification"} + or set(qualification) != {"artifact_digests", "exact_main_sha", "qualification"} | composition_keys or qualification.get("exact_main_sha") != request.product_source or qualification.get("qualification") != "forge-production-distribution" or qualification.get("artifact_digests") != exact_qualified or not isinstance(publication, Mapping) - or set(publication) != {"observed_artifact_digests", "readback", "registry"} + or set(publication) != {"observed_artifact_digests", "readback", "registry"} | composition_keys or publication.get("observed_artifact_digests") != exact_observed or publication.get("readback") != "PASS" or publication.get("registry") != "pypi" @@ -461,6 +529,9 @@ def _normal_release_evidence( raise InstalledForgeUpdateError( "normal release-complete publication, policy, or cleanup lineage is noncanonical" ) + if composition_keys: + _validate_criterion_qualification(qualification["criterion_completion"], request) + _validate_criterion_qualification(publication["criterion_completion"], request) return { "wheel": str(Path(request.wheel)), "wheel_sha256": request.wheel_sha256, diff --git a/tests/fixtures/criterion-completion-installed-summary.json b/tests/fixtures/criterion-completion-installed-summary.json new file mode 100644 index 0000000..5dc7fd3 --- /dev/null +++ b/tests/fixtures/criterion-completion-installed-summary.json @@ -0,0 +1,206 @@ +{ + "artifact": { + "version": "2.7.25", + "wheel_sha256": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "limitations": [ + "JSON artifact properties only; no application behavior claim.", + "Codex process, ExecutionHost and immutable repository transport are external fixtures.", + "This is not provider-backed planning or live EP autonomy acceptance." + ], + "qualification": "INSTALLED_COMPOSITION_WITH_EXTERNAL_FIXTURES", + "scenarios": [ + { + "actions": 2, + "assessments": 2, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + } + ], + "original_observations_preserved": true, + "planner_invocations": 2, + "same_mission_and_approval": true, + "scenario": "partial", + "separate_process_reopen": true, + "status": "COMPLETED", + "submissions": 2, + "waiting_reason": null + }, + { + "actions": 1, + "assessments": 1, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + } + ], + "original_observations_preserved": true, + "planner_invocations": 1, + "same_mission_and_approval": true, + "scenario": "single", + "separate_process_reopen": true, + "status": "COMPLETED", + "submissions": 1, + "waiting_reason": null + }, + { + "actions": 2, + "assessments": 2, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + } + ], + "original_observations_preserved": true, + "planner_invocations": 2, + "same_mission_and_approval": true, + "scenario": "misleading", + "separate_process_reopen": true, + "status": "COMPLETED", + "submissions": 2, + "waiting_reason": null + }, + { + "actions": 1, + "assessments": 1, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + } + ], + "original_observations_preserved": true, + "planner_invocations": 1, + "same_mission_and_approval": true, + "scenario": "invalid", + "separate_process_reopen": true, + "status": "BLOCKED", + "submissions": 1, + "waiting_reason": "MISSION_NO_PROGRESS_LIMIT_REACHED" + }, + { + "actions": 1, + "assessments": 1, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + } + ], + "original_observations_preserved": true, + "planner_invocations": 1, + "same_mission_and_approval": true, + "scenario": "missing", + "separate_process_reopen": true, + "status": "BLOCKED", + "submissions": 1, + "waiting_reason": "MISSION_NO_PROGRESS_LIMIT_REACHED" + }, + { + "actions": 1, + "assessments": 1, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + } + ], + "original_observations_preserved": true, + "planner_invocations": 1, + "same_mission_and_approval": true, + "scenario": "no-progress", + "separate_process_reopen": true, + "status": "BLOCKED", + "submissions": 1, + "waiting_reason": "MISSION_NO_PROGRESS_LIMIT_REACHED" + }, + { + "actions": 1, + "assessments": 1, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + } + ], + "original_observations_preserved": true, + "planner_invocations": 1, + "same_mission_and_approval": true, + "scenario": "limit", + "separate_process_reopen": true, + "status": "BLOCKED", + "submissions": 1, + "waiting_reason": "MISSION_ACTION_LIMIT_REACHED" + }, + { + "actions": 2, + "assessments": 2, + "criteria": [ + { + "criterion": "SYNTHETIC-K2", + "reason": "ALL_APPROVED_REQUIREMENTS_PROVEN", + "status": "PROVEN" + }, + { + "criterion": "SYNTHETIC-K1", + "reason": "REQUIRED_OBSERVATIONS_UNPROVEN", + "status": "UNSATISFIED" + } + ], + "original_observations_preserved": true, + "planner_invocations": 2, + "same_mission_and_approval": true, + "scenario": "regression", + "separate_process_reopen": true, + "status": "BLOCKED", + "submissions": 2, + "waiting_reason": "MISSION_ACTION_LIMIT_REACHED" + } + ] +} diff --git a/tests/test_installed_forge_update.py b/tests/test_installed_forge_update.py index db19e19..5783eb0 100644 --- a/tests/test_installed_forge_update.py +++ b/tests/test_installed_forge_update.py @@ -295,6 +295,16 @@ def _normal_release_request(self, version="2.7.24", existing_version="2.7.22"): "github_release": {"draft": False}, }, }, sort_keys=True), encoding="utf-8") + if version == "2.7.25": + # Captured from the actual installed-composition command used by + # both workflow stages; only the synthetic wheel binding changes. + summary = json.loads((Path(__file__).parent / "fixtures" / + "criterion-completion-installed-summary.json").read_text()) + summary["artifact"]["wheel_sha256"] = wheel_digest.removeprefix("sha256:") + document = json.loads(receipt.read_text()) + document["qualification"]["criterion_completion"] = summary + document["publication_receipt"]["criterion_completion"] = summary + receipt.write_text(json.dumps(document, sort_keys=True)) request = update.UpdateRequest(**{ **self.request.__dict__, "operation_id": "forge-update-2724-test-002", @@ -308,6 +318,50 @@ def _normal_release_request(self, version="2.7.24", existing_version="2.7.22"): }) return request + def test_2725_requires_exact_qualifying_and_published_installed_composition(self): + request = self._normal_release_request("2.7.25", "2.7.24") + self.assertEqual(update.validate_qualified_artifact(request)["release_route"], "NORMAL") + receipt = Path(request.qualification_receipt) + original = json.loads(receipt.read_text()) + for section in ("qualification", "publication_receipt"): + for mutation in ("missing", "wrong-wheel", "source-only", "missing-case", "duplicate-case", + "wrong-count", "false-completion", "lost-history", "malformed-criterion", + "wrong-regression-criterion", "wrong-block-reason", "extra-field"): + with self.subTest(section=section, mutation=mutation): + document = json.loads(json.dumps(original)) + report = document[section]["criterion_completion"] + if mutation == "missing": + del document[section]["criterion_completion"] + elif mutation == "wrong-wheel": + report["artifact"]["wheel_sha256"] = "f" * 64 + elif mutation == "source-only": + report["qualification"] = "SOURCE_COMPOSITION_WITH_EXTERNAL_FIXTURES" + elif mutation == "missing-case": + report["scenarios"].pop() + elif mutation == "duplicate-case": + report["scenarios"][-1] = report["scenarios"][0] + elif mutation == "wrong-count": + report["scenarios"][0]["planner_invocations"] = 1 + elif mutation == "false-completion": + report["scenarios"][-1]["status"] = "COMPLETED" + elif mutation == "lost-history": + report["scenarios"][0]["original_observations_preserved"] = False + elif mutation == "malformed-criterion": + report["scenarios"][0]["criteria"][0]["criterion"] = [] + elif mutation == "wrong-regression-criterion": + criteria = report["scenarios"][-1]["criteria"] + criteria[0]["criterion"], criteria[1]["criterion"] = ( + criteria[1]["criterion"], criteria[0]["criterion"]) + elif mutation == "wrong-block-reason": + report["scenarios"][-1]["waiting_reason"] = "completion_assessment_failed:VALUEERROR" + else: + report["scenarios"][-1]["unchecked_claim"] = "PASS" + receipt.write_text(json.dumps(document)) + changed = update.UpdateRequest(**{**request.__dict__, + "qualification_receipt_sha256": update.file_digest(receipt)}) + with self.assertRaises(update.InstalledForgeUpdateError): + update.validate_qualified_artifact(changed) + def test_normal_release_receipt_is_accepted_only_for_supported_normal_transitions(self) -> None: request = self._normal_release_request() evidence = update.validate_qualified_artifact(request)