gate: check 7 (register citations) + content WARNings on the memory index
gates / gates (push) Successful in 8s
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user