#!/usr/bin/env python3 """queue_index.py -- the work queue's table of contents, and the gate that it is current. WHY THIS EXISTS `WORK-QUEUE.md` is 1.2 MB and 9,953 lines long at `39312c2`. It has no table of contents. Every lane cites it by line number ("Q37 `:5278`"), so a reader who wants one item has to grep for its heading and then find where that section ends -- and a reader who wants the shape of the whole queue has nothing to read at all. The shape is not obvious even after a grep. Measured on 2026-10-17 at HEAD (`ece5228`): 49 `## ` sections carry 45 distinct ids, because four ids (Q37, Q38, Q39, Q43) own a second section each -- a `closure` or `claimed` block appended under a fresh heading. And 9 further ids (Q41, Q42, Q46, Q47, Q50, Q51, Q52, Q53, Q63) have NO heading of their own: they are opened as bold records nested inside somebody else's section. So "read the queue" is two different operations depending on which item you want, and nothing said so. THE RECORD RULE (Q63, 2026-10-17). An id is *opened by a record, not mentioned by prose*: a heading, a bold lead that opens with the id (`**Qnn -- ...**` / `**Qnn opened -- ...**`), or a line whose own lead is a `Status:` marker. Before that rule the detector admitted every id token that had no heading, so a tick quoting `--outside = Q63/Q77/Q99` opened three ids, an artifact path (`out/Q54-READBACK.txt`) opened Q54, and the range `Q31-Q62` opened Q62. The first *mention* also used to be reported as the record, which is wrong twice: Q53 is first named in prose well before it is opened (:7246 prose vs :7292 record) and Q63 is first named inside the very tick that quotes it (:9909) but opened at :9918. The table below now carries the record line, the first-mention line and the form that admitted the id, so the difference is visible instead of implied. An id is `Q` and one or more digits, no width (Q103, 2026-10-27). The pattern read `Q` plus one or two digits, so from Q100 up an id was not a section and not even a token: a `## Q100 -- ...` heading parsed as body text of the section before it and that section's byte count absorbed the whole tail behind it. Three-digit ids are now sections, tokens and record ids alike, with no ceiling restored at three digits -- a fixed width is the defect, so a three-digit ceiling would only postpone it. The trailing word boundary is what stops `Q100` matching inside `Q1000`. TIER TWO HAS BOUNDS NOW (Q103). A `**Qnn -- ...**` record that opens an id with no heading of its own is an item too: its block runs from the record line to the line before the next item opens, so `Q98`'s row stopped carrying the 40,957 bytes of `Q100`-`Q103` and each of those ids has a row and an extent. WHAT IT MEASURES -- structural facts only, one row per item -- a `## ` section, or (Q103) a record block that opens an id with no heading of its own id, line, end_line, role, title, lines, bytes, first `Status:` marker. role=opening the first section carrying that id role=continuation a later section for an id already seen (a closure or claim block); its `role_detail` names the first word lines/bytes the section's own extent, header line to last line status the first `Status:` line in the section, VERBATIM and truncated. NOT adjudicated: whether an item is open is `tools/queue_claim_check.py`'s verdict, not this tool's. Two instruments disagreeing about closure is a bug this tool declines to introduce. Plus the two legs that make the structure visible: nested ids opened by a record inside a section that has no heading of their own: the record's line, the first line that mentions it, the section that contains it, and the shape/form of the record line outside ids referenced by the prose corpus (checkins/ replies/ recoveries/ audits/ access/ decisions/ docs/ and the top-level documents) but absent from the queue entirely -- `tools/` is excluded on purpose: its fixtures carry synthetic ids by construction, and this tool's own selftest is one of them. The `outside` leg is REPORTED, not embedded: `--outside` prints it, and the rendered document does not contain it. It cannot. That set is a function of the working tree's prose, not of the queue, so baking it into a document that `--check` byte-compares against a fresh render means any checkin that quotes an id turns the queue's gate RED for a reason that has nothing to do with the queue. Measured on 2026-10-17: `--outside` read Q63 from `recoveries/`, then Q63+Q77+Q99 once a checkin described the test that names Q77 -- same queue, same sha256, three different documents. A table of contents must be a function of the file it indexes. Headings inside a fenced block are never sections; a heading whose id is followed by `closure --` or `§9 follow-on --` is still that id's section. WHAT IT DOES NOT DO It does not grade the work, does not adjudicate closure, and does not read git history. It answers "what is in this file and where", and it keeps the generated copy of that answer honest: `--write` renders `docs/queue-index.md` with the parse pinned inside a machine block, and `--check` re-renders the whole document -- prose table included, compared byte for byte -- and goes RED the moment the two disagree. (The pin alone was not enough: the first whole-gate run found the table above the pin drifted while the pin still matched, because the document had begun listing itself in its own corpus. The rendered file is excluded from that corpus, which makes `--write` a fixed point, and the bytes are now compared whole.) The document is therefore a function of `WORK-QUEUE.md` alone: the same queue renders the same bytes whatever the prose corpus says, which is what makes `--check` a statement about the queue's currency and not about the last person to write a checkin. `--check` exit codes: 0 current, 1 stale (with the delta printed), 2 the document carries no parseable pinned block, 3 the queue or the document could not be read at all (missing path, or bytes that are not UTF-8). An unreadable input is a precondition failure, never a stale index and never a traceback: `--queue` at a missing path or at a binary used to raise FileNotFoundError / UnicodeDecodeError out of `run()` with rc 1, which reads like "stale" to every caller that only checks the exit code. READ-ONLY except `--json PATH` and `--write PATH`. USAGE python3 tools/queue_index.py --index # 49 sections, one screen each side python3 tools/queue_index.py --outside # ids the prose corpus names and the queue does not python3 tools/queue_index.py --json out.json python3 tools/queue_index.py --write docs/queue-index.md python3 tools/queue_index.py --check docs/queue-index.md python3 tools/queue_index.py --selftest # fixture corpus, 80 cells Re-run `--write` after any append to the queue. The document carries the queue's sha256, so a stale copy is a RED check, never a silent lie. """ import argparse import hashlib import json import os import re import subprocess import sys import tempfile QUEUE_DEFAULT = "WORK-QUEUE.md" DOC_PATH = "docs/queue-index.md" BEGIN = "" END = "" # THE ID CLASS (Q103, 2026-10-27). An id is `Q` and one or more digits -- no # width. It was `\d{1,2}`, so from Q100 up an item was not a section and not even # a token: `## Q100 -- ...` parsed as body text of whatever section preceded it # and the eight-figure byte count of a whole tail accrued to its neighbour. The # trailing `\b` is load-bearing in the other direction: without it `Q100` is a # prefix-match inside `Q1000` and `Q1000` would be indexed as the item Q100. # A width ceiling is deliberately NOT restored: `\d{1,3}` reproduces this exact # defect one digit later, which is the shape of the bug, not a fix for it. HEADING = re.compile(r"^##\s+(Q\d+)\b(.*)$") ID_TOKEN = re.compile(r"\bQ\d+\b") STATUS = re.compile(r"^\s*\*{0,2}Status:\s*(.*)$") FENCE = re.compile(r"^(```|~~~)") # A record opens with its id; a mention merely carries it. `BULLET` strips the # markdown that can precede a record without being part of it, and `OPEN_SEP` is # what may follow the id in a bold lead that is *opening* an item (Q46's # "**Q46 (Helm lane) — OPEN ...**" and Q63's "**Q63 opened — ...**" both count; # a "**Q7, measured:**" bold run does not). BULLET = re.compile(r"^[\s>|]*(?:[-*+]\s+)?") OPEN_SEP = re.compile(r"\s*(?:—|–|-|:|\(|\bopened\b|\bopen\b|\bOPEN\b|\bOPENED\b)") ROLE_WORDS = ("closure", "claimed", "§", "status") def record_form(line, qid): """The form in which `line` opens `qid`, or None if it only mentions it. The counting rule is settled in `WORK-QUEUE.md` at Q63 (2026-10-17): an id is *opened by a record, not mentioned by prose*. Three forms count -- HEADING `## Qnn ...` (a section; never nested) BOLD-LEAD after bullet/bold markers the line opens `**Qnn` and the id is followed by a separator or the word open/opened STATUS-LEAD the line's own lead is a `Status:` marker (bold or plain) and it names the id A bare token inside a sentence, a tick quoting tool output (`= Q63/Q77/Q99`), an artifact filename (`out/Q54-READBACK.txt`) or a range (`Q31–Q62`) matches none of them, which is the point: those were opening ids before this rule. """ s = line.strip() m = HEADING.match(s) if m and m.group(1) == qid: return "HEADING" body = BULLET.sub("", s) if qid not in ID_TOKEN.findall(body): return None if body.startswith("**Status:") or body.startswith("Status:"): return "STATUS-LEAD" if body.startswith("**"): inner = body[2:].lstrip("*").lstrip() m = ID_TOKEN.match(inner) if m and m.group(0) == qid and OPEN_SEP.match(inner[m.end():]): return "BOLD-LEAD" return None def record_detail(line, qid): """The first word after a record's bold lead -- `DONE`, `CLOSED`, `opened`. The heading loop's `role_detail` already names the first word after a continuation heading's dash; this is the same field for a continuation record, so the table says *why* a second block exists without a reader having to open it. """ body = BULLET.sub("", line.strip()) if not body.startswith("**"): return "" inner = body[2:].lstrip("*").lstrip() if inner.startswith(qid): inner = inner[len(qid):] for word in inner.lstrip("—-–: ").split(): return word.strip("*().,;:").strip("*") return "" def split_lines(text): """Lines without their terminators, plus the byte length each line occupies.""" lines = text.split("\n") trailing_newline = text.endswith("\n") if trailing_newline: lines = lines[:-1] return lines, trailing_newline def line_bytes(line, is_last, trailing_newline): if is_last and not trailing_newline: return len(line.encode("utf-8")) return len(line.encode("utf-8")) + 1 def status_value(raw): """The value of a `Status:` marker, in either emphasis form. `**Status: value**` puts the value *inside* the bold run; `**Status:** value` closes the bold *before* it. The `STATUS` pattern consumes the optional leading `**`, so cutting at the first `**` is right for the first form and returns "" for the second -- which published a stated status as `--`. Q13:1129 (`**Status:** 1 of 8 has a decision ...`) was the live case. """ raw = raw.strip() while raw.startswith("**"): raw = raw[2:].strip() if "**" in raw: raw = raw.split("**", 1)[0].strip() return raw def fence_mask(lines): """1-based line numbers to skip: fence delimiters and every line they hide. A fenced block is quoted material, not document structure -- headings inside it are not sections, and id tokens inside it are not records. Stated once here and reused by parse(), rather than restated verbatim in each scan loop. """ out = set() in_fence = False for i, ln in enumerate(lines, 1): if FENCE.match(ln.strip()): in_fence = not in_fence out.add(i) elif in_fence: out.add(i) return out def parse(text): """Return the structural parse: items, nested ids, id map, byte accounting. An *item* is a heading section OR a record block (Q103, 2026-10-27). An id opened by a record -- `**Q100 -- OPEN ...**` inside somebody else's section -- is an item with its own extent, from its record line to the line before the next item opens, instead of being counted as prose inside its neighbour. The two tiers are one table here and `kind` says which: before this, a record opening an id with no heading handed its bytes to the section around it (Q98's row carried 40,957 bytes of four later items) and an id from Q100 up was not even a token. """ lines, trailing_newline = split_lines(text) n = len(lines) sizes = [line_bytes(ln, i == n - 1, trailing_newline) for i, ln in enumerate(lines)] fenced = fence_mask(lines) headings = [] # (line_no, id, rest) for i, ln in enumerate(lines, 1): if i in fenced: continue m = HEADING.match(ln) if m: headings.append((i, m.group(1), m.group(2).strip())) heading_ids = {h[1] for h in headings} # every id token in the queue, counted as a mention, plus the first line that # opens it as a record. Fenced lines are skipped: an id in a code block is # quoted text, not a record. # # Q63 (2026-10-17): the first *mention* is not the record. Q53 is first named # in prose 46 lines before its `**Q53 opened -- ...**` line, and Q63 is named # inside a tick quoting tool output at :9909 but opened at :9918. So a nested # id is admitted by its first record-form line, and an id that never has one # (Q54/Q55/Q56/Q58 as artifact filenames, Q62 inside `Q31-Q62`, Q77/Q99 inside # a quoted `--outside` list) is not an item at all. mentioned = {} for i, ln in enumerate(lines, 1): if i in fenced: continue for m in ID_TOKEN.finditer(ln): qid = m.group(0) hit = mentioned.get(qid) if hit is None: hit = mentioned[qid] = {"line": i, "refs": 0, "record": None, "form": None} hit["refs"] += 1 if hit["record"] is None: form = record_form(ln, qid) if form: hit["record"] = i hit["form"] = form # tier two: ids opened by a record and owning no heading of their own records = [(mentioned[qid]["record"], qid, mentioned[qid]["form"]) for qid in sorted(mentioned, key=lambda s: int(s[1:])) if qid not in heading_ids and mentioned[qid]["record"] is not None] # tier two, later records (Q103, 2026-10-28): an id's SECOND block-initial # record -- a closure, a correction -- is an item too, with its own bounds. # Before this, only the first record line per id cut anything, so every later # closure block was charged to whichever item preceded it: measured on this # queue before the fix, `**Q103 -- DONE ...**` (30 lines appended at the foot) # grew Q104's row from 5,990 to 14,007 bytes, and Q100's own DONE block at # :12915 sat inside Q101's row. That is the same swallow the id class had, # one level down. # # The admission rule is deliberately narrow, because `record_form` alone is # too loose here: 171 lines in this queue carry a record form for some id and # 49 ids have more than one, but most of the extras are bullet status lines # (`- **Q16:** unchanged at ...`) that are prose inside an item and must not # split it. A later record is a boundary only when it BEGINS a block: the # line opens with `**` at column 0, and the line above it is blank or a `---` # rule or an HTML comment. Measured: 52 lines qualify, every one of them a # standalone block in the file. def begins_block(i): prev = lines[i - 2].strip() if i >= 2 else "" return prev == "" or prev == "---" or prev.startswith("