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
+33
View File
@@ -1,3 +1,36 @@
## instructions_gate.py — check 7 (register citations) and content WARNings on the index (2026-08-06)
**Check 7: a citation that calls a register item OPEN must be right.** Four instruction files said CI
was still owed, citing R-168, four days after it closed; one contradicted itself. A trim is a VOLUME
operation and does not validate content — this class, at least, is mechanical.
Three design decisions, each earned during the build:
- **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`)
that cite an item as the source of a fact. A gate that noisy gets switched off — R-29's own lesson.
**This is a deliberate deviation from the task's literal wording**, taken to keep the gate alive.
- **The state marker is not self-closing.** Real rows read `**SHIPPED — and the alarm is
DEMONSTRATED**`. The first 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.
- **The exemption is scoped to the citation's CLAUSE.** A line-wide test pardoned `OPEN R-25b`
because "shipped" appeared earlier in the same line, in a title and a filename. Found only because
the red-proof failed to go red.
**Check 6 gains content WARNings on `MEMORY.md`** — version literals, host addresses, expired
statements, and stale-open citations — all WARN, never FAIL. Claude writes that file between
sessions, so a hard failure would refuse a human's push over a line no human typed; the warning is
read by the same model that will next edit the file, which makes the loop self-correcting.
**The expired-statement class carries the sharpest lesson.** A scan on 2026-08-06 reported three
expired statements in the index and **every one was false** — each matched the ISO date inside a
markdown link TARGET, i.e. a filename — while **missing the one real expired claim** in the same
file, whose deadline was written `~08-02` and contained no ISO date at all. Link targets are now
stripped before any content judgement, and the deadline pattern matches the bare `MM-DD` form.
First run over the real index: 32 version literals, 4 host addresses, 0 expired, 0 stale citations.
Suite 39 -> 60 assertions. Red-proofs against real files both ways, quoted in the ledger.
## install_workspace.py + rules_report.py — the workspace survives the machine (2026-08-06, R-229)
The workspace-root `CLAUDE.md` and the `InstructionsLoaded` hook shape every session and existed on
+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)
+159
View File
@@ -295,6 +295,165 @@ def test_memory_store_without_index_fails():
check("message says unreachable knowledge", "unreachable knowledge" in out)
# --- check 7: register citations ----------------------------------------------------------------
#
# The founding case is "CI is still owed (R-168)" shipped four days after R-168 closed, in four
# files, one of which contradicted itself. 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 like "(R-161)" or "R-117 spike §6.3", and a gate that noisy gets switched
# off — which is the fate R-29 documented.
def make_register(tmp, rows):
"""rows: list of (item, what, state-cell). Mirrors the real 6-column table shape."""
d = os.path.join(tmp, "felhom.eu", "documentation", "backlog")
os.makedirs(d, exist_ok=True)
body = ["| ID | What | State | Blocked on | Next action | Owner |", "|---|---|---|---|---|---|"]
for item, what, st in rows:
body.append("| **%s** | %s | %s | — | — | operator |" % (item, what, st))
with open(os.path.join(d, "OPEN-ITEMS.md"), "w", encoding="utf-8") as fh:
fh.write("\n".join(body) + "\n")
with open(os.path.join(d, "ROADMAP.md"), "w", encoding="utf-8") as fh:
fh.write("| R-900 | a historical item, closed long ago and pruned from OPEN-ITEMS |\n")
REG_ROWS = [
("R-168", "~~CI: no runner exists~~", "**SHIPPED — and the alarm is DEMONSTRATED** (2026-08-02)"),
("R-161", "volume-persistence gate", "**REDUCED SCOPE — open** (operator ruling)"),
]
def test_citation_claiming_open_about_a_closed_item_fails():
"""The founding case, verbatim."""
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(tmp, "# Repo\n\nBoth facts are why CI is still owed (`OPEN-ITEMS.md` R-168).\n")
rc, out = run_gate(root)
check("stale-open citation exits non-zero", rc != 0, "rc=%d" % rc)
check("message names the item", "R-168" in out)
check("message says the register disagrees", "register says it is closed" in out)
def test_citation_that_says_closed_passes():
"""Explicitly required by the spec: a correct citation must survive."""
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(
tmp, "# Repo\n\na bypass is noticed even though it is not blocked (R-168, CLOSED 2026-08-02).\n"
)
rc, out = run_gate(root)
check("citation saying CLOSED exits 0", rc == 0, "rc=%d" % rc)
def test_bare_provenance_citation_of_a_closed_item_passes():
"""Load-bearing negative: citing a closed item as the SOURCE OF A FACT is not a claim it is open."""
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(tmp, "# Repo\n\nThe canonical runner shape (R-168) is what survives.\n")
rc, out = run_gate(root)
check("provenance citation of a closed item exits 0", rc == 0, "rc=%d" % rc)
def test_openness_claim_about_an_actually_open_item_passes():
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(tmp, "# Repo\n\n**R-161 stays open at reduced scope** — enforcement is by convention.\n")
rc, out = run_gate(root)
check("openness claim about an OPEN item exits 0", rc == 0, "rc=%d" % rc)
def test_closed_word_elsewhere_on_the_line_does_not_pardon_a_stale_claim():
"""Regression. The exemption is scoped to the citation's CLAUSE, not the whole line.
`- [Customer RESET shipped](customer-reset-shipped-2026-07-17.md) ; OPEN R-25b` contains
"shipped" twice once in a title, once in a filename and a line-wide exemption pardoned the
stale claim sitting at the end of it. Found by this check's own red-proof failing to go red,
which is the only reason it was found at all."""
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(
tmp,
"# Repo\n\n- [Customer RESET shipped](customer-reset-shipped-2026-07-17.md) — "
"teardown first; OPEN R-168\n",
)
rc, out = run_gate(root)
check("a distant 'shipped' does not pardon the claim", rc != 0, "rc=%d" % rc)
check("the stale item is still named", "R-168" in out)
def test_citation_of_an_item_in_no_register_fails():
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(tmp, "# Repo\n\nSee R-4242 for the rationale.\n")
rc, out = run_gate(root)
check("citation of a nonexistent item exits non-zero", rc != 0, "rc=%d" % rc)
check("message says it is in neither register", "NEITHER" in out)
def test_citation_in_an_html_comment_is_not_policed():
"""Comments are stripped before injection, so a historical citation parked there costs nothing."""
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(tmp, "# Repo\n\n<!-- history: CI was still owed (R-168) at the time -->\n")
rc, out = run_gate(root)
check("citation inside a comment is not policed", rc == 0, "rc=%d" % rc)
def test_citation_check_covers_rule_files_too():
with tempfile.TemporaryDirectory() as tmp:
make_register(tmp, REG_ROWS)
root = make_repo(
tmp, "# Repo\n",
rules={"g.md": '---\npaths: ["**/*.go"]\n---\n\nCI is still owed (R-168).\n'},
)
rc, out = run_gate(root)
check("a rule file's stale citation also fails", rc != 0, "rc=%d" % rc)
check("message names the rule file", "g.md" in out)
# --- check 6: content WARNings on the memory index ------------------------------------------------
def test_memory_expired_statement_warns_but_does_not_fail():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md) — demo boxes REMOTE till ~08-02\n", topics={"a.md": "# A\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("expired statement in the index does NOT fail", rc == 0, "rc=%d" % rc)
check("expired statement WARNs", "EXPIRED statement" in out)
def test_memory_date_inside_a_link_target_is_not_an_expired_statement():
"""THE load-bearing negative. On 2026-08-06 a scan matched the ISO date in the link TARGET and
reported three expired statements, every one of them false, while missing the one real expired
claim in the same file whose deadline was written `~08-02` with no ISO date at all."""
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [Customer RESET](customer-reset-shipped-2026-07-17.md) — teardown first\n",
topics={"customer-reset-shipped-2026-07-17.md": "# X\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("a dated FILENAME is not an expired statement", rc == 0 and "EXPIRED" not in out, "rc=%d" % rc)
def test_memory_future_deadline_does_not_warn():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md) — rig stays up until 2026-12-31\n", topics={"a.md": "# A\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("a future deadline does not warn", "EXPIRED" not in out)
def test_memory_version_literal_and_address_warn_but_do_not_fail():
with tempfile.TemporaryDirectory() as tmp:
make_memory(tmp, "# Index\n- [a](a.md) — ctrl 0.162.0 on 192.168.0.180\n", topics={"a.md": "# A\n"})
root = make_repo(tmp, "# Repo\n")
rc, out = run_gate(root)
check("version literal in the index does NOT fail", rc == 0, "rc=%d" % rc)
check("version literal WARNs", "version literal" in out)
check("host address WARNs", "host address" in out)
check("loopback is not reported", "127.0.0.1" not in out)
def main():
print("test_instructions_gate")
for fn in sorted(