Apply GH-DEC-2026-017 to the layer declaration; close KG-IN-0007.

INTENT.md frontmatter governs and layer.yaml is marked derived from it.
standard_version is removed from both forms (A12). The checker and its
tests change in the same commit: they no longer require the field, they
reject it, they fold case against the four-token vocabulary (A9) instead
of comparing with plain equality, and they still fail a layer disagreement
that survives the fold (A11). Neither layer value is re-spelled.

The ruling answers kings-guard's "validated-against" reading of the field
(§5: version-scoped state belongs in the derived conformance record), so
that version now lives in the checker as VALIDATED_AGAINST and is printed
in every run's output. The standard: path in INTENT.md is unversioned to
match the reference form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 63291@bnt-lap001
Assistant-Session: 8bd77868-ca68-4f49-bb1e-d539ecc0d703
This commit is contained in:
tegwick 2026-09-21 07:35:40 +02:00
parent 6752e5e570
commit 5120adf55a
6 changed files with 259 additions and 31 deletions

View file

@ -2,10 +2,18 @@
# NetKingdom security layer declaration (statute §11, companion §2). # NetKingdom security layer declaration (statute §11, companion §2).
# Machine-readable form; the prose below is the same claim in our own voice. # Machine-readable form; the prose below is the same claim in our own voice.
# Full contact map and gap records: layer.yaml # 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 layer: Staff
role: null # Engines only role: null # Engines only
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md # No `standard_version` here or in the sidecar (GH-DEC-2026-017 §5 / A12): the
standard_version: "0.7" # 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_by: decisions/decisions.md#KG-DEC-2026-001
declared_at: "2026-08-29" declared_at: "2026-08-29"
conformance_state: blocked-clean 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 > 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.)* > 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 > `scripts/check_layer_conformance.py` and tested in
> `tests/test_layer_conformance.py`. Prose cannot distinguish a declaration from > `tests/test_layer_conformance.py`. Prose cannot distinguish a declaration from
> a transcribed review, so those are authoritative and this note is commentary > a transcribed review, so those are authoritative and this note is commentary

View file

@ -360,3 +360,6 @@ dereferencing a judgment. Argument: `docs/StatuteV08Review.md` F1.
§3.4 properties holding. §3.4 properties holding.
- `layer.yaml` is not edited by this decision. Its `standard_version: "0.7"` - `layer.yaml` is not edited by this decision. Its `standard_version: "0.7"`
stays pending the B4 ruling (`KG-IN-0007`, commentary F4). 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`).

View file

@ -233,14 +233,13 @@ state_hub_intake_id: "01a0c14f-7db6-7787-aa16-a6bb06d745d2"
id: KG-IN-0007 id: KG-IN-0007
kind: intake kind: intake
title: §11 declaration-form precedence and case sensitivity title: §11 declaration-form precedence and case sensitivity
status: open status: closed
origin: cross-repo origin: cross-repo
origin_ref: flex-auth FLEX-WP-0030 boundaries-review B1 (corrected 2026-09-21) origin_ref: flex-auth FLEX-WP-0030 boundaries-review B1 (corrected 2026-09-21)
priority: medium priority: medium
owner: kings-guard owner: kings-guard
requested_by: flex-auth requested_by: flex-auth
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
blocked_on: gate-house ruling on §11 precedence and §3 case sensitivity
related: related:
- KG-DEC-2026-004 - KG-DEC-2026-004
- KG-DEC-2026-001 - KG-DEC-2026-001
@ -261,6 +260,19 @@ description: >
an assent; the field means two things and the statute does not say which. When 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. the rulings land, align both files and the version field in one commit.
Position and argument: docs/StatuteV08Review.md F3 and F4. 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' created: '2026-09-21'
updated: '2026-09-21' updated: '2026-09-21'
state_hub_intake_id: "01a0c14f-935d-7e08-9f2a-fffd0dba2473" state_hub_intake_id: "01a0c14f-935d-7e08-9f2a-fffd0dba2473"

View file

@ -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) # Assent: decisions/decisions.md KG-DEC-2026-001 (kings-guard's own voice, §11)
# Validate: python3 scripts/check_layer_conformance.py # Validate: python3 scripts/check_layer_conformance.py
# #
@ -13,22 +19,41 @@
# capabilities that would need them sit at zero instead. Under §11 that is # capabilities that would need them sit at zero instead. Under §11 that is
# BLOCKED-CLEAN, which MUST NOT rank below conforming. # BLOCKED-CLEAN, which MUST NOT rank below conforming.
# #
# OPEN — deliberately not resolved here (KG-IN-0007, commentary F3/F4): # RULED, 2026-09-21 — GH-DEC-2026-017, applied here (closes KG-IN-0007):
# * `layer: staff` below and `layer: Staff` in INTENT.md are both forms §11 # * Precedence: INTENT.md governs; this file is derived (§1 / A11). Hence
# permits, and §11 does not say which governs when both are present. Two # `derived: true` and `derived_from: INTENT.md` below.
# conforming conformance runs therefore disagree about this repository. # * Case: §3's vocabulary is closed at four tokens {Taxonomy, Tooling, Engine,
# Neither file is being changed until gate-house rules precedence AND # Staff} and comparison is ASCII case-insensitive (§2, §3 / A9). `layer:
# whether the §3 vocabulary is case-sensitive; choosing here would be # staff` below and `layer: Staff` in INTENT.md are THE SAME VALUE and neither
# kings-guard authoring a ruling it does not own. # is re-spelled. Do not "fix" either to match the other; the checker folds.
# * `standard_version` below names the version this declaration was VALIDATED # * Version: a layer declaration MUST NOT carry a standard version (§5 / A12).
# against, not the version of any assent — assents live in # `standard_version: "0.7"` is removed from this file and from INTENT.md.
# decisions/decisions.md, each naming its own version. Whether a declaration #
# may carry a version at all is flex-auth's B4, also unruled. # 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. # Assent to §9.5 at v0.8 is KG-DEC-2026-004 and does not move this file.
schema_version: "0.1" schema_version: "0.1"
framework: netkingdom-security-layer-model 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 repository: kings-guard
layer: staff layer: staff
declared_by: decisions/decisions.md#KG-DEC-2026-001 declared_by: decisions/decisions.md#KG-DEC-2026-001

