Backlog triage Part C: every open row has a Category (11) and a Sev (P1-P4); OPEN-ITEMS grouped by category, severity order; register_shape_gate RULES 5-8 (columns by header, category, sev, defined state) with five decoys seen red; CLAUDE.md + PROMPT-TEMPLATE: new rows filed into their category
gates / gates (push) Successful in 29s

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:18:42 +02:00
parent 71b8c8c62b
commit 9f77865c2a
6 changed files with 523 additions and 346 deletions
+3 -1
View File
@@ -132,7 +132,9 @@ something, not only sessions that touch `documentation/` — which is why it is
the parallel-session caveat above).
- **`REUSE.md`**, if a shared helper or pattern moved (same commit).
- **`OPEN-ITEMS.md`** — every finding, with a number. **A row you close moves to `CLOSED-ITEMS.md` in the
same commit** — `closed_register_gate.py` RULE 3 refuses a finished row left in the open register.
same commit** — `closed_register_gate.py` RULE 3 refuses a finished row left in the open register. A
**new row** goes into its category's section with one **Category** and one **Sev** (P1–P4); the scale
and the eleven names head `OPEN-ITEMS.md`, and `register_shape_gate.py` refuses anything else.
- **Root `STATUS.md`** — at the end of every session in which something shipped, broke or was
decided. It is a **view** of `OPEN-ITEMS.md`; nothing may exist only there. One screen, written for
the operator in plain language, and deliberately **not** `CONTEXT.md`.
+3 -1
View File
@@ -371,7 +371,9 @@ or **what is open changed**), update **all four** in the SAME session:
that changes an **architectural contract** (tiers, targets, cadences, trust boundaries) updates the
owning design doc in the same session. It was ruled but never written here, in the template CC
actually reads — so it bound nobody. Now it does.
- **`backlog/OPEN-ITEMS.md`** — the register, and the single source of truth for open work. A task
- **`backlog/OPEN-ITEMS.md`** — the register, and the single source of truth for open work. **A new row
is filed into its category's section with one Category and one Sev (P1–P4)** — the scale and the eleven
categories head the file, and `register_shape_gate.py` refuses a row without them (2026-10-03). A task
that changes what is open without touching it re-creates exactly the thread-loss the register was
built to solve: `REPORT.md` is overwritten every session, so nothing durable may live only there.
File diff suppressed because one or more lines are too long
+10
View File
@@ -1,3 +1,13 @@
## Every open row has a category and a severity (2026-10-03, backlog triage, Part C)
- `register_shape_gate.py` **RULES 5–8**: a register row splits (pipes outside backticks) into exactly the cells its
table header declares, and the header has `ID`, `Category`, `Sev`, `What`, `State`, `Owner` (5); the Category is
one of the eleven (6); the Sev is `P1`–`P4` (7); the State leads with a defined word (8). `OPEN-ITEMS.md` now reads
`| ID | Category | Sev | What | State | Blocked on | Next action | Owner |`, one section per category.
- `test_gate_decoys.py`: the register-shape fixture moved to the new header; five new decoys — an old-shape row
under the new header, a near-miss category, `P3-LOW` as a Sev, an undefined state word, a pipe outside backticks.
**All five passed the previous gate — seen red** — and all five convict now.
## 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
+63 -3
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
"""register_shape_gate.py — OPEN-ITEMS.md is a TABLE, and a table has a shape (R-627, 2026-09-22).
WHAT IT CONVICTS ON (a FAIL, exit 1), four rules and nothing else:
WHAT IT CONVICTS ON (a FAIL, exit 1), seven rules (RULE 2 is reserved) and nothing else:
RULE 1 — every register row (`| **R-nnn** | …`) ends with `|`, so it HAS a state cell.
RULE 2 — reserved. **There is deliberately no cell-COUNT rule, and that is a measurement, not an
@@ -17,6 +17,23 @@ WHAT IT CONVICTS ON (a FAIL, exit 1), four rules and nothing else:
the rows after it render as a separate table — and, worse, a script that walks "the
table" stops there.
RULE 5 — (2026-10-03) every register row sits under a table header and splits — on pipes OUTSIDE
backticks, `register_table.split_cells` — into exactly as many cells as that header declares.
RULE 2's measurement still stands for a naive split; a backtick-aware split CAN count, and on
2026-10-03 it did for 437 of 442 rows — the five it could not were real defects (a stray extra
cell, rows written five columns wide), repaired before this rule was registered.
RULE 6 — (2026-10-03) the `Category` cell is EXACTLY one of the eleven names in `CATEGORIES`.
RULE 7 — (2026-10-03) the `Sev` cell is exactly `P1`, `P2`, `P3` or `P4`.
RULE 8 — (2026-10-03) the `State` cell LEADS with one of `register_table.OPEN_STATES`. A finished word
there is `closed_register_gate.py` RULE 3's conviction; any OTHER word is convicted here,
because a state nobody defined ("PARTLY CLOSED", "PLUMBING COMPLETE") is how 18 rows became
unreadable to every script before the 2026-10-03 triage.
WHY RULES 5–8 (2026-10-03, operator request). Every open row now carries one category and one severity
so the register can be read as a list of priorities, and a new row is filed into its category from the
start. A column that a script cannot read reliably is not a column — so the shape is enforced, by the
header's column NAMES, not by positions.
WHY IT EXISTS, and the cost that bought it. On 2026-09-21 the update night appended measured
results to eight existing rows with a regex that matched a row's trailing state cell. On two rows
(**R-446** and **R-458**) it consumed the state cell and did not put it back; the cell reappeared as
@@ -45,10 +62,24 @@ USAGE
python3 scripts/register_shape_gate.py <file> # any file, for the red-proof
Exit: 0 clean · 1 convicted.
"""
import os
import re
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import register_table # noqa: E402 — the shared row reader (2026-10-03)
# The eleven categories (operator request 2026-10-03; the reviewer's default, operator may reverse).
# The same list, in the same order, heads OPEN-ITEMS.md's "How a row is filed" section.
CATEGORIES = (
"Install & onboarding", "Apps & catalog", "App updates", "Backup & restore", "Storage & devices",
"Security & access", "Box system & updates", "Monitoring & notifications", "Hub & operator",
"Business & legal", "Process & tooling",
)
SEVERITIES = ("P1", "P2", "P3", "P4")
REQUIRED_COLUMNS = ("ID", "Category", "Sev", "What", "State", "Owner")
ROW = re.compile(r'^\| \*\*(R-\d+[a-z]?)\*\* \|') # 2026-09-30: a letter suffix (R-88a, R-88b, R-209a) is a row too — it was skipped, 3 rows unchecked
REG = Path(__file__).resolve().parent.parent / "documentation" / "backlog" / "OPEN-ITEMS.md"
@@ -108,6 +139,34 @@ def check(path):
f"{ROW.match(lines[j]).group(1)} at line {j+1}. A blank line ends "
f"the markdown table."))
# RULES 5–8 — by the table's own header (2026-10-03)
for n, rid, columns, cells, _line in register_table.rows(path):
if columns is None:
bad.append((n, rid, "RULE 5 — the row sits under no `| ID | … |` table header, so no column "
"of it can be read by name"))
continue
missing = [c for c in REQUIRED_COLUMNS if c not in columns]
if missing:
bad.append((n, rid, "RULE 5 — its table header lacks the column(s) %s" % ", ".join(missing)))
continue
if cells is None:
continue # RULE 1 has already convicted it
if len(cells) != len(columns):
bad.append((n, rid, "RULE 5 — %d cells where the header declares %d. A literal `|` outside "
"backticks makes a cell; put it inside `code`." % (len(cells), len(columns))))
continue
cat = register_table.cell(columns, cells, "Category")
if cat not in CATEGORIES:
bad.append((n, rid, "RULE 6 — Category %r is not one of the eleven (%s)"
% (cat, " · ".join(CATEGORIES))))
sev = register_table.cell(columns, cells, "Sev")
if sev not in SEVERITIES:
bad.append((n, rid, "RULE 7 — Sev %r is not P1, P2, P3 or P4" % sev))
word = register_table.first_word(register_table.cell(columns, cells, "State") or "")
if word not in register_table.OPEN_STATES and not register_table.CLOSED_FAMILY.match(word):
bad.append((n, rid, "RULE 8 — the State leads with %r, which is not a defined state (%s)"
% (word, " · ".join(register_table.OPEN_STATES))))
return rows, len(seen), bad
@@ -119,8 +178,9 @@ def main():
if rows != ids:
print(f"register-shape: {rows - ids} row(s) are DUPLICATES")
if not bad:
print("register-shape: OK — every row ends with a state cell, every id is unique, "
"and no blank line splits the table")
print("register-shape: OK — every row ends with a state cell, every id is unique, no blank "
"line splits the table, and every row has its columns, a known category, a P1–P4 "
"severity and a defined state")
return 0
print(f"register-shape: CONVICTED — {len(bad)} problem(s)")
for line_no, rid, why in bad:
+32 -5
View File
@@ -48,7 +48,9 @@ COVERS = {
"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"),
"2026-09-21 append produced; plus the genuine article, which must pass; and "
"(2026-10-03) an old-shape row under the new header, a near-miss category, an "
"old rank tag as Sev, an undefined state word, and a pipe outside backticks"),
"decoy-coverage": "a gate registered in a runner with no decoy and no exemption (its red-proof)",
"guide-quote": ("R-596: SEVEN cases in scripts/test_guide_quote_gate.py, run from here so "
"this suite stays the single entry point. The load-bearing one is "
@@ -356,11 +358,12 @@ _spec = _ilu.spec_from_file_location("rsg", os.path.join(ROOT, "scripts", "regis
_rsg = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_rsg)
_HEADER = "| ID | Category | Sev | What | State | Blocked on | Next action | Owner |"
_HEALTHY = "\n".join([
"| ID | What | State |",
"|---|---|---|",
"| **R-901** | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** |",
"| **R-902** | **Another finding.** | **CLOSED 2026-09-01** |",
_HEADER,
"|---|---|---|---|---|---|---|---|",
"| **R-901** | Backup & restore | P3 | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** | — | — | CC |",
"| **R-902** | Process & tooling | P4 | **Another finding.** | **CLOSED 2026-09-01** | — | — | CC |",
"",
])
@@ -412,6 +415,30 @@ _rsg_case("suffix-row-eaten-state",
_HEALTHY.rstrip() + "\n| **R-903a** | **A split finding.** |\n".replace(" |\n", "\n"),
must_convict=True, expect_rule="RULE 1")
# 2026-10-03 — RULES 5–8: the columns, the category, the severity and the state word. Each decoy is a
# shape a session filing a row by hand would really produce.
_R901 = "| **R-901** | Backup & restore | P3 |"
# decoy 5 — a row filed with no category (the old 3-column habit, under the new header)
_rsg_case("old-shape-row-under-new-header",
_HEALTHY.rstrip() + "\n| **R-903** | **A finding filed the old way.** | **READY** |\n",
must_convict=True, expect_rule="RULE 5")
# decoy 6 — a near-miss category (the brief's own short name, not the defined one)
_rsg_case("unknown-category",
_HEALTHY.replace(_R901, "| **R-901** | Backup | P3 |"),
must_convict=True, expect_rule="RULE 6")
# decoy 7 — an old rank tag in the Sev cell instead of P1–P4
_rsg_case("old-rank-tag-as-sev",
_HEALTHY.replace(_R901, "| **R-901** | Backup & restore | P3-LOW |"),
must_convict=True, expect_rule="RULE 7")
# decoy 8 — a state word nobody defined (one of the 18 real ones the triage found)
_rsg_case("undefined-state-word",
_HEALTHY.replace("**READY — owner: CC**", "**PLUMBING COMPLETE** (controller v0.196.0)"),
must_convict=True, expect_rule="RULE 8")
# decoy 9 — a literal pipe in prose OUTSIDE backticks, which renders as an extra column
_rsg_case("pipe-outside-backticks",
_HEALTHY.replace("**A finding.**", "**A finding.** owner: CC | operator"),
must_convict=True, expect_rule="RULE 5")
print()
if fails:
for f in fails: