gate: check 7 (register citations) + content WARNings on the memory index
gates / gates (push) Successful in 8s

Check 7 catches "cites a register item and calls it open when it is not" -- the R-168 class, four
files, one self-contradicting. Trigger is an openness CLAIM, not any citation: policing every
mention would fire on ~30 legitimate provenance citations and the gate would be switched off.
Deliberate deviation from the task's literal wording, to keep it alive.

Two bugs found by the check's own red-proofs, both of which would have shipped:
- the state marker is not self-closing (**SHIPPED - text**), so the first parser read R-168 itself
  as OPEN -- a gate that cannot convict its founding case is decoration;
- the CLOSED exemption was line-wide, so "shipped" in a title pardoned "OPEN R-25b".

Check 6 gains WARN-only content classes on MEMORY.md. Link targets are stripped first: the earlier
scan reported three expired statements, all three false (dates in filenames), while missing the one
real expired claim, whose deadline was written ~08-02 with no ISO date.

39 -> 60 assertions. All four runners green.
This commit is contained in:
2026-08-06 11:39:34 +02:00
parent 92a076c239
commit 15fa5273ba
3 changed files with 434 additions and 1 deletions
+242 -1
View File
@@ -40,7 +40,26 @@ CHECKS (each names the file and the reason; a missing input is a FAILURE, never
a directory that is not a git repo, so the copy is the only version-controlled record of it;
nothing but this check enforces that they agree.
6. The auto-memory index (<workspace>/.claude-memory/MEMORY.md) is within its line and byte
limits, and every top-level topic file is referenced by it.
limits, every top-level topic file is referenced by it, and its CONTENT is warned about
(version literals, host addresses, expired statements, stale-open register citations).
7. Every R-nnn cited in a CLAUDE.md or a .claude/rules/*.md that is CALLED OPEN really is open.
WHY 7 EXISTS (2026-08-06). Four instruction files asserted that CI was still owed, citing R-168,
four days after it closed — and one of the four contradicted itself, its release section already
saying R-168 mails the failure. A trim is a VOLUME operation: it does not validate content, and the
sentence survived because it read as settled. This class, at least, is mechanical.
- THE TRIGGER IS AN OPENNESS CLAIM, NOT ANY CITATION. Requiring every mention of a non-open item
to carry "CLOSED" would fire on ~30 legitimate provenance citations — "(R-161)", "R-117 spike
§6.3" — which cite an item as the SOURCE OF A FACT, not as outstanding work. A gate that noisy
is switched off, which is the fate R-29 documented.
- THE EXEMPTION IS SCOPED TO THE CITATION'S CLAUSE. A line-wide test pardoned a stale claim
because the words "shipped" appeared elsewhere on the same line, in a title and a filename.
- A CITATION OF AN ITEM IN NEITHER REGISTER FAILS. A reference to nothing cannot be checked by
anyone, which is worse than a stale one.
- THE STATE MARKER IS NOT SELF-CLOSING. Real rows read `**SHIPPED — and the alarm is
DEMONSTRATED**`. An earlier parser required `**SHIPPED**` and therefore read R-168 — the row
this check exists for — as open. A gate that cannot convict its own founding case is decoration.
WHY 6 EXISTS, AND WHY ITS THREE OUTCOMES DIFFER (2026-08-06).
@@ -276,6 +295,52 @@ def check_memory(workspace_root, failures, warnings, tally):
% (index, nbytes, MAX_MEMORY_BYTES)
)
# --- content classes, as WARNINGS (see memory_content_warnings for why not failures) ---
cw = memory_content_warnings(text, today_tuple())
tally.append(
" memory index content : %d version literal(s), %d host address(es), %d expired "
"statement(s) [WARN only]"
% (len(cw["versions"]), len(cw["addresses"]), len(cw["expired"]))
)
# Fourth class, beyond the three the CLAUDE.md files are failed on: a stale-open register
# citation. Added because TWO of the three false statements found in this file on 2026-08-06
# were exactly this ("R-193 decision open", "OPEN R-25b" — both closed), and both were found by
# hand. Check 7 polices the hand-written files for it; nothing policed this one.
reg = register_state(workspace_root)
if reg:
stale_cites = []
for lineno, raw in enumerate(text.split("\n"), 1):
for item in sorted(set(CITE_RE.findall(raw))):
cl = clause_around(raw, item)
if not (OPENNESS_RE.search(cl) and not SAYS_CLOSED_RE.search(cl)):
continue
if reg.get(item, "missing") != "open":
stale_cites.append((lineno, item, raw.strip()[:90]))
if stale_cites:
tally.append(" memory stale citations : %d [WARN only]" % len(stale_cites))
for lineno, item, ctx in stale_cites[:6]:
warnings.append(
"%s:%d: calls %s open, but the register does not.\n %s"
% (index, lineno, item, ctx)
)
for kind, label, why in (
("expired", "EXPIRED statement", "it is read as current fact"),
("versions", "version literal", "the fleet is not uniform, so it is stale within a day"),
("addresses", "host address", "operations/nodes.md is the single home for these"),
):
for lineno, hit, ctx in cw[kind][:6]:
warnings.append(
"%s:%d: %s %r%s. Not a failure: Claude writes this file between sessions, so "
"this warning is aimed at the model that will next edit it, not at whoever is "
"pushing.\n %s" % (index, lineno, label, hit, why, ctx)
)
if len(cw[kind]) > 6:
warnings.append(
"%s: ... and %d more %s(s) — full list from the tally counts above."
% (index, len(cw[kind]) - 6, label)
)
referenced = memory_referenced(text)
on_disk = set(
n for n in os.listdir(store)
@@ -307,6 +372,181 @@ def check_memory(workspace_root, failures, warnings, tally):
)
# A date inside a markdown link TARGET is a filename, not a claim: `[x](vacation-...-2026-07-20.md)`.
# Matching those produced THREE false "expired statement" findings on 2026-08-06 — every one of the
# three reported that day — while missing the one real expired claim in the same file, whose deadline
# was written `~08-02` and carried no ISO date at all. A scan that reports the wrong three and misses
# the right one is worse than no scan.
LINK_TARGET_RE = re.compile(r"\]\([^)]*\)")
# Deadline language, including the bare MM-DD form the real one used.
DEADLINE_RE = re.compile(
r"\b(till|until|by)\s+~?\s*(\d{4}-\d{2}-\d{2}|\d{2}-\d{2})\b", re.I
)
LOOPBACK = ("127.0.0.1", "0.0.0.0", "255.255.255.255")
IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
def memory_content_warnings(text, today):
"""The three content classes the CLAUDE.md files are FAILED on, applied to the index as WARNINGS.
WARN and not FAIL, deliberately: Claude writes this file between sessions, so a hard failure
would refuse a human's push over a line no human typed. The warning IS the mechanism — it is read
by the same model that writes the file, which makes the loop self-correcting rather than a chore
for a person.
"""
out = {"versions": [], "addresses": [], "expired": []}
for i, raw in enumerate(text.split("\n"), 1):
# strip link targets before ANY content judgement — a filename is not an assertion
line = LINK_TARGET_RE.sub("]()", raw)
for m in VERSION_RE.finditer(line):
out["versions"].append((i, m.group(), raw.strip()[:90]))
for m in IPV4_RE.finditer(line):
if m.group() not in LOOPBACK:
out["addresses"].append((i, m.group(), raw.strip()[:90]))
for m in DEADLINE_RE.finditer(line):
token = m.group(2)
if len(token) == 5: # MM-DD — assume the current year, which is how it was written
when = (today[0], int(token[:2]), int(token[3:]))
else:
when = tuple(int(x) for x in token.split("-"))
if when < today:
out["expired"].append((i, m.group(0), raw.strip()[:90]))
return out
def register_state(workspace_root):
"""Map R-nnn -> 'open' | 'closed' | 'historical' | 'missing'.
OPEN-ITEMS.md is the authority on what is OPEN and says so in its own header. An item is open
when it OWNS A ROW there whose State cell is not a closed marker. An item with no row of its own
is not open work — it may still be mentioned inside another row, and it is usually in ROADMAP.md,
which keeps the full history. Anything in neither file is a reference to nothing.
"""
base = os.path.join(workspace_root, "felhom.eu", "documentation", "backlog")
reg = os.path.join(base, "OPEN-ITEMS.md")
hist = os.path.join(base, "ROADMAP.md")
if not os.path.exists(reg):
return None
with io_open(reg) as fh:
reg_text = fh.read()
hist_text = ""
if os.path.exists(hist):
with io_open(hist) as fh:
hist_text = fh.read()
state = {}
for line in reg_text.split("\n"):
m = re.match(r"\|\s*\*\*(R-\d+[a-z]?)\*\*\s*\|", line)
if not m:
continue
cells = [c.strip() for c in line.rstrip().strip("|").split("|")]
# The State column is index 2 in BOTH table shapes here (`ID|What|State` and
# `ID|What|State|Blocked on|Next action|Owner`). Scan from there rather than taking the
# last cell: a row whose prose contains a `|` splits into extra cells and would otherwise
# be read from the wrong one.
#
# THE MARKER IS NOT SELF-CLOSING. Real rows read `**SHIPPED — and the alarm is
# DEMONSTRATED**`, not `**SHIPPED**`. An earlier version of this parser required the
# closing `**` immediately after the word and therefore read R-168 — the row this whole
# check exists for — as OPEN. A gate that cannot convict its own founding case is decoration.
state[m.group(1)] = "open"
for cell in cells[2:]:
if not cell.startswith("**"):
continue
head = cell[: cell.find("**", 2) + 2] if cell.find("**", 2) > 0 else cell
if re.search(r"\b(CLOSED|SHIPPED|DONE|COMPLETE)\b", head) and not re.search(
r"\bopen\b", head, re.I
):
state[m.group(1)] = "closed"
break
mentioned = set(re.findall(r"\bR-\d+[a-z]?\b", reg_text)) | set(
re.findall(r"\bR-\d+[a-z]?\b", hist_text)
)
for item in mentioned:
state.setdefault(item, "historical")
return state
# Language that CLAIMS an item is still outstanding. The check fires on these, not on every
# citation — see check 7's rationale in the module docstring.
OPENNESS_RE = re.compile(
r"\b(still owed|is owed|are owed|still open|stays open|remains open|still pending|"
r"pending\b|still needed|not yet|awaiting|awaits|outstanding|unresolved|to be done|"
r"OPEN\s+R-\d)", re.I
)
SAYS_CLOSED_RE = re.compile(r"\b(CLOSED|DONE|SHIPPED|RESOLVED|COMPLETE)\b", re.I)
CITE_RE = re.compile(r"\bR-\d+[a-z]?\b")
# Clause boundaries. The exemption is scoped to the clause holding the citation, NOT the whole line:
# `- [Customer RESET shipped](…) — …; OPEN R-25b` contains "shipped" twice, in a title and a
# filename, and a line-wide exemption pardoned the stale claim at the end of it. Found by this
# check's own red-proof, which failed to go red.
CLAUSE_SPLIT_RE = re.compile(r"[;.]\s+|\s+—\s+|\s+\|\s+")
def clause_around(line, item):
"""The clause of `line` containing `item` — what "the same sentence" means for the exemption."""
for part in CLAUSE_SPLIT_RE.split(line):
if re.search(r"\b%s\b" % re.escape(item), part):
return part
return line
def check_citations(root, workspace_root, failures, warnings, tally):
"""Check 7 — a citation that calls a register item OPEN must be right.
Scope: <root>/CLAUDE.md and <root>/.claude/rules/*.md. Effective text only, so a citation
parked in an HTML comment for history is not policed.
"""
state = register_state(workspace_root)
if state is None:
tally.append(" register citations : n/a (no OPEN-ITEMS.md — not this workspace)")
return
targets = []
claude_md = os.path.join(root, "CLAUDE.md")
if os.path.exists(claude_md):
targets.append(claude_md)
rules_dir = os.path.join(root, ".claude", "rules")
if os.path.isdir(rules_dir):
targets += [
os.path.join(rules_dir, n) for n in sorted(os.listdir(rules_dir)) if n.endswith(".md")
]
cited = 0
for path in targets:
with io_open(path) as fh:
eff = effective(fh.read())
for lineno, line in enumerate(eff.split("\n"), 1):
items = CITE_RE.findall(line)
if not items:
continue
cited += len(items)
for item in sorted(set(items)):
cl = clause_around(line, item)
claims_open = bool(OPENNESS_RE.search(cl)) and not SAYS_CLOSED_RE.search(cl)
st = state.get(item, "missing")
if st == "missing":
failures.append(
"%s:%d: cites %s, which appears in NEITHER OPEN-ITEMS.md nor ROADMAP.md. "
"A reference to nothing is worse than a stale one — it cannot be checked "
"by anyone.\n %s" % (path, lineno, item, line.strip()[:110])
)
elif claims_open and st != "open":
failures.append(
"%s:%d: claims %s is still open, but the register says it is %s. Four "
"instruction files asserted CI was still owed (R-168) four days after it "
"closed, and one contradicted itself about it — a trim is a volume "
"operation and does not validate content. Either drop the claim or say "
"CLOSED in the same sentence.\n %s"
% (path, lineno, item, "not an open row" if st == "historical" else st,
line.strip()[:110])
)
tally.append(" register citations : %d cited, %d register items known" % (cited, len(state)))
def io_open(path):
return open(path, "r", encoding="utf-8")
@@ -338,6 +578,7 @@ def main(argv):
check_rules(root, failures, tally)
check_workspace_copy(os.path.dirname(root), failures, tally)
check_memory(os.path.dirname(root), failures, warnings, tally)
check_citations(root, os.path.dirname(root), failures, warnings, tally)
for line in tally:
print(line)