View file

@ -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, does not exist (§9.2). That is precisely the "small convenience" §6 warns about,
and it would arrive as a one-line import. 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 Review dates are reported, never enforced: a date-triggered failure breaks the
build on a calendar day with no code change. 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 from __future__ import annotations
@ -35,6 +46,18 @@ import yaml
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src" / "kings_guard" SRC = ROOT / "src" / "kings_guard"
DECL = ROOT / "layer.yaml" 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. # Import roots that would constitute a direct Tooling-layer client under §4.
# Matched against the top-level module of every import in src/. # 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"} 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: def load_declaration() -> dict:
"""The derived form, layer.yaml (§11 derived-artifact rule)."""
if not DECL.exists(): if not DECL.exists():
print(f"FAIL: no declaration at {DECL.relative_to(ROOT)} (§11)", file=sys.stderr) _malformed(f"no declaration at {DECL.relative_to(ROOT)} (§11)")
raise SystemExit(2)
try: try:
data = yaml.safe_load(DECL.read_text()) data = yaml.safe_load(DECL.read_text())
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr) print(f"FAIL: {DECL.name} is not parseable: {exc}", file=sys.stderr)
raise SystemExit(2) from exc 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: if key not in data:
print(f"FAIL: {DECL.name} missing required key '{key}' (§11)", file=sys.stderr) _malformed(f"{DECL.name} missing required key '{key}' (§11)")
raise SystemExit(2) if data["derived"] is not True:
if data["layer"] != "staff": _malformed(f"{DECL.name} must be marked 'derived: true' (§11, GH-DEC-2026-017 §1)")
print(f"FAIL: declared layer is '{data['layer']}', expected 'staff'", file=sys.stderr) if data["derived_from"] != "INTENT.md":
raise SystemExit(2) _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 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]: def imported_modules(path: Path) -> set[str]:
"""Top-level module name of every import in one file.""" """Top-level module name of every import in one file."""
try: try:
@ -147,6 +231,7 @@ def main() -> int:
parser.add_argument("--report", action="store_true", help="print the declaration summary") parser.add_argument("--report", action="store_true", help="print the declaration summary")
args = parser.parse_args() args = parser.parse_args()
governing = load_governing_layer()
decl = load_declaration() decl = load_declaration()
declared = {c.get("id") for c in decl.get("tooling_contacts") or []} declared = {c.get("id") for c in decl.get("tooling_contacts") or []}
hits = scan() hits = scan()
@ -157,7 +242,9 @@ def main() -> int:
checks = decl.get("agent_principal_rule_checks") or {} checks = decl.get("agent_principal_rule_checks") or {}
if args.report: 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" tooling contacts declared: {len(declared)}")
print(f" unowned capabilities (§11 blocked-clean): " print(f" unowned capabilities (§11 blocked-clean): "
f"{len(decl.get('unowned_capabilities') or [])}") 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" form = meta.get("form", "unspecified") if isinstance(meta, dict) else "unspecified"
print(f" - {name}: {form} (claimed={rules.get(name)})") 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: if undeclared:
print("", file=sys.stderr) print("", file=sys.stderr)
print("FAIL: undeclared Tooling-layer client (§11 undeclared violation)", 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: if not args.report:
print( print(
f"OK: no direct Tooling client in {SRC.relative_to(ROOT)}; " 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 return 0

View file

@ -34,10 +34,91 @@ def test_declaration_exists_and_declares_staff():
assert DECL.exists(), "no layer.yaml — §11 requires a machine-readable declaration" assert DECL.exists(), "no layer.yaml — §11 requires a machine-readable declaration"
data = yaml.safe_load(DECL.read_text()) data = yaml.safe_load(DECL.read_text())
assert data["repository"] == "kings-guard" 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" 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(): def test_no_tooling_contacts_declared():
"""The blocked-clean position: nothing to declare, because nothing is touched.""" """The blocked-clean position: nothing to declare, because nothing is touched."""
data = yaml.safe_load(DECL.read_text()) data = yaml.safe_load(DECL.read_text())