diff --git a/00-META/checks/README.md b/00-META/checks/README.md index b6f5d1e3..6b1cbb31 100644 --- a/00-META/checks/README.md +++ b/00-META/checks/README.md @@ -62,13 +62,18 @@ allocate the same one). And a core issue — one opened from 2026-10-07 whose `l repository — resolves only with `replay:` (an id in mesh-lab's replays register) or `replay-none:` saying why none is possible ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)); it failed on a resolved core issue carrying neither before it passed. And an accepted record that puts work -off — "until renamed", "left for later", "deferred", "a later change" — names the issue that tracks it, in that -paragraph or in a dated progressive insight that says what it tracks +off — "until renamed", "left for later", "not decided here", "postponed", "future work", "deferred", "a later +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 ([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 -it found deferrals without an issue in ADR 0241, 0244 and 0270; held from 2026-10-09 it failed on 0270, and on -0244 once its insight brought it under the rule, before each named its issue. `python3 00-META/checks/cycle.py` +it found deferrals without an issue in ADR 0241, 0244 and 0270; held from 2026-10-09 it failed on 0270 and 0273 +(whose "not decided here" sits beside a citation of issue 340 that tracks none of it), and on 0244's four +deferral places once its insight brought it under the rule — on three of them while the insight, in §6, named +the issue without quoting them — before each named its issue (355, 357, 354). `python3 00-META/checks/cycle.py` ## words.py diff --git a/00-META/checks/cycle.py b/00-META/checks/cycle.py index 7e7c4aa8..908f1d02 100644 --- a/00-META/checks/cycle.py +++ b/00-META/checks/cycle.py @@ -63,31 +63,62 @@ REPLAY_ID = re.compile(r"^R\d+\b") # from DEFERRALS_FROM, and an older one corrected under the rule (a progressive insight dated from then # on): an edit made under the rule is held to it. The phrases are the ones records use to put work off; # "for later" alone is not one, because records say "never queued for later" and "too narrow for later". +# The tracking issue is named as tracking it, beside the deferral or in a dated insight in the same section +# or quoting it (ISSUE_REF, covers). DEFERRALS_FROM = "2026-10-09" DEFERRAL = re.compile(r"(?i)until (?:it is |they are |the code (?:is )?)?renamed|until the code rename|" r"\ba later rename|not renamed here|identifier to rename|" - r"left (?:for|to) (?:a )?later(?! record)|" + r"left (?:for|to) (?:a )?later|not decided here|\bpostponed\b|future work|" r"\bdeferred\b|later work|not built here|\ba later change") -ISSUE_REF = re.compile(r"\bissue (\d{3})\b|04-ISSUES/(\d{3})-") +# The issue that tracks a deferral is named as tracking it -- "tracked by issue N", or "issue N tracks" -- +# because a record cites issues for many reasons, and an issue cited as context beside a deferral (ADR 0273 +# names issue 340 in the very sentence that leaves a channel undecided) tracks nothing. +ISSUE_REF = re.compile(r"(?i)tracked (?:by|in|as)\s+\[?issue (\d{3})|\[?issue (\d{3})\]?(?:\([^)]*\))?\s+tracks\b") INSIGHT = re.compile(r"\*\*Progressive insights?[ \t]*[—–-][ \t]*(\d{4}-\d{2}-\d{2})") def paragraphs(path): - """A record's body as (first line number, text) paragraphs: frontmatter and fenced code left out, - a block quote's markers stripped, so a quoted insight reads as the paragraph it is.""" + """A record's body as (first line number, section heading, 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.""" 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 = [], first + out, line, section = [], 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: - out.append((line, stripped)) + heading = re.match(r"^#{1,6}\s+(.*)", stripped) + if heading: + section = heading.group(1).strip() + # 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)) + at += item.count("\n") + 1 line += chunk.count("\n") + 2 return out +QUOTE = re.compile(r"\"([^\"]{12,})\"|\u201c([^\u201d]{12,})\u201d") + + +def flat(text): + """Text compared for a quotation: markup and case dropped, whitespace collapsed.""" + return re.sub(r"\s+", " ", re.sub(r"[*`_]", "", text)).strip().lower() + + +def covers(insight_section, insight_text, section, text): + """An insight speaks about a deferral paragraph when it stands in the same section, or quotes it.""" + if insight_section == section: + return True + body = flat(text) + return any(flat(a or b) in body for a, b in QUOTE.findall(insight_text)) + + def issue_refs(text, issues): return {n for a, b in ISSUE_REF.findall(text) for n in (a, b) if n and n in issues} @@ -269,8 +300,8 @@ def main(): "design doc's decisions:, a research became:, an issue, or 00-META") # ---- deferrals ------------------------------------------------------------------- - # A paragraph that puts work off names the issue that tracks it, or the record carries a - # dated progressive insight that names the issue and says what it tracks. An issue that has + # A paragraph or list item that puts work off names the issue that tracks it, or a dated + # progressive insight that speaks about it (its section, or a quotation of it) does. An issue that has # since resolved still counts: the deferred work landed, which is what the pointer was for. for name, (path, status) in records.items(): front = frontmatter(path) or {} @@ -278,17 +309,21 @@ def main(): if status != "accepted": continue paras = paragraphs(path) - insights = [(d, text) for line, text in paras for d in INSIGHT.findall(text)] - held = decided >= DEFERRALS_FROM or any(d >= DEFERRALS_FROM for d, _ in insights) + 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 - covering = set() - for d, text in insights: - if DEFERRAL.search(text): - covering |= issue_refs(text, seen) - for line, text in paras: + # 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: m = DEFERRAL.search(text) - if not m or issue_refs(text, seen) or covering: + # 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): + continue + if not m or issue_refs(text, seen): + continue + if any(covers(isec, itext, section, text) for isec, itext in tracking): continue bad(path, "line %d defers work (%r) and names no issue that tracks it -- a deferral nothing " "tracks reads as done (issue 354). Name the issue beside it, with its owner, or in a " diff --git a/00-META/process/02-graduation.md b/00-META/process/02-graduation.md index 145ac980..05db4aec 100644 --- a/00-META/process/02-graduation.md +++ b/00-META/process/02-graduation.md @@ -14,7 +14,8 @@ number. Format and rules are in [`02-DECISIONS/README.md`](../../02-DECISIONS/README.md). State evidence, not assertion, and record the options that were rejected — that is the half worth keeping. **A record that puts work off** — a rename, a check, a case it does not handle — names the issue that - tracks it and the owner who will do it, in the sentence that defers it, or the deferral is not accepted: + tracks it and the owner who will do it, in the sentence that defers it ("tracked by issue N"), or the + deferral is not accepted: a deferral nothing tracks reads as done ([issue 354](../../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md), ADR 0244's code renames). [`cycle.py`](../checks/cycle.py) holds every accepted record dated from diff --git a/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md b/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md index 2207c96a..8ec4d1f8 100644 --- a/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md +++ b/02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md @@ -182,9 +182,11 @@ work: descriptions call a build request an *ask*; - the nftables module's tool `firewall_rules`, which serves the packet filter. -> **Progressive insight — 2026-10-09.** This section deferred the code renames and named no issue, owner or -> date for them; two days later the operator believed them done, and nothing in this record said otherwise. -> They are tracked by +> **Progressive insight — 2026-10-09.** This record deferred the code renames in four places and named no +> issue, owner or date for them: this section, §2 ("listed in §6 for a later rename and is not renamed here"), +> §3 ("Code that still carries an old name is named on an *Identifier until renamed* line") and §4's table +> ("`mesh-host` an identifier until the code rename"). Two days later the operator believed them done, and +> nothing in this record said otherwise. They are tracked by > [issue 354](../04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md), > which holds the inventory of what carries the old names, the risk of renaming the paths of a self-updating > daemon on live nodes, and the staged plan; each stage is owned by the repository it names, and the diff --git a/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md b/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md index 0b7cab05..3e3a810a 100644 --- a/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md +++ b/02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md @@ -377,6 +377,12 @@ Recording a proposal needs no yes: it writes the register and nothing a session for an agent the mesh runs rather than the operator's; and a channel of the operator's own that can tell the operator from an agent (issue 340's "once we build a UI"), which would become a third route. +> **Progressive insight — 2026-10-09.** The four things this section leaves undecided — clearing old copies +> of hand versions, a per-project lesson, lessons for an agent the mesh runs, and a channel of the operator's +> own — were left with no issue, owner or date, which is how ADR 0244's deferred renames went unnoticed +> (issue 354). They are tracked by +> [issue 357](../04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md). The decision stands. + ## How it is checked | Rule | Checked by | diff --git a/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md index ff9b25ef..295218b2 100644 --- a/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md +++ b/04-ISSUES/354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md @@ -44,10 +44,11 @@ On 2026-10-09 the operator took the rename as long implemented and live. It is n from one whose work landed. - **It is not one record's slip.** The check this issue adds, run over every accepted record from the date ADR 0244 was accepted, found deferrals without an issue in ADR 0241 ("a later change requires them - once both holders serve them"), ADR 0270 ("left for a later change") and ADR 0244 itself. ADR 0270 is - dated 2026-10-09, is held to the rule, and has + once both holders serve them"), ADR 0270 ("left for a later change"), ADR 0273 ("not decided here", four + items) and ADR 0244 itself. ADR 0270 and 0273 are dated 2026-10-09, are held to the rule, and have [issue 355](../355-a-held-delivery-of-a-module-that-runs-on-one-machine-only-is-never-covered/00-report.md) - now; ADR 0241 is dated before the rule's start and stands as found. + and [issue 357](../357-what-adr-0273-left-undecided-has-no-owner/00-report.md) now; ADR 0241 is dated + before the rule's start and stands as found. - **Impact.** The operator plans against a mesh that does not exist: in conversation the mesh says node-engine, while every file, unit, package and condition on a node says `mesh-host`, so a person at a machine (`systemctl status nox-mesh-host`, `ls /var/lib/mesh-host`) and a person reading this repository @@ -64,9 +65,10 @@ On 2026-10-09 the operator took the rename as long implemented and live. It is n owner, or the deferral is not accepted. Playbooks [02](../../00-META/process/02-graduation.md) (graduation) and [04](../../00-META/process/04-build-handoff.md) (handoff) say it; [`cycle.py`](../../00-META/checks/cycle.py) checks it: an accepted record dated from 2026-10-09, or an - older one corrected under the rule, whose text puts work off ("until renamed", "left for later", - "deferred", "a later change") names an issue in that paragraph or in a dated progressive insight that - says what it tracks. ADR 0244 carries the insight that points here. + older one corrected under the rule, whose text puts work off ("until renamed", "left for later", "not + decided here", "postponed", "future work", "deferred", "a later change") names the issue as tracking it + ("tracked by issue N"), in that paragraph or list item, or in a dated progressive insight in the same + section or quoting it. ADR 0244 carries the insight that points here. This resolves when the acceptance sentence holds on every node. It is a core issue ([ADR 0237](../../02-DECISIONS/0237-a-change-is-judged-against-the-mesh-that-runs-before-it-merges-on-the-build-seat.md)): diff --git a/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md b/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md new file mode 100644 index 00000000..6edec2c9 --- /dev/null +++ b/04-ISSUES/357-what-adr-0273-left-undecided-has-no-owner/00-report.md @@ -0,0 +1,41 @@ +--- +status: open +opened: 2026-10-09 +located-in: [] +fixed-by: +amended-design: +--- + +# 357. What ADR 0273 left undecided has no owner + +## Symptom + +[ADR 0273](../../02-DECISIONS/0273-the-agents-home-is-the-modules-and-the-operators-personal-rules-and-lessons-are-registered-through-it.md) +(2026-10-09: the agent's home is the module's, and the operator's rules and lessons are registered through it) +says in its consequences that four things are not decided there, and names no issue, owner or date for any of +them. The check added with +[issue 354](../354-a-decided-rename-was-never-scheduled-and-the-operator-believed-it-live/00-report.md) found +them on the day the record was accepted. This issue is the pointer the record lacked; the record carries a +dated insight naming it. + +1. **Clearing old copies of hand versions.** The agent module keeps the hand version of every file it undoes, + in its state, and "clearing old copies is not decided here, as ADR 0243 left it". Without it the module's + state only grows. +2. **A per-project lesson** — one bound to a repository rather than the operator. +3. **Lessons for an agent the mesh runs**, rather than the operator's own agent. +4. **A channel of the operator's own that can tell the operator from an agent** — what issue 340 calls + "once we build a UI" — which would become a third route for registering into the operator's home. + +## Why it is an issue + +A deferral nothing tracks reads as done (issue 354). Each of the four is a gap a reader of ADR 0273 can only +find by reading its consequences to the end; the first is also a store that grows without a bound. + +## Open + +- Whether each item needs a decision record of its own, or whether item 1 is a setting of the agent module + that its owner (the `claude-code` module in the catalogue) can decide in a change. +- Item 4 waits on the operator's channels (ADR 0259) and the controller's terminal; its owner is not known + until the channel is designed. +- `located-in:` is filled when each item's owner is confirmed. The item resolved first is struck through here + with its pull request, and the issue resolves when all four are decided or declined.