diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index eea52c0..f407e38 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -13,8 +13,19 @@ comparing (`GH-DEC-2026-017` §2, amendment A9): two spellings of a token do not describe two boundaries, and a check that reports findings about capital letters buries the one real disagreement it exists to find. -Neither form carries a `standard_version`, and their absence is enforced here -and by tests (`GH-DEC-2026-017` §5, amendment A12). +Neither form carries a version of the standard or of its companion, in any key +or value (`GH-DEC-2026-017` §5, amendment A12 as refined by A12 r2 / +`GH-DEC-2026-020` §1-§2). The rule reaches content, not a key name: a +`standard_version` key, a `companion_version` key, and a version-bearing path +such as `standard: .../security-layer-model_v0.7.md` are the same pin. Comments +and a file's own `schema_version` are not reached. Stance, claims and +evidence-classification maps (`pep-stance.yaml`) are NOT declarations and this +check does not read them (`GH-DEC-2026-020` §3). + +The version belongs to the run (`GH-DEC-2026-020` §4): every run prints +VALIDATED_AGAINST and SCOPE below, including the PASS line, following +kings-guard's pattern. A retained copy of this output is a derived artifact +whose version is owed by whoever retains it. Makes §11's second mechanical check real: @@ -47,6 +58,61 @@ DECL = ROOT / "layer.yaml" VALID_SHAPES = {"5.1", "5.2", "5.3"} +# What every run checks against, printed on every run (GH-DEC-2026-020 §4, A12 r2). +# The accepted text is v0.7 at net-kingdom@66dc491; the amendments that already +# govern through their decision records 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 and GH-DEC-2026-020 (A9-A13, A12 r2; gate-house@d8c82a8)" +) +# What every run ranges over. pep-stance.yaml is deliberately outside it. +SCOPE = "INTENT.md frontmatter, layer.yaml, src/warden/**/*.py" + +# A12 r2: a version of the standard or companion in any key or value of the +# declaration. Keys: anything naming a standard/companion version. Values: a +# versioned file name or path (`_v0.7`, `-v0.8.md`) or a bare version string on a +# version-named key. `schema_version` is the file's own schema, not reached. +VERSION_KEY = re.compile(r"(standard|companion).*version|version.*(standard|companion)", re.I) +VERSION_IN_VALUE = re.compile(r"[_\-.]v\d+(\.\d+)*(\.md)?\b|@v?\d+\.\d+", re.I) +NOT_REACHED_KEYS = {"schema_version"} + + +def find_version_pins(node, where: str = "") -> list[str]: + """Every place in a parsed declaration that carries a standard/companion version. + + Walks every key and value (comments are gone after parsing, which is the + A12 r2 exclusion). Returns human-readable locations; empty means clean. + """ + pins: list[str] = [] + if isinstance(node, dict): + for k, v in node.items(): + here = f"{where}.{k}" if where else str(k) + if str(k) in NOT_REACHED_KEYS: + continue + if VERSION_KEY.search(str(k)): + pins.append(f"{here} (key names a standard/companion version)") + continue + pins.extend(find_version_pins(v, here)) + elif isinstance(node, list): + for i, v in enumerate(node): + pins.extend(find_version_pins(v, f"{where}[{i}]")) + elif isinstance(node, str) and VERSION_IN_VALUE.search(node): + pins.append(f"{where} = {node!r} (value carries a version)") + return pins + + +def _reject_version_pins(label: str, node) -> None: + pins = find_version_pins(node) + if pins: + print( + f"MALFORMED: {label} carries a standard/companion version — a layer " + "declaration MUST NOT, in any key or value (§11 as amended by A12 r2, " + "GH-DEC-2026-020 §1-§2):" + ) + for p in pins: + print(f" {p}") + raise SystemExit(2) + # §3's vocabulary, closed, four tokens (GH-DEC-2026-017 §3, amendment A9). The # canonical spellings are §4's catalog-column forms; comparison is ASCII # case-insensitive, so the fold is what is stored and `Taxonomy` is in the set — @@ -80,12 +146,7 @@ def load_governing_layer() -> str: if "layer" not in front: print("MALFORMED: INTENT.md frontmatter has no 'layer' key — §11's declaration") raise SystemExit(2) - if "standard_version" in front: - print( - "MALFORMED: INTENT.md frontmatter carries 'standard_version' — a layer " - "declaration MUST NOT carry a standard version (§11 as amended by A12)" - ) - raise SystemExit(2) + _reject_version_pins("INTENT.md frontmatter", front) layer = front["layer"] if _fold(layer) not in LAYER_VOCABULARY: print( @@ -139,12 +200,7 @@ def load_declaration() -> dict: ) raise SystemExit(2) # A12: the version has no home in a declaration, governing or derived. - if "standard_version" in decl: - print( - "MALFORMED: layer.yaml carries 'standard_version' — a layer declaration " - "MUST NOT carry a standard version (§11 as amended by A12)" - ) - raise SystemExit(2) + _reject_version_pins("layer.yaml", decl) if _fold(decl["layer"]) not in LAYER_VOCABULARY: print( f"MALFORMED: layer.yaml declares layer {decl['layer']!r}, outside §3's " @@ -197,6 +253,10 @@ def main() -> int: ap.add_argument("--report", action="store_true", help="also print the declaration and gap review dates") args = ap.parse_args() + # Printed before anything can fail, so even a MALFORMED run states what it + # checked against and over what (GH-DEC-2026-020 §4). + print(f"validated against: {VALIDATED_AGAINST}") + print(f"scope: {SCOPE}") governing = load_governing_layer() decl = load_declaration() declared = {c["module"].split("/")[-1] for c in decl["tooling_contacts"]} @@ -258,9 +318,15 @@ def main() -> int: print(f" {m}") if ok and not args.report: - print(f"PASS — {len(found)} module(s) with Tooling contact, all declared.") + print( + f"PASS — {len(found)} module(s) with Tooling contact, all declared; " + f"validated against {VALIDATED_AGAINST}" + ) elif ok: - print("\nPASS — every direct Tooling contact maps to a declared shape.") + print( + "\nPASS — every direct Tooling contact maps to a declared shape; " + 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 df15f4b..97e0a5f 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -32,6 +32,17 @@ def _intent_frontmatter() -> dict: return yaml.safe_load("\n".join(lines[1:end])) +def _checker(): + import importlib.util + + spec = importlib.util.spec_from_file_location( + "check_layer_conformance", ROOT / "scripts" / "check_layer_conformance.py" + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def _fold(value: str) -> str: return str(value).strip().encode("ascii", "ignore").decode().lower() @@ -81,6 +92,51 @@ class TestDeclaration: assert "standard_version" not in _decl() assert "standard_version" not in _intent_frontmatter() + def test_no_version_anywhere_in_either_declaration(self): + """A12 r2 / GH-DEC-2026-020 §1-§2: content, not a key name. + + A versioned `standard:` path or a `companion_version` is the same pin as + `standard_version`, so the guard walks every key and value of both forms. + """ + checker = _checker() + assert checker.find_version_pins(_intent_frontmatter()) == [] + assert checker.find_version_pins(_decl()) == [] + assert not str(_intent_frontmatter()["standard"]).endswith(".md") + + def test_checker_catches_a_versioned_standard_path(self): + checker = _checker() + pins = checker.find_version_pins( + {"layer": "Staff", "standard": "net-kingdom/canon/standards/security-layer-model_v0.7.md"} + ) + assert pins and pins[0].startswith("standard") + + def test_checker_catches_a_companion_version(self): + checker = _checker() + assert checker.find_version_pins({"layer": "Staff", "companion_version": "0.2"}) + assert checker.find_version_pins({"nested": {"standard_version": "0.7"}}) + + def test_schema_version_is_not_reached(self): + assert _checker().find_version_pins({"schema_version": "0.2", "layer": "Staff"}) == [] + + def test_stance_map_is_outside_the_run(self): + """GH-DEC-2026-020 §3: a stance map keeps its version; the run must not read it.""" + stance = yaml.safe_load((ROOT / "pep-stance.yaml").read_text()) + assert "standard_version" in stance, "pep-stance.yaml keeps its clause-scoped version" + assert "pep-stance" not in _checker().SCOPE + + def test_every_run_states_version_and_scope(self): + """GH-DEC-2026-020 §4: the version belongs to the run, printed every time.""" + checker = _checker() + out = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "check_layer_conformance.py")], + capture_output=True, + text=True, + ).stdout + assert f"validated against: {checker.VALIDATED_AGAINST}" in out + assert f"scope: {checker.SCOPE}" in out + pass_line = next(ln for ln in out.splitlines() if ln.startswith("PASS")) + assert checker.VALIDATED_AGAINST in pass_line + def test_every_tooling_contact_maps_to_a_declared_shape(self): """§11 mechanical check — the guard against a new undeclared client.""" result = subprocess.run( diff --git a/wiki/playbooks/netkingdom-layer-declaration.md b/wiki/playbooks/netkingdom-layer-declaration.md index e2cf87c..68289ce 100644 --- a/wiki/playbooks/netkingdom-layer-declaration.md +++ b/wiki/playbooks/netkingdom-layer-declaration.md @@ -51,8 +51,10 @@ anyone else's files. property that does not change when the standard is revised, and a version in the declaration makes every revision read as though it invalidated every declaration. Keeping it "for information" was declined explicitly — a field - that is present will be branched on. Version-scoped state belongs in the - derived conformance record. If your checker lists `standard_version` as a + that is present will be branched on. Version-scoped state belongs to the + conformance *run* (see the 2026-09-21 `GH-DEC-2026-020` section below; the + "derived conformance record" this line used to name was a defect in §5 and + nobody is required to emit one). If your checker lists `standard_version` as a required key, or prints it in a report line, it will now **reject a conforming declaration** — fix the checker in the same commit. 2. **Add `derived: true` and `derived_from: INTENT.md`.** The sidecar is a derived @@ -82,6 +84,49 @@ ops-warden's applied instance of this change set is commit-local: `INTENT.md`, `layer.yaml`, `scripts/check_layer_conformance.py`, `tests/test_layer_conformance.py`. Read those four together rather than the sidecar alone. +## Reach and run-version change set — 2026-09-21 (`GH-DEC-2026-020`, A12 r2) + +**If you copied ops-warden's checker before this change, it enforces A12 by key +name only, and prints no version.** Both are now defects. `GH-DEC-2026-020` +refines A12 as A12 r2; verify it in `gate-house` `decisions/decisions.md` and +`docs/amendments/v0.8-section-11-declaration-amendments.md` (§ "A12 r2"), not +from this page. + +1. **A version anywhere in the declaration counts.** The declaration is every key + and value of your `INTENT.md` frontmatter and of your derived sidecar. No key + or value carries a version of the standard **or of its companion**, including + a version-bearing path: `standard: .../security-layer-model_v0.7.md` becomes + `standard: .../security-layer-model` (`GH-DEC-2026-020` §1). A de-versioned + path was a required change, not a voluntary one. +2. **`companion_version` counts** and comes out of the declaration (§2). +3. **Not reached:** comments, and a file's own `schema_version`. Keeping or + removing them is equally fine; do not edit them just to tidy. +4. **Not a declaration, not checked:** stance maps (`pep-stance.yaml`), claims + maps, evidence classifications. Their version is what makes them re-readable + when clause text changes; keep it. Your checker **MUST NOT** apply A12 to + them (§3). +5. **The version belongs to the run.** A re-runnable checker is sufficient — + not "for now" — if **every** run prints the standard version or commit it + checks against and the scope it ranged over, including the PASS line (§4). + No repository must emit a durable conformance record; whoever retains a run's + output owes that copy's version. The pattern is kings-guard's: a + `VALIDATED_AGAINST` constant in the checker, printed on every run. +6. **Widen your checker from key name to content.** ops-warden's + `scripts/check_layer_conformance.py` now carries `VALIDATED_AGAINST`, `SCOPE` + and `find_version_pins()`, which walks every parsed key and value, skips + `schema_version`, and flags any key naming a standard/companion version and + any value carrying a versioned file name or path. It prints version and scope + before loading anything, so even a MALFORMED run states them. Copy that, set + `VALIDATED_AGAINST` to what *your* run checks against, and add tests that fail + if a versioned `standard:` path or a `companion_version` comes back + (`tests/test_layer_conformance.py` has the reference set). Change declaration, + checker and tests in the same commit, and re-spell no layer value. + +**Citation.** Cite the ruling by the decision's body section: `GH-DEC-2026-017` +§5, statute A12 (now A12 r2). This page's "§5" is canonical +(`GH-DEC-2026-020`); the decision's `rationale:` part numbers are a summary and +are not cited. + ## Ownership boundary `gate-house` owns what the model requires. Each repository owns the truth of its diff --git a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md b/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md index a7c7e25..9758916 100644 --- a/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md +++ b/workplans/WARDEN-WP-0034-layer-model-v07-conformance.md @@ -292,6 +292,12 @@ says version-scoped state belongs in the derived conformance record, which checker that emits nothing durable, not an emitted record. Whether §11 expects an emitted artifact or a re-runnable check discharges it is asked of gate-house in message `4220413a` and unanswered. Nothing applied above depends on the answer. +**Closed 2026-09-21 by `GH-DEC-2026-020` §4 (gate-house@104f3fc):** §5 was wrong +that the record exists; a re-runnable checker is sufficient if every run prints +the version it checks against and its scope. Applied: the checker now prints +`VALIDATED_AGAINST` and `SCOPE` on every run and enforces A12 r2 by content, not +key name (versioned `standard:` path, `companion_version`); `pep-stance.yaml` +untouched per §3; the playbook carries the adopter change set. **Ruled the same day, and still `wait` — deliberately.** `GH-DEC-2026-017` (message `3715e247`) landed hours later: `INTENT.md` governs, the sidecar is a