[FIX] PENDING-164 (c)+(d): prior-art search, one implementation, two surfaces

Steward-authorized in session. prior-art.py searches commit messages across every
owned repo with NO count window and reports the register's mention count beside
it; the executor runs it as a CLI (d) and governance-mcp.py exposes it as
prior_art (c). One implementation on purpose — a value computed twice on two
sides of a boundary is how the parties end up with different answers.

Verified on the case that motivated the item: 'LFS' returns 20 commits including
0677e8a and 95760ff, both past repo_activity's 100-commit floor, one of them in
dotfiles which is not in REPOS. The positive control forced the enumeration to be
COMPUTED from remote ownership rather than copied from REPOS, or 95760ff would
have been unreachable and the control would have failed.

Had this existed this morning, one command before filing PENDING-163 would have
returned 0677e8a and 400c054.

⚠ Its first run returned zero and the control caught it: sh() discarded stdout on
non-zero exit, and find over $HOME exits 1 from 154 unreadable Library dirs while
printing all 37 repos. Third false-zero of the day, first one caught before being
believed — the difference is that the jurist pre-specified what it must return.

AMENDMENT 1's census: mechanical half runs (661 candidates, narrowed to 270),
interpretive half does not. Identifying WHICH mechanism a commit decided about is
interpretation, not extraction. Limit declared rather than a column manufactured.
The backlog is NOT censused and no number here is one.

