Files
management/docs/sources/upstream/neckbeard-v0.3.1/scripts/check_locked.py
T
Thore Cimbal 33a0ee7aec chore: upgrade the neckbeard baseline from v0.1.1 to v0.3.1
Three minor releases at once. Byte-identical surface: only WORKFLOW.md
actually changed (+128/-2); CLAUDE.md and the four templates are
untouched. AGENTS.md takes the new upstream prefix (+22/-2) and keeps our
project section unchanged.

The two declared-extended files were reconciled by hand, because nothing
compares them: validate.py gains upstream's check_vendored_portable, and
schema.yaml gains vendored, project_section_marker, the judged link field
and the ledger and verdict types. gen_status.py needed nothing — upstream
did not touch it. Recorded in the new HERKUNFT.md, including that this
reconciliation has no contradictor and will be forgotten next time.

Newly adopted: check_harvest.py and judge.py, plus docs/ledger/ and
docs/verdict/ with their templates. Deliberately not adopted:
check_locked.py — pruefe_sperrliste.py has done that job here since
2026-08-20, and two tools for one rule is maintenance without gain.

The v0.1.1 baseline stays where it is; docs/sources is immutable.
2026-08-21 12:00:00 +00:00

352 lines
13 KiB
Python

#!/usr/bin/env python3
"""check_locked.py — contradict an edit to an artifact the rules call binding.
`AGENTS.md` has said since v0.1.0 that accepted decision records are
"binding; never edited, only superseded", and that `docs/sources/` holds
immutable originals. Both rules were prose, and prose is the category of
rule the second field report found broken without exception: an accepted
record was amended, committed and pushed, and the violation was caught by
chance rather than by anything that refused. The generated index carries
the same kind of prohibition and was never once violated, because a script
contradicts it. This is that script for the other two.
Locked:
* every file under `docs/sources/` — modification, deletion or rename.
Adding a new source is not an edit and stays permitted.
* every ADR under `docs/adr/` that carried `status: accepted` *before*
the range. A newly added ADR is not locked, whatever status it
arrives with.
Permitted, because the ADR template prescribes exactly this edit: setting
`status:` and `superseded_by:` on an accepted record when a newer one
replaces it. Nothing else about the file may change in that commit.
Scope is the range it is given, and only that. Checking history would
report violations nobody can undo, which paints the pipeline permanently
red — and a permanently red check reports nothing. That failure class is
issue 0022's subject; walking into it here would be the same mistake with
a different subject.
Fails closed (exit 1, never "clean"):
* a revision range git cannot resolve
* a blob git cannot produce
⚠️ This sees one push. A violation that reaches the default branch by some
path this never runs on stays unseen; that is a boundary, not an oversight.
Usage:
python scripts/check_locked.py [--range <a>..<b>]
python scripts/check_locked.py --selftest
"""
from __future__ import annotations
import difflib
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
LOCKED_DIR = "docs/sources/"
ADR_DIR = "docs/adr/"
PERMITTED_FIELD = re.compile(r"^(status|superseded_by):", re.I)
STATUS_ACCEPTED = re.compile(r"^status:\s*accepted\s*$", re.I | re.M)
class LockError(RuntimeError):
"""The check could not answer the question. Never a pass."""
def git(root: Path, *args: str) -> str:
"""Run git, or raise. A failure is never an empty result."""
proc = subprocess.run(
["git", "-C", str(root), *args],
capture_output=True, text=True,
)
if proc.returncode != 0:
raise LockError(
f"git {' '.join(args)} failed: {proc.stderr.strip() or 'no output'}"
)
return proc.stdout
def split_range(rev_range: str) -> tuple[str, str]:
"""'a..b' -> ('a', 'b'). An empty right side means HEAD."""
if ".." not in rev_range:
raise LockError(f"not a revision range: {rev_range!r} (expected a..b)")
base, _, head = rev_range.partition("..")
base, head = base.strip(), head.strip() or "HEAD"
if not base:
raise LockError(f"range has no base: {rev_range!r}")
return base, head
def changed_paths(root: Path, base: str, head: str) -> list[tuple[str, str]]:
"""[(status, path)] for the range. Status is git's A/M/D/R letter."""
out = git(root, "diff", "--name-status", "--no-renames", base, head)
entries: list[tuple[str, str]] = []
for line in out.splitlines():
if not line.strip():
continue
parts = line.split("\t")
entries.append((parts[0][0], parts[-1]))
return entries
def blob(root: Path, rev: str, path: str) -> str | None:
"""File content at a revision, or None if it did not exist there."""
proc = subprocess.run(
["git", "-C", str(root), "show", f"{rev}:{path}"],
capture_output=True, text=True,
)
if proc.returncode != 0:
if "exists on disk, but not in" in proc.stderr or "does not exist" in proc.stderr:
return None
if "path" in proc.stderr and "not in" in proc.stderr:
return None
return None
return proc.stdout
def was_accepted(root: Path, base: str, path: str) -> bool:
"""Did this ADR carry `status: accepted` before the range?"""
before = blob(root, base, path)
return before is not None and bool(STATUS_ACCEPTED.search(before))
def only_supersede_fields(root: Path, base: str, head: str, path: str) -> bool:
"""True if the change touches nothing but status/superseded_by."""
before = blob(root, base, path)
after = blob(root, head, path)
if before is None or after is None:
return False
diff = difflib.unified_diff(
before.splitlines(), after.splitlines(), n=0, lineterm=""
)
touched = [
line[1:].strip()
for line in diff
if line[:1] in "+-" and not line.startswith(("+++", "---"))
]
if not touched:
return False
return all(PERMITTED_FIELD.match(line) for line in touched)
def check(root: Path, base: str, head: str) -> list[str]:
findings: list[str] = []
for state, path in changed_paths(root, base, head):
if path.startswith(LOCKED_DIR):
if state != "A":
findings.append(
f"{path}: immutable source, {_verb(state)} in this range"
)
continue
if path.startswith(ADR_DIR):
if not was_accepted(root, base, path):
continue
if state == "D":
findings.append(f"{path}: accepted decision record, deleted")
elif only_supersede_fields(root, base, head, path):
continue
else:
findings.append(
f"{path}: accepted decision record, edited — supersede it "
f"with a new record instead"
)
return findings
def _verb(state: str) -> str:
return {"M": "modified", "D": "deleted", "R": "renamed"}.get(state, "changed")
def report(findings: list[str]) -> None:
if not findings:
print("check_locked: no findings")
return
print(f"check_locked: {len(findings)} finding(s)\n")
for f in findings:
print(f" {f}")
print(
"\nAn accepted record is superseded, never edited; a source under "
f"{LOCKED_DIR} is never changed at all."
)
# --------------------------------------------------------------------------
# Positive control. A gate that has only ever been seen green is a
# hypothesis — WORKFLOW.md, Gate 4.
# --------------------------------------------------------------------------
def selftest() -> int:
passed = 0
failed: list[str] = []
def check_that(name: str, condition: bool) -> None:
nonlocal passed
if condition:
passed += 1
print(f" ok {passed + len(failed)} {name}")
else:
failed.append(name)
print(f" FAIL {passed + len(failed)} {name}")
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
run = lambda *a: git(root, *a)
git(root, "init", "-q", ".")
run("config", "user.email", "selftest@invalid")
run("config", "user.name", "selftest")
adr = root / ADR_DIR
adr.mkdir(parents=True)
(adr / "0001-locked.md").write_text(
"---\ntype: adr\nid: \"0001\"\nstatus: accepted\n"
"superseded_by: null\n---\n\n# ADR-0001\n\nBody.\n",
encoding="utf-8",
)
src = root / LOCKED_DIR
src.mkdir(parents=True)
(src / "digest.md").write_text("original\n", encoding="utf-8")
(root / "README.md").write_text("free\n", encoding="utf-8")
run("add", "-A")
run("commit", "-qm", "base")
base = run("rev-parse", "HEAD").strip()
# 1 — an accepted record, edited
(adr / "0001-locked.md").write_text(
"---\ntype: adr\nid: \"0001\"\nstatus: accepted\n"
"superseded_by: null\n---\n\n# ADR-0001\n\nBody, amended.\n",
encoding="utf-8",
)
run("add", "-A"); run("commit", "-qm", "amend an accepted adr")
check_that("an edit to an accepted record is reported",
len(check(root, base, "HEAD")) == 1)
run("reset", "-q", "--hard", base)
# 2 — an immutable source, edited
(src / "digest.md").write_text("rewritten\n", encoding="utf-8")
run("add", "-A"); run("commit", "-qm", "edit a source")
check_that("an edit under the immutable sources is reported",
len(check(root, base, "HEAD")) == 1)
run("reset", "-q", "--hard", base)
# 3 — negative control
(root / "README.md").write_text("free, changed\n", encoding="utf-8")
run("add", "-A"); run("commit", "-qm", "ordinary change")
check_that("an ordinary change is not reported",
check(root, base, "HEAD") == [])
run("reset", "-q", "--hard", base)
# 4 — the one edit the template prescribes
(adr / "0001-locked.md").write_text(
"---\ntype: adr\nid: \"0001\"\nstatus: superseded\n"
"superseded_by: docs/adr/0002-newer.md\n---\n\n# ADR-0001\n\nBody.\n",
encoding="utf-8",
)
run("add", "-A"); run("commit", "-qm", "supersede")
check_that("setting status and superseded_by stays permitted",
check(root, base, "HEAD") == [])
run("reset", "-q", "--hard", base)
# 5 — a new record may arrive accepted
(adr / "0002-newer.md").write_text(
"---\ntype: adr\nid: \"0002\"\nstatus: accepted\n---\n\n# ADR-0002\n",
encoding="utf-8",
)
run("add", "-A"); run("commit", "-qm", "add an accepted adr")
check_that("a newly added record is not locked",
check(root, base, "HEAD") == [])
run("reset", "-q", "--hard", base)
# 6 — a new source may be added
(src / "second.md").write_text("new digest\n", encoding="utf-8")
run("add", "-A"); run("commit", "-qm", "add a source")
check_that("adding a new source is not an edit",
check(root, base, "HEAD") == [])
run("reset", "-q", "--hard", base)
# 7 — a source that is deleted
(src / "digest.md").unlink()
run("add", "-A"); run("commit", "-qm", "delete a source")
check_that("deleting an immutable source is reported",
len(check(root, base, "HEAD")) == 1)
run("reset", "-q", "--hard", base)
# 8 — fails closed on a range git cannot resolve
check_that("an unresolvable range raises instead of reporting nothing",
_raises(lambda: check(root, "no-such-ref", "HEAD")))
# 9 — fails closed on a malformed range, through main()
check_that("main() rejects a range that is not a..b",
main(["--range", "HEAD"], root=root) == 1)
# 10 — end to end through main(): a real violation exits 1
(adr / "0001-locked.md").write_text(
"---\ntype: adr\nid: \"0001\"\nstatus: accepted\n"
"superseded_by: null\n---\n\n# ADR-0001\n\nBody, amended again.\n",
encoding="utf-8",
)
run("add", "-A"); run("commit", "-qm", "amend again")
check_that("main() exits 1 on a real violation",
main(["--range", f"{base}..HEAD"], root=root) == 1)
run("reset", "-q", "--hard", base)
# 11 — end to end through main(): a clean range exits 0
(root / "README.md").write_text("free, again\n", encoding="utf-8")
run("add", "-A"); run("commit", "-qm", "ordinary")
check_that("main() exits 0 on a clean range",
main(["--range", f"{base}..HEAD"], root=root) == 0)
total = passed + len(failed)
print()
if failed:
print(f"selftest: {len(failed)} of {total} assertions FAILED")
for name in failed:
print(f" - {name}")
return 1
print(f"selftest: {total} assertions passed")
return 0
def _raises(fn) -> bool:
try:
fn()
except LockError:
return True
except Exception:
return False
return False
def main(argv: list[str] | None = None, *, root: Path | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
if "--selftest" in args:
return selftest()
rev_range = "origin/main..HEAD"
if "--range" in args:
i = args.index("--range")
if i + 1 >= len(args):
print("check_locked: --range needs a value", file=sys.stderr)
return 1
rev_range = args[i + 1]
repo = root or Path(os.environ.get("CHECK_LOCKED_ROOT", ".")).resolve()
try:
base, head = split_range(rev_range)
findings = check(repo, base, head)
except LockError as exc:
print(f"check_locked: {exc}", file=sys.stderr)
print("unchecked is not passed.", file=sys.stderr)
return 1
report(findings)
return 1 if findings else 0
if __name__ == "__main__":
sys.exit(main())