diff --git a/REPOSITORY_LIFECYCLE.md b/REPOSITORY_LIFECYCLE.md index de1f38d..1fd4d43 100644 --- a/REPOSITORY_LIFECYCLE.md +++ b/REPOSITORY_LIFECYCLE.md @@ -80,13 +80,14 @@ revocation, release drift, an authority failure, or missing evidence produces the same state. The validator never restores an older admission or replaces the state with Beta, Experimental, or Early access. -The signed ledger currently has no active admissions. These are the derived -states: +The signed ledger currently has one active admission, for `flow`. The other +six targets have none. Product-wide Production requires all seven. These are +the derived states: | Target | Current state | Role | |------------|-----------|------| | `openadapt` | **Not actively admitted** | `OpenAdapt` launcher/meta-package and unified CLI | -| `flow` | **Not actively admitted** | `openadapt-flow` compiler and governed runtime | +| `flow` | **Production** | `openadapt-flow` compiler and governed runtime | | `cloud` | **Not actively admitted** | Proprietary control plane and hosted execution surface | | `desktop` | **Not actively admitted** | Desktop recording, qualification, execution, evidence, and repair cockpit | | `capture` | **Not actively admitted** | Native screen, input, timing, window, and media capture | diff --git a/scripts/validate_production_lifecycle.py b/scripts/validate_production_lifecycle.py index a4e4250..f04b3df 100755 --- a/scripts/validate_production_lifecycle.py +++ b/scripts/validate_production_lifecycle.py @@ -245,6 +245,12 @@ } +def is_product_production(active: Mapping[str, str]) -> bool: + """True only when every one of the seven targets has an active admission.""" + + return set(active) == set(EXPECTED_TARGETS) + + class LifecycleError(ValueError): """The lifecycle state is not supported by its evidence.""" @@ -1906,11 +1912,10 @@ def validate( f"admission {target_id} release sequence is not continuous" ) seen.append(sequence) - # remote-safe-synthetic is a real package admission. It is not - # seven-target Production and it does not flip MockMed - # production_acceptance. - if admission["evidence_class"] != "remote-safe-synthetic": - active[target_id] = admission_id + # remote-safe-synthetic is a real package admission for this + # target. One row is not seven-target Production. It does not + # flip MockMed production_acceptance. + active[target_id] = admission_id continue admission = _closed( item, diff --git a/tests/test_production_lifecycle.py b/tests/test_production_lifecycle.py index a720ea6..d535461 100644 --- a/tests/test_production_lifecycle.py +++ b/tests/test_production_lifecycle.py @@ -1103,7 +1103,38 @@ def test_published_policy_is_accepted(self) -> None: # The Flow 1.34.0 row is issued 2026-09-02T18:24:25Z. NOW is the # retained v1 fixture clock and is before that instant. published_now = datetime(2026, 9, 2, 19, 0, 0, tzinfo=timezone.utc) - self.assertEqual(lifecycle.validate_files(ROOT, now=published_now), {}) + active = lifecycle.validate_files(ROOT, now=published_now) + release = json.loads( + ( + ROOT + / "production-evidence/objects/sha256/79/" + "790122a25c87e456c6e45d25ebf5cd029b21b8511265b062fd9129b74aa1dd82" + ".qualification-release.json" + ).read_text(encoding="utf-8") + ) + self.assertEqual(release["evidence_class"], "remote-safe-synthetic") + self.assertEqual(release["target"], "flow") + self.assertEqual(active, {"flow": release["admission_id_sha256"]}) + self.assertEqual(len(lifecycle.EXPECTED_TARGETS), 7) + self.assertFalse(lifecycle.is_product_production(active)) + + def test_synthetic_flow_release_is_one_active_target_not_product_production( + self, + ) -> None: + published_now = datetime(2026, 9, 2, 19, 0, 0, tzinfo=timezone.utc) + active = lifecycle.validate_files(ROOT, now=published_now) + self.assertEqual(len(active), 1) + self.assertIn("flow", active) + self.assertEqual( + set(lifecycle.EXPECTED_TARGETS) - set(active), + {"agent", "capture", "cloud", "desktop", "docs", "openadapt"}, + ) + self.assertFalse(lifecycle.is_product_production(active)) + seven = { + target_id: f"admission:{target_id}" + for target_id in lifecycle.EXPECTED_TARGETS + } + self.assertTrue(lifecycle.is_product_production(seven)) def test_check_profile_accepts_the_published_repository(self) -> None: completed = subprocess.run(