anthropic critique of skill adjustments

This commit is contained in:
pbean 2026-04-30 16:02:43 -07:00
parent dd1efb3c61
commit 1136a59e0c
17 changed files with 196 additions and 39 deletions

View File

@ -11,16 +11,14 @@ This skill produces and maintains the **v7 epic-first folder tree** for an initi
**Acts as:** a product strategist and technical specifications writer collaborating with the user as a peer. The user owns product vision and priorities; this skill brings requirements decomposition, sizing judgment, and the v7 schema. Conversational throughout — soft gates ("ready to move on?") rather than rigid menus.
**One skill, three modes × three interaction styles:**
**One skill, three modes:**
- **Create** — no `epics/` tree yet. Walks intent → discovery → epic design → per-epic authoring → validate → finalize.
- **Edit** — the tree exists. Routes by user phrasing or flag to add-epic, split-epic, merge-epics, rename-epic, refine-story, re-derive-deps, or re-validate. Never re-walks intent or discovery.
- **Migrate** — a v6 monolithic `epics.md` (or sharded directory) exists but no v7 tree. Offers leave-alone, run-canonical-helper, or walk-through-manually.
Interaction style is orthogonal:
Plus a **deterministic surface** for pipelines:
- **Guided** (default) — conversational dialog with soft gates and per-epic checkpoints. Right for first-timers and complex initiatives.
- **YOLO** (`--yolo`) — same flow, but discovery summary, the soft gate dialog at each stage, and the per-epic checkpoint are skipped. The skill proposes the full epic list in one shot, authors every epic end-to-end, then surfaces a single batched recap before validation. Right for experts on their third initiative this quarter.
- **From-spec** (`--from-spec <path>`) — Stages 13 skipped entirely. A structured spec drives Stages 4 and 5 deterministically. Right for pipelines and pre-drafted plans.
**Headless surfaces:**
@ -28,7 +26,7 @@ Interaction style is orthogonal:
- `--re-validate` (alias `--headless` / `-H`) runs strict validation only and emits JSON. Pair with `--coverage-strict` to fail CI on uncovered requirements.
- `--from-spec <path>` runs end-to-end authoring + validation deterministically and emits JSON. Implicitly headless; pass `--coverage-strict` to fail on uncovered requirements.
All other modes are interactive (Guided and YOLO).
Create, edit, and migrate are interactive.
**Owns:** front-matter schemas (`resources/`), bootstrap and validation scripts (`scripts/`), the inventory cache at `{initiative_store}/.bmad-cache/inventory.json`, and the only writers of the epic tree. **Does not own:** `governance.md` or `initiative-context.md` authoring, `initiative_store` config plumbing, downstream status transitions beyond `draft`.
@ -90,11 +88,7 @@ If the user passed `--re-validate`, `--headless`, or `-H` (or said "re-validate"
If the user passed `--from-spec <path>`, set `{mode}=from-spec` and `{spec_path}=<path>`. Set `{headless_mode}=true` by default (override only if the user is interactive). Skip Stages 13, route directly to `prompts/from-spec.md`.
### 3. YOLO interaction style
If the user passed `--yolo` (or said "yolo this", "go fast", "don't ask me", or similar in their opening message), set `{yolo}=true`. Otherwise `{yolo}=false`. YOLO is orthogonal to the mode — both create and migrate flows respect it. Edit-mode sub-flows ignore `{yolo}` because they are inherently interactive graph reasoning.
### 4. Mode by filesystem state
### 3. Mode by filesystem state
- If `{initiative_store}/epics/` does not exist OR exists but contains no epic folders → `{mode}=create`.
- If `{initiative_store}/epics/` contains v7 epic folders (any folder matching `NN-*` with an `epic.md` inside) → `{mode}=edit`.
@ -102,7 +96,7 @@ If the user passed `--yolo` (or said "yolo this", "go fast", "don't ask me", or
If both v7 folders and a v6 file exist, prefer `edit` and surface the v6 file in Stage 1 as a one-line note.
### 5. Edit sub-mode dispatch (only when `{mode}=edit`)
### 4. Edit sub-mode dispatch (only when `{mode}=edit`)
Detect from the user's opening message:
@ -120,7 +114,7 @@ Detect from the user's opening message:
Set `{edit_submode}` to the matched value before routing.
### 6. Route
### 5. Route
- `create``prompts/intent.md`
- `migrate``prompts/intent.md` (it offers the migrate three-options branch when `{mode}=migrate`)
@ -128,7 +122,7 @@ Set `{edit_submode}` to the matched value before routing.
- `headless``prompts/validate.md`
- `from-spec``prompts/from-spec.md`
Carry `{mode}`, `{yolo}`, `{spec_path}` (when set), and `{edit_submode}` (when set) into the routed prompt.
Carry `{mode}`, `{spec_path}` (when set), and `{edit_submode}` (when set) into the routed prompt.
## Stages

View File

@ -8,7 +8,7 @@
## Pre-flight
Before launching the artifact-analyzer, tell the user (in 35 lines) what you're about to scan: the resolved `{planning_artifacts}` path, the resolved `{project_knowledge}` path, and any user-pointed paths from Stage 1. This lets a misconfigured path surface immediately rather than as an empty result. Skip the pre-flight in `{yolo}=true` and `{mode}=headless`.
Before launching the artifact-analyzer, tell the user (in 35 lines) what you're about to scan: the resolved `{planning_artifacts}` path, the resolved `{project_knowledge}` path, and any user-pointed paths from Stage 1. This lets a misconfigured path surface immediately rather than as an empty result. Skip the pre-flight in `{mode}=headless`.
## Subagent fan-out
@ -59,12 +59,10 @@ Lists may be empty. Each requirement entry must have a unique `code`.
Tell the user in 48 lines: counts (FRs, NFRs, UX-DRs, debt items), the starter-template note if any, governance constraints if any, and any gaps. Do not dump the full inventory — they have the source documents. Mention the cache path so they know where the inventory lives.
In `{yolo}=true` collapse to a single line: "Inventory: N FRs, M NFRs, K UX-DRs (cached at `.bmad-cache/inventory.json`)."
In `{mode}=headless` skip the summary entirely.
Soft gate (interactive only): "Anything missing or wrong here, or shall we move on to designing the epic list?"
## Stage Complete
When the user confirms (or `{yolo}=true` auto-confirms), route to `prompts/epic-design.md`. The inventory remains on disk; later stages re-read it rather than relying on working memory.
When the user confirms, route to `prompts/epic-design.md`. The inventory remains on disk; later stages re-read it rather than relying on working memory.

View File

@ -85,8 +85,6 @@ python3 scripts/validate_initiative.py --initiative-store {initiative_store} --l
Before starting the next epic, confirm with the user that this epic is complete. The next epic does not begin until the current is approved.
In `{yolo}=true`, **skip the per-epic checkpoint** entirely — author the full epic list end-to-end, then surface a single batched recap before routing to validation.
## After all epics are authored
Route to `prompts/validate.md` for full-tree strict validation.
@ -106,4 +104,4 @@ After any edit-mode flow finishes, route to `prompts/validate.md` strict.
## Stage Complete
Stage 4 ends when every approved epic has its `epic.md` and all its story files written, the per-epic non-strict validation passes for each, and the user has confirmed completion (or `{yolo}=true` auto-confirmed after the batch recap).
Stage 4 ends when every approved epic has its `epic.md` and all its story files written, the per-epic non-strict validation passes for each, and the user has confirmed completion.

View File

@ -35,13 +35,9 @@ Mentally compute the cross-epic dependency graph. If you find any cycle (Epic A
If the user wants to pressure-test the epic shape, they may invoke `bmad-advanced-elicitation` (deeper critique methods) or `bmad-party-mode` (multi-agent perspectives) explicitly. **Do not present these as a menu** — only invoke when the user asks.
## YOLO mode
When `{yolo}=true`, propose the entire epic list in one message — title, intent, `depends_on`, theme, and FR/UX-DR allocations for every epic — and ask the user once whether to lock it in or revise. Skip the per-step dialog; rely on the cycle check and Stage 5 to catch problems.
## Soft gate
"Does this epic list capture the initiative? Anything missing, anything overlapping that should be consolidated?" When the user is satisfied, the list is approved and Stage 3 is complete. Skip in `{yolo}=true` after the one-shot proposal is approved.
"Does this epic list capture the initiative? Anything missing, anything overlapping that should be consolidated?" When the user is satisfied, the list is approved and Stage 3 is complete.
## Edit-mode flows

View File

@ -52,8 +52,6 @@ After the user picks:
This stage is conversational. Confirm with a soft prompt rather than a menu — "Anything else to add about the initiative, or should we move on to scanning the project?" Users almost always remember one more thing when given a graceful exit ramp.
In `{yolo}=true` skip the soft gate entirely once the three items are settled.
## Stage Complete
Stage 1 ends when the chosen mode's exit conditions above are met. Carry the initiative title, primary intent, story-type mix, and any volunteered details into the next stage in working memory — none of this is written to disk yet (Stage 2 writes the inventory to `.bmad-cache/inventory.json`).

View File

@ -37,7 +37,7 @@ If `{mode}=headless`:
### 1. Surface failures conversationally
For each error in `findings`, explain it in one sentence and offer to fix. Group by file when several errors land on the same path. Common patterns and the right next step:
For each error in `findings`, explain it in one sentence and offer to fix. Group by file when several errors land on the same path. The full code → meaning → fix lookup lives at `resources/validation-error-codes.md` — load it when surfacing failures so the explanation and fix are accurate. Common shortcuts to the right next step:
- **Schema errors** (`*-extra-keys`, `*-missing-keys`, `*-bad-status`, `*-bad-type`) → loop back to `prompts/epic-authoring.md` for that one file, edit the front matter, re-validate.
- **`epic-nn-mismatch` / `story-epic-mismatch`** → likely a hand-edit of the front matter; the folder name is canonical, so update the front matter to match.
@ -45,8 +45,6 @@ For each error in `findings`, explain it in one sentence and offer to fix. Group
- **`epic-dep-cycle`** → cross-epic graph has a loop. Loop back to `prompts/epic-design.md` (re-derive-deps flow) to fix it.
- **`story-numbering-gaps`** → use `scripts/rename_story.py --to-nn` to fill the gap or renumber the survivors.
If the failure-pattern set ever grows past ~10 entries, extract this list to `resources/validation-error-codes.md` to keep this prompt tight.
### 2. Coverage check
When `--inventory` was passed, the validator already produced `coverage_missing` deterministically — surface those codes conversationally and route into the **coverage-fix** edit-mode entry point in `prompts/epic-authoring.md`.

View File

@ -21,7 +21,7 @@ metadata: # OPTIONAL — free-form table; BMad
- **title** — Always emitted with double quotes by `init_story.py`. Inner double quotes escaped with `\`.
- **type** — Drives body-skeleton generation: `task` omits the As-a/I-want/So-that stanza by default; `bug` and `spike` make it optional; `feature` requires it.
- **status**`init_story.py` always writes `draft`. Promotion to any other value is owned by downstream skills (`bmad-dev-story` etc.). This skill never auto-promotes.
- **epic** — The enclosing folder name (e.g. `01-billing-stripe`), not just the NN. The folder name wins on conflict; the validator flags drift.
- **epic** — The enclosing folder name (e.g. `01-billing-stripe`), not just the NN. Emitted unquoted (the dash makes it unambiguously a string in YAML). The folder name wins on conflict; the validator flags drift.
- **depends_on** — Inline YAML list. Two reference forms:
- **Within-epic:** the sibling story's basename without `.md` — e.g. `04-define-schema`.
- **Cross-epic:** `<epic-folder>/<story-basename>` — e.g. `02-auth-migration/04-session-management`.

View File

@ -0,0 +1,147 @@
# Validation Error Codes
The lookup table for every error and warning `scripts/validate_initiative.py` emits — what each code means, how it arises, and how to fix it. Loaded by `prompts/validate.md` when surfacing failures conversationally.
The output shape is always:
```json
{"level": "error|warning", "code": "<code>", "message": "<human text>", "path": "<absolute path>"}
```
`level` controls the exit code: any `error` causes the validator to return 1; warnings don't. `coverage-missing` is the one code whose level depends on a flag (`--coverage-strict` escalates it).
## Schema errors
These fire when a file's front matter doesn't match the locked schema in `resources/epic-frontmatter-schema.md` and `resources/story-frontmatter-schema.md`.
| Code | Meaning | Fix |
| --- | --- | --- |
| `epic-frontmatter-parse` | The validator's loose YAML parser couldn't read the front matter. Usually a missing `:`, an unindented continuation, or unclosed `---`. | Open the `epic.md` and check the front-matter block. The parser tolerates inline lists and inline strings; reject indented multi-line strings. |
| `epic-extra-keys` | The epic's front matter has a top-level key that isn't in `{title, epic, status, depends_on, metadata}`. | Move the key under `metadata:` (which is free-form), or remove it. |
| `epic-missing-keys` | A required top-level key is absent: `title`, `epic`, `status`, or `depends_on`. | Add it. `init_epic.py` always emits all four; this fires when a hand-edit dropped one. |
| `epic-bad-status` | `status` value isn't one of `draft / ready / in-progress / review / done / blocked`. | Set to a valid enum value. New epics should be `draft`. |
| `epic-nn-mismatch` | `epic:` field doesn't match the folder's NN prefix. The folder name is canonical. | Edit the front matter to match the folder. If the folder name itself is wrong, use `rename_epic.py --to-nn`. |
| `epic-deps-not-list` | `depends_on:` is not a YAML list. | Make it `[]` (empty) or `["01", "02"]`. Inline lists only. |
| `story-frontmatter-parse` | Same as above, for a story file. | Check the front-matter block. |
| `story-extra-keys` | Story has a top-level key not in `{title, type, status, epic, depends_on, metadata}`. | Move under `metadata:` or remove. |
| `story-missing-keys` | A required key is absent. | Add it. |
| `story-bad-type` | `type` isn't one of `feature / bug / task / spike`. | Pick one. Story type drives body skeleton choices (the user-story stanza is required for `feature`, optional for `bug`/`spike`, absent for `task`). |
| `story-bad-status` | Same as `epic-bad-status` for a story. | Set a valid status. |
| `story-bad-prefix` | Story filename doesn't start with `NN-`. | Rename to match the convention. The validator expects `^\d+-`. |
| `story-epic-mismatch` | Story's `epic:` field doesn't match its enclosing folder name. The folder name is canonical. | Edit the front matter to match the folder. Or move the file with `move_story.py`. |
| `story-deps-not-list` | `depends_on:` is not a YAML list. | Make it `[]` or an inline list. |
## Dependency errors
These fire when references don't resolve.
| Code | Meaning | Fix |
| --- | --- | --- |
| `epic-dep-unresolved` | An epic's `depends_on` references an NN that has no corresponding folder in the tree. | Either fix the typo, or remove the dep, or create the missing epic. |
| `epic-dep-cycle` | The cross-epic depends_on graph has a cycle (Epic A → B → A, directly or transitively). The message lists the cycle. | Break the cycle. If the cycle reflects a real bidirectional dependency, the epics should probably be merged or the seam reconsidered. |
| `story-dep-unresolved` | A story's `depends_on` entry doesn't resolve — either as a within-epic basename or as a cross-epic `<folder>/<basename>` ref. | Fix the typo, or use `move_story.py` / `rename_story.py` going forward (they update refs atomically). For after-the-fact fixes, edit `depends_on:` directly and re-validate. |
## Structural errors
| Code | Meaning | Fix |
| --- | --- | --- |
| `no-epics-dir` | `{initiative_store}/epics/` doesn't exist. | Either you have the wrong `--initiative-store`, or no tree has been generated yet. Run the skill in create mode. |
| `missing-epic-md` | A folder matches `NN-*` but has no `epic.md` inside. | Either the folder is bogus (delete it) or the `epic.md` was lost (regenerate with `init_epic.py` and re-fill). |
| `story-numbering-gaps` | Story NNs in an epic are not sequential `01..N`. The message lists what was found vs expected. | Use `rename_story.py --to-nn` to fill the gap or renumber the survivors. |
## Coverage findings
| Code | Meaning | Fix |
| --- | --- | --- |
| `coverage-missing` | An inventory code from `--inventory <file>` does not appear textually in any story body. **Warning** by default; **error** under `--coverage-strict`. | Use the `coverage-fix` edit-mode entry point in `prompts/edit-mode.md` — extend an existing story's `## Coverage` section, or add a new story for the missing code. |
## Sizing warnings
| Code | Meaning | Fix |
| --- | --- | --- |
| `story-oversized` (warning) | A story's body is more than 3× the epic mean (computed when the epic has ≥ 3 stories). | Often a real signal that the story should be split. Sometimes the threshold is too tight for legitimately-large foundational stories — judgment call. The warning never fails CI. Pass `--lax` to suppress mid-flow. |
## Validator runtime errors
These exit `2` and aren't structured findings.
| Symptom | Cause | Fix |
| --- | --- | --- |
| `template missing: ...` | `resources/epic-md-template.md` or `resources/story-md-template.md` not found. | Reinstall the skill; resources are required. |
| `inventory file not found: ...` | `--inventory <path>` was passed but the file doesn't exist. | Either the path is wrong, or the inventory cache was deleted. The cache lives at `{initiative_store}/.bmad-cache/inventory.json` and is auto-deleted at finalize — that's expected. |
| `could not parse <path>: ...` | The inventory file isn't valid JSON. | Fix the JSON. |
## from-spec errors
`scripts/from_spec.py` validates the spec before doing any work; on failure it returns:
```json
{"error": "invalid spec", "details": ["epic[0] missing `title`", "..."]}
```
Common shapes:
| Detail | Cause | Fix |
| --- | --- | --- |
| `spec must contain a non-empty epics list` | Top-level `epics` is missing or empty. | Add at least one epic. |
| `epic[N] missing nn` / `missing title` | A required epic field is absent. | Add it. |
| `epic[N].stories[M] missing nn / title / type` | A required story field is absent. | Add it. |
| `epic[N].stories[M].type invalid: '...'` | `type` is not `feature` / `task` / `bug` / `spike`. | Pick a valid type. |
If the spec is valid but `init_epic.py` or `init_story.py` fails (e.g. a folder collision), the envelope returns:
```json
{"error": "init_epic.py failed", "details": "epic folder already exists: ...", "epic": "..."}
```
Usually means the target initiative store isn't empty. Either clean it (`rm -rf <store>/epics`) or generate into a fresh path.
## parse_v6_epics.py warnings
The parser emits warnings in its `warnings[]` array rather than failing. The LLM driving the migrate flow surfaces these to the user for confirmation.
| Warning shape | Meaning |
| --- | --- |
| `no '## Requirements Inventory' section found` | The v6 file doesn't have the canonical inventory heading. The migrate flow proceeds without populating the requirements block. |
| `no '## Epic N:' headings found; file may not be canonical v6` | The parser couldn't find epic headings. Likely a hand-edited v6 file. Pick option 3 from the migrate menu (walk through manually). |
| `epic N (Title): no stories parsed` | An epic heading was found but no `### Story N.M:` blocks under it. Either the v6 file has stories elsewhere, or the epic is genuinely empty. |
| `epic N story M: no acceptance criteria parsed` | The `**Acceptance Criteria:**` block is missing or formatted differently. The v7 story will have empty ACs; fix in the LLM-driven migrate confirmation step. |
| `input <path> is a directory; sharded v6 input — flatten first` | Sharded v6 detected. The migrate flow offers to flatten before re-parsing. |
## Common operator scenarios
### "The skill won't let me edit because it thinks it's create mode"
Mode detection is filesystem-driven. Either `{initiative_store}/epics/` has no folders matching `NN-*`, or your `{initiative_store}` is pointing at the wrong path. Check `_bmad/config.yaml` and the resolution chain documented in `SKILL.md` Step 4 ("Load Config").
### "Validation passes but I know there's a coverage gap"
You forgot to pass `--inventory`. Without it, the validator only emits `mentioned_requirements` (the textual extraction) and won't compare to anything. The coverage gate requires the inventory file to compare against.
### "I want to delete an epic and the skill won't let me"
By design — see `prompts/edit-mode.md` under "Boundaries". Run `rm -rf {initiative_store}/epics/<folder>`, then `re-validate` to surface every dep ref that pointed at the deleted epic.
### "rename_epic.py refuses to renumber"
Probably the new NN collides with another existing epic. The message says which one. Renumber that epic out of the way first.
### "I keep getting story-numbering-gaps after a manual delete"
`rm`'ing a story file leaves a gap (e.g. 01, 02, 04). Fix with `rename_story.py --epic <e> --from <basename> --to-nn N` to renumber the survivors into a contiguous sequence.
### "Validator output is huge — I just want to see one thing"
For a quick view of the tree shape: `--summary-only` (JSON) or `--tree` (plain text). Both skip the findings list.
For a single epic's findings: `--epic <folder>`. Cross-epic refs still resolve against the whole tree, so a story whose dep references another epic gets reported correctly.
### "validate_initiative.py exits 0 but the JSON has errors"
The exit code only counts `error`-level findings. Schema and dep checks are always errors; coverage findings default to warnings unless `--coverage-strict`. If you want exit 1 on warnings too, post-process the JSON:
```bash
out=$(python3 scripts/validate_initiative.py --initiative-store $S)
warns=$(echo "$out" | jq '.summary.warnings')
[ "$warns" -gt 0 ] && exit 1 || exit 0
```

View File

@ -90,7 +90,7 @@ def main() -> int:
f"title: {yaml_quote(args.title)}\n"
f"type: {args.type}\n"
"status: draft\n"
f"epic: {yaml_quote(args.epic)}\n"
f"epic: {args.epic}\n"
f"depends_on: {deps_yaml}\n"
"---\n\n"
)

View File

@ -68,7 +68,7 @@ def main() -> int:
return 1
text = src_path.read_text(encoding="utf-8")
text = re.sub(r"^epic:.*$", f"epic: {yaml_quote(args.to_epic)}", text, count=1, flags=re.MULTILINE)
text = re.sub(r"^epic:.*$", f"epic: {args.to_epic}", text, count=1, flags=re.MULTILINE)
# The moved story's own depends_on may carry bare basenames that referenced
# within-epic siblings in src_epic; those refs now need cross-epic form.
new_text_lines: list[str] = []

View File

@ -60,7 +60,7 @@ USER_STORY_RE = re.compile(
)
AC_RE = re.compile(r"\*\*Acceptance Criteria\*\*:?\s*\n+", re.IGNORECASE)
GIVEN_WHEN_THEN_RE = re.compile(r"^[-*]\s+(?:Given|When|Then|And|But)\b.*$", re.MULTILINE | re.IGNORECASE)
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR)\d+(?:\.\d+)?\b")
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR|D|R)\d+(?:\.\d+)?\b")
REQUIREMENT_LINE_RE = re.compile(r"^[-*]\s+(?:\*\*)?(FR\d+(?:\.\d+)?|NFR\d+(?:\.\d+)?|UX-DR\d+(?:\.\d+)?)(?:\*\*)?:?\s*(.*)$", re.MULTILINE)

