diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index 94624c4..c08070d 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -13,7 +13,16 @@ its own right, reported rather than resolved away by precedence. Layer values are compared against §3's closed four-token vocabulary after an ASCII case-fold (GH-DEC-2026-017 §2-§3, amendment A9). Nothing is re-spelled: `Engine` and `engine` are one token. Neither form carries a standard version -(GH-DEC-2026-017 §5, amendment A12), and its return is rejected. +(GH-DEC-2026-017 §5, amendment A12 r2 / GH-DEC-2026-020), and its return is +rejected. The rule reaches content, not a key name: every key and value of the +INTENT.md frontmatter and of layer.yaml is walked, so a versioned `standard:` or +`companion:` path and a `companion_version` are caught as well as a +`standard_version`. Comments and `schema_version` are not reached. Stance maps +and evidence classifications (pep-stance.yaml, evidence-classification.yaml) +are not declarations and A12 is never applied to them (GH-DEC-2026-020 §3). + +The version belongs to the run (GH-DEC-2026-020 §4): every run prints +VALIDATED_AGAINST and the scope it ranged over, including the PASS line. Mechanical checks: @@ -51,17 +60,67 @@ DECISION_SURFACE = re.compile( LAYER_VOCABULARY = {"taxonomy", "tooling", "engine", "staff"} EXPECTED_LAYER = "engine" +# The standard text this checker was built and validated against, printed on +# every run (GH-DEC-2026-020 §4, A12 r2). v0.7 is the accepted text in force; +# the v0.8 §11 amendments it already applies are named alongside. Bump this +# when the checker is re-validated against a newer accepted text. +VALIDATED_AGAINST = "net-kingdom/canon/standards/security-layer-model_v0.7.md" +AMENDMENTS_APPLIED = "v0.8 A9, A11, A12 r2 (GH-DEC-2026-017, GH-DEC-2026-020)" +SCOPE = ( + "declaration = INTENT.md frontmatter + layer.yaml (every key and value); " + "source = src/secrets_engine/**/*.py; " + "not reached by A12: pep-stance.yaml, evidence-classification.yaml" +) + +# A12 r2: a version of the standard or its companion, anywhere in the +# declaration. `schema_version` is the declaration file's own schema and is +# not reached. +VERSION_KEY = re.compile(r"version", re.IGNORECASE) +UNREACHED_KEYS = {"schema_version"} +VERSIONED_REF = re.compile( + r"(?i)(security-layer-model|security-companion|layer-model|companion)" + r"[^\s]*?(?:[_@-]v?\d+(?:\.\d+)*|\bv\d+(?:\.\d+)*)" +) +STANDARD_KEYS = {"standard", "companion", "framework"} +BARE_VERSION = re.compile(r"(?i)(?:^|[_@\s-])v?\d+\.\d+(?:\.\d+)*\b") + def _fold(value: object) -> str: """ASCII case-fold, per §3 as amended: two spellings of a token are one token.""" return str(value).strip().encode("ascii", "ignore").decode().lower() +def _version_hits(data: object, path: str = "") -> list[str]: + """Every place a standard or companion version appears in a declaration.""" + hits: list[str] = [] + if isinstance(data, dict): + for key, value in data.items(): + here = f"{path}.{key}" if path else str(key) + if str(key) in UNREACHED_KEYS: + continue + if VERSION_KEY.search(str(key)): + hits.append(f"key {here!r}") + continue + if isinstance(value, str) and str(key).lower() in STANDARD_KEYS: + if BARE_VERSION.search(value): + hits.append(f"{here}: {value!r}") + continue + hits.extend(_version_hits(value, here)) + elif isinstance(data, list): + for n, item in enumerate(data): + hits.extend(_version_hits(item, f"{path}[{n}]")) + elif isinstance(data, str) and VERSIONED_REF.search(data): + hits.append(f"{path}: {data!r}") + return hits + + def _no_standard_version(where: str, data: dict) -> None: - if "standard_version" in data: + hits = _version_hits(data) + if hits: print( - f"MALFORMED: {where} carries 'standard_version' — a layer declaration " - "MUST NOT carry a standard version (§11 as amended by A12)" + f"MALFORMED: {where} carries a standard or companion version at " + f"{'; '.join(hits)} — a layer declaration MUST NOT carry one in any " + "key or value (§11 as amended by A12 r2, GH-DEC-2026-020 §1-§2)" ) raise SystemExit(2) @@ -172,6 +231,8 @@ def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--report", action="store_true") args = ap.parse_args() + print(f"validated against: {VALIDATED_AGAINST} [{AMENDMENTS_APPLIED}]") + print(f"scope: {SCOPE}") front = intent_frontmatter() decl = load_declaration() @@ -239,10 +300,14 @@ def main() -> int: if ok and not args.report: print( "PASS — Engine/Lifecycle declaration present, no decision surface, " - f"{len(found)} OpenBao adapter module(s) owned." + f"{len(found)} OpenBao adapter module(s) owned; " + f"validated against {VALIDATED_AGAINST}." ) elif ok: - print("\nPASS — declaration, owned tooling, and PEP stance path hold.") + print( + "\nPASS — declaration, owned tooling, and PEP stance path hold; " + f"validated against {VALIDATED_AGAINST}." + ) return 0 if ok else 1 diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 26478b1..c74affe 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -141,6 +141,86 @@ def test_checker_rejects_a_returning_standard_version(tmp_path, monkeypatch): assert raised.value.code == 2 +def _run_with(tmp_path, monkeypatch, front_patch: dict, decl_patch: dict) -> int: + checker = _checker() + front = {**_front(), **front_patch} + decl = {**_decl(), **decl_patch} + intent = tmp_path / "INTENT.md" + intent.write_text("---\n" + yaml.safe_dump(front) + "---\n\n# INTENT\n", encoding="utf-8") + sidecar = tmp_path / "layer.yaml" + sidecar.write_text(yaml.safe_dump(decl), encoding="utf-8") + monkeypatch.setattr(checker, "INTENT", intent) + monkeypatch.setattr(checker, "DECL", sidecar) + monkeypatch.setattr(sys, "argv", ["check_layer_conformance.py"]) + try: + return checker.main() + except SystemExit as exc: + return int(exc.code) + + +@pytest.mark.parametrize( + ("front_patch", "decl_patch"), + [ + # GH-DEC-2026-020 §1: a version-bearing standard: path counts. + ({"standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"}, {}), + ({"standard": "net-kingdom/canon/standards/security-layer-model_v0.8"}, {}), + ({}, {"standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"}), + ({"companion": "net-kingdom/SECURITY-COMPANION_v0.2.md"}, {}), + # GH-DEC-2026-020 §2: companion_version counts. + ({"companion_version": "0.2"}, {}), + ({}, {"companion_version": "0.2"}), + ({"standard_version": "0.7"}, {}), + ({}, {"standard_version": "0.8"}), + ], +) +def test_checker_rejects_a_version_anywhere_in_the_declaration( + tmp_path, monkeypatch, capsys, front_patch, decl_patch +): + """A12 r2 reaches content, not a key name (GH-DEC-2026-020 §1-§2).""" + assert _run_with(tmp_path, monkeypatch, front_patch, decl_patch) == 2 + out = capsys.readouterr().out + assert "standard or companion version" in out + # The version belongs to the run: stated even when the run fails. + assert "validated against: net-kingdom/canon/standards/security-layer-model_v" in out + assert "scope:" in out + + +def test_schema_version_is_not_reached(tmp_path, monkeypatch): + """GH-DEC-2026-020 §1: a declaration file's own schema version is not reached.""" + assert _run_with(tmp_path, monkeypatch, {}, {"schema_version": "0.2"}) == 0 + + +def test_stance_and_classification_versions_are_not_reached(): + """GH-DEC-2026-020 §3: stance maps and classifications keep their version, + and the checker never applies A12 to them.""" + checker = _checker() + assert "pep-stance.yaml" in checker.SCOPE and "evidence-classification.yaml" in checker.SCOPE + for path in (STANCE, CLASSIFICATION): + data = yaml.safe_load(path.read_text(encoding="utf-8")) + # Each carries a version A12 would reject if it were applied there... + assert checker._version_hits(data), f"{path.name} keeps its standard_version" + # ...and the real-tree run still passes: A12 is not applied to them. + result = subprocess.run( + [sys.executable, str(SCRIPT)], cwd=ROOT, capture_output=True, text=True, check=False + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_every_run_states_version_and_scope(): + """GH-DEC-2026-020 §4: the version and scope are printed on every run, PASS included.""" + checker = _checker() + for argv in ([], ["--report"]): + result = subprocess.run( + [sys.executable, str(SCRIPT), *argv], + cwd=ROOT, capture_output=True, text=True, check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert f"validated against: {checker.VALIDATED_AGAINST}" in result.stdout + assert "scope: declaration = INTENT.md frontmatter + layer.yaml" in result.stdout + pass_line = [l for l in result.stdout.splitlines() if l.startswith("PASS")] + assert pass_line and checker.VALIDATED_AGAINST in pass_line[0] + + def test_checker_passes_on_the_real_tree(): result = subprocess.run( [sys.executable, str(SCRIPT)],