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
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:
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
+412
-336
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user