View File

@ -102,7 +102,7 @@ def main() -> int:
if sf.name == "epic.md":
continue
t = sf.read_text(encoding="utf-8")
new = re.sub(r"^epic:.*$", f"epic: {yaml_quote(new_folder)}", t, count=1, flags=re.MULTILINE)
new = re.sub(r"^epic:.*$", f"epic: {new_folder}", t, count=1, flags=re.MULTILINE)
if new != t:
sf.write_text(new, encoding="utf-8")
refs_updated += 1

View File

@ -36,7 +36,8 @@ class TestInitStory(unittest.TestCase):
content = Path(json.loads(r.stdout)["path"]).read_text(encoding="utf-8")
self.assertIn("As a {{user_type}}", content)
self.assertIn("type: feature", content)
self.assertIn(f'epic: "{epic}"', content)
self.assertIn(f"epic: {epic}", content)
self.assertNotIn(f'epic: "{epic}"', content)
self.assertIn("status: draft", content)
# The user-story marker comments stay so the LLM can locate the block.
self.assertIn("USER_STORY_START", content)

View File

@ -35,7 +35,7 @@ class TestMoveStory(unittest.TestCase):
data = json.loads(r.stdout)
self.assertEqual(data["new"], "02-mig/01-register")
moved = (store / "epics" / "02-mig" / "01-register.md").read_text(encoding="utf-8")
self.assertIn('epic: "02-mig"', moved)
self.assertIn("epic: 02-mig", moved)
self.assertIn('"01-auth/01-schema"', moved)
self.assertNotIn('depends_on: ["01-schema"]', moved)
self.assertFalse((store / "epics" / "01-auth" / "02-register.md").exists())

