diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index 1e8b691..b39daf4 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -30,10 +30,14 @@ sys.path.insert(0, str(ROOT / "src")) from user_engine.layer_yaml import load_mapping, load_mapping_text # noqa: E402 -# The standard text this checker was built and validated against. The version -# belongs to the run, not to the declaration (GH-DEC-2026-020 §4, A12 r2); it is -# printed on every run. Bump it when the checker is re-validated. -VALIDATED_AGAINST = "net-kingdom/canon/standards/security-layer-model_v0.7.md" +# What every run checks against, printed on every run (GH-DEC-2026-020 §4, +# GH-DEC-2026-021 §2). The accepted text is v0.7 at net-kingdom@66dc491; the +# decision records whose rulings this checker enforces beyond v0.7 are named with it. +VALIDATED_AGAINST = ( + "net-kingdom/canon/standards/security-layer-model_v0.7.md (net-kingdom@66dc491) " + "as amended by GH-DEC-2026-017, GH-DEC-2026-020 and GH-DEC-2026-021 " + "(A9-A13, A12 r3; gate-house@39d9287)" +) # What every run ranges over. Stance, claims and classification maps # (pep-stance.yaml) are not declarations and are not checked for versions @@ -45,9 +49,15 @@ SCOPE = ( # A12 r2: no standard or companion version in any key or value of the # declaration. Comments are not parsed; schema_version is not reached. -VERSION_KEY = re.compile(r"(?:^|_)version$", re.IGNORECASE) -VERSION_VALUE = re.compile(r"(?:^|[_\-/\s])v?\d+\.\d+(?:\.\d+)*(?:\.md)?(?=$|[\s/])|_v\d+", re.IGNORECASE) +# Detector converged on the estate reference, ops-warden's (GH-DEC-2026-021 §3): +# a version token (`v?N.N`) in an identity-bearing value (`standard:`, +# `companion:`) is a pin (A12 r3); keys naming neither, e.g. `intent_version`, +# are not reached. +VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)", re.I) +VERSION_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.I) UNREACHED_KEYS = {"schema_version"} +IDENTITY_KEYS = {"standard", "companion"} +IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I) TOOLING_IMPORTS = { "hvac": "OpenBao / Vault client", @@ -81,7 +91,9 @@ def fold(value: object) -> str: ) -def find_versions(node: object, where: str, path: str = "") -> list[str]: +def find_versions( + node: object, where: str, path: str = "", identity: bool = False +) -> list[str]: """Every key or value in a declaration that carries a version (A12 r2).""" found: list[str] = [] if isinstance(node, dict): @@ -92,12 +104,14 @@ def find_versions(node: object, where: str, path: str = "") -> list[str]: if VERSION_KEY.search(str(key)): found.append(f"{where}: key '{here}'") continue - found.extend(find_versions(value, where, here)) + found.extend(find_versions(value, where, here, str(key).lower() in IDENTITY_KEYS)) elif isinstance(node, list): for index, item in enumerate(node): - found.extend(find_versions(item, where, f"{path}[{index}]")) + found.extend(find_versions(item, where, f"{path}[{index}]", identity)) elif isinstance(node, str) and VERSION_VALUE.search(node): found.append(f"{where}: value of '{path}' = {node!r}") + elif isinstance(node, str) and identity and IDENTITY_VERSION.search(node): + found.append(f"{where}: identity-bearing value of '{path}' = {node!r}") return found @@ -106,7 +120,7 @@ def reject_versions(data: dict, where: str) -> None: if found: print( "FAIL: a layer declaration MUST NOT carry a standard or companion " - "version in any key or value (GH-DEC-2026-017 §5, GH-DEC-2026-020, A12 r2)", + "version in any key or value (GH-DEC-2026-017 §5, GH-DEC-2026-020, GH-DEC-2026-021, A12 r3)", file=sys.stderr, ) for item in found: diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 25425c0..eea3cdc 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -81,6 +81,15 @@ class LayerDeclarationTests(unittest.TestCase): # pep-stance.yaml keeps its version fields (GH-DEC-2026-020 §3). self.assertTrue(module.find_versions(load_mapping(STANCE_FILE), "stance")) + def test_version_scan_matches_estate_reference_detector(self): + # GH-DEC-2026-021 §3 (A12 r3): a version token in an identity-bearing + # value is a pin; a key naming neither standard nor companion is not. + module = _checker() + self.assertTrue(module.find_versions({"standard": "security-layer-model 0.7"}, "x")) + self.assertTrue(module.find_versions({"companion": ["SECURITY-COMPANION v0.2"]}, "x")) + self.assertEqual(module.find_versions({"intent_version": "0.3"}, "x"), []) + self.assertEqual(module.find_versions({"note": "revised 0.7 wording"}, "x"), []) + def test_every_run_prints_version_and_scope(self): module = _checker() for argv in ([], ["--report"]):