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
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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user