View File

@ -36,7 +36,7 @@ class TestRenameEpic(unittest.TestCase):
self.assertEqual(data["new"], "01-user-authentication")
self.assertTrue((store / "epics" / "01-user-authentication" / "epic.md").is_file())
schema = (store / "epics" / "01-user-authentication" / "01-schema.md").read_text(encoding="utf-8")
self.assertIn('epic: "01-user-authentication"', schema)
self.assertIn("epic: 01-user-authentication", schema)
mailer = (store / "epics" / "02-migration" / "01-mailer.md").read_text(encoding="utf-8")
self.assertIn('"01-user-authentication/01-schema"', mailer)
v = _run(VALIDATE, "--initiative-store", str(store))

View File

@ -120,6 +120,33 @@ class TestValidateInitiative(unittest.TestCase):
errors = [f for f in data["findings"] if f["code"] == "coverage-missing"]
self.assertEqual(errors[0]["level"], "error")
def test_inventory_coverage_recognizes_debt_and_research_codes(self) -> None:
# The validator's mentioned-codes regex must include D (debt) and R (research)
# in addition to FR/NFR/UX-DR. Otherwise tech-debt and research initiatives
# always report spurious coverage-missing on inventory codes the stories
# actually reference.
with tempfile.TemporaryDirectory() as tmp:
store = Path(tmp)
_build_clean_tree(store)
inv = store / "inventory.json"
inv.write_text(json.dumps({
"requirements": {
"debt": [{"code": "D1", "text": "Remove legacy /charge endpoint"}],
"research": [{"code": "R1", "text": "Investigate webhook ordering"}],
}
}), encoding="utf-8")
schema = store / "epics" / "01-auth" / "01-schema.md"
schema.write_text(
schema.read_text(encoding="utf-8") + "\n## Coverage\n- AC1: D1, R1\n",
encoding="utf-8",
)
r = _run(VALIDATE, "--initiative-store", str(store), "--inventory", str(inv))
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
data = json.loads(r.stdout)
self.assertEqual(data["summary"]["coverage_missing"], [])
self.assertIn("D1", data["summary"]["mentioned_requirements"])
self.assertIn("R1", data["summary"]["mentioned_requirements"])
def test_summary_only_emits_per_story_metadata(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
store = Path(tmp)

View File

@ -48,7 +48,7 @@ STORY_REQUIRED = {"title", "type", "status", "epic", "depends_on"}
EPIC_KEYS = {"title", "epic", "status", "depends_on", "metadata"}
EPIC_REQUIRED = {"title", "epic", "status", "depends_on"}
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR)\d+(?:\.\d+)?\b")
REQUIREMENT_CODE_RE = re.compile(r"\b(?:UX-DR|NFR|FR|D|R)\d+(?:\.\d+)?\b")
def parse_frontmatter(text: str) -> tuple[dict | None, str | None]: