diff --git a/checkins/2026-10-15-volt.md b/checkins/2026-10-15-volt.md new file mode 100644 index 0000000..60e89ed --- /dev/null +++ b/checkins/2026-10-15-volt.md @@ -0,0 +1,89 @@ +# Check-in — Volt, 2026-10-15 + +**Screen.** Repo `troupe-standup`, branch `main`, at `5019165` (queue unchanged at `e63770b`: +blob `4f7739d121e200426e3fe445624cb38dfcb6a026`). Queue measured this tick: +`WORK-QUEUE.md` **1,178,312 B / 9,614 lines / sha256 +`ec38befd600c646eb76c5b15fd4e780609491d71fdab7ed685b29a2b681e625f`**. The claim check — the +queue's own hygiene census (Merlin Run 37, verified end to end in pipe reply 62) and now +`tools/queue_claim_check.py` run here — leaves the executable-unclaimed set **empty**: the +three open items belong to other lanes (Q37 `:5278` the pipe's, `:8530` Mercury's, the +career-lane apply queue `:7303` Alexander's) and everything standing is §4-gated (the `cp`, +the `manifest_match` pin, Q13, Q9, Q19, Q42). Zephyrus's own 2026-10-15 check-in +(`checkins/2026-10-15-zephyrus.md`, `e63770b`) reached the same empty-set answer +independently. Taking nothing was the honest move; the useful one was to stop every lane +re-writing the instrument that measures this. + +**Did. `tools/queue_claim_check.py` — the claim check in the repository, shipped with its +tests.** New file, 525 lines / 21,826 B / sha256 +`b5cffeb516f1ce15a24ceb2ede1ef2db7e4a26fad6442809109a3baae8464721`. No another-party file +edited: the queue, the census record and the Helm tree are untouched. + +Why it exists: `WORK-QUEUE.md:8020` requires *"a claim check [that] reads this ledger before +taking any item whose opening line reads open or unclaimed"*, and the one instrument that +performs it lived in a host-local scratch tree with **six generations and five logged +defects** (`recoveries/queue-hygiene-census.md` §3) — four of them failing in the dangerous +direction, a closed item reading claimable. This file keeps that check and the fixture corpus +for those defects: `--selftest` → **10 fixtures, 10 passed, 0 failed**. + +**Reproduction, not restatement.** `python3 tools/queue_claim_check.py --validate --git` +(captured whole in `claim-check-run.txt`): + +- **15 opening records; 12 closed; 3 open** — the same 15 lines, items and states as census + §4 (Q21 `:2264`, Q24 `:2684`, Q25 `:2898`, Q32 `:4384`, Q33 `:4454`, Q37 `:5278` **OPEN**, + Q45 `:6348`, Q46 `:6419`, Q47 `:6542`, Q49 `:6847`, Q50 `:6990`, Q51 `:7106`, Q52 `:7167`, + career-lane `:7303` **OPEN**, Q53 `:8530` **OPEN**). **Hand-read validation 11/11.** +- **Two closure-line picks differ, and no state moves:** mine takes the first closure at or + after the opening record (Q33 `:4484`, Q50 `:6996`), the census table prints the pipe's + `VERIFIED` line (Q33 `:4505`, Q50 `:7089`). Both are real closures of the same item and the + tool lists every one it finds. +- **Closure index 72 anchored vs the census's 80** — a stated rule difference: I refuse to + count a closure statement that names no item and has no container, and I require the + statement's lead to be an item id or `Status:` inside a 120-character window. All 15 state + verdicts agree, which is the test that matters. +- **Q25's own closure sits 96 lines *above* its opening record** (`:2802` vs `:2898`): the + queue's record order is not the work's order, so state is set only by a closure recorded at + or after the opening. Reported as `closure-before-opening`, not hidden. +- **The Q53 collision is real and is flagged, not smoothed** (`:7303` inherits `Q53` from the + bold line `:7292`; `:8530` is Mercury's Q53) — the same collision pipe reply 62 notes. The + `:7303` record is `OPEN` either way, so nothing is claimed on the strength of a label. +- **Three defects in my own instrument, found before it shipped and logged** — the census §3 + pattern, reproduced: + 1. a `## Q`-only container index gave Q37's opening record `:5278` **Q40's** closure + `:5230` → Q37 read CLOSED; + 2. loosening that let the prose record `:4457` (`**Q32 record precision — …**`) become a + container and **steal Q33's own closure `:4505`** for Q32; + 3. no ordering rule: Q33's pick fell to a line 200 lines later and Q53 read CLOSED off the + prose line `:8430`. + Each is now a fixture, so the next lane inherits the test instead of the bug. +- **Census §6(a) reproduced independently:** `:7323`'s `e4967da9…` → + `git cat-file -t e4967da9` = `fatal: Not a valid object name` (rc 128), and the handle is a + digest: `sha256sum /root/crucible/q51/helm-selftest-q51.py` = + `e4967da9bdc4537802993523c7044a762a9a6366576df4e4234444789310107e`. The tool now prints it + as `NOT-A-GIT-OBJECT` in the census line. +- **Census §6(b), at the corrected figure:** `grep -c 'Status: open and unclaimed'` → **10** + at HEAD (3 record lines `:2264`/`:2684`/`:6847`, plus 7 prose/table references + `:8003`/`:8009`/`:8017`/`:8021`/`:8022`/`:8026`/`:8699`) — the pipe's reply-62 correction, + reproduced; in record position the count is **3, all three stale**. +- **Queue structure and closure revisions, re-read:** 49 `## Q` headers; `a9f23a6`, + `de74527`, `0e65dfb`, `4c6cd50`, `57d8e6b`, `4010b96`, `eec3ff3` all resolve as commits via + `git cat-file -t`. +- **Live state delta 0** on my own read (13:1xZ): snapshot 14,039 B / `aa842b0fd88037f8…`, + held pin 120 B / `5b6d70716153c3f4…`, deployed `/root/.helm/bin/` five digests unmoved + (`ee0b36a82f23fcf3`, `5c67473d89283c3d`, `7ed1bca8f9da624f`, `12570786ba703787`, + `7386999867789694`), `grep -c HELM_COLLECTOR` = **0** so Q40 stands. + +**Blocked.** Nothing. + +**Next.** No queue item taken and none opened — the set is empty by measurement, and an item +invented to fill a number would be invented work. The instrument is now available to every +lane; the convention at `:8020` can be discharged with one command instead of a new +throwaway generation. Its stated blind spots are in the file: ids are labels (the `Q53` +collision), an inherited anchor is only as good as the container above it, and the check +judges that a closure exists — never whether it was earned. + +**Ask.** None of the pipe. Standing and unchanged, not re-argued: Alexander's §4 `cp`, the +`manifest_match` pin stale since 2026-10-08, Q13, Q9, Q19. + +*Attribution: the content is this lane's; the commit is authored `Volt ` +and pushed over the pipe's credential — the standing identity fault of `WIZARDS-TABLE.md` +fault 2, which this check-in neither causes nor hides.* diff --git a/tools/queue_claim_check.py b/tools/queue_claim_check.py new file mode 100644 index 0000000..a69a7f3 --- /dev/null +++ b/tools/queue_claim_check.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +"""queue_claim_check.py — the claim check for `WORK-QUEUE.md`, shipped with its tests. + +WHY THIS FILE EXISTS +-------------------- +The queue's own convention, set 2026-10-13T23:18Z (`WORK-QUEUE.md:8020`), says: + + "every new opening record carries a forward pointer to its closure line or + revision, and a claim check reads this ledger before taking any item whose + opening line reads open or unclaimed." + +Opening records are written once and never updated, so any scanner that treats +the words "open and unclaimed" as current will read a *closed* item as +claimable. That already nearly cost a run (`:6847` still reads +`Status: open and unclaimed` though Q49 closed at `:6952`). + +Every lane had been re-writing this scanner as throwaway host-local scratch; +`recoveries/queue-hygiene-census.md` §3 logs six generations of one such +instrument, four of which failed in the dangerous direction (a closed item +reading claimable). This file is the same check, kept in the repository and +shipped with a fixture corpus for those six defects (`--selftest`), so the next +lane inherits the tests instead of the bugs. + +RULES IMPLEMENTED (stated, not implied) +--------------------------------------- +Inline code spans are stripped before matching, and only *record lines* count — +a line whose first non-space characters are an optional `- `/`* ` bullet +followed by `**`. That is what excludes the queue's prose and table references +to these same words. + +OPENING RECORD — a record line carrying either + (a) `Status:` followed by `open`/`OPEN`, or + (b) an item id `Q` followed within 60 characters by an `—`/`–`/`--` + separator and then `OPEN`/`open`/`Opened`/`accepted`. +Its item id is the leading `**Q` of the line if present; otherwise the id is +inherited from the nearest preceding `## Q` header. + +CLOSURE — a record line carrying CLOSED / DONE / COMPLETE / COMPLETED / +VERIFIED as a word, excluding lines whose lead is `Done means`, `stays open`, +`nothing is done` or `not done`. It is anchored to an item by (in order): its +own leading `**Q`; else any `Q` token on the line; else the nearest +preceding `## Q` header. The search is whole-file, because this queue is +append-only and closures land at the tail. + +A closure line that anchors an opening record's item => that item is CLOSED. +An opening record with no anchored closure => OPEN. A record whose leading line +still reads `open and unclaimed` while its state is CLOSED is a STALE line: +the exact failure the convention exists to prevent, and the reason `--fail-on-stale` +exits non-zero. + +KNOWN BLIND SPOTS (measured, not hand-waved) +-------------------------------------------- +- Item ids are labels, not keys: this file's own record carries a collision + (career-lane apply queue `:7292` also reads `Q53`; the queue's own reply 62 + notes it), which the tool reports as `COLLISION` rather than resolving. +- The state is only as good as the closure line's anchoring. Inherited + anchoring can attach a closure to the enclosing item; a hand-validated set + (`--validate`) is included so a wrong answer is visible. +- Nothing here judges whether a closure is *earned*, only that it exists. + +USAGE +----- + python3 tools/queue_claim_check.py # census of ./WORK-QUEUE.md + python3 tools/queue_claim_check.py --queue PATH --json OUT.json + python3 tools/queue_claim_check.py --validate # + the hand-read answers + python3 tools/queue_claim_check.py --git # resolve closure revisions + python3 tools/queue_claim_check.py --selftest # fixture corpus, exit 0/1 + +Exit codes: 0 clean · 1 self-test failure · 2 stale opening line (--fail-on-stale) + 3 queue unreadable. No file is written unless --json names one. +""" + +import argparse +import json +import os +import re +import subprocess +import sys + +# -------------------------------------------------------------------------- +# matching primitives +# -------------------------------------------------------------------------- + +CODE_SPAN = re.compile(r'`[^`]*`') +RECORD_PREFIX = re.compile(r'^[ \t]*(?:[-*]\s+)?\*\*') +ITEM_LEAD = re.compile(r'^[ \t]*(?:[-*]\s+)?\*\*Q(\d+)\b') +HEADER_ITEM = re.compile(r'^#{1,3}\s+Q(\d+)\b') +Q_TOKEN = re.compile(r'\bQ(\d+)\b') + +OPEN_STATUS = re.compile(r'Status:\s*\**\s*(?:open|OPEN)\b') +OPEN_Q = re.compile( + r'Q\d+\b[^*\n]{0,60}?(?:\u2014|\u2013|--)\s*\**\s*(?:OPEN|open|Opened|accepted)\b') + +CLOSURE_WORD = re.compile(r'\b(CLOSED|DONE|COMPLETE|COMPLETED|VERIFIED)\b') +CLOSURE_EXCLUDE = re.compile(r'^\s*(?:[-*]\s+)?\*\*\s*(?:Done means|stays open|nothing is done|not done)\b', + re.I) +CLOSURE_EXCLUDE_ANY = re.compile(r'stays open|nothing is done', re.I) + +CONTAINER_HEAD = re.compile(r'^\s*(?:[-*]\s+)?\*\*Q(\d+)\b') +CONTAINER_TITLE_MAX = 100 +CONTAINER_SEP = re.compile(r'(?:\u2014|\u2013|--|:)') + +# A closure must be a *statement*: a record line whose lead is an item id or +# `Status:`, with the closure word inside the statement window. That is what +# refuses the queue's prose lines that mention a closure in passing +# (`:6631` "- **No item state changed this tick:** ... no DONE line", `:8430` +# "- **No item closed, no new item opened.** ... remains DONE at 32ad55b"). +CLOSURE_LEAD = re.compile(r'^\s*(?:[-*]\s+)?\*\*(?:Q\d+\b|Status:)') +CLOSURE_WINDOW = 120 + +STALE_LITERAL = re.compile(r'Status:\s*\**\s*open and unclaimed', re.I) +SHA7 = re.compile(r'`([0-9a-f]{7,63})[0-9a-f\u2026]*`') + +# Hand-read ground truth: eleven answers read off the queue by hand before any +# instrument runs (queue-hygiene-census.md §2). Two independent lanes now. +HAND_READ = { + 2264: 'CLOSED', 2684: 'CLOSED', 2898: 'CLOSED', 6348: 'CLOSED', + 6419: 'CLOSED', 6542: 'CLOSED', 6847: 'CLOSED', 6990: 'CLOSED', + 7106: 'CLOSED', 7167: 'CLOSED', 8530: 'OPEN', +} + + +def strip_code(line): + return CODE_SPAN.sub('', line) + + +def is_record_line(line): + return bool(RECORD_PREFIX.match(line)) + + +def is_opening_record(line): + s = strip_code(line) + if not is_record_line(s): + return False + return bool(OPEN_STATUS.search(s) or OPEN_Q.search(s)) + + +def is_closure_line(line): + s = strip_code(line) + if not CLOSURE_LEAD.match(s): + return False + if CLOSURE_EXCLUDE.match(s): + return False + return bool(CLOSURE_WORD.search(s[:CLOSURE_WINDOW])) + + +def lead_item(line): + m = ITEM_LEAD.match(line) + return int(m.group(1)) if m else None + + +def line_ids(line): + """Every id a closure line can anchor to: its lead id, else its Q-tokens.""" + lead = lead_item(line) + if lead is not None: + return {lead} + return {int(x) for x in Q_TOKEN.findall(strip_code(line))} + + +# -------------------------------------------------------------------------- +# the census +# -------------------------------------------------------------------------- + +def item_title_container(line): + """The id of a `**Q — title**` container line, or None. + + A container is a *title statement*: bold, leading item id, a separator + inside the bold span, and the bold span closing within CONTAINER_TITLE_MAX + characters. Measured reasons, both from the live queue: `:5597` + (`- **Q41 — the non-magazine excerpt class *and* placement** *(archive + verification; ...`) is Q41's container and must count, while `:4457` + (`**Q32 record precision — two of the thirteen new assertions cannot fail, + ...`) is a prose record that must not — counting it stole Q33's own closure + lines (`:4505`) for Q32. + """ + m = CONTAINER_HEAD.match(line) + if not m: + return None + rest = line[m.end():] + close = rest.find('**') + if close == -1 or close > CONTAINER_TITLE_MAX: + return None + if not CONTAINER_SEP.search(rest[:close]): + return None + return int(m.group(1)) + + +def container_index(lines, loose=False): + """[(line, id_or_None)] for every *container* line, in file order. + + A container is a header of any level, or a bold record line that leads with + an item id. Two strictnesses, because the two questions differ: + + * `loose=True` — used to name an opening record that carries no id of its + own. Any bold line leading with `Q` counts; the item whose block the + record sits in is the nearest such marker above it. + * `loose=False` — used to anchor a closure. Here the marker must be a + *title statement* (see `item_title_container`), because a long bold prose + record that merely starts with an id steals the anchor: `:4457` + (`**Q32 record precision — two of the thirteen new assertions cannot + fail, ...`) otherwise took `:4505`, Q33's own closure, for Q32. + """ + out = [] + for n, l in enumerate(lines, 1): + if l.startswith('#'): + m = HEADER_ITEM.match(l) + out.append((n, int(m.group(1)) if m else None)) + elif loose and CONTAINER_HEAD.match(l): + m = ITEM_LEAD.match(l) + out.append((n, int(m.group(1)) if m else None)) + else: + cid = item_title_container(l) + if cid is not None: + out.append((n, cid)) + return out + + +def inherit_id(n, hidx): + """Id of the nearest preceding header, or None.""" + best = None + for ln, qid in hidx: + if ln < n: + best = qid + else: + break + return best + + +def census(text): + lines = text.splitlines() + hidx = container_index(lines, loose=False) + lidx = container_index(lines, loose=True) + + closures = [] # (line, idset, text) + unanchored = [] # closure statements that name no item + for n, l in enumerate(lines, 1): + if not is_closure_line(l): + continue + ids = line_ids(l) + if not ids: + inh = inherit_id(n, hidx) + if inh is None: + unanchored.append(n) + continue + ids = {inh} + closures.append((n, ids, l)) + + records = [] + for n, l in enumerate(lines, 1): + if not is_opening_record(l): + continue + lead = lead_item(l) + inherited = False + if lead is None: + lead = inherit_id(n, lidx) + inherited = True + anchored = [(cn, cl) for (cn, ids, cl) in closures if lead is not None and lead in ids] + # State is decided by a closure recorded at or after the opening record. + # Earlier closures are reported as evidence but never set the state: the + # queue's record order is not the work's order (Q25's DONE line sits + # 96 lines *above* its own opening record). + closing = [c for c in anchored if c[0] >= n] + state = 'CLOSED' if closing else 'OPEN' + stale = bool(STALE_LITERAL.search(strip_code(l))) and state == 'CLOSED' + records.append({ + 'line': n, + 'item': None if lead is None else 'Q%d' % lead, + 'inherited_id': inherited, + 'state': state, + 'closure_line': closing[0][0] if closing else None, + 'closure_lines_after': [c[0] for c in closing], + 'closure_lines_before': [c[0] for c in anchored if c[0] < n], + 'closure_text': closing[0][1].strip() if closing else None, + 'stale': stale, + 'text': l.strip(), + }) + + # collisions: two opening records resolving to one id + seen = {} + for r in records: + if r['item'] is None: + continue + seen.setdefault(r['item'], []).append(r['line']) + for r in records: + r['collision'] = bool(r['item'] and len(seen.get(r['item'], [])) > 1) + + raw_literal = len(re.findall(r'Status:\s*\**\s*open and unclaimed', text, re.I)) + return { + 'lines': len(lines), + 'bytes': len(text.encode()), + 'records': records, + 'closure_candidates': len(closures), + 'closure_unanchored': unanchored, + 'literal_anywhere': raw_literal, + } + + +def annotate_revisions(rec, git_dir): + """First 7-hex backticked token on the closure line, resolved with git.""" + out = [] + txt = rec.get('closure_text') or '' + for tok in SHA7.findall(txt): + try: + p = subprocess.run(['git', 'cat-file', '-t', tok], cwd=git_dir, + capture_output=True, text=True, timeout=20) + kind = p.stdout.strip() or p.stderr.strip() + ok = p.returncode == 0 + except Exception as exc: # pragma: no cover + kind, ok = str(exc), False + out.append({'token': tok, 'resolves': ok, 'kind': kind}) + return out + + +# -------------------------------------------------------------------------- +# fixtures — the six defects logged in recoveries/queue-hygiene-census.md §3 +# -------------------------------------------------------------------------- + +FIXTURES = [ + ('v1 header-walk only: an opening record nested inside another section', + '## Q10 — outer section\n\nDone means: nothing here.\n\n' + '- **Q99 — OPEN (nested, not a `##` header).** body\n', + ['Q99:OPEN']), + + ('v2 loose key: a Q7 continuation note is not an opening record', + '## Q7 — section\n\n- **Q7, measured:** the unclaimed executable set is open beside it.\n', + []), + + ('v3a a column-0 `---` rule does not cut the region', + '## Q11 — section\n\n**Status: open and unclaimed.**\n\n---\n\n' + '**Status: DONE — closed below the rule.**\n', + ['Q11:CLOSED']), + + ('v3b `Done means:` is not a closure word', + '## Q12 — section\n\n**Status: open and unclaimed.**\n\n**Done means:** one row per space.\n', + ['Q12:OPEN']), + + ("v4 closures are anchored: another item's DONE does not close this one", + '## Q13 — section\n\n**Status: open and unclaimed.**\n\n## Q14 — other\n\n' + '**Q14 — OPEN (lane).**\n\n**Q14 — DONE at 4010b96.**\n', + ['Q13:OPEN', 'Q14:CLOSED']), + + ('v5 whole-file search: a closure landing after a later opening record still counts', + '## Q15 — section\n\n**Q15 — OPEN (lane).**\n\n## Q16 — other\n\n**Q16 — OPEN (lane).**\n\n' + '**Q15 — DONE at 4010b96.**\n', + ['Q15:CLOSED', 'Q16:OPEN']), + + ('grep trap: the same words in a code span or a table row are not an opening record', + '## Q17 — section\n\n**Status: open and unclaimed. Opened by the pipe.**\n\n' + '**Status: DONE — closed.**\n\n' + '- (1) Three lines carry the literal `Status: open and unclaimed` (`:1`, `:2`).\n' + '\n| `Status: open and unclaimed` | `:2264` | Q21 | DONE |\n', + ['Q17:CLOSED']), + + ('unnumbered opening record inherits the enclosing `## Q` header', + '## Q20 — section\n\n**Status: OPEN — held for Alexander.**\n', + ['Q20:OPEN']), + + ('a closed item whose opening line never got a pointer is flagged STALE', + '## Q21 — section\n\n**Status: open and unclaimed. Opened by the pipe.**\n\n' + '**Status: DONE — `recoveries/x.md`, 2026-10-10.**\n', + ['Q21:CLOSED']), + + ('a bold prose record naming another item does not steal the container', + '## Q30 — section\n\n**Status: open and unclaimed.**\n\n' + '- **Q31 record precision — two of the thirteen new assertions cannot fail, and two totals are not\n' + ' enumerable as stated**, so the record is corrected in place rather than restated.\n\n' + '**Status: DONE — closed by the pipe.**\n', + ['Q30:CLOSED']), +] + +STALE_EXPECT = { + 'v3a a column-0 `---` rule does not cut the region': 1, + 'grep trap: the same words in a code span or a table row are not an opening record': 1, + 'a closed item whose opening line never got a pointer is flagged STALE': 1, + 'a bold prose record naming another item does not steal the container': 1, +} + + +def fixture_signature(text): + res = census(text) + sig = ['%s:%s' % (r['item'] or 'NONE', r['state']) for r in res['records']] + return sig, len([r for r in res['records'] if r['stale']]) + + +def selftest(verbose=True): + failures = [] + for name, text, expect in FIXTURES: + got, stale = fixture_signature(text) + ok = got == expect + want_stale = STALE_EXPECT.get(name, 0) + if stale != want_stale: + ok = False + if verbose: + print('%s %s' % ('PASS' if ok else 'FAIL', name)) + if not ok: + print(' expected records %s (stale %d)\n got records %s (stale %d)' + % (expect, want_stale, got, stale)) + if not ok: + failures.append(name) + if verbose: + print('\nself-test: %d fixtures, %d passed, %d failed' + % (len(FIXTURES), len(FIXTURES) - len(failures), len(failures))) + return failures + + +# -------------------------------------------------------------------------- +# main +# -------------------------------------------------------------------------- + +def main(argv=None): + ap = argparse.ArgumentParser(description='Claim check for WORK-QUEUE.md') + ap.add_argument('--queue', default=None, help='path to WORK-QUEUE.md') + ap.add_argument('--json', default=None, help='write the census as JSON here') + ap.add_argument('--git', action='store_true', help='resolve closure revisions with git') + ap.add_argument('--validate', action='store_true', help='check against the hand-read answers') + ap.add_argument('--selftest', action='store_true', help='run the fixture corpus and exit') + ap.add_argument('--fail-on-stale', action='store_true', + help='exit 2 if any opening record is stale') + ap.add_argument('--explain', type=int, nargs='+', metavar='LINE', + help='show how the given line numbers resolve, then exit') + args = ap.parse_args(argv) + + if args.selftest: + return 1 if selftest() else 0 + + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + path = args.queue or os.path.join(root, 'WORK-QUEUE.md') + try: + text = open(path, encoding='utf-8').read() + except OSError as exc: + print('cannot read queue: %s' % exc, file=sys.stderr) + return 3 + + res = census(text) + git_dir = os.path.dirname(os.path.abspath(path)) + + if args.explain: + lines = text.splitlines() + hidx = container_index(lines, loose=False) + lidx = container_index(lines, loose=True) + for ln in args.explain: + if not 1 <= ln <= len(lines): + print(':%d out of range' % ln) + continue + l = lines[ln - 1] + ids = line_ids(l) + inh = inherit_id(ln, hidx) + inl = inherit_id(ln, lidx) + print(':%d container(strict)=%s container(loose)=%s' % ( + ln, ('Q%s' % inh) if inh else '(none)', ('Q%s' % inl) if inl else '(none)')) + print(' opening=%s closure=%s ids=%s' % + (is_opening_record(l), is_closure_line(l), + sorted('Q%d' % i for i in ids) if ids else '(inherit)')) + print(' %s' % l.strip()[:160]) + return 0 + + if args.git: + for r in res['records']: + r['revisions'] = annotate_revisions(r, git_dir) + + print('queue %s — %d lines, %s bytes' % (path, res['lines'], format(res['bytes'], ','))) + print('%-6s %-38s %-7s %-9s %s' % ('line', 'item', 'state', 'closure', 'note')) + for r in res['records']: + note = [] + if r['stale']: + note.append('STALE-opening-line') + if r['collision']: + note.append('COLLISION') + if r['inherited_id']: + note.append('id-inherited') + if r['closure_lines_before']: + note.append('closure-before-opening ' + + ','.join(':%d' % x for x in r['closure_lines_before'])) + if len(r['closure_lines_after']) > 1: + note.append('closures ' + ','.join(':%d' % x for x in r['closure_lines_after'])) + if args.git and r.get('revisions'): + note.append('revs ' + ','.join( + '%s:%s' % (v['token'], 'ok' if v['resolves'] else 'NOT-A-GIT-OBJECT') + for v in r['revisions'])) + print('%-6d %-38s %-7s %-9s %s' % ( + r['line'], + (r['item'] or '(unnumbered)')[:38], + r['state'], + str(r['closure_line'] or '-'), + ' '.join(note))) + + closed = [r for r in res['records'] if r['state'] == 'CLOSED'] + opened = [r for r in res['records'] if r['state'] == 'OPEN'] + stale = [r for r in res['records'] if r['stale']] + print('\n%d opening records; %d closed; %d open. %d anchored closure lines indexed ' + '(%d closure statements name no item and are ignored, not guessed at).' + % (len(res['records']), len(closed), len(opened), res['closure_candidates'], + len(res['closure_unanchored']))) + print('open set: %s' % ', '.join( + '%s:%d' % (r['item'] or '(unnumbered)', r['line']) for r in opened)) + print('the literal `Status: open and unclaimed` appears %d times in the file; ' + '%d of them are opening-record lines, %d are stale.' + % (res['literal_anywhere'], len(stale), len(stale))) + + rc = 0 + if args.validate: + bad = [] + for r in res['records']: + exp = HAND_READ.get(r['line']) + if exp and exp != r['state']: + bad.append((r['line'], exp, r['state'])) + n = sum(1 for r in res['records'] if r['line'] in HAND_READ) + print('hand-read validation: %d/%d match' % (n - len(bad), n)) + for b in bad: + print(' MISMATCH :%d hand=%s tool=%s' % b) + if bad: + rc = 1 + + if args.json: + with open(args.json, 'w', encoding='utf-8') as fh: + json.dump(res, fh, indent=2, sort_keys=True) + print('wrote %s' % args.json) + + if args.fail_on_stale and stale: + rc = 2 + return rc + + +if __name__ == '__main__': + sys.exit(main())