Extending the read-only guarantee to delegates found a pre-existing hole: bare
.replace flagged str.replace() (why it had never been extended), and wake-digest,
a delegate since before today, was never covered. Its only real mutation is
emit_brief(), its hook role, unreachable from any tool. Now a declared exemption
per delegate, so a new mutating function fails until named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvZAKSf9aqratbqHbU9LK5
This commit is contained in:
David F Glidden
2026-08-26 19:15:41 +02:00
co-authored by Claude Opus 5
parent 46aa3d15a2
commit b28cc03737
3 changed files with 366 additions and 1 deletions
+119 -1
View File
@@ -63,6 +63,7 @@ def _load(mod_name, filename):
wd = _load("wake_digest", "wake-digest.py") # the single definition of "open item"
pa = _load("prior_art", "prior-art.py") # PENDING-164 (c): commit history, no window
# ---- the enumerated domain: keys, never paths -------------------------------
FILES = {
@@ -428,6 +429,53 @@ def t_pair(args):
return "\n".join(out)
# ── PENDING-164 (c) ───────────────────────────────────────────────────────────────
# repo_activity caps at 100 commits. At chamber-library's rate that floor sat five weeks
# short of 0677e8a — the commit retiring LFS — so when the jurist recommended adopting
# LFS on 2026-08-26, NO instrument available to it could have reached the refutation.
# 'Search prior art before proposing' was not a rule the jurist could follow.
#
# This removes the asymmetry rather than papering over it: one implementation
# (prior-art.py), two consumers — the executor's CLI and this tool — so the answer cannot
# differ by surface. It returns the substrate, not testimony about it.
def t_prior_art(args):
"""Commit-message history for a named mechanism, across every owned repo, unbounded."""
term = (args.get("term") or "").strip()
if not term:
return "ERROR: term is required (the mechanism's name, e.g. 'LFS', 'submodule')."
if len(term) < 2:
return "ERROR: term too short to be discriminating."
ok, notes, _ = pa.controls()
hits = pa.search_commits(term)
reg = pa.register_mentions(term)
out = []
if not ok:
out.append("⚠ INSTRUMENT NOT VERIFIED — result unestablished. "
+ "; ".join(notes))
out.append("")
out.append(f"PRIOR ART: {term!r}")
out.append(f" commits mentioning it : {len(hits)} (all branches, NO count window)")
out.append(f" register mentions : {reg} (PENDING, PENDING-archive, REVIEWED)")
out.append("")
for repo, sha, date, subj in sorted(hits, key=lambda h: h[2])[:60]:
out.append(f" {date} {repo:32} {sha} {subj[:72]}")
if len(hits) > 60:
out.append(f" … {len(hits) - 60} further commit(s) not listed.")
out.append("")
if hits and reg == 0:
out.append(" \u26a0 FINDING — PENDING-164's condition exactly: this mechanism has a")
out.append(" history in the repos and NO trace in the authorization record. Whatever")
out.append(" was decided about it was decided in a commit message. Read those")
out.append(" commits before proposing anything about it.")
elif hits:
out.append(" Both surfaces carry it. A ruling can post-date the commit that")
out.append(" motivated it, or precede the one that undid it — read both.")
else:
out.append(" No prior art. \u26a0 Weak absence: it means no COMMIT MESSAGE names this")
out.append(" term, not that nothing was decided about it.")
return "\n".join(out)
TOOLS = [
("governance_state", t_state,
"Current governance state, computed live: every open authorization item with its "
@@ -474,6 +522,16 @@ TOOLS = [
"Full governance-drift-check output: claims in CLAUDE.md the substrate contradicts. "
"Detection only — correcting doctrine requires steward authorization.",
{"type": "object", "properties": {}}),
("prior_art", t_prior_art,
"Commit-message history for a named mechanism across every owned repo, with NO "
"commit-count window, plus whether the authorization register mentions it. Use this "
"BEFORE proposing or ruling on any named mechanism — repo_activity caps at 100 "
"commits and that floor has already hidden a decision this system had made. Commits "
"with zero register mentions is the finding, not the noise.",
{"type": "object", "properties": {
"term": {"type": "string",
"description": "The mechanism's name, e.g. 'LFS', 'submodule', 'worktree'."}},
"required": ["term"]}),
("repo_activity", t_repo,
"Branch, uncommitted-file status and recent commits for one of the active repos.",
{"type": "object", "properties": {
@@ -550,8 +608,16 @@ def write_calls(src):
duly found all of them, in its own definition. That is the day's recurring shape:
an instrument whose domain includes itself. The AST sees calls, not characters."""
import ast
MUTATORS = {"remove", "unlink", "rename", "replace", "rmdir", "mkdir", "makedirs",
MUTATORS = {"remove", "unlink", "rename", "rmdir", "mkdir", "makedirs",
"chmod", "truncate", "write", "writelines", "write_text", "write_bytes"}
# `replace` is NOT in MUTATORS as a bare attribute: str.replace() is ubiquitous and
# flagging it made this check unusable on any file that manipulates text — which is
# why it had never been extended past this file. os.replace/Path.replace ARE caught,
# by qualified name below. Narrowed 2026-08-26 when extending the guarantee to
# delegates surfaced three false positives in wake-digest.py (lines 142, 150, 888,
# every one a string replace) alongside one real write.
QUALIFIED = {("os", "replace"), ("shutil", "move"), ("shutil", "rmtree"),
("shutil", "copy"), ("shutil", "copy2"), ("shutil", "copytree")}
out = []
for n in ast.walk(ast.parse(src)):
if not isinstance(n, ast.Call):
@@ -565,6 +631,9 @@ def write_calls(src):
out.append(f"open(mode={mode!r}) at line {n.lineno}")
elif isinstance(f, ast.Attribute) and f.attr in MUTATORS:
out.append(f"{f.attr}() at line {n.lineno}")
elif (isinstance(f, ast.Attribute) and isinstance(f.value, ast.Name)
and (f.value.id, f.attr) in QUALIFIED):
out.append(f"{f.value.id}.{f.attr}() at line {n.lineno}")
return out
@@ -693,6 +762,19 @@ def selftest():
"2026-08-05; this control goes red if a header is ever hidden again]",
"MATCHES OUTSIDE ANY ITEM" not in t_search({"query": "the"}))
print("\nprior_art — PENDING-164 (c): the window that hid 0677e8a:")
chk("prior_art returns the commit repo_activity's 100-window could not reach "
"[0677e8a, 2026-06-05, five weeks past the floor]",
"0677e8a" in t_prior_art({"term": "LFS"}))
chk("prior_art reaches dotfiles, which is NOT in REPOS [95760ff]",
"95760ff" in t_prior_art({"term": "LFS"}))
chk("prior_art reports the register count alongside the commits "
"[the asymmetry IS the finding]",
"register mentions" in t_prior_art({"term": "LFS"}))
chk("prior_art on a nonsense term reports no prior art, and calls the absence weak "
"[negative control]",
"No prior art" in t_prior_art({"term": "zzqqxx-not-a-real-term-9971"}))
chk("prior_art refuses an empty term", t_prior_art({}).startswith("ERROR"))
chk("repo_activity refuses an unlisted repo",
t_repo({"repo": "/etc"}).startswith("ERROR: unknown repo"))
chk("repo_activity ACCEPTS a listed repo [positive control]",
@@ -740,6 +822,42 @@ def selftest():
src = open(__file__, encoding="utf-8").read()
chk("no filesystem-mutating call in this file",
write_calls(src) == [])
# ⚠ The guarantee is scoped to the file the AST reads. Adding prior_art (PENDING-164
# (c)) put a DELEGATION outside that scope: t_prior_art calls into prior-art.py, whose
# code this check never saw. A structural guarantee with a hole where it delegates is
# the shape of every other defect in this thread — so the delegate is checked too, and
# any future delegate must be added here or the guarantee silently narrows.
# Each delegate must have NO mutating call, except in functions DECLARED here as
# unreachable from this server. Declared, never inferred — the same shape as the hook
# allowlist: a new mutating function in a delegate fails until someone names it and
# says why. wake-digest.py is also a SessionStart hook, and emit_brief() is its hook
# role; no tool in this file calls it.
_delegates = {"prior-art.py": set(), "wake-digest.py": {"emit_brief"}}
for _fn, _exempt in sorted(_delegates.items()):
_dsrc = open(os.path.join(SCRIPTS, _fn), encoding="utf-8").read()
_tree = __import__("ast").parse(_dsrc)
_spans = {f.name: (f.lineno, f.end_lineno) for f in __import__("ast").walk(_tree)
if isinstance(f, __import__("ast").FunctionDef)}
_bad = []
for _call in write_calls(_dsrc):
_ln = int(_call.rsplit(" ", 1)[-1])
_in = [n for n, (a, b) in _spans.items() if a <= _ln <= (b or a)]
if not any(n in _exempt for n in _in):
_bad.append(f"{_call} in {_in or ['<module level>']}")
chk(f"DELEGATE {_fn}: no mutating call outside its declared exemptions {sorted(_exempt) or '(none)'} "
"[the read-only guarantee must not stop at this file's edge]",
_bad == [], )
if _bad:
for _b in _bad:
print(f" {_b}")
chk("the delegate check DOES flag an undeclared mutation [positive control — an "
"exemption list that never refuses is not a check]",
write_calls("import os\nos.remove('x')\n") != [])
chk("str.replace() is NOT flagged as a mutation [negative control — it was, and that "
"false positive is why this guarantee had never been extended]",
write_calls("s = 'a'.replace('a','b')\n") == [])
chk("os.replace() IS still flagged [positive control for the narrowing above]",
write_calls("import os\nos.replace('a','b')\n") != [])
chk("the checker DOES flag writes when present [positive control — a text search "
"here would match its own token list, which is how the first version of this "
"check failed]",
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Prior-art search over commit history — PENDING-164 options (c) and (d).
WHY THIS EXISTS
On 2026-06-05 the steward adopted Git LFS in chamber-library, found it a misfit,
retired it, and rewrote seventeen commits to undo it (0677e8a). A per-repo hook
exemption replaced it (400c054). Neither decision appears anywhere in PENDING.md,
PENDING-archive.md or REVIEWED.md.
Twelve weeks later the executor filed PENDING-163 recommending a route toward LFS,
and the jurist recommended adopting it outright. Three exchanges were spent before
the refutation surfaced, and it surfaced by accident — the string 'pre-lfs-export'
appeared in an unrelated directory listing.
The symptom was two parties reasoning toward a retired mechanism. The disease is that
the record where such a decision is supposed to be findable does not contain it, so
the only party who could have found it is the one with a filesystem. That is the
asymmetry the three-party model exists to work around.
WHAT IT DOES
(c) Makes the substrate REACHABLE rather than copying it: searches commit messages
across the steward's repos, unbounded by any commit-count window, and reports
whether the same term appears in the authorization register. Exposed to the
jurist through governance-mcp.py, whose repo_activity caps at 100 commits — a
floor that sat five weeks short of 0677e8a.
(d) Gives the executor a pre-proposal check: before any [PROPOSAL] naming a mechanism
by name, run this and report the result. Mechanizes the discipline on the side
that has the filesystem.
--census runs the one-time backward sweep (PENDING-164 AMENDMENT 1): adoption and
retirement verbs across every owned repo, each hit checked against the register.
THE FINDING THIS LOOKS FOR
Not "were there commits". The asymmetry: commits mention it, the register does not.
That is a decision taken in a repo and never routed into the authorization record.
ENUMERATION IS COMPUTED, NOT HAND-HELD
Owned repos are those whose remotes point at the steward's hosts. A hand-maintained
list is the failure mode PENDING-108 already measured: it is correct until someone
forgets, and nothing reports the forgetting. Note the positive control below forced
this: 0677e8a is in chamber-library and 95760ff is in dotfiles, and dotfiles is not
in governance-mcp.py's REPOS.
"""
import re
import subprocess
import sys
from pathlib import Path
HOME = Path.home()
OWNED_HOSTS = ("github.com/davidglidden", "davidglidden/", "git.skemantix.com")
REGISTER = [HOME / "PENDING.md", HOME / "PENDING-archive.md", HOME / "REVIEWED.md"]
# The census's verbs. Deliberately about ADOPTION and RETIREMENT of mechanisms, not about
# ordinary change — "fix", "update" and "refactor" would return everything and measure
# nothing.
# ⚠ NARROWED after the first run, and the number is recorded because it is the finding.
# The first verb set — which included "remove", "replace with", "no longer", "switch to" —
# returned 661 candidates across 8 repos. That is a haystack, not a census: those verbs
# catch ordinary development. Narrowed to forms that are almost always ABOUT A MECHANISM
# rather than about a file.
#
# ⚠ AND THE HONEST LIMIT, stated here rather than discovered later: the jurist's
# specification has two halves — grep the verbs, THEN check each hit against the register.
# The first half is mechanical and is what this does. The second requires reading each
# commit to identify WHICH mechanism it decided about, and that is interpretation, not
# extraction. This instrument does not do it and does not pretend to. Same shape as
# PENDING-151 step 1, where propositions could not be extracted mechanically either.
CENSUS_VERBS = ["retire", "retired", "retiring", "abandon", "deprecat", "migrat",
"stop using", "move away from", "back out", "revert to",
"no longer use", "no longer using", "roll back", "un-adopt"]
def sh(cmd, cwd=None):
"""stdout REGARDLESS of exit code.
⚠ Earned, not stylistic. The first form returned "" on a non-zero exit. `find` over
$HOME exits 1 because 154 directories under Library are unreadable — while printing
all 37 repos to stdout. So the repo list came back empty, every search returned zero,
and the instrument would have reported "no prior art" for everything. The positive
control caught it; without the control it was a clean, confident, wrong zero — the
exact failure this whole item is about. git returns empty stdout when it genuinely
fails, so nothing false is admitted by ignoring the code here."""
try:
r = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, timeout=90)
return r.stdout
except Exception:
return ""
def owned_repos():
"""Repos whose remotes point at the steward's hosts. Computed, never listed."""
out = sh(["find", str(HOME), "-maxdepth", "5", "-type", "d", "-name", ".git"])
repos = []
for line in out.splitlines():
if not line.strip():
continue
r = Path(line).parent
rem = sh(["git", "-C", str(r), "remote", "-v"])
if any(h in rem for h in OWNED_HOSTS):
repos.append(r)
return sorted(set(repos))
def search_commits(term, repos=None):
"""Every commit whose message mentions `term`, in any branch, with NO count window."""
hits = []
for r in (repos if repos is not None else owned_repos()):
out = sh(["git", "-C", str(r), "log", "--all", "--format=%h\t%ad\t%s",
"--date=short", "-i", f"--grep={term}"])
for line in out.splitlines():
parts = line.split("\t", 2)
if len(parts) == 3:
hits.append((r.name, parts[0], parts[1], parts[2]))
return hits
def register_mentions(term):
"""Items in the authorization record mentioning `term`. The other half of the asymmetry."""
n = 0
pat = re.compile(re.escape(term), re.I)
for f in REGISTER:
if f.exists():
n += len(pat.findall(f.read_text(errors="replace")))
return n
def controls():
"""Same-run positive controls. The jurist specified these: the instrument must return
two commits already known to exist, or it measured nothing and its zero means nothing."""
ok, notes = True, []
hits = search_commits("LFS")
shas = {h[1] for h in hits}
for known, why in (("0677e8a", "chamber-library — LFS retired, 17 commits rewritten"),
("95760ff", "dotfiles — global git-lfs hooks removed")):
if not any(s.startswith(known[:7]) for s in shas):
ok = False
notes.append(f"CONTROL FAIL: {known} not returned ({why})")
# negative control: a term that cannot plausibly be in any commit message
if search_commits("zzqqxx-not-a-real-term-9971"):
ok = False
notes.append("CONTROL FAIL: a nonsense term returned hits")
return ok, notes, len(hits)
def report(term):
ok, notes, _ = controls()
if not ok:
print("⚠ INSTRUMENT NOT VERIFIED — the result below is unestablished.")
for n in notes:
print(" " + n)
print()
hits = search_commits(term)
reg = register_mentions(term)
print(f"PRIOR ART: {term!r}")
print(f" commits mentioning it : {len(hits)} (all branches, no count window)")
print(f" register mentions : {reg} (PENDING, PENDING-archive, REVIEWED)")
print()
for repo, sha, date, subj in sorted(hits, key=lambda h: h[2]):
print(f" {date} {repo:34} {sha} {subj[:70]}")
print()
if hits and reg == 0:
print(" ⚠ FINDING — PENDING-164's condition exactly: this mechanism has a history in")
print(" the repos and NO trace in the authorization record. Whatever was decided")
print(" about it was decided in a commit message. Read those commits before")
print(" proposing anything about it.")
elif hits:
print(" Both surfaces carry it. Read the register items AND the commits — a ruling")
print(" can post-date the commit that motivated it, or precede the one that undid it.")
else:
print(" No prior art found. ⚠ Absence here is weak evidence: it means no COMMIT")
print(" MESSAGE names this term, not that nothing was decided about it.")
return 0 if ok else 2
def census():
ok, notes, nlfs = controls()
print("BACKWARD CENSUS — PENDING-164 AMENDMENT 1")
print(f" instrument verified: {'YES' if ok else 'NO'}"
+ ("" if ok else " ⚠ RESULT UNESTABLISHED"))
for n in notes:
print(" " + n)
repos = owned_repos()
print(f" owned repos (computed from remotes): {len(repos)}")
print(f" positive control: 'LFS' returns {nlfs} commits incl. 0677e8a and 95760ff")
print()
seen, rows = set(), []
for verb in CENSUS_VERBS:
for repo, sha, date, subj in search_commits(verb, repos):
if sha in seen:
continue
seen.add(sha)
rows.append((date, repo, sha, subj))
print(f" candidate adoption/retirement commits: {len(rows)}")
print(" (a candidate is a commit whose SUBJECT reads like a mechanism decision;")
print(" classification is the reader's, not this instrument's)")
print()
for date, repo, sha, subj in sorted(rows, reverse=True):
print(f" {date} {repo:34} {sha} {subj[:74]}")
return 0 if ok else 2
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "--census":
sys.exit(census())
if len(sys.argv) > 1 and sys.argv[1] == "--selftest":
ok, notes, n = controls()
print(f"controls: {'PASS' if ok else 'FAIL'} ('LFS' → {n} commits)")
for x in notes:
print(" " + x)
sys.exit(0 if ok else 1)
if len(sys.argv) < 2:
print("usage: prior-art.py <mechanism-name> | --census | --selftest")
sys.exit(64)
sys.exit(report(" ".join(sys.argv[1:])))