Backlog triage Part B: 125 finished rows + 20 id-less rows moved to CLOSED-ITEMS (full text at 9e2786c); open rows normalised to one 6-column shape; narratives archived verbatim; closed_register_gate RULE 3 refuses a finished row in OPEN-ITEMS (decoys, seen red); rules rehomed to CONTEXT + 07 §11; loose notes triaged; R-814..R-819 filed; register 444 -> 325
gates / gates (push) Successful in 30s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-03 09:17:22 +02:00
parent 9e2786c907
commit 71b8c8c62b
22 changed files with 1080 additions and 815 deletions
+14
View File
@@ -1,3 +1,17 @@
## The open register refuses a finished row (2026-10-03, backlog triage)
- `closed_register_gate.py` **RULE 3**: no row in `OPEN-ITEMS.md` may LEAD its `State` cell with a finished word
(CLOSED, SHIPPED, FIXED, KILLED, DECIDED, ANSWERED, …). Why: `PROMPT-TEMPLATE.md` §N.7 said "move it" since
2026-08-22 with no gate, and on 2026-10-03 the open register held 113 finished rows (a quarter of 824 KB). Seen
red on the real register before the move (118 convicted) and by decoy.
- New helper `register_table.py`: splits a row on pipes OUTSIDE backticks (measured: 437 of 442 rows then split to
their declared width; the five that did not were real shape defects, repaired), and reads a cell BY THE COLUMN
NAME in the table's own header, so a gate survives a column being added.
- `instructions_gate.py`: reads the register's State by column name (the old positional scan would read the new
title cell), and counts ids cited in the new narratives archive as history, not "a reference to nothing".
- `test_gate_decoys.py`: two decoys — a finished row left in the open register (convicts; **without RULE 3 it
passed — seen red**), and R-87's shape, a READY row whose prose says "closed" about another row (must pass).
## ISO build refuses a dirty or unpushed tree (2026-09-30, R-730)
- `iso/build-felhom-iso.sh`: a clean-tree gate right after argument parsing — any uncommitted or untracked change, or
+31 -2
View File
@@ -2,11 +2,24 @@
# -*- coding: utf-8 -*-
"""closed_register_gate.py — CLOSED-ITEMS.md holds closed work ONLY (R-405, 2026-08-31).
WHAT IT CONVICTS ON (a FAIL, exit 1), two rules and nothing else:
WHAT IT CONVICTS ON (a FAIL, exit 1), three rules and nothing else:
RULE 1 — no row in `CLOSED-ITEMS.md` may carry an OPEN state word (READY, OPEN, BLOCKED,
WATCHING, WAITING-ON-OPERATOR) as the LEADING VERDICT of its state cell.
RULE 2 — no `R-` identifier may have a table row in BOTH `CLOSED-ITEMS.md` and `OPEN-ITEMS.md`.
RULE 3 — (2026-10-03) no row in `OPEN-ITEMS.md` may carry a CLOSED-family word (CLOSED, SHIPPED,
FIXED, KILLED, DECIDED, ANSWERED, SUPERSEDED, …; the list is `register_table.CLOSED_FAMILY`)
as the LEADING VERDICT of its `State` cell. A finished row moves to `CLOSED-ITEMS.md` IN THE
SAME COMMIT that finishes it.
WHY RULE 3, and the cost that bought it. RULES 1–2 guard the closed file and nothing guarded the open
one. `PROMPT-TEMPLATE.md` §N.7 has said since 2026-08-22 "compress what this session closed … it moves
to `CLOSED-ITEMS.md`", and it was followed only sometimes: on 2026-10-03 the open register held **113
rows whose leading verdict was finished** (107 CLOSED, plus SHIPPED, FIXED, RULED, EXECUTED, ANSWERED,
DECIDED) among 442 — a quarter of the file, at 824 KB. A rule without a gate is a wish. RULE 3 reads
the `State` column BY NAME from the table's header (`register_table.rows`), never a fixed index, and
it reads the LEADING verdict only — R-378's lesson applies here exactly as in RULE 1: the open row
`READY — RE-RANKED UP (R-86 closed)` must pass, and does (decoy `closed-register/open-row-closed-word`).
WHY IT EXISTS, and the cost that bought it. The 2026-08-22 compression sweep (commit `ef6ac6f`,
R-376..R-378) moved finished work out of `OPEN-ITEMS.md`. Its classifier matched a status word
@@ -50,6 +63,9 @@ import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import register_table # noqa: E402 — the shared row reader (2026-10-03)
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CLOSED = os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md")
OPEN = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
@@ -137,6 +153,19 @@ def main():
for rid in sorted(closed_ids & set(open_ids), key=lambda r: (int(re.sub(r"\D", "", r)), r)):
convicted.append((open_ids[rid], rid, "", "has a row in BOTH registers"))
# RULE 3 — a finished row left in the OPEN register (2026-10-03)
finished_in_open = []
for n, rid, columns, cells, _ in register_table.rows(OPEN):
state = register_table.cell(columns, cells, "State")
if state is None:
continue # an unreadable row is register_shape_gate.py's conviction, not a pass here
verdict = register_table.leading_verdict(state)
if register_table.CLOSED_FAMILY.match(verdict):
finished_in_open.append((n, rid, verdict))
for n, rid, verdict in finished_in_open:
convicted.append((n, rid, verdict, "OPEN-ITEMS.md row whose leading verdict is FINISHED — move "
"it to CLOSED-ITEMS.md in the same commit that closes it"))
print("closed-register gate — %d closed rows with a readable state cell, %d open rows, "
"%d convicted, %d warnings" % (checked, len(open_ids), len(convicted), len(warnings)))
@@ -145,7 +174,7 @@ def main():
if convicted:
print()
print("CONVICTED — CLOSED-ITEMS.md is for closed work only:")
print("CONVICTED — CLOSED-ITEMS.md is for closed work only, OPEN-ITEMS.md for open work only:")
for n, rid, verdict, why in convicted:
print(" L%-5d %-7s %s%s" % (n, rid, why, (" — verdict %r" % verdict) if verdict else ""))
print()
+27
View File
@@ -503,10 +503,29 @@ def register_state(workspace_root):
closed_text = fh.read()
state = {}
# 2026-10-03: the register declares its columns in each table's header, and since the triage of
# that day its rows read `| ID | Category | Sev | What | State | … |` — the State is no longer
# index 2, and the first bold cell is now the TITLE, whose words ("shipped", "closed") describe
# the finding, not its state. `register_table` reads the State column BY NAME; the positional
# scan below is kept only for a row in a table without a header (none today).
by_name = {}
try:
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import register_table
for _n, rid, columns, cells, _line in register_table.rows(reg):
st = register_table.cell(columns, cells, "State")
if st is not None:
by_name[rid] = ("closed" if register_table.CLOSED_FAMILY.match(
register_table.leading_verdict(st)) else "open")
except ImportError:
by_name = {}
for line in reg_text.split("\n"):
m = re.match(r"\|\s*\*\*(R-\d+[a-z]?)\*\*\s*\|", line)
if not m:
continue
if m.group(1) in by_name:
state[m.group(1)] = by_name[m.group(1)]
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
@@ -536,6 +555,14 @@ def register_state(workspace_root):
if m and m.group(1) not in state:
state[m.group(1)] = "closed"
# 2026-10-03: the register's narrative sections (campaign write-ups, rulings, ranking
# paragraphs) moved word for word to this archive. An id cited only there — R-153, named in the
# 2026-08-02 intake paragraph and in felhom.eu/CLAUDE.md — is history, not "a reference to nothing".
narr = os.path.join(workspace_root, "felhom.eu", "documentation", "archive",
"OPEN-ITEMS-narratives-2026-10-03.md")
if os.path.exists(narr):
with io_open(narr) as fh:
hist_text += "\n" + fh.read()
mentioned = set(re.findall(r"\bR-\d+[a-z]?\b", reg_text)) | set(
re.findall(r"\bR-\d+[a-z]?\b", hist_text)
) | set(re.findall(r"\bR-\d+[a-z]?\b", closed_text))
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""register_table.py — read OPEN-ITEMS.md's rows the way a reader sees them (2026-10-03).
A helper, not a gate: `register_shape_gate.py` and `closed_register_gate.py` both import it, so the
two gates cannot disagree about where a row's cells are.
WHY A HELPER EXISTS. Both gates used to split a row on every `|`. `register_shape_gate.py`'s own
docstring records why that cannot count cells: register prose carries literal pipes. Measured on
2026-10-03 against the register at `9305288` (442 rows): **every literal pipe but five sat inside
inline code** (`--format '{{.Names}}|{{.Image}}'`, `owner: CC | …` inside backticks). A split that
skips pipes inside backticks and escaped `\\|` gave the declared column count for 437 rows; the five
it did not were genuine shape defects (a stray extra cell, two rows written five columns wide), and
they were repaired in the same commit. So the cell count CAN be computed after all — by reading
code spans the way a Markdown renderer does.
WHAT IT CANNOT SEE: a pipe in plain prose outside backticks still makes an extra cell. That is a
real rendering defect (the table shows an extra column), so a gate convicting it is right.
THE COLUMN COMES FROM THE HEADER, never from a fixed index. Each register table declares its
columns (`| ID | Category | Sev | What | State | … |`); `rows()` remembers the last header it saw
and reports each row's cells by NAME. A gate that hard-codes "the state is cell 3" is wrong the day
someone adds a column — the closed gate's original `cells[3]` assumption is exactly that.
"""
import io
import re
ROW = re.compile(r"^\| \*\*(R-\d+[a-z]?)\*\* \|")
HEADER = re.compile(r"^\|\s*ID\s*\|")
# A verdict ends at the first separator. `READY — RE-RANKED UP (R-86 closed)` leads with READY.
SEPARATOR = re.compile(u"[—–(:,.;]|\\s-\\s")
# THE TWO VOCABULARIES. A leading verdict is the first word of the state cell (emphasis stripped).
# OPEN_STATES is the whole list a register row may lead with; anything else is either finished
# (CLOSED_FAMILY — the row belongs in CLOSED-ITEMS.md) or unknown (a word nobody defined).
OPEN_STATES = ("READY", "OPEN", "BLOCKED", "WATCHING", "WAITING-ON-OPERATOR", "NARROWED",
"DEFERRED", "VERIFY")
CLOSED_FAMILY = re.compile(r"^(CLOSED|SHIPPED|FIXED|KILLED|DONE|SUPERSEDED|OBSOLETE|WITHDRAWN|"
r"MERGED|DISCHARGED|RULED|MOVED|BANKED|PROVEN-LIVE|EXECUTED|ANSWERED|"
r"FOLDED|DECIDED|RESOLVED|DOCUMENTED|PASSED|COMPLETE|COMPLETED|✅)",
re.I)
def split_cells(line):
"""The cells of a markdown table row, skipping pipes inside `code` and escaped `\\|`.
Returns the inner cells only (the empty strings outside the outer pipes are dropped), or None
when the row does not end with `|` — such a row has lost its last cell.
"""
s = line.rstrip()
if not s.endswith("|"):
return None
out, cur, code, i = [], [], False, 0
while i < len(s):
ch = s[i]
if ch == "\\" and i + 1 < len(s) and s[i + 1] == "|":
cur.append("\\|")
i += 2
continue
if ch == "`":
code = not code
if ch == "|" and not code:
out.append("".join(cur))
cur = []
else:
cur.append(ch)
i += 1
out.append("".join(cur))
return [c.strip() for c in out[1:-1]]
def leading_verdict(cell):
"""The verdict word(s) before the first separator, with Markdown emphasis stripped."""
text = cell.replace("*", "").replace("~", "").replace("`", "").strip()
return SEPARATOR.split(text)[0].strip()
def first_word(cell):
v = leading_verdict(cell)
return v.split()[0].upper() if v.split() else ""
def rows(path):
"""Yield (line_no, id, columns, cells, line) for every register row.
`columns` is the header of the table the row sits in (a list of names, or None when no header
has been seen yet); `cells` is split_cells(line) (None when the row lost its last pipe).
"""
columns = None
for n, line in enumerate(io.open(path, encoding="utf-8"), 1):
line = line.rstrip("\n")
if HEADER.match(line):
columns = split_cells(line)
continue
m = ROW.match(line)
if not m:
continue
yield n, m.group(1), columns, split_cells(line), line
def cell(columns, cells, name):
"""The named cell of a row, or None when the table has no such column or the row is short."""
if not columns or cells is None or name not in columns:
return None
i = columns.index(name)
return cells[i] if i < len(cells) else None
+29 -1
View File
@@ -43,7 +43,9 @@ COVERS = {
"observations": "R-419: prose SAYING it carries no marker, plus both genuine markers",
"reuse-refs": "a cited .go path that does not exist; the .md hole is asserted as R-422",
"golden-currency": "R-410: an empty directory with a perfect name, checked by what it COUNTED",
"closed-register": "a verdict cell reading open, and a row with no state cell at all",
"closed-register": ("a verdict cell reading open, a row with no state cell at all, and (RULE 3, "
"2026-10-03) a FINISHED row left in OPEN-ITEMS.md — with R-87's shape, a READY "
"row whose prose says 'closed' about another row, asserted to PASS"),
"register-shape": ("R-627: a row whose state cell was EATEN (no trailing pipe), a DUPLICATED "
"id, and a blank line splitting the table — each the exact shape the "
"2026-09-21 append produced; plus the genuine article, which must pass"),
@@ -210,6 +212,32 @@ decoy("closed-register/unreadable-row", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **R-905** | A row with no state cell at all. |\n"))
# --- closed-register RULE 3 (2026-10-03): a FINISHED row left in the OPEN register ---------------
# On 2026-10-03 the open register held 113 rows whose leading verdict was finished — a quarter of the
# file. The decoy is that exact shape: a row a session closed in place and never moved. The genuine
# article beside it is R-87's shape from R-378: a READY row whose prose says "closed" about ANOTHER
# row — it must pass, or the gate repeats the 2026-08-22 sweep's mistake one file over.
# The row is built from the register's CURRENT header, so the decoy follows a column change rather
# than silently testing a shape the file no longer has.
_OPEN_REG = os.path.join(ROOT, "documentation", "backlog", "OPEN-ITEMS.md")
def _open_row(rid, state):
cols = None
for line in io.open(_OPEN_REG, encoding="utf-8"):
if line.startswith("| ID |"):
cols = [c.strip() for c in line.strip().strip("|").split("|")]
fill = {"ID": "**%s**" % rid, "Category": "Process & tooling", "Sev": "P4",
"What": "**A decoy finding.**", "State": state, "Owner": "CC"}
return u"\n" + u"| " + u" | ".join(fill.get(c, u"—") for c in cols) + u" |\n"
decoy("closed-register/finished-row-in-open", "closed_register_gate.py",
append_to(_OPEN_REG, _open_row("R-906", "**CLOSED 2026-10-03 — shipped in v9.9.9**")))
decoy("closed-register/open-row-closed-word (BY DESIGN)", "closed_register_gate.py",
append_to(_OPEN_REG, _open_row("R-907", "**READY — RE-RANKED UP 2026-08-03 (R-86 closed)**")),
expect="accept")
# --- hub-copy: an ENGLISH retrieval promise, planted in the bundle (R-558) -----------------------
#
# TWO SHAPES AT ONCE, and the second is why this decoy exists at all.