diff --git a/layer.yaml b/layer.yaml index b721977..05094de 100644 --- a/layer.yaml +++ b/layer.yaml @@ -85,5 +85,5 @@ non_tooling_clients: operation: "HTTP to the Custodian State Hub for work records" write: true note: >- - Outside §5 by the v0.7 scope rule. Recorded, not policed. Carries no + Outside §5 by the standard's scope rule. Recorded, not policed. Carries no tenant-fact authority and no secret payload. diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index 0e24089..d833448 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -7,7 +7,8 @@ Read-only. Makes two mechanical checks: derived artifact (derived: true, derived_from: INTENT.md) that must agree with it after ASCII case-folding. The layer is one of §3's closed four tokens (Taxonomy, Tooling, Engine, Staff). Neither form carries a - standard_version (GH-DEC-2026-017 §1-§5, v0.8 amendments A9, A11, A12). + version of the standard or its companion in any key or value + (GH-DEC-2026-017 §1-§5, GH-DEC-2026-020, amendments A9, A11, A12 r2). 2. No catalogued Tooling client (OpenBao, key-cape) appears in src/ unless it maps to a declared §5.1 / §5.2 / §5.3 entry. @@ -15,6 +16,17 @@ PostgreSQL / SQLite / httpx-to-flex-auth / httpx-to-audit-core are not Tooling contacts. They are listed in layer.yaml non_tooling_clients so the inventory is total. +A12 r2 reaches content, not a key name: a `*_version` key (standard_version, +companion_version, ...), a versioned `security-layer-model_v0.7.md` or +companion path, and a bare `v0.7` token all count. `schema_version` and YAML +comments are not reached. Stance, claims and classification maps +(pep-stance.yaml, pip-claims.yaml) are not declarations; this check does not +read them for A12 and must not (A12 r2, GH-DEC-2026-020 §3). + +Every run prints the standard text it checks against (VALIDATED_AGAINST) and +the scope it ranged over, on success and on failure (GH-DEC-2026-020 §4). +There is no emitted conformance record; the version lives in the run. + Exit 0 clean, 1 undeclared contact, 2 declaration malformed. """ from __future__ import annotations @@ -36,6 +48,28 @@ SRC = ROOT / "src" / "tenant_engine" DECL = ROOT / "layer.yaml" INTENT = ROOT / "INTENT.md" +# The standard text this checker was built and validated against +# (GH-DEC-2026-020 §4, A12 r2: the version belongs to the run). Bump it when +# the checker is re-validated against a newer accepted text. +VALIDATED_AGAINST = ("net-kingdom/canon/standards/security-layer-model_v0.8.md " + "@ net-kingdom f9e1611, with gate-house " + "docs/amendments/v0.8-section-11-declaration-amendments.md " + "A9, A11, A12 r2 @ gate-house 104f3fc") +SCOPE = ("declaration = INTENT.md frontmatter + layer.yaml (every key and value); " + "tooling scan = src/tenant_engine/**/*.py; " + "not reached: pep-stance.yaml, pip-claims.yaml, comments, schema_version") + +# A12 r2: what reads as a version of this standard or its companion. +_VERSION_KEY = re.compile(r"(?:^|_)version$", re.IGNORECASE) +_EXEMPT_KEYS = {"schema_version"} # the declaration file's own schema, not reached +_VERSIONED_PATH = re.compile( + r"(?:security-layer-model|security[-_]companion|companion)[^\s]*?[_-]v?\d+(?:\.\d+)+", + re.IGNORECASE) +# A bare version token (v0.7). A declaration names no other versioned text, so +# a bare token reads as this standard's. File-name suffixes of other +# standards (…_v0.1.md) are preceded by '_' and are not matched here. +_BARE_VERSION = re.compile(r"(? str: "abcdefghijklmnopqrstuvwxyz")) +def version_findings(data: object, where: str) -> list[str]: + """Every key or value of a parsed declaration that carries a version (A12 r2).""" + found: list[str] = [] + + def walk(node: object, path: str) -> None: + if isinstance(node, dict): + for key, value in node.items(): + sub = f"{path}.{key}" if path else str(key) + if str(key) in _EXEMPT_KEYS: + continue + if _VERSION_KEY.search(str(key)): + found.append(f"{where}: key '{sub}' is a version key") + continue + if isinstance(key, str): + walk(key, sub + " (key)") + walk(value, sub) + elif isinstance(node, list): + for i, item in enumerate(node): + walk(item, f"{path}[{i}]") + elif isinstance(node, str): + if _VERSIONED_PATH.search(node) or _BARE_VERSION.search(node): + found.append(f"{where}: '{path}' carries a version: {node.strip()[:80]!r}") + + walk(data, "") + return found + + +def _reject_versions(data: object, where: str) -> None: + findings = version_findings(data, where) + if findings: + print("FAIL: a layer declaration must not carry a version of the standard " + "or its companion, in any key or value (A12 r2, GH-DEC-2026-020)", + file=sys.stderr) + for line in findings: + print(f" {line}", file=sys.stderr) + raise SystemExit(2) + + def load_declaration() -> dict: if not DECL.exists(): print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr) @@ -76,10 +148,7 @@ def load_declaration() -> dict: print(f"FAIL: {DECL.name} must be marked derived: true, derived_from: INTENT.md " "(§11, GH-DEC-2026-017 §1)", file=sys.stderr) raise SystemExit(2) - if "standard_version" in data: - print(f"FAIL: {DECL.name} carries standard_version; a layer declaration " - "must not (GH-DEC-2026-017, A12)", file=sys.stderr) - raise SystemExit(2) + _reject_versions(data, DECL.name) if _fold(data["layer"]) not in LAYER_VOCABULARY: print(f"FAIL: {DECL.name} layer {data['layer']!r} is outside the closed " f"vocabulary {sorted(LAYER_VOCABULARY)}", file=sys.stderr) @@ -106,10 +175,7 @@ def intent_frontmatter() -> dict: if "layer" not in data: print("FAIL: INTENT.md frontmatter has no layer: key (§11)", file=sys.stderr) raise SystemExit(2) - if "standard_version" in data: - print("FAIL: INTENT.md frontmatter carries standard_version; a layer " - "declaration must not (GH-DEC-2026-017, A12)", file=sys.stderr) - raise SystemExit(2) + _reject_versions(data, "INTENT.md frontmatter") if _fold(data["layer"]) not in LAYER_VOCABULARY: print(f"FAIL: INTENT.md layer {data['layer']!r} is outside the closed " f"vocabulary {sorted(LAYER_VOCABULARY)}", file=sys.stderr) @@ -154,6 +220,10 @@ def main() -> int: parser.add_argument("--report", action="store_true") args = parser.parse_args() + # GH-DEC-2026-020 §4: every run states what it checked against and its scope. + print(f"checking against: {VALIDATED_AGAINST}") + print(f"scope: {SCOPE}") + decl = load_declaration() intent = intent_frontmatter() @@ -187,7 +257,8 @@ def main() -> int: if not args.report: print(f"OK: Engine/PIP declaration matches INTENT.md; no Tooling client in " - f"{SRC.relative_to(ROOT)}") + f"{SRC.relative_to(ROOT)}; no version in the declaration; " + f"validated against {VALIDATED_AGAINST}") return 0 diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index c23c92a..f44de77 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -58,10 +58,82 @@ def test_intent_frontmatter_agrees_with_layer_yaml(): assert _fold(intent["layer"]) in {"taxonomy", "tooling", "engine", "staff"} +def _checker(): + spec = importlib.util.spec_from_file_location("check_layer_conformance", SCRIPT) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + def test_no_declaration_carries_a_standard_version(): - """GH-DEC-2026-017 / A12: neither form carries a standard version.""" + """A12 r2 / GH-DEC-2026-020: no version in any key or value of either form.""" + checker = _checker() assert "standard_version" not in yaml.safe_load((ROOT / "layer.yaml").read_text()) assert "standard_version" not in _intent() + assert checker.version_findings(yaml.safe_load((ROOT / "layer.yaml").read_text()), + "layer.yaml") == [] + assert checker.version_findings(_intent(), "INTENT.md") == [] + + +def test_versioned_standard_path_or_companion_version_fails(): + """GH-DEC-2026-020 §1-§2: the pin is caught under any name, not only standard_version.""" + checker = _checker() + base = {"layer": "Engine", "role": "PIP"} + bad = [ + {**base, "standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"}, + {**base, "companion_version": "0.2"}, + {**base, "companion": "net-kingdom/SECURITY-COMPANION.md v0.2"}, + {**base, "standard_version": "0.8"}, + {**base, "notes": [{"note": "Outside §5 by the v0.7 scope rule."}]}, + ] + for data in bad: + assert checker.version_findings(data, "x"), data + + +def test_schema_version_comments_and_section_numbers_are_not_reached(): + checker = _checker() + data = yaml.safe_load( + "# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md\n" + "schema_version: '0.1'\nstandard: netkingdom-security-layer-model\n" + "declared_shapes: {'5.1': [], '5.2': []}\ndeclared_at: '2026-08-29'\n" + "ref: net-kingdom/canon/standards/tenant-engine-boundary-contract_v0.1.md\n") + assert checker.version_findings(data, "x") == [] + + +def test_checker_does_not_apply_a12_to_stance_or_claims_maps(): + """A12 r2 / GH-DEC-2026-020 §3: those files keep their version and are not read for it.""" + for name in ("pep-stance.yaml", "pip-claims.yaml"): + assert "standard_version" in yaml.safe_load((ROOT / name).read_text()) + assert _run().returncode == 0 + + +def test_every_run_states_version_and_scope(tmp_path): + """GH-DEC-2026-020 §4: the version lives in the run, on success and on failure.""" + checker = _checker() + ok = _run() + assert ok.stdout.count(checker.VALIDATED_AGAINST) >= 2 # header and OK line + assert "scope:" in ok.stdout + report = _run("--report") + assert checker.VALIDATED_AGAINST in report.stdout and "scope:" in report.stdout + + +def test_a_failing_run_still_states_version_and_scope(tmp_path, monkeypatch, capsys): + checker = _checker() + fake = tmp_path / "INTENT.md" + fake.write_text("---\nlayer: Engine\nrole: PIP\n" + "standard: net-kingdom/canon/standards/security-layer-model_v0.7.md\n---\n") + monkeypatch.setattr(checker, "INTENT", fake) + monkeypatch.setattr(sys, "argv", ["check_layer_conformance.py"]) + try: + checker.main() + except SystemExit as exc: + assert exc.code == 2 + else: # pragma: no cover + raise AssertionError("a versioned standard: path must fail") + out = capsys.readouterr() + assert checker.VALIDATED_AGAINST in out.out and "scope:" in out.out + assert "carries a version" in out.err def test_checker_rejects_a_divergence_that_survives_the_fold(tmp_path, monkeypatch):