From 95edc658c860cf762516ed0e62b94e8a1ca2c3ee Mon Sep 17 00:00:00 2001 From: baw <244261081+baw-dev@users.noreply.github.com> Date: Wed, 30 Sep 2026 15:13:07 -0400 Subject: [PATCH] ipq 1.0.1: an archive checks a record against its section's spec Inside a role's archive.md, spec_for now prefers the matching spec whose in= names the record's section, so a closed entry archived in full can be checked against a Closed spec instead of the Open one. Elsewhere it takes the first matching spec, as before. --- CHANGES.md | 16 ++++++++++++++++ skill/tools/ipq.py | 15 +++++++++++---- tests/test_ipq.py | 23 +++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 281ad59..444a948 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,22 @@ Each entry has a permanent anchor, `#vMAJOR.MINOR.PATCH`, and a matching git tag, so a project can link to the exact release it was checked with. Headings may be reworded; anchors never change. + +## 1.0.1: an archive checks a record against its section's spec (template schema 2) + +**For every project:** +- **Closed entries can be archived in full.** Inside a role's `archive.md`, + a record is now checked against the spec whose `in` names the section it + sits under, when a template declares one; otherwise against the first + matching spec, as before. So a project can add, to an adapted delta + template, a second `D-` record spec with `in="Closed"`, and archive closed + entries with the fields a closed entry has. Before, every archived `D-` + record was checked against the Open spec and needed `Waits on`. Outside an + archive nothing changes, so a full record cannot sit under Closed in + `delta.md` and be counted as open. + +**Updating documents:** nothing to do. A pin of `"1.0"` stays valid. + ## 1.0.0: first public release (template schema 2) diff --git a/skill/tools/ipq.py b/skill/tools/ipq.py index 85b7dbb..9241193 100644 --- a/skill/tools/ipq.py +++ b/skill/tools/ipq.py @@ -50,7 +50,7 @@ sys.modules.setdefault("ipq", sys.modules[__name__]) # so ipq_visuals shares this module when run as a script import ipq_visuals as visuals # noqa: E402 -__version__ = "1.0.0" +__version__ = "1.0.1" SKILL = Path(__file__).resolve().parents[1] TEMPLATES = SKILL / "templates" @@ -451,8 +451,15 @@ def rel(self, path): def parse_records(doc, specs): """Find heading records, condensed records, and table records in `doc`.""" - def spec_for(id, kind): - return next((s for s in specs if s.kind == kind and s.matches(id)), None) + def spec_for(id, kind, section=None): + found = [s for s in specs if s.kind == kind and s.matches(id)] + if section is not None and is_archive(doc): + # An archive holds finished records in full, under the section that names their + # finished state, so a spec declared for that section wins over the working one. + placed = [s for s in found if section in s.sections] + if placed: + return placed[0] + return found[0] if found else None for h in doc.headings: if h.level < 3: @@ -460,7 +467,7 @@ def spec_for(id, kind): m = RECORD_HEADING.match(plain(h.text).strip()) if not m or not re.search(r"[\d-]", m.group(1)): continue - spec = spec_for(m.group(1), "record") + spec = spec_for(m.group(1), "record", doc.section_of(h.line)) if not spec: continue r = Record(m.group(1), "heading", spec, doc.section_of(h.line), h.line, m.group(2)) diff --git a/tests/test_ipq.py b/tests/test_ipq.py index eb8d0e2..f27f14f 100644 --- a/tests/test_ipq.py +++ b/tests/test_ipq.py @@ -378,6 +378,29 @@ def adapt(self, role, old, new): self.assertIn(old, text) p.write_text(text.replace(old, new, 1)) + def test_an_archive_uses_the_spec_for_its_section(self): + """A closed entry archived in full answers to a Closed record spec, not the Open one. + The preference holds only inside an archive; delta.md keeps the first matching spec.""" + upstream = (REPO / "skill" / "templates" / "delta.md").read_text() + open_line = next(l for l in upstream.splitlines() if l.startswith('') + self.config(history={"delta": "archive"}) + with open(self.docs / "delta.md", "a") as f: + f.write("\n## Closed\n\n- **D-009** Retention was undecided — settled in the prd" + " · found 2026-09-01 · closed 2026-09-10\n") + (self.docs / "delta").mkdir() + (self.docs / "delta" / "archive.md").write_text( + "# Acme — Delta archive\n\n**Answers:** the full text of closed entries.\n\n" + "## Summary\n\nClosed entries.\n\n## Contents\n\n## Closed\n\n" + '\n### D-009 — Retention was undecided\n\n' + "- **Owner:** product\n- **Found:** 2026-09-01\n- **Closed:** 2026-09-10\n" + "- **Canonical source:** [R-CORE-01](../prd.md#r-core-01)\n") + self.fix() + archived = [f for f in self.findings() if f[1].name == "archive.md" and f[0] == "error"] + self.assertEqual(archived, [], archived) + self.assertNotIn(("error", "fields"), self.rules()) + def test_adapted_template_adds_a_section_and_a_field(self): self.adapt("backlog", "## Blocked\n", "## Readiness track\n\n\n## Blocked\n") self.adapt("backlog", "Blocked by; Done when*", "Blocked by; Track; Done when*")