diff --git a/INTENT.md b/INTENT.md index 448c97d..b0f288f 100644 --- a/INTENT.md +++ b/INTENT.md @@ -2,10 +2,18 @@ # NetKingdom security layer declaration (statute §11, companion §2). # Machine-readable form; the prose below is the same claim in our own voice. # Full contact map and gap records: layer.yaml +# THE layer declaration (§11, GH-DEC-2026-017 §1). `layer.yaml` is derived from +# this frontmatter and must agree with it; it does not govern. Comparison of the +# §3 vocabulary is ASCII case-insensitive, so `Staff` here and `staff` there are +# the same value and neither is re-spelled (GH-DEC-2026-017 §2). layer: Staff role: null # Engines only -standard: net-kingdom/canon/standards/security-layer-model_v0.7.md -standard_version: "0.7" +# No `standard_version` here or in the sidecar (GH-DEC-2026-017 §5 / A12): the +# declared layer is a standing property. The version the conformance check was +# built and validated against is recorded by the checker itself +# (VALIDATED_AGAINST in scripts/check_layer_conformance.py); assent versions +# live with each assent in decisions/decisions.md. +standard: net-kingdom/canon/standards/security-layer-model declared_by: decisions/decisions.md#KG-DEC-2026-001 declared_at: "2026-08-29" conformance_state: blocked-clean @@ -20,7 +28,9 @@ principal_kinds: [human, agent] > ratified by `gate-house/decisions/decisions.md` GH-DEC-2026-001; assented here > by `decisions/decisions.md` KG-DEC-2026-001 on 2026-08-28, against v0.1.)* > -> **The declaration is the frontmatter above plus `layer.yaml`**, checked by +> **The declaration is the frontmatter above.** `layer.yaml` is its derived +> machine-readable form (contact map and gap records), marked derived and required +> to agree with it (`GH-DEC-2026-017` §1); both are checked by > `scripts/check_layer_conformance.py` and tested in > `tests/test_layer_conformance.py`. Prose cannot distinguish a declaration from > a transcribed review, so those are authoritative and this note is commentary diff --git a/decisions/decisions.md b/decisions/decisions.md index 45aab6a..61c2659 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -360,3 +360,6 @@ dereferencing a judgment. Argument: `docs/StatuteV08Review.md` F1. §3.4 properties holding. - `layer.yaml` is not edited by this decision. Its `standard_version: "0.7"` stays pending the B4 ruling (`KG-IN-0007`, commentary F4). + *Superseded 2026-09-21:* B4 was ruled in `GH-DEC-2026-017` §5 and the field was + removed under `KG-IN-0007`; the validated-against version now lives in + `scripts/check_layer_conformance.py` (`VALIDATED_AGAINST`). diff --git a/intakes/intakes.md b/intakes/intakes.md index 79a737d..ab0988f 100644 --- a/intakes/intakes.md +++ b/intakes/intakes.md @@ -233,14 +233,13 @@ state_hub_intake_id: "01a0c14f-7db6-7787-aa16-a6bb06d745d2" id: KG-IN-0007 kind: intake title: §11 declaration-form precedence and case sensitivity -status: open +status: closed origin: cross-repo origin_ref: flex-auth FLEX-WP-0030 boundaries-review B1 (corrected 2026-09-21) priority: medium owner: kings-guard requested_by: flex-auth standard: net-kingdom/canon/standards/security-layer-model_v0.8.md -blocked_on: gate-house ruling on §11 precedence and §3 case sensitivity related: - KG-DEC-2026-004 - KG-DEC-2026-001 @@ -261,6 +260,19 @@ description: > an assent; the field means two things and the statute does not say which. When the rulings land, align both files and the version field in one commit. Position and argument: docs/StatuteV08Review.md F3 and F4. +resolution: > + Ruled by gate-house GH-DEC-2026-017 (amendments A9, A11, A12) and applied. + Precedence: INTENT.md governs; layer.yaml is now marked `derived: true`, + `derived_from: INTENT.md`. Case: the §3 vocabulary is closed at four tokens + and compared ASCII case-insensitively, so `Staff` and `staff` are one value + and neither file was re-spelled; the checker and tests now fold instead of + comparing with plain equality, and still fail a disagreement that survives + the fold. Version: removed from both forms. The ruling answers kings-guard's + validated-against reading rather than leaving room for it (§5: version-scoped + state belongs in the derived conformance record; keeping the field for + information was declined), so the validated-against version moved to + VALIDATED_AGAINST in scripts/check_layer_conformance.py and is printed in + every run's output. Field and checker changed in one commit. created: '2026-09-21' updated: '2026-09-21' state_hub_intake_id: "01a0c14f-935d-7e08-9f2a-fffd0dba2473" diff --git a/layer.yaml b/layer.yaml index 23741d4..df3c489 100644 --- a/layer.yaml +++ b/layer.yaml @@ -1,6 +1,12 @@ -# kings-guard — NetKingdom security layer declaration +# kings-guard — DERIVED form of the NetKingdom security layer declaration # -# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md +# THIS FILE DOES NOT GOVERN. The declaration is the `layer:` key in INTENT.md's +# frontmatter; this file is a derived artifact under §11's derived-artifact rule +# and must agree with it (GH-DEC-2026-017 §1, amendment A11). A disagreement +# between the two forms is a finding in its own right and is reported by +# scripts/check_layer_conformance.py, never resolved away by precedence. +# +# Framework: net-kingdom/canon/standards/security-layer-model # Assent: decisions/decisions.md KG-DEC-2026-001 (kings-guard's own voice, §11) # Validate: python3 scripts/check_layer_conformance.py # @@ -13,22 +19,41 @@ # capabilities that would need them sit at zero instead. Under §11 that is # BLOCKED-CLEAN, which MUST NOT rank below conforming. # -# OPEN — deliberately not resolved here (KG-IN-0007, commentary F3/F4): -# * `layer: staff` below and `layer: Staff` in INTENT.md are both forms §11 -# permits, and §11 does not say which governs when both are present. Two -# conforming conformance runs therefore disagree about this repository. -# Neither file is being changed until gate-house rules precedence AND -# whether the §3 vocabulary is case-sensitive; choosing here would be -# kings-guard authoring a ruling it does not own. -# * `standard_version` below names the version this declaration was VALIDATED -# against, not the version of any assent — assents live in -# decisions/decisions.md, each naming its own version. Whether a declaration -# may carry a version at all is flex-auth's B4, also unruled. +# RULED, 2026-09-21 — GH-DEC-2026-017, applied here (closes KG-IN-0007): +# * Precedence: INTENT.md governs; this file is derived (§1 / A11). Hence +# `derived: true` and `derived_from: INTENT.md` below. +# * Case: §3's vocabulary is closed at four tokens {Taxonomy, Tooling, Engine, +# Staff} and comparison is ASCII case-insensitive (§2, §3 / A9). `layer: +# staff` below and `layer: Staff` in INTENT.md are THE SAME VALUE and neither +# is re-spelled. Do not "fix" either to match the other; the checker folds. +# * Version: a layer declaration MUST NOT carry a standard version (§5 / A12). +# `standard_version: "0.7"` is removed from this file and from INTENT.md. +# +# History of the unruled state (kept, not rewritten): before the ruling this +# block recorded both questions as OPEN and changed neither file, because +# picking a form would have been kings-guard authoring a ruling gate-house holds. +# It also argued that `standard_version` here meant the version this declaration +# was VALIDATED against, not an assent version, so flex-auth's B4 might not +# reach it. GH-DEC-2026-017 §5 answers that reading rather than leaving room for +# it: version-scoped state — which is what "validated against" is — belongs in +# the derived conformance record, and keeping the field "for information" was +# declined because a field that is present will be branched on. The +# validated-against version therefore now lives in the checker, as +# VALIDATED_AGAINST in scripts/check_layer_conformance.py, and is printed in +# every run's output (`make check-layer`). Assent versions still live with each +# assent in decisions/decisions.md. # Assent to §9.5 at v0.8 is KG-DEC-2026-004 and does not move this file. schema_version: "0.1" framework: netkingdom-security-layer-model -standard_version: "0.7" + +# §11 derived-artifact marking (GH-DEC-2026-017 §1 / A11). +derived: true +derived_from: INTENT.md + +# NO `standard_version` KEY, AND DO NOT ADD ONE BACK (GH-DEC-2026-017 §5 / A12). +# Absence is enforced by scripts/check_layer_conformance.py. + repository: kings-guard layer: staff declared_by: decisions/decisions.md#KG-DEC-2026-001 diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py index 9126428..c172e4e 100644 --- a/scripts/check_layer_conformance.py +++ b/scripts/check_layer_conformance.py @@ -16,10 +16,21 @@ OpenBao or cluster client during an incident because the engine surface still does not exist (§9.2). That is precisely the "small convenience" §6 warns about, and it would arrive as a one-line import. +Declaration form (GH-DEC-2026-017, amendments A9, A11, A12): INTENT.md's +frontmatter `layer:` key governs; layer.yaml is a derived artifact that must be +marked derived, name INTENT.md, and agree with it. Layer comparison ASCII-folds +case against §3's closed four-token vocabulary, so `Staff` and `staff` agree and +neither file is re-spelled. A disagreement that survives the fold is reported as +a finding, not resolved by precedence. Neither form may carry a +`standard_version`; the version this check was built and validated against is +the checker's own, VALIDATED_AGAINST below, and is printed in every run's output +— this script's output is the derived conformance record A12 points to. + Review dates are reported, never enforced: a date-triggered failure breaks the build on a calendar day with no code change. -Exit 0 clean, 1 undeclared contact found, 2 declaration malformed. +Exit 0 clean, 1 undeclared contact found or the two declaration forms disagree, +2 declaration malformed. """ from __future__ import annotations @@ -35,6 +46,18 @@ import yaml ROOT = Path(__file__).resolve().parents[1] SRC = ROOT / "src" / "kings_guard" DECL = ROOT / "layer.yaml" +INTENT = ROOT / "INTENT.md" + +# The standard text this checker was built and validated against. This is the +# derived conformance record's version (GH-DEC-2026-017 §5 / A12), moved here +# from layer.yaml's former `standard_version: "0.7"`, which recorded the same +# fact in a place the ruling says a declaration must not carry it. Bump it when +# the checker is re-validated against a newer accepted text. +VALIDATED_AGAINST = "net-kingdom/canon/standards/security-layer-model_v0.7.md" + +# §3's closed layer vocabulary (A9): four tokens, compared ASCII case-insensitively. +LAYER_VOCABULARY = {"taxonomy", "tooling", "engine", "staff"} +EXPECTED_LAYER = "staff" # kings-guard's own §4 row, folded # Import roots that would constitute a direct Tooling-layer client under §4. # Matched against the top-level module of every import in src/. @@ -77,25 +100,86 @@ CREDENTIAL_LITERAL = re.compile( SKIP_CREDENTIAL_SCAN_DIRS = {".git", ".venv", "__pycache__", ".pytest_cache", ".ruff_cache"} +def fold(value: object) -> str: + """ASCII case-fold, per §3 as amended (A9): two spellings are one token.""" + return str(value).encode("ascii", "replace").decode("ascii").lower() + + +def _malformed(message: str) -> None: + print(f"FAIL: {message}", file=sys.stderr) + raise SystemExit(2) + + +def load_governing_layer() -> str: + """The declaration: INTENT.md frontmatter `layer:` (§11, GH-DEC-2026-017 §1).""" + if not INTENT.exists(): + _malformed(f"no {INTENT.name} — §11's governing declaration form") + lines = INTENT.read_text().splitlines() + if not lines or lines[0].strip() != "---": + _malformed(f"{INTENT.name} has no frontmatter to carry the declaration (§11)") + try: + end = lines[1:].index("---") + 1 + except ValueError: + _malformed(f"{INTENT.name} frontmatter is not terminated") + try: + front = yaml.safe_load("\n".join(lines[1:end])) or {} + except yaml.YAMLError as exc: + _malformed(f"{INTENT.name} frontmatter is not parseable: {exc}") + if "layer" not in front: + _malformed(f"{INTENT.name} frontmatter has no 'layer' key — §11's declaration") + if "standard_version" in front: + _malformed( + f"{INTENT.name} frontmatter carries 'standard_version' — a layer " + "declaration MUST NOT carry a standard version (§11 as amended by A12)" + ) + layer = front["layer"] + if fold(layer) not in LAYER_VOCABULARY: + _malformed( + f"{INTENT.name} declares layer {layer!r}, outside §3's closed vocabulary " + f"{sorted(LAYER_VOCABULARY)} (case-insensitive)" + ) + if fold(layer) != EXPECTED_LAYER: + _malformed(f"{INTENT.name} declares layer {layer!r}, expected Staff (§4 row)") + return str(layer) + + def load_declaration() -> dict: + """The derived form, layer.yaml (§11 derived-artifact rule).""" if not DECL.exists(): - print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr) - raise SystemExit(2) + _malformed(f"no declaration at {DECL.relative_to(ROOT)} (§11)") try: data = yaml.safe_load(DECL.read_text()) except yaml.YAMLError as exc: print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr) raise SystemExit(2) from exc - for key in ("layer", "repository", "tooling_contacts", "standard_version"): + for key in ("layer", "repository", "tooling_contacts", "derived", "derived_from"): if key not in data: - print(f"FAIL: {DECL.name} missing required key '{key}' (§11)", file=sys.stderr) - raise SystemExit(2) - if data["layer"] != "staff": - print(f"FAIL: declared layer is '{data['layer']}', expected 'staff'", file=sys.stderr) - raise SystemExit(2) + _malformed(f"{DECL.name} missing required key '{key}' (§11)") + if data["derived"] is not True: + _malformed(f"{DECL.name} must be marked 'derived: true' (§11, GH-DEC-2026-017 §1)") + if data["derived_from"] != "INTENT.md": + _malformed( + f"{DECL.name} derives from {data['derived_from']!r}; §11 names INTENT.md " + "as the governing declaration" + ) + if "standard_version" in data: + _malformed( + f"{DECL.name} carries 'standard_version' — a layer declaration MUST NOT " + "carry a standard version (§11 as amended by A12)" + ) + if fold(data["layer"]) not in LAYER_VOCABULARY: + _malformed( + f"{DECL.name} declares layer {data['layer']!r}, outside §3's closed " + f"vocabulary {sorted(LAYER_VOCABULARY)} (case-insensitive)" + ) return data +def forms_disagree(governing: object, derived: object) -> bool: + """A11: the derived form must agree with INTENT.md, after the A9 case fold.""" + return fold(governing) != fold(derived) + + def imported_modules(path: Path) -> set[str]: """Top-level module name of every import in one file.""" try: @@ -147,6 +231,7 @@ def main() -> int: parser.add_argument("--report", action="store_true", help="print the declaration summary") args = parser.parse_args() + governing = load_governing_layer() decl = load_declaration() declared = {c.get("id") for c in decl.get("tooling_contacts") or []} hits = scan() @@ -157,7 +242,9 @@ def main() -> int: checks = decl.get("agent_principal_rule_checks") or {} if args.report: - print(f"kings-guard — layer {decl['layer']}, standard v{decl['standard_version']}") + print(f"kings-guard — layer: {governing} (declared in INTENT.md; §11 governing form)") + print(f" layer.yaml: derived from {decl['derived_from']}, layer: {decl['layer']}") + print(f" checker validated against: {VALIDATED_AGAINST}") print(f" tooling contacts declared: {len(declared)}") print(f" unowned capabilities (§11 blocked-clean): " f"{len(decl.get('unowned_capabilities') or [])}") @@ -173,6 +260,15 @@ def main() -> int: form = meta.get("form", "unspecified") if isinstance(meta, dict) else "unspecified" print(f" - {name}: {form} (claimed={rules.get(name)})") + if forms_disagree(governing, decl["layer"]): + print("", file=sys.stderr) + print("FAIL: the two declaration forms disagree (§11, A11)", file=sys.stderr) + print(f" INTENT.md (governs): layer: {governing}", file=sys.stderr) + print(f" layer.yaml (derived): layer: {decl['layer']}", file=sys.stderr) + print(" Case is already folded, so this is a real layer disagreement.", file=sys.stderr) + print(" It is a finding in its own right; precedence does not erase it.", file=sys.stderr) + return 1 + if undeclared: print("", file=sys.stderr) print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", file=sys.stderr) @@ -206,7 +302,8 @@ def main() -> int: if not args.report: print( f"OK: no direct Tooling client in {SRC.relative_to(ROOT)}; " - "no standing credential (§5, §11, §3.4 rule 1)" + "no standing credential (§5, §11, §3.4 rule 1); " + f"declaration forms agree; validated against {VALIDATED_AGAINST}" ) return 0 diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py index 9ed4674..c1a80fa 100644 --- a/tests/test_layer_conformance.py +++ b/tests/test_layer_conformance.py @@ -34,10 +34,91 @@ def test_declaration_exists_and_declares_staff(): assert DECL.exists(), "no layer.yaml — §11 requires a machine-readable declaration" data = yaml.safe_load(DECL.read_text()) assert data["repository"] == "kings-guard" - assert data["layer"] == "staff" + # Folded, not equal: §3's vocabulary is case-insensitive (GH-DEC-2026-017 §2). + # An equality assertion here would perform the re-spelling the ruling declined. + assert data["layer"].lower() == "staff" assert data["framework"] == "netkingdom-security-layer-model" +def _load_checker(): + import importlib.util + + spec = importlib.util.spec_from_file_location("check_layer_conformance", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _intent_frontmatter() -> dict: + lines = (ROOT / "INTENT.md").read_text().splitlines() + assert lines[0].strip() == "---" + end = lines[1:].index("---") + 1 + return yaml.safe_load("\n".join(lines[1:end])) + + +def test_intent_governs_and_the_sidecar_is_marked_derived(): + """GH-DEC-2026-017 §1 / A11: INTENT.md governs; layer.yaml is derived from it.""" + front = _intent_frontmatter() + assert front["layer"].lower() == "staff" + data = yaml.safe_load(DECL.read_text()) + assert data["derived"] is True + assert data["derived_from"] == "INTENT.md" + + +def test_no_standard_version_in_either_form(): + """GH-DEC-2026-017 §5 / A12: a layer declaration carries no standard version.""" + assert "standard_version" not in _intent_frontmatter() + assert "standard_version" not in yaml.safe_load(DECL.read_text()) + # The validated-against version moved to the conformance record, not away. + assert _load_checker().VALIDATED_AGAINST.startswith( + "net-kingdom/canon/standards/security-layer-model" + ) + + +def test_the_two_forms_agree_after_folding_case(): + """`Staff` in INTENT.md and `staff` in layer.yaml are one value, not a finding.""" + module = _load_checker() + front = _intent_frontmatter() + data = yaml.safe_load(DECL.read_text()) + assert not module.forms_disagree(front["layer"], data["layer"]) + assert not module.forms_disagree("Staff", "STAFF") + + +def test_a_real_layer_disagreement_survives_the_fold(): + """The fold must not blind the check: Staff vs Engine is still a finding.""" + module = _load_checker() + assert module.forms_disagree("Staff", "engine") + + +def test_vocabulary_is_the_four_tokens_including_taxonomy(): + """A9: closed at four tokens; a three-token validator carries the defect.""" + assert _load_checker().LAYER_VOCABULARY == {"taxonomy", "tooling", "engine", "staff"} + + +def test_checker_rejects_a_declaration_carrying_a_standard_version(tmp_path, monkeypatch): + module = _load_checker() + decl = tmp_path / "layer.yaml" + decl.write_text(DECL.read_text() + '\nstandard_version: "0.7"\n') + monkeypatch.setattr(module, "DECL", decl) + monkeypatch.setattr(module, "ROOT", tmp_path) + with pytest.raises(SystemExit) as exc: + module.load_declaration() + assert exc.value.code == 2 + + +def test_checker_rejects_an_unmarked_sidecar(tmp_path, monkeypatch): + module = _load_checker() + data = yaml.safe_load(DECL.read_text()) + data.pop("derived") + decl = tmp_path / "layer.yaml" + decl.write_text(yaml.safe_dump(data)) + monkeypatch.setattr(module, "DECL", decl) + monkeypatch.setattr(module, "ROOT", tmp_path) + with pytest.raises(SystemExit) as exc: + module.load_declaration() + assert exc.value.code == 2 + + def test_no_tooling_contacts_declared(): """The blocked-clean position: nothing to declare, because nothing is touched.""" data = yaml.safe_load(DECL.read_text())