Skip to content

Commit 643f6d8

Browse files
eduralphclaude
andcommitted
feat(brief): parse optional Depends on / Conflicts with fields (#36)
Add depends_on() / conflicts_with() accessors over the existing lenient field parser, plus the two optional brief-template fields. Parser-first half of #36: safe on its own because unknown bullet labels are already ignored, so existing briefs and the scheduler are unaffected until the DAG-gated dispatch change lands. Ids are normalised to bare form (strip leading '#' and the issue_ prefix) to match how cfg.bundle(id) keys bundles, so a brief may write #36 / 36 / issue_36 interchangeably. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7d8bc48 commit 643f6d8

3 files changed

Lines changed: 60 additions & 0 deletions

File tree

template/src/pdca_harness/brief.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,33 @@ def test_files(brief_path: Path) -> list[Path]:
5353
# Pull anything that looks like a path token out of the field value.
5454
tokens = re.findall(r"[\w./-]+\.\w+", raw)
5555
return [Path(t) for t in tokens]
56+
57+
58+
def depends_on(brief_path: Path) -> list[str]:
59+
"""Issue ids this bundle must wait for — each must be COMPLETE before it runs.
60+
61+
The optional ``- **Depends on:** <id>[, <id>…]`` field (docs 09). Absent ⇒
62+
``[]`` ⇒ today's sort-by-name scheduling, unaffected.
63+
"""
64+
return _id_list(field(brief_path, "depends on", "depends_on"))
65+
66+
67+
def conflicts_with(brief_path: Path) -> list[str]:
68+
"""Issue ids that must never run in the same concurrent wave as this bundle.
69+
70+
The optional ``- **Conflicts with:** <id>[, <id>…]`` field (docs 09): a pair
71+
that edits a shared resource and so cannot be co-scheduled across lanes.
72+
"""
73+
return _id_list(field(brief_path, "conflicts with", "conflicts_with"))
74+
75+
76+
def _id_list(raw: str) -> list[str]:
77+
"""Issue ids out of a comma/space-separated field value, normalised to bare ids.
78+
79+
Tolerates a leading ``#`` and the ``issue_`` bundle prefix so a brief may write
80+
``#36`` / ``36`` / ``issue_36`` interchangeably; matches how ``cfg.bundle(id)``
81+
keys bundles. Mirrors :func:`test_files`' tokenise-the-value approach.
82+
"""
83+
if not raw:
84+
return []
85+
return [t.lstrip("#").removeprefix("issue_") for t in re.findall(r"#?[\w./-]+", raw)]

template/templates/brief.md.tpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
satisfy this by guarding a single module? If yes, it's the narrow symptom-sentence —
1515
widen it. Omit only for non-structural behavioural bug fixes (principles.md §1.1).>
1616
- **Repo + branch target:** <owner/repo> @ <branch> (resolve here at Plan — do not leave to Do)
17+
- **Depends on:** <id>[, <id>…] (optional — batch/lane scheduling waits until these bundles are COMPLETE before this one runs; docs 09)
18+
- **Conflicts with:** <id>[, <id>…] (optional — never co-schedule these in the same concurrent wave, e.g. they edit a shared file; docs 09)
1719
- **Surfaces:** <where the change is observable — `gui` (touches the frontend / an E2E
1820
through the app is needed), `data` (backend/logic only), or `both`. Drives which
1921
runtime gates apply (e.g. an E2E gate runs only when this is `gui`). Optional.>

template/tests/test_brief.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,33 @@ def test_colon_inside_bold_does_not_leak(self) -> None:
3737
self.assertFalse(val.startswith("*"), val)
3838

3939

40+
class OrderingFields(unittest.TestCase):
41+
"""The optional Depends on / Conflicts with fields (docs 09, issue #36)."""
42+
43+
def _brief(self, body: str) -> Path:
44+
f = Path(tempfile.mkdtemp()) / "brief.md"
45+
f.write_text(body, encoding="utf-8")
46+
return f
47+
48+
def test_depends_on_parses_comma_and_space_separated_ids(self) -> None:
49+
f = self._brief("- **Depends on:** #36, 11 issue_42\n")
50+
# leading '#' and the issue_ prefix are both normalised to bare ids
51+
self.assertEqual(brief.depends_on(f), ["36", "11", "42"])
52+
53+
def test_conflicts_with_parses_list(self) -> None:
54+
f = self._brief("- **Conflicts with:** C1, T1\n")
55+
self.assertEqual(brief.conflicts_with(f), ["C1", "T1"])
56+
57+
def test_absent_field_is_empty_list(self) -> None:
58+
f = self._brief("- **Slug:** no-ordering\n")
59+
self.assertEqual(brief.depends_on(f), [])
60+
self.assertEqual(brief.conflicts_with(f), [])
61+
62+
def test_empty_value_is_empty_list(self) -> None:
63+
f = self._brief("- **Depends on:**\n- **Conflicts with:** \n")
64+
self.assertEqual(brief.depends_on(f), [])
65+
self.assertEqual(brief.conflicts_with(f), [])
66+
67+
4068
if __name__ == "__main__":
4169
unittest.main()

0 commit comments

Comments
 (0)