Skip to content

Commit aa0a57b

Browse files
authored
Merge pull request #71 from eduralph/feat/67-plan-pointer
feat(plan): Plan-as-pointer brief variant for host planning artifacts
2 parents 02a4756 + 2499577 commit aa0a57b

5 files changed

Lines changed: 98 additions & 4 deletions

File tree

docs/03-plan.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ INTEGRATION §2", "addon convention per INTEGRATION §3". That's the payoff of
100100
("which branch? where does the test go?") against your repo's concretizations
101101
instead of guessing.
102102

103+
### The Plan can be a pointer
104+
105+
If your project already plans through its own artifacts — an ADR, an enhancement
106+
proposal, a normative spec under change control — you don't restate that here. Use
107+
`templates/plan-pointer.md.tpl`: a thin brief that **references** the host document
108+
(`- **Planning artifact:** docs/adr/0042-thing.md`) and carries only the few fields the
109+
driver parses (slug, success criterion, branch target, test file). Do reads the
110+
referenced artifact as the authoritative plan and cites it; the rest of the cycle is
111+
unchanged. This lets PDCA wrap a host's existing planning process instead of imposing
112+
its own document shape.
113+
103114
## STOP discipline
104115

105116
Every brief carries the rule that the cycle never ships before sign-off:

template/src/pdca_harness/brief.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ def disposition_hint(brief_path: Path) -> str:
4949
return field(brief_path, "disposition hint", "disposition")
5050

5151

