diff --git a/00-META/checks/README.md b/00-META/checks/README.md index 6b1cbb31..9ff0e5c4 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -66,7 +66,8 @@ off — "until renamed", "left for later", "not decided here", "postponed", "fut change" — names the issue that tracks it, as tracking it ("tracked by issue N", or "issue N tracks"), in that paragraph or list item, or in a dated progressive insight that speaks about it: one in the same section, or one that quotes it. An issue cited for another reason tracks nothing, and one insight does not excuse every deferral -in a record. A rejected option is not work put off and is not held +in a record. A rejected option is not work put off and is not held, judged by its `##` section, so an option under a +`###` subheading of "Considered Options" is skipped too ([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md): ADR 0244 deferred the code renames with no issue, and two days later the operator believed them done). Held to it: a record dated from 2026-10-09, and an older one corrected under the rule. Measured from ADR 0244's date diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py index 908f1d02..becc8725 100644 --- a/00-META/checks/cycle.py +++ b/00-META/checks/cycle.py @@ -78,26 +78,29 @@ INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2 def paragraphs(path): - """A record's body as (first line number, section heading, text) paragraphs: frontmatter and fenced + """A record's body as (first line number, section heading, chapter, text) paragraphs: frontmatter and fenced code left out, a block quote's markers stripped, so a quoted insight reads as the paragraph it is. The - section is the nearest heading above the paragraph; a heading is its own paragraph and its own section.""" + section is the nearest heading above the paragraph; a heading is its own paragraph and its own section. + The chapter is the nearest `#` or `##` heading, which a `###` subheading inside it does not change.""" text = open(path, encoding="utf-8").read() m = re.match(r"^---\n.*?\n---\n?", text, re.S) body, first = (text[m.end():], text[: m.end()].count("\n") + 1) if m else (text, 1) body = re.sub(r"(?ms)^\s*```.*?^\s*```", lambda x: re.sub(r"[^\n]", " ", x.group(0)), body) - out, line, section = [], first, "" + out, line, section, chapter = [], first, "", "" for chunk in body.split("\n\n"): stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip() if stripped: heading = re.match(r"^#{1,6}\s+(.*)", stripped) if heading: section = heading.group(1).strip() + if len(stripped) - len(stripped.lstrip("#")) <= 2: + chapter = section # A list's top-level items are units of their own: a Consequences list is one paragraph, and an # issue named in one item tracks nothing in the next. items = re.split(r"\n(?=[-*] |\d+\. )", stripped) at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n") for item in items: - out.append((at, section, item)) + out.append((at, section, chapter, item)) at += item.count("\n") + 1 line += chunk.count("\n") + 2 return out @@ -309,17 +312,18 @@ def main(): if status != "accepted": continue paras = paragraphs(path) - insights = [(d, sec, text) for line, sec, text in paras for d in INSIGHT.findall(text)] + insights = [(d, sec, text) for line, sec, _, text in paras for d in INSIGHT.findall(text)] held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _, _ in insights) if not held: continue # A dated insight naming an issue covers the deferrals it speaks about -- those in its own section, # or ones it quotes -- and no other: one insight must not excuse every deferral in the record. tracking = [(sec, text) for d, sec, text in insights if issue_refs(text, seen)] - for line, section, text in paras: + for line, section, chapter, text in paras: m = DEFERRAL.search(text) - # A rejected option is a decision not to do it, not work put off. - if re.match(r"(?i)(options? rejected|considered options|rejected)", section): + # A rejected option is a decision not to do it, not work put off -- judged by its chapter, so an + # option under a `###` subheading of "## Considered Options" is skipped as well. + if re.match(r"(?i)(options? rejected|considered options|rejected)", chapter): continue if not m or issue_refs(text, seen): continue