diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index f407e38..432c4a2 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -58,12 +58,13 @@ 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. +# 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 and GH-DEC-2026-020 (A9-A13, A12 r2; gate-house@d8c82a8)" + "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. pep-stance.yaml is deliberately outside it. SCOPE = "INTENT.md frontmatter, layer.yaml, src/warden/**/*.py" @@ -72,12 +73,18 @@ SCOPE = "INTENT.md frontmatter, layer.yaml, src/warden/**/*.py" # 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. +# GH-DEC-2026-021 §1/§3 (A12 r3): a version is reached only as a pin. Any version +# token (`v?N.N`) in the value of an identity-bearing key (`standard:`, +# `companion:`) is a pin; a revision cited in other prose is provenance and is not +# reached. Keys such as `intent_version` name neither and are not flagged. 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"} +IDENTITY_KEYS = {"standard", "companion"} +IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I) -def find_version_pins(node, where: str = "") -> list[str]: +def find_version_pins(node, where: str = "", identity: bool = False) -> 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 @@ -92,12 +99,14 @@ def find_version_pins(node, where: str = "") -> list[str]: if VERSION_KEY.search(str(k)): pins.append(f"{here} (key names a standard/companion version)") continue - pins.extend(find_version_pins(v, here)) + pins.extend(find_version_pins(v, here, str(k).lower() in IDENTITY_KEYS)) elif isinstance(node, list): for i, v in enumerate(node): - pins.extend(find_version_pins(v, f"{where}[{i}]")) + pins.extend(find_version_pins(v, f"{where}[{i}]", identity)) elif isinstance(node, str) and VERSION_IN_VALUE.search(node): pins.append(f"{where} = {node!r} (value carries a version)") + elif isinstance(node, str) and identity and IDENTITY_VERSION.search(node): + pins.append(f"{where} = {node!r} (identity-bearing value carries a version)") return pins diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 97e0a5f..a131324 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -115,6 +115,21 @@ class TestDeclaration: assert checker.find_version_pins({"layer": "Staff", "companion_version": "0.2"}) assert checker.find_version_pins({"nested": {"standard_version": "0.7"}}) + def test_version_token_in_identity_value_is_a_pin(self): + """GH-DEC-2026-021 §3: any `v?N.N` in a standard:/companion: value is a pin.""" + checker = _checker() + pins = checker.find_version_pins({"standard": "security-layer-model v0.7"}) + assert pins and pins[0].startswith("standard") + assert checker.find_version_pins({"companion": "SECURITY-COMPANION 0.2"}) + assert checker.find_version_pins({"nested": {"standard": ["security-layer-model v0.8"]}}) + + def test_prose_citation_and_intent_version_are_not_reached(self): + """GH-DEC-2026-021 §1 (A12 r3): prose provenance and `intent_version` pass.""" + checker = _checker() + assert checker.find_version_pins( + {"layer": "Staff", "note": "Outside §5 by the v0.5 scope rule", "intent_version": "0.1.0"} + ) == [] + def test_schema_version_is_not_reached(self): assert _checker().find_version_pins({"schema_version": "0.2", "layer": "Staff"}) == [] diff --git a/wiki/playbooks/netkingdom-layer-declaration.md b/wiki/playbooks/netkingdom-layer-declaration.md index 6e3c4e2..55be51e 100644 --- a/wiki/playbooks/netkingdom-layer-declaration.md +++ b/wiki/playbooks/netkingdom-layer-declaration.md @@ -122,15 +122,16 @@ from this page. (`tests/test_layer_conformance.py` has the reference set). Change declaration, checker and tests in the same commit, and re-spell no layer value. -### Reference detector and `VALIDATED_AGAINST` — practice, not a ruling +### Reference detector and `VALIDATED_AGAINST` — the estate reference (`GH-DEC-2026-021`) The estate's checkers each wrote their own version detector in the 020 round, and they diverge: some flag any key *ending* in `version`, some any key *containing* it, one flags `vN.N` anywhere in prose while the others ignore prose. The -checkers also disagree on what `VALIDATED_AGAINST` names. Neither point is -ruled by `GH-DEC-2026-020`. What follows is the reference form to copy so the -copies stop diverging; it is steward's practice, and a gate-house ruling wins -over it. +checkers also disagree on what `VALIDATED_AGAINST` names. `GH-DEC-2026-021` §3 +rules that the detector below is the **estate reference** every checker's copy +converges on — when the copy is next edited, and no later than the commit that +re-points its `VALIDATED_AGAINST` after the v0.8 flip (`GH-WP-0004-T11`). Each +repository implements its own copy. **One detector.** Copy this from ops-warden's checker as-is: @@ -138,35 +139,50 @@ over it. 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"} +IDENTITY_KEYS = {"standard", "companion"} +IDENTITY_VERSION = re.compile(r"\bv?\d+\.\d+", re.I) -def find_version_pins(node, where=""): # walk every parsed key and value +def find_version_pins(node, where="", identity=False): # walk every parsed key and value ... # see scripts/check_layer_conformance.py ``` - **Keys:** flag a key that names a *standard or companion* version (`standard_version`, `companion_version`, `standard_version_reviewed`) — not every key containing `version`. `schema_version` is the file's own schema and - is skipped (A12 r2). + is skipped (A12 r2). **Do not flag a key such as `intent_version`**: it names + neither the standard nor its companion. `informed-decision`'s declaring + frontmatter carries `intent_version: 0.1.0` (the version of its own + `INTENT.md`), and a detector that flags every key containing or ending in + `version` fails it wrongly (`GH-DEC-2026-021`, Context). - **Values:** flag a version carried in a *file name or path* (`_v0.7`, `-v0.8.md`, `@0.7`) — the `standard: …_v0.7.md` case §1 rules on. +- **Identity-bearing values:** any version token (`v?N.N`) in the value of a + `standard:` or `companion:` key is a pin (`GH-DEC-2026-021` §3), so + `standard: security-layer-model v0.7` fails. +- **Deliberate over-reach:** the path pattern flags a versioned path of *any* + document in a declaration, wider than A12's "this standard or its companion". + `GH-DEC-2026-021` §3 accepts this; a repository that needs such a path raises + it, and it is not waived by allowlist. - **Comments:** never read; the detector runs on parsed YAML, so they are gone. - **Prose citations in a value** (e.g. a rationale string saying "the v0.5 scope - rule"): **pending.** Whether A12 reaches them is an open question to - gate-house. The reference detector deliberately does not flag a bare `vN.N` - preceded by a space, so it neither enforces nor waives an answer that has not - been given. Do not widen or narrow this in your copy until gate-house rules. + rule"): **not reached** (`GH-DEC-2026-021` §1, A12 r3). A revision cited in + prose is provenance, not a pin. The reference detector does not flag a bare + `vN.N` outside a `standard:`/`companion:` value, and no allowlist is needed. + A12 r3 is out for assent; if it is rejected in favour of the literal reading, + the reference is widened to any `vN.N` in any value. **Name the text in force.** While v0.8's acceptance flip is held (`GH-DEC-2026-019`), `VALIDATED_AGAINST` names the **accepted** text — `security-layer-model_v0.7.md`, with a net-kingdom commit — plus the decision -records whose amendments already govern (`GH-DEC-2026-017`, `GH-DEC-2026-020`), -with a gate-house commit. Naming the proposed v0.8 states a check against text +records whose rulings the checker enforces beyond v0.7 (`GH-DEC-2026-017`, +`GH-DEC-2026-020`, `GH-DEC-2026-021`), with a gate-house commit recommended +(`GH-DEC-2026-021` §2). Naming the proposed v0.8 states a check against text that does not yet govern. Change it in the same commit that follows the flip. ops-warden's constant is the example: ```text 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) +as amended by GH-DEC-2026-017, GH-DEC-2026-020 and GH-DEC-2026-021 (A9-A13, A12 r3; gate-house@39d9287) ``` **Citation.** Cite the ruling by the decision's body section: `GH-DEC-2026-017`