52+
def planning_artifact(brief_path: Path) -> str:
53+
"""The host planning artifact this brief points at, or "" if it's a self-contained brief.
54+
55+
The optional ``- **Planning artifact:** <path|url>`` field (issue #67, ``plan-pointer``
56+
template): a reference to the host's OWN plan (an ADR / proposal / spec) that Do treats
57+
as authoritative. Absent ⇒ an ordinary brief that carries its own spec.
58+
"""
59+
return field(brief_path, "planning artifact", "plan artifact", "plan source")
60+
61+
5262
def test_files(brief_path: Path) -> list[Path]:
5363
"""Paths named by the brief's test-requirement field, relative to the bundle.
5464

template/src/pdca_harness/leaves.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def do_plan(d: Path, cfg: Config, csv: str | None = None) -> None:
146146
def _plan_prompt(cfg: Config, csv: str | None, d: Path) -> str:
147147
fix_tpl = cfg.templates_dir / "brief.md.tpl"
148148
geps_tpl = cfg.templates_dir / "design-proposal.md.tpl"
149+
pointer_tpl = cfg.templates_dir / "plan-pointer.md.tpl"
149150
issue_id = d.name.removeprefix("issue_")
150151
tracker_csv = csv or cfg.tracker_export_csv
151152
notes = d / "notes.json"
@@ -178,9 +179,11 @@ def _plan_prompt(cfg: Config, csv: str | None, d: Path) -> str:
178179
f"to {fix_tpl} — it fits bug fixes AND ordinary new functionality. Use {geps_tpl} "
179180
"(a design proposal) ONLY for the exception: a change significant enough to "
180181
"warrant a proposal (major architecture / API / UX). Not every feature is a "
181-
"design proposal — when in doubt use the normal brief. Keep the parsed "
182-
"`- **Label:** value` field shape; resolve the repo + branch target per "
183-
"INTEGRATION §2. One bundle = one brief.md. Plan only."
182+
f"design proposal — when in doubt use the normal brief. Use {pointer_tpl} when the "
183+
"plan ALREADY lives in a host artifact (an ADR / proposal / normative spec): the "
184+
"brief then POINTS at that document (a `Planning artifact:` reference) instead of "
185+
"restating it. Keep the parsed `- **Label:** value` field shape; resolve the repo + "
186+
"branch target per INTEGRATION §2. One bundle = one brief.md. Plan only."
184187
)
185188

186189

@@ -286,7 +289,9 @@ def _build_prompt(d: Path) -> str:
286289
f"You are the Do builder. Read {d}/brief.md. Build to satisfy its **Success "
287290
"criterion** (the real end result), not a narrower proxy — an item is done only "
288291
"when that end result holds, proven red→green; a green mechanical check on "
289-
"something adjacent is not done. If brief.md carries an '## Iteration N — "
292+
"something adjacent is not done. If brief.md names a **Planning artifact** (an "
293+
"ADR / proposal / spec), READ that document — it is the authoritative plan and the "
294+
"brief only points at it; build to it and cite it. If brief.md carries an '## Iteration N — "
290295
"carry-forward' block, address it (the previous attempt's rationale + failing "
291296
"gate) and do NOT repeat the rejected approach. Produce, in the bundle directory "
292297
f"{d}: (1) patch.diff — a unified diff against the brief's target branch; "
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Brief (pointer) — issue <id> / <slug>
2+
3+
> A Plan artifact that is a **pointer**, for hosts that plan through their OWN
4+
> artifacts (an ADR, an enhancement proposal, a normative spec) rather than a brief
5+
> authored here (docs 03). The host's document IS the plan; this file just references
6+
> it and carries the few fields the driver parses. Use this instead of `brief.md.tpl`
7+
> when the planning decision already lives — and is reviewed/governed — elsewhere.
8+
>
9+
> Keep the `- **Label:** value` lines: Do reads the spec fields from them, and the
10+
> driver/SUMMARY read slug / success criterion / branch. Do reads the **Planning
11+
> artifact** as the authoritative plan; this brief does not restate it.
12+
13+
- **Slug:** <short-kebab-slug>
14+
- **Planning artifact:** <path or URL to the host's ADR / proposal / spec that IS the
15+
plan — e.g. `docs/adr/0042-thing.md`, or a permalinked spec section. Do treats this
16+
as authoritative; cite it.>
17+
- **Defect / goal:** <one line: what this realizes — the observable problem or capability>
18+
- **Success criterion:** <the observable condition that means it works — what the shipped test asserts>
19+
- **Repo + branch target:** <owner/repo> @ <branch> (resolve here at Plan — do not leave to Do)
20+
- **Onto branch:** <remote>/<existing-pr-branch> (optional — stack onto an open PR's branch; docs 03)
21+
- **Depends on:** <id>[, <id>…] (optional — scheduling waits until these are COMPLETE; docs 09)
22+
- **Conflicts with:** <id>[, <id>…] (optional — never co-schedule in one wave; docs 09)
23+
- **Scope:** <the one logical change this realizes> / out of scope: <what is excluded>
24+
- **Test file:** <path where the regression test ships must fail pre-change, pass post-change>
25+
- **Citations expected:** Do must cite path:line on the target branch AND the Planning artifact for every change.
26+
- **Disposition hint:** <likely-fix | likely-close | see brief.md.tpl for the full set>
27+
28+
## STOP discipline
29+
30+
Draft only until Check sign-off. A draft PR MAY be opened for CI; the PR MUST NOT be
31+
marked ready before sign-off accepts.

template/tests/test_flow_slice.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from pdca_harness.config import Config, LeafConfig
2020

2121
DESIGN_TPL = Path(__file__).resolve().parents[1] / "templates" / "design-proposal.md.tpl"
22+
POINTER_TPL = Path(__file__).resolve().parents[1] / "templates" / "plan-pointer.md.tpl"
2223

2324

2425
def _stub_config(root: Path) -> Config:
@@ -528,6 +529,42 @@ def test_feature_brief_flows_and_renders_goal(self) -> None:
528529
self.assertIn("the capability this adds", summary) # the Goal value, not blank
529530

530531

532+
class PlanPointerBrief(unittest.TestCase):
533+
"""A pointer-brief (issue #67): the Plan is a reference to the host's own planning
534+
artifact (ADR / proposal / spec), not a brief authored here. It carries the same
535+
parsed-field contract, so the driver treats it as a normal PLANNED brief."""
536+
537+
def setUp(self) -> None:
538+
self.tmp = Path(tempfile.mkdtemp())
539+
self.cfg = _stub_config(self.tmp)
540+
self.d = self.cfg.bundle("ADR")
541+
self.d.mkdir(parents=True)
542+
shutil.copyfile(POINTER_TPL, self.d / "brief.md") # the plan-pointer template
543+
544+
def tearDown(self) -> None:
545+
shutil.rmtree(self.tmp, ignore_errors=True)
546+
547+
def test_template_keeps_driver_parsed_fields(self) -> None:
548+
fields = brief.parse_fields(self.d / "brief.md")
549+
for label in ("slug", "success criterion", "repo + branch target", "test file",
550+
"planning artifact"):
551+
self.assertIn(label, fields, f"plan-pointer template lost parsed field: {label}")
552+
553+
def test_planning_artifact_reader(self) -> None:
554+
# brief.planning_artifact reads the pointer; a self-contained brief returns "".
555+
self.assertTrue(brief.planning_artifact(self.d / "brief.md"))
556+
plain = self.cfg.bundle("PLAIN")
557+
plain.mkdir(parents=True)
558+
(plain / "brief.md").write_text("- **Slug:** x\n", encoding="utf-8")
559+
self.assertEqual(brief.planning_artifact(plain / "brief.md"), "")
560+
561+
def test_pointer_brief_flows_to_signoff(self) -> None:
562+
# A pointer-brief is PLANNED and drives Do→Check→sign-off offline like any brief.
563+
self.assertEqual(state.state(self.d), state.PLANNED)
564+
self.assertEqual(driver.run_issue(self.d, self.cfg), state.AWAITING_SIGNOFF)
565+
self.assertTrue((self.d / "SUMMARY.md").exists())
566+
567+
531568
_TOY_BRIEF = (
532569
"- **Slug:** {slug}\n"
533570
"- **Defect:** the count is off by one.\n"

0 commit comments

Comments
 (0)