#!/usr/bin/env python3 """queue-extract.py -- independently extract the item census from WORK-QUEUE.md. LANE: Extract - data extraction and tables. Item = a `## Qnn` section, or (Q103) a block-initial record that opens an id with no heading of its own, or (Q103) a later block-initial record for an id already an item. Each item owns the lines up to the next opening. Output: TSV id kind line end_line lines bytes role role_detail status status_line title """ import re import sys QUEUE = "/root/troupe-standup/WORK-QUEUE.md" HEADING = re.compile(r"^##\s+(Q\d+)\b(.*)$") STATUS = re.compile(r"^\s*\*{0,2}Status:\s*(.*)$") FENCE = re.compile(r"^(```|~~~)") BULLET = re.compile(r"^[ \t>|]*(?:[-*+] ?|[-*+]$)") OPEN_SEP = re.compile(r"\s*(?:—|–|-|:|\(|\bopened\b|\bob\b|\bOPEN\b|\bOPENED\b)") ID_TOKEN = re.compile(r"\bQ\d+\b") def split_lines(text): lines = text.split("\n") trailing = text.endswith("\n") if trailing: lines = lines[:-1] return lines, trailing def line_bytes(line, is_last, trailing): if is_last and not trailing: return len(line.encode("utf-8")) return len(line.encode("utf-8")) + 1 def status_value(raw): 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): 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 record_form(line, qid): s = line.strip() if HEADING.match(s) and HEADING.match(s).group(1) == qid: return "HEADING" body = s m = re.match(r"^[ \t>|]*(?:[-*+] ?|[-*+]$)", body) if m: body = body[m.end():] 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() mm = ID_TOKEN.match(inner) if mm and mm.group(0) == qid and OPEN_SEP.match(inner[mm.end():]): return "BOLD-LEAD" return None def build_items(lines, fenced): headings = [] 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} # first record-form line per id, and reference counts # mention scan: first record-form line per id, and reference counts first_record = {} for i, ln in enumerate(lines, 1): if i in fenced: continue for m in ID_TOKEN.finditer(ln): qid = m.group(0) if qid not in first_record: frm = record_form(ln, qid) if frm: first_record[qid] = (i, frm) # Only ids WITHOUT a heading own an item: restrict the boundary set to those, # otherwise a heading id's first prose mention (e.g. "…appended at the end of # this file (Q89, Q90)…") carves its section short. first_record = {qid: v for qid, v in first_record.items() if qid not in heading_ids} # later block-initial records: a continuation block for an already-open id later = [] for i, ln in enumerate(lines, 1): if i in fenced or not ln.startswith("**"): continue prev = lines[i - 2].strip() if i >= 2 else "" if prev != "" and prev != "---" and not prev.startswith("