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