diff --git a/INTENT.md b/INTENT.md index 05f71ca..f15fe9d 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,7 +1,7 @@ --- layer: Engine role: PIP -standard: net-kingdom/canon/standards/security-layer-model_v0.7.md +standard: net-kingdom/canon/standards/security-layer-model companion: net-kingdom/SECURITY-COMPANION.md declared: "2026-08-29" --- diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index 551e155..1e8b691 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -8,12 +8,17 @@ INTENT.md frontmatter governs the layer; layer.yaml is its derived form. The failure this exists to catch is a convenience: an OpenBao, Vault, LDAP, or cluster client arriving as one import. That is an undeclared violation. +Every run prints the standard text it checks against (VALIDATED_AGAINST) and +the scope it ranged over (SCOPE), on the OK line and in --report. The version +lives in the run, not in the declaration (GH-DEC-2026-020, A12 r2). + Exit 0 clean, 1 undeclared contact found, 2 declaration malformed. """ from __future__ import annotations import argparse import ast +import re import sys from pathlib import Path @@ -25,6 +30,25 @@ 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 ranges over. Stance, claims and classification maps +# (pep-stance.yaml) are not declarations and are not checked for versions +# (GH-DEC-2026-020 §3). +SCOPE = ( + "declaration: INTENT.md frontmatter + layer.yaml (all keys and values); " + "imports: src/user_engine/**/*.py" +) + +# 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) +UNREACHED_KEYS = {"schema_version"} + TOOLING_IMPORTS = { "hvac": "OpenBao / Vault client", "bao": "OpenBao client", @@ -57,6 +81,39 @@ def fold(value: object) -> str: ) +def find_versions(node: object, where: str, path: str = "") -> list[str]: + """Every key or value in a declaration that carries a version (A12 r2).""" + found: list[str] = [] + if isinstance(node, dict): + for key, value in node.items(): + here = f"{path}.{key}" if path else str(key) + if str(key) in UNREACHED_KEYS: + continue + if VERSION_KEY.search(str(key)): + found.append(f"{where}: key '{here}'") + continue + found.extend(find_versions(value, where, here)) + elif isinstance(node, list): + for index, item in enumerate(node): + found.extend(find_versions(item, where, f"{path}[{index}]")) + elif isinstance(node, str) and VERSION_VALUE.search(node): + found.append(f"{where}: value of '{path}' = {node!r}") + return found + + +def reject_versions(data: dict, where: str) -> None: + found = find_versions(data, where) + 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)", + file=sys.stderr, + ) + for item in found: + print(f" {item}", file=sys.stderr) + raise SystemExit(2) + + def load_intent_layer() -> str: """Return the governing `layer:` value from INTENT.md frontmatter (§11).""" try: @@ -73,6 +130,7 @@ def load_intent_layer() -> str: except ValueError as exc: print(f"FAIL: {INTENT.name} frontmatter is not parseable: {exc}", file=sys.stderr) raise SystemExit(2) from exc + reject_versions(front, f"{INTENT.name} frontmatter") if "layer" not in front: print(f"FAIL: {INTENT.name} frontmatter has no 'layer:' key (§11)", file=sys.stderr) raise SystemExit(2) @@ -94,13 +152,7 @@ def load_declaration() -> dict: if key not in data: print(f"FAIL: {DECL.name} missing required key '{key}' (§11)", file=sys.stderr) raise SystemExit(2) - if "standard_version" in data: - print( - f"FAIL: {DECL.name} carries standard_version; a layer declaration " - "MUST NOT carry a standard version (GH-DEC-2026-017, A12)", - file=sys.stderr, - ) - raise SystemExit(2) + reject_versions(data, DECL.name) if data["derived"] is not True or data["derived_from"] != "INTENT.md": print( f"FAIL: {DECL.name} must be marked derived: true, derived_from: INTENT.md " @@ -192,6 +244,8 @@ def main() -> int: f"user-engine — layer {decl['layer']}/{decl['role']} " f"(derived from {decl['derived_from']})" ) + print(f" checked against: {VALIDATED_AGAINST}") + print(f" scope: {SCOPE}") print(" tooling contacts declared: 0") print(f" own-store declarations: {len(own_store)}") print(f" own-store imports: {len(own_store_hits)}") @@ -219,7 +273,7 @@ def main() -> int: if not args.report: print( f"OK: no catalogued Tooling client in {SRC.relative_to(ROOT)} " - "(Engine/PIP, §11)" + f"(Engine/PIP, §11); checked against {VALIDATED_AGAINST}; scope: {SCOPE}" ) return 0 diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 3c1233d..25425c0 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -60,6 +60,40 @@ class LayerDeclarationTests(unittest.TestCase): self.assertNotIn("standard_version", decl) self.assertNotIn("standard_version", front) + def test_intent_standard_path_is_unversioned(self): + # GH-DEC-2026-020 §1: a version inside the standard: path is a + # standard version under A12 r2; companion_version is one too (§2). + front = load_mapping_text((ROOT / "INTENT.md").read_text().split("---", 2)[1]) + decl = load_mapping(LAYER) + self.assertEqual( + front["standard"], "net-kingdom/canon/standards/security-layer-model" + ) + for mapping in (front, decl): + self.assertNotIn("companion_version", mapping) + module = _checker() + self.assertEqual(module.find_versions(front, "INTENT.md"), []) + self.assertEqual(module.find_versions(decl, "layer.yaml"), []) + + def test_version_scan_leaves_schema_version_and_stance_alone(self): + module = _checker() + self.assertEqual(module.find_versions({"schema_version": "0.1"}, "x"), []) + self.assertNotIn("pep-stance", module.SCOPE) + # pep-stance.yaml keeps its version fields (GH-DEC-2026-020 §3). + self.assertTrue(module.find_versions(load_mapping(STANCE_FILE), "stance")) + + def test_every_run_prints_version_and_scope(self): + module = _checker() + for argv in ([], ["--report"]): + result = subprocess.run( + [sys.executable, str(SCRIPT), *argv], + capture_output=True, + text=True, + cwd=ROOT, + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn(module.VALIDATED_AGAINST, result.stdout) + self.assertIn(module.SCOPE, result.stdout) + def test_vocabulary_is_four_tokens_compared_after_fold(self): module = _checker() self.assertEqual( @@ -74,6 +108,17 @@ class LayerDeclarationTests(unittest.TestCase): good = load_mapping(LAYER) cases = { "standard_version": dict(good, standard_version="0.7"), + "companion_version": dict(good, companion_version="0.2"), + "versioned standard path": dict( + good, + standard="net-kingdom/canon/standards/security-layer-model_v0.7.md", + ), + "versioned companion path": dict( + good, companion="net-kingdom/SECURITY-COMPANION.md v0.2" + ), + "nested version key": dict( + good, catalog_entry=dict(good["catalog_entry"], standard_version="0.8") + ), "not derived": dict(good, derived=False), "divergent layer": dict(good, layer="staff"), }