Skip a rejected option by its chapter, so one under a ### subheading is skipped too
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

cycle.py skipped a rejected option only when "Options rejected" was its nearest heading; a record that
lists its options under ### subheadings of "## Considered Options" had them held as deferrals.
This commit is contained in:
jochen
2026-10-09 19:32:16 +02:00
parent 169200fe55
commit 189dbcef7a
2 changed files with 14 additions and 9 deletions
+2 -1
View File
@@ -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 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 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 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): ([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 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 it: a record dated from 2026-10-09, and an older one corrected under the rule. Measured from ADR 0244's date
+12 -8
View File
@@ -78,26 +78,29 @@ INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2
def paragraphs(path): 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 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() text = open(path, encoding="utf-8").read()
m = re.match(r"^---\n.*?\n---\n?", text, re.S) 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, 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) 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"): for chunk in body.split("\n\n"):
stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip() stripped = "\n".join(re.sub(r"^\s*>\s?", "", l) for l in chunk.split("\n")).strip()
if stripped: if stripped:
heading = re.match(r"^#{1,6}\s+(.*)", stripped) heading = re.match(r"^#{1,6}\s+(.*)", stripped)
if heading: if heading:
section = heading.group(1).strip() 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 # 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. # issue named in one item tracks nothing in the next.
items = re.split(r"\n(?=[-*] |\d+\. )", stripped) items = re.split(r"\n(?=[-*] |\d+\. )", stripped)
at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n") at = line + chunk[: len(chunk) - len(chunk.lstrip())].count("\n")
for item in items: for item in items:
out.append((at, section, item)) out.append((at, section, chapter, item))
at += item.count("\n") + 1 at += item.count("\n") + 1
line += chunk.count("\n") + 2 line += chunk.count("\n") + 2
return out return out
@@ -309,17 +312,18 @@ def main():
if status != "accepted": if status != "accepted":
continue continue
paras = paragraphs(path) 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) held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _, _ in insights)
if not held: if not held:
continue continue
# A dated insight naming an issue covers the deferrals it speaks about -- those in its own section, # 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. # 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)] 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) m = DEFERRAL.search(text)
# A rejected option is a decision not to do it, not work put off. # A rejected option is a decision not to do it, not work put off -- judged by its chapter, so an
if re.match(r"(?i)(options? rejected|considered options|rejected)", section): # option under a `###` subheading of "## Considered Options" is skipped as well.
if re.match(r"(?i)(options? rejected|considered options|rejected)", chapter):
continue continue
if not m or issue_refs(text, seen): if not m or issue_refs(text, seen):
continue continue