Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<a id="v1.0.1"></a>
## 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.

<a id="v1.0.0"></a>
## 1.0.0: first public release (template schema 2)

Expand Down
15 changes: 11 additions & 4 deletions skill/tools/ipq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -451,16 +451,23 @@ 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:
continue
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))
Expand Down
23 changes: 23 additions & 0 deletions tests/test_ipq.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<!-- ipq:record id="D-'))
self.adapt("delta", open_line, open_line + "\n"
'<!-- ipq:record id="D-\\d{3,}" in="Closed" fields="Owner*; Found*; Closed*; Canonical source" -->')
self.config(history={"delta": "archive"})
with open(self.docs / "delta.md", "a") as f:
f.write("\n## Closed\n\n- <a id=\"d-009\"></a>**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"
'<a id="d-009"></a>\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<!-- ipq:optional -->\n\n## Blocked\n")
self.adapt("backlog", "Blocked by; Done when*", "Blocked by; Track; Done when*")
Expand Down
Loading