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). the parallel-session caveat above).
- **`REUSE.md`**, if a shared helper or pattern moved (same commit). - **`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 - **`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 - **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 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`. 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 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 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. 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 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. 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) ## 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_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 -*- # -*- coding: utf-8 -*-
"""register_shape_gate.py — OPEN-ITEMS.md is a TABLE, and a table has a shape (R-627, 2026-09-22). """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 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 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 the rows after it render as a separate table — and, worse, a script that walks "the
table" stops there. 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 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 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 (**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 python3 scripts/register_shape_gate.py <file> # any file, for the red-proof
Exit: 0 clean · 1 convicted. Exit: 0 clean · 1 convicted.
""" """
import os
import re import re
import sys import sys
from pathlib import Path 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 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" 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"{ROW.match(lines[j]).group(1)} at line {j+1}. A blank line ends "
f"the markdown table.")) 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 return rows, len(seen), bad
@@ -119,8 +178,9 @@ def main():
if rows != ids: if rows != ids:
print(f"register-shape: {rows - ids} row(s) are DUPLICATES") print(f"register-shape: {rows - ids} row(s) are DUPLICATES")
if not bad: if not bad:
print("register-shape: OK — every row ends with a state cell, every id is unique, " print("register-shape: OK — every row ends with a state cell, every id is unique, no blank "
"and no blank line splits the table") "line splits the table, and every row has its columns, a known category, a P1–P4 "
"severity and a defined state")
return 0 return 0
print(f"register-shape: CONVICTED — {len(bad)} problem(s)") print(f"register-shape: CONVICTED — {len(bad)} problem(s)")
for line_no, rid, why in bad: 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"), "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 " "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 " "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)", "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 " "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 " "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) _rsg = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_rsg) _spec.loader.exec_module(_rsg)
_HEADER = "| ID | Category | Sev | What | State | Blocked on | Next action | Owner |"
_HEALTHY = "\n".join([ _HEALTHY = "\n".join([
"| ID | What | State |", _HEADER,
"|---|---|---|", "|---|---|---|---|---|---|---|---|",
"| **R-901** | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** |", "| **R-901** | Backup & restore | P3 | **A finding.** Its text mentions `owner: CC | and a literal pipe` on purpose. | **READY — owner: CC** | — | — | CC |",
"| **R-902** | **Another finding.** | **CLOSED 2026-09-01** |", "| **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"), _HEALTHY.rstrip() + "\n| **R-903a** | **A split finding.** |\n".replace(" |\n", "\n"),
must_convict=True, expect_rule="RULE 1") 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() print()
if fails: if fails:
for f in fails: for f in fails: