wdi-method 0.6.8 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,11 +29,10 @@ page it is on.
29
29
 
30
30
  ## Install
31
31
 
32
- Three steps, in this order. The first is required before step 3 will run; the second can wait until
33
- **Both are required, and the installer refuses without either.** BMad has always been checked; the ticket
34
- engines are checked too, because every repo that learned they were missing learned it inside `wdi-build`
35
- with a spec already open. `--skip-engines-check` is the escape for the two cases that earn it: CI, and a
36
- repo that will never reach G5.
32
+ Three steps, in this order, and **step 3 refuses until steps 1 and 2 are done** through the TUI and
33
+ through `--yes` alike. BMad has always been checked; the engines are checked too, because every repo that
34
+ learned they were missing learned it inside `wdi-build` with a spec already open. `--skip-engines-check`
35
+ is the escape for the two cases that earn it: CI, and a repo that will never reach G5.
37
36
 
38
37
  **1. BMad Method** — in the product repo, picking the same agents you will give this installer:
39
38
 
package/bin/wdi-method.js CHANGED
@@ -1234,6 +1234,78 @@ function setLanguagePolicy(target, { docLanguage, docFilenameLanguage, chosen })
1234
1234
  // move, because moving it takes a decision about meaning: which PRD an `FR` belongs to, whether a
1235
1235
  // sentence was an assumption or a constraint. The `wdi-upgrade` skill does that half. This only
1236
1236
  // DETECTS it, cheaply, so the summary can say how much is waiting and where.
1237
+ /** Specs whose folder is not where the convention puts it — and that are still WORK.
1238
+ *
1239
+ * A closed spec is exempt, and one measured repo is why: ten closed specs, none open. Reporting all
1240
+ * ten would ask somebody to move ten folders of finished work and repoint every cite into them, for
1241
+ * nothing — `spec_folder` still resolves, and a closed spec's ticket file is already allowed to be
1242
+ * gone. The same exemption `ticket-status-one-home` grants, for the same reason: the convention binds
1243
+ * work, not the record of work that is done.
1244
+ *
1245
+ * Scanned line by line rather than parsed: this installer has no YAML reader, and both the flat
1246
+ * `specs:` shape and the pre-rename `waves:` one open a row the same way.
1247
+ */
1248
+ function specsOutsideScratch(text) {
1249
+ const out = [];
1250
+ let id = "";
1251
+ let status = "";
1252
+ let folder = "";
1253
+ const flush = () => {
1254
+ if (id && folder && status !== "closed" && !folder.startsWith(".scratch/")) out.push(id);
1255
+ id = "";
1256
+ status = "";
1257
+ folder = "";
1258
+ };
1259
+ for (const line of text.split(/\r?\n/)) {
1260
+ const row = /^\s{2}-\s+id:\s*(\S+)/.exec(line);
1261
+ if (row) {
1262
+ flush();
1263
+ id = row[1].replace(/['"]/g, "");
1264
+ continue;
1265
+ }
1266
+ if (!id) continue;
1267
+ const st = /^\s+status:\s*(\S+)/.exec(line);
1268
+ if (st && !status) status = st[1].replace(/['"]/g, "");
1269
+ const sf = /^\s+spec_folder:\s*(\S+)/.exec(line);
1270
+ if (sf && !folder) folder = sf[1].replace(/['"]/g, "");
1271
+ }
1272
+ flush();
1273
+ return out;
1274
+ }
1275
+
1276
+ /** Specs still in the pre-rename plan shape that are NOT closed — the ones with work left in them.
1277
+ *
1278
+ * Same scanner shape as `specsOutsideScratch`, and the same exemption for the same reason: the
1279
+ * convention binds work, not the record of work that is done.
1280
+ */
1281
+ function specsInLegacyShape(text) {
1282
+ const out = [];
1283
+ let id = "";
1284
+ let status = "";
1285
+ let legacy = false;
1286
+ const flush = () => {
1287
+ if (id && legacy && status !== "closed") out.push(id);
1288
+ id = "";
1289
+ status = "";
1290
+ legacy = false;
1291
+ };
1292
+ for (const line of text.split(/\r?\n/)) {
1293
+ const row = /^\s{2}-\s+id:\s*(\S+)/.exec(line);
1294
+ if (row) {
1295
+ flush();
1296
+ id = row[1].replace(/['"]/g, "");
1297
+ if (/^W\d+$/.test(id)) legacy = true;
1298
+ continue;
1299
+ }
1300
+ if (!id) continue;
1301
+ const st = /^\s+status:\s*(\S+)/.exec(line);
1302
+ if (st && !status) status = st[1].replace(/['"]/g, "");
1303
+ if (/^\s+(epics|stories):/.test(line)) legacy = true;
1304
+ }
1305
+ flush();
1306
+ return out;
1307
+ }
1308
+
1237
1309
  function pendingUpgrades(target) {
1238
1310
  const has = (...p) => fs.existsSync(path.join(target, ...p));
1239
1311
  const read = (...p) => (has(...p) ? fs.readFileSync(path.join(target, ...p), "utf8") : "");
@@ -1247,7 +1319,32 @@ function pendingUpgrades(target) {
1247
1319
  };
1248
1320
  const items = [];
1249
1321
  if (has(".control", "registry", "requirements.yaml")) items.push("requirements.yaml → goals.yaml + requirements-<slug>.yaml");
1250
- if (/^\s*-\s*id:\s*W\d+|^\s*(epics|stories):/m.test(read(".control", "registry", "specs.yaml"))) items.push("specs.yaml rows still W<n>/epics/stories (wdi-build re-cuts)");
1322
+ // The file the engines actually read. `/setup-matt-pocock-skills` writes its own answer here no
1323
+ // `specs.yaml`, no predefined path — and `seedAgentDocs` will not overwrite a file the product owns,
1324
+ // so without this probe the repo never learns why its tickets scatter.
1325
+ if (has("docs", "agents", "issue-tracker.md")
1326
+ && !read("docs", "agents", "issue-tracker.md").includes("seeded by `wdi-method`")) {
1327
+ items.push("docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
1328
+ }
1329
+ const strays = specsOutsideScratch(read(".control", "registry", "specs.yaml"));
1330
+ if (strays.length) {
1331
+ items.push(`spec_folder outside .scratch/<spec-id>-<slug>/ on ${strays.join(", ")} `
1332
+ + `(the folder moves, then its cites)`);
1333
+ }
1334
+ // Reported only where it is still WORK. A closed pre-rename wave is read correctly (0.6.7 taught
1335
+ // `Corpus.tickets()` to flatten `epics`/`stories` in memory), its `W<n>` id is a retired alias by
1336
+ // design, and its ticket files are already allowed to be gone. Nothing there is waiting to move.
1337
+ //
1338
+ // Until 0.6.11 this fired on every legacy row and pointed at `wdi-build` to "re-cut" it. That
1339
+ // instruction outlived the design it came from: `wdi-build` Phase 2 invokes `to-spec`/`to-tickets`
1340
+ // to write a NEW contract and publish new tickets, and has no mode that converts an old wave.
1341
+ // Three repos carrying twenty, forty-five and ten closed legacy rows were each told to run a skill
1342
+ // that would answer "not mine" and stop.
1343
+ const legacyOpen = specsInLegacyShape(read(".control", "registry", "specs.yaml"));
1344
+ if (legacyOpen.length) {
1345
+ items.push(`${legacyOpen.join(", ")} still in the W<n>/epics/stories shape and not closed `
1346
+ + `(flattened into tickets, id kept as its retired alias)`);
1347
+ }
1251
1348
  if (/^## (Executive Summary|Vision|Assumptions|Prerequisites)\s*$/m.test(read(".what", "_product-brief", "brief.md"))) items.push("brief.md in the 14-section shape");
1252
1349
  // Sections by NAME: the numbers moved between kits (Non-Goals was §7 in one, §5 in the next).
1253
1350
  if (anyIn(".what/_prd", "prd.md", /^## (\d+\.\s*)?(Document Purpose|Glossary|Non-Goals|Open Questions|Assumptions Index)\b|\*\*Proof of done:\*\*/m)) items.push("a prd.md in the 12-section shape, or with FR blocks");
@@ -1807,6 +1904,17 @@ async function runWizard(pre) {
1807
1904
  process.exit(1);
1808
1905
  }
1809
1906
 
1907
+ // Step 2, refused in step 2's place. This used to be a line in the Detected note and nothing more,
1908
+ // so an interactive install or update sailed past a repo with no engines in it — the same repo the
1909
+ // `--yes` path refuses. The order matters as much as the stop: BMad is step 1, so a repo missing
1910
+ // both is told about BMad first rather than sent to install the second thing.
1911
+ const engineGate = enginesReport(target);
1912
+ if (!engineGate.present && !pre.skipEngines) {
1913
+ p.note(enginesMissingMessage(engineGate.missing), "Engines next");
1914
+ p.outro("Install them into this repo, then run this again: npx wdi-method");
1915
+ process.exit(1);
1916
+ }
1917
+
1810
1918
  let first = !hasWdi;
1811
1919
  if (hasWdi) {
1812
1920
  const update = cancelIf(
@@ -63,6 +63,41 @@ decision or a PRD MAY point into it. Research, brainstorming, forge, and PRFAQ r
63
63
  A run folder MUST NOT be deleted **while anything still needs it** — the `update` intents re-read the original
64
64
  inputs in place. "Never deleted" is not the rule; the rule is a **retirement condition**, and it is below.
65
65
 
66
+ ### A withdrawn promise STAYS in the registry
67
+
68
+ A `BG` · `CAP` · `FR` · `NFR` · `UC` the product stops promising is marked, never deleted:
69
+
70
+ ```yaml
71
+ - id: CAP-8
72
+ title: "Publish an order as a public page"
73
+ status: withdrawn
74
+ withdrawn_by: DEC-026
75
+ ```
76
+
77
+ **Why the row stays.** One repo deleted two withdrawn capabilities and paid for it in twelve
78
+ `refs-resolve` findings: eight `DEC-` rows still named them in `serves:`, and six of those eight
79
+ genuinely served them at the time. The other repair — editing those decisions — is refused by the
80
+ section above: a `DEC-` is a record of what happened, and a retired name inside one is a fact about
81
+ the past.
82
+
83
+ **So a withdrawn row is read two ways, and both matter.** It is still **defined**: every old
84
+ reference resolves, and `id-allocated-once` still refuses the number to anything else — an id is
85
+ allocated once, withdrawal included. It is no longer **promised**: no `UC` is owed, no ticket, no RTM
86
+ row, and `promise_progress` is not dragged down by something nobody promises any more.
87
+
88
+ **Two rules keep it honest**, and `withdrawn-recorded` is what enforces both:
89
+
90
+ - `withdrawn_by` MUST name a `DEC-` that exists. Retiring an id is decision-worthy on this method's
91
+ own terms — the **ID chain** row under § *Landing that MUST be confirmed first* says so outright —
92
+ and without the pointer `withdrawn` is only a word that quiets a validator.
93
+ - A live row MUST NOT hang off a withdrawn one. An `FR` under a withdrawn `CAP` still promises
94
+ something whose capability nobody promises: withdraw it too, or move it under something live.
95
+ Withdrawal that takes half a chain with it silently is worse than the deletion it replaced, because
96
+ deletion at least went red.
97
+
98
+ `wdi-product` owns the edit, because it owns the row. The withdrawal itself goes through
99
+ `wdi-decision` first — the `DEC-` is what `withdrawn_by` points at.
100
+
66
101
  ## Who lands what
67
102
 
68
103
  There is no separate placement skill. A skill lands the output of the layer **it owns**, and the landing is
@@ -1,183 +1,184 @@
1
- ---
2
- status: Accepted
3
- ---
4
-
5
- # Method Glossary
6
-
7
- **Loaded when:** a method term is unclear, and before coining a new one.
8
-
9
- The words this method uses for **itself**. What the product is about belongs to
10
- `.control/product-glossary.md` instead. A term defined here MUST NOT be redefined there, and the
11
- reverse holds too. The test: would this term still apply on a different product? Yes → here.
12
-
13
- ## Layers
14
-
15
- | Term | Means |
16
- |---|---|
17
- | **Corpus** | The four layers below. `_bmad-output/` is not part of it |
18
- | **`.constitution/`** | How we work. Rules. Rarely changes |
19
- | **`.control/`** | What currently holds and what has been decided. Facts. Changes often. Equals `{project_knowledge}` |
20
- | **`.what/`** | What was promised |
21
- | **`.how/`** | How it is built |
22
- | **Workspace** | `_bmad-output/` — work in progress, committed but not curated |
23
- | **Placement test** | Is this file still true after its spec has passed? Yes → corpus. No → workspace |
24
-
25
- ## Units
26
-
27
- | Term | Means |
28
- |---|---|
29
- | **Product Component** (PC) | A domain slice — the box at C4 L3. Lives in `.what/<pc>/` and `.how/<pc>/`, and its `containers:` says where it runs |
30
- | **Logical Component** (LC) | One addressable unit of build. Registered in `components.yaml` with `type` · `container` · `owner` · `area` |
31
- | **Container** | Something that **runs its own code or stores its own data** and can be replaced without rebuilding another one. The C4 L2 term, and the value of an LC's `container` field. Two questions decide it and both MUST be yes — `architecture-guide.md` owns the test. Shipping two containers in one release does **not** merge them |
32
- | **`built`** | A container's one boolean. `true` when we write what is inside it, `false` when we deploy someone else's implementation. It decides whether the container gets an L3, an `LC`, and a heading in the codebase map — `container-built` checks all three |
33
- | **External system** | Something the product talks to whose **runtime we do not deploy**. It belongs at C4 L1, and MUST NOT be a container, an `LC`'s `container`, or a heading in the codebase map. What we lean on it for lives in `cross-cutting.md` or an integration contract |
34
- | **`_platform`** | **Not a component.** The home for what belongs to no Product Component: the spine, the C4 set, `cross-cutting.md`, the three inventories, and whatever it **owns**. A legitimate value in every position that asks which component owns something — `platform_owns`, an inventory row, an `LC`. Four kinds today: data · endpoint · job · screen. It carries no `mode`, no `risk_accepted`, and no G4 |
35
- | **Library** | An includable artifact — compiled into or imported by something else, never run on its own. Not a container |
36
- | **Slot** | A numbered sub-folder of a PC. `.what/<pc>/` 02–05 is reading order; `.how/<pc>/` 01–06 is ABCE classification. The two numberings do **not** mean the same thing |
37
- | **Kernel** | The `SRS-<pc>.md` or `SDD-<pc>.md` file itself. Content SHOULD stay in it until it grows past roughly 400 lines — a suggestion, not a threshold |
38
-
39
- A PC is not a container: a container runs, a PC is a domain. One container holds several PCs, and
40
- one PC may appear in more than one container. Because they cross, neither list implies the other, and
41
- the crossing MUST be written down rather than inferred — that is the PC × container matrix at C4 L2,
42
- rendered from each PC's `containers:`.
43
-
44
- ## Flow
45
-
46
- There is no "stage". The five gates are the only flow vocabulary, and the word **area** is not used at all —
47
- there is only Product Component.
48
-
49
- | Term | Means |
50
- |---|---|
51
- | **Gate** | One of five decision points: G1 Problem · G2 Product · G3 Blueprint · G4 Component · G5 Release. Named after what is decided there, not after the work before it |
52
- | **`mode`** | The one knob for **document depth**, and nothing else. `catalog` · `outline` · `guarded` · `deep`, default `catalog`. Two scopes — global in `index.yaml`, per component in `components.yaml`, and the per-component one wins. **No third scope** |
53
- | **`risk_accepted`** | The one field for **review intensity**, and nothing else. `low` · `medium` · `high`. It MUST NOT be derived from `mode`, nor `mode` from it |
54
- | **Blueprint** | The whole-product portrait decided once at G3: one line per use case, table, endpoint, and screen, plus entities, actors, the spine, C4, and cross-component rules. It names a **gate and a skill**, never a document or a folder |
55
- | **Spec** | One unit of delivered work: the tickets that reach one outcome. Opens at G4 or G5, closes at G5, recorded in `specs.yaml`. Its document is `SPEC.md`, **optional at size `S`** — one term, one entry, and the document is named after the unit the way a `DEC-` file is named after its decision |
56
- | **Release** | What a PRD promises. One release MAY span several specs; the relation is data, never inferred from numbering |
57
- | **Spec size** | `S` ≤3 tickets no new FR · `M` 4–12 · `L` >12 or a new container. MAY be raised mid-flight, MUST NOT be lowered. It does **not** choose which gates are active — that is `mode`. It decides two things: whether G4 and G5 merge into one session (`S`), and whether `SPEC.md` is written at all (`M` and up) |
58
- | **Fast Path** | A fix that skips all gates: ≤1 ticket, no FR/UC/`AD-N`/domain-model change, no money, personal data, or third-party integration |
59
- | **Step** | One of the five points inside `wdi-build`'s ship pipeline — plan · build · panel · publish · CI. It is **not** a gate and not a stage, and the word MUST NOT be used for anything at gate altitude |
60
-
61
- ## Artifacts
62
-
63
- | Term | Means |
64
- |---|---|
65
- | **Brief** | One problem, one user, one measure. Singleton, spans releases |
66
- | **PRD** | What is promised for **one initiative**, across every release it touches. FR and NFR numbered from the registry |
67
- | **SRS** | Per PC: what the system must do. `.what/` — slices space, while the PRD slices initiative. It **exists at every `mode`**, carrying the actor list and the use case catalogue |
68
- | **SDD** | Per PC: how it is built. `.how/`. At `mode: catalog` it is a skeleton, and that is a finished state |
69
- | **Inventory** | One of three living registers at product level — tables, endpoints, screens. Written as a plan when there is no code, **derived** from code once there is |
70
- | **Architecture spine** | `ARCHITECTURE-SPINE.md` — invariants as `AD-N`, each carrying Binds · Prevents · Rule. It constrains; it does not describe |
71
- | **C4** | L1 system context · L2 containers · L3 components, one file per container. L1+L2 together are what other methods call the HLD |
72
- | **`DESIGN.md`** | UX per PC, in `.how/<pc>/01-ux/` |
73
- | **`EXPERIENCE.md`** | The user-facing journey, in `.what/<pc>/04-usecases/` |
74
- | **`DEC-`** | One decision worth remembering, numbered globally. Lives in `.control/decisions/`. Recording is **not mandatory**; it freezes at `applied`, not at `accepted` |
75
- | **SPEC** | The document of **one spec**: a projection of `.what/` + `.how/` that MUST NOT contain anything new. Not read by humans, and **not written at size `S`** — there the tickets are the contract |
76
- | **Ticket** | One unit of build: a tracer-bullet vertical slice, complete through every layer, verifiable on its own, sized to one fresh context window. Carries the tickets that **block** it. Status is read from the ticket itself, never copied elsewhere |
77
- | **Structure map** | `.control/structure-codebase.md` and `structure-document.md` — where things actually are today |
78
- | **Mandate** | A `DEC-` of `type: mandate`: the owner's one dated, expiring delegation that lets `wdi-autopilot` decide what the skills would otherwise have asked. Its parameters live on its registry row only; every decision taken under it is a row in the ledger, `.control/memlog/autopilot-<date>.md` |
79
- | **Memlog** | The record of *why* while an artifact was written. Never copied into a document; a source when writing a `DEC-`. It is a **run log**, and it MUST NOT be searched as an index of decisions — `.control/generated/decisions.md` is that |
80
-
81
- ## Identifiers
82
-
83
- | Code | For |
84
- |---|---|
85
- | `BG-` · `FR-` · `NFR-` · `UJ-` | Business goal · functional requirement · non-functional requirement · user journey |
86
- | `UC-` | Use case |
87
- | `AD-` | An invariant in the architecture spine |
88
- | `DEC-` | A decision |
89
- | `LC-` | A Logical Component |
90
- | `OQ-` | An open question |
91
- | `RTR-` | **Retired.** It was an archived retrospective in `.control/reports/`; the retrospective step and `V19` went together. A frozen `RTR-` file stays where it is |
92
- | `CAP-` | A capability the planning unit |
93
- | `NT-` | A non-technical fact |
94
- | `BUG-` · `HOT-` | A defect · a hotfix |
95
- | `goal-has-fr`–`container-built` | Validators. `V10` fell and its number is not reused |
96
-
97
- IDs are allocated **globally** and never restart per document, per component, or per release. The chain
98
- that must hold end to end: `BG FR UC ticket test`.
99
-
100
- `ADR-` is **retired**. It was renamed to `DEC-` on 2026-08-18 with the numbers unchanged, so `ADR-004`
101
- inside a document frozen before that date is an alias for `DEC-004`, and those documents MUST NOT be
102
- rewritten for the prefix.
103
-
104
- ## Registry and generated
105
-
106
- | Term | Means |
107
- |---|---|
108
- | **Registry** | `.control/registry/` — the source of truth for IDs and plans. Written through `wdi-*` skills |
109
- | **Generated** | `.control/generated/` — derived from the registry. Written by the generator only, by hand **never** |
110
- | **RTM** | Requirements traceability matrix. Generated, never hand-maintained |
111
- | **Validator** | A script that answers what can be counted. It does not replace a gate checklist, which answers what must be judged |
112
-
113
- ## BMad terms
114
-
115
- BMad terms live in a WDI glossary because WDI runs on BMad. Each is defined **as WDI uses it**;
116
- where BMad's own meaning is wider, the narrower one here wins.
117
-
118
- | Term | Means |
119
- |---|---|
120
- | **Stories mode** | **Retired.** It was BMad's route — `SPEC.md` + `stories.yaml` produced by `bmad-spec`. The engine layer below G5 no longer runs it; `bmad-guide.md` says what replaced it |
121
- | **Skill class** | `A` living document, straight to the corpus · `B` living but wrongly granular, lands neutral then is placed · `C` spent after its work · `D` no artifact |
122
- | **Companion** | A side file a BMad skill produces next to its main output. The lasting ones are promoted by the distillation table |
123
- | **Distillation** | Promoting what is durable out of `_bmad-output/` before a spec closes. What is not promoted dies with the folder |
124
- | **`persistent_facts`** | Files a skill always reads. Routing alone does not achieve this |
125
- | **`doc_standards`** | Rule files a skill checks its output against. Facts MUST NOT be installed here, and neither MUST anything at `status: Reference` |
126
- | **`{project_knowledge}`** | The config variable pointing at `.control/` |
127
- | **`_bmad/custom/`** | Where every BMad override lives. `.claude/skills/bmad-*/customize.toml` MUST NOT be edited — it is overwritten on update |
128
-
129
- ## Retired — MUST NOT be used as current
130
-
131
- | Retired | Instead |
132
- |---|---|
133
- | `ADR-` | `DEC-` — same numbers, new prefix |
134
- | `ANX-`, and the annex concept | Nothing. Zero annexes were ever born |
135
- | `SCP-` | A `DEC-` of `type: course-correction` |
136
- | `layer:` on a decision | `touches:`, filled from what actually changed |
137
- | "Stage 1"…"Stage 5" as flow vocabulary | The five gates |
138
- | `epics.md` · `sprint-status.yaml` · `bmad-sprint-planning` · `bmad-create-epics-and-stories` · `stories.yaml` | Tickets, each carrying its own status and its blocking edges |
139
- | Validator `V10` | Nothing. Its number is not reused |
140
- | `bmad-help` as the answer to "where am I" | `wdi-help` |
141
- | The skills `wdi-analysis` · `wdi-architecture` · `wdi-design` · `wdi-glossary` · `wdi-structure` · `wdi-apply` · `wdi-correct-course` · `wdi-wave` · `wdi-ship-story` · `wdi-product-brief` · `wdi-meeting` · `wdi-project-log` | The eighteen in `why/README.md`. `why/rationale.md` says which absorbed which, and why |
142
- | An Indonesian synonym for a `mode` value *ringkas*, *terjaga*, *katalog* as prose | The English value, used as written: `catalog` · `outline` · `guarded` · `deep` |
143
-
144
- ## Synonyms that MUST NOT be coined
145
-
146
- A synonym for a term that already has an entry is drift, and `wdi-reconcile` hunts for it.
147
-
148
- | Do not say | Say | Because |
149
- |---|---|---|
150
- | application · app · service, for a deployable | **container** | The term is already defined at C4 L2 and carried by every LC |
151
- | infrastructure · third-party · dependency, for something inside the boundary | **container** with `built: false` | Calling it something else is how a container ends up with no row, no owner, and no NFR |
152
- | container, for something whose runtime we do not deploy | **external system** | It has no `built`, no L3, and no heading. Registering it as a container promises a section of the codebase map that will never exist |
153
- | module · package, for a unit of build | **Logical Component** | `components.yaml` names it, and `lc-registered` resolves against that name |
154
- | epic · sprint, for a batch of work | **spec** | Both belong to the sprint route this method dropped |
155
- | **wave**, for a unit of delivered work | **spec** | Retired when the engine layer below G5 changed. See the retired-alias rule below |
156
- | **story**, for a unit of build | **ticket** | Same retirement. A ticket is a vertical slice that blocks and is blocked; a story was a row in a file BMad owned |
157
- | area, for a domain slice | **Product Component** | The word "area" is not used anywhere in this method |
158
- | platform, for a Product Component | **`_platform`**, and only for what is not one | Registering `_platform` as a PC gives it a `mode`, an SRS, and a G4 it has no use for |
159
- | profile · tier · level, for document depth | **`mode`** | One knob, four values, and no matrix behind it |
160
- | feature, for a domain slice | **Product Component** | A feature is a promise; a PC is a folder pair with an owner |
161
- | requirements document | **PRD** or **SRS** | They cut different axes time versus space and merging the names merges the documents |
162
-
163
- ## Retired terms keep their frozen documents
164
-
165
- `wave` and `story` are **retired aliases** of `spec` and `ticket`. A `wave` or a `story` appearing in a
166
- document frozen before the change a closed `DEC-`, an `RTR-`, minutes, `why/rationale.md`'s record of what
167
- happened reads as its replacement, and those documents **MUST NOT be rewritten for the term.** This is the
168
- same rule Article 6 already applies to `ADR-NNN` `DEC-NNN`, and for the same reason: a frozen record that
169
- cites a name is evidence, and rewriting evidence to tidy a vocabulary destroys the thing that made it useful.
170
-
171
- What MUST use the new term: every `Accepted` guide, every skill, every registry, and anything written from
172
- here on.
173
-
174
- ## Rules
175
-
176
- - A new method term MUST be added here in the same pass it first appears, not defined where it is
177
- used. Adding one is a change to the **method itself** `wdi-blueprint` MAY propose it and MUST NOT
178
- write it, because a method term binds every project the method is installed in.
179
- - One term MUST NOT have two entries. Two meanings mean two terms.
180
- - A domain term MUST go to `.control/product-glossary.md` instead. If it is unclear which, ask whether the
181
- term would survive being applied to a different product: yes here, no there.
182
- - Common technical terms stay in English when the industry name is the one that matches the code,
183
- the error message, or the reader's expectation.
1
+ ---
2
+ status: Accepted
3
+ ---
4
+
5
+ # Method Glossary
6
+
7
+ **Loaded when:** a method term is unclear, and before coining a new one.
8
+
9
+ The words this method uses for **itself**. What the product is about belongs to
10
+ `.control/product-glossary.md` instead. A term defined here MUST NOT be redefined there, and the
11
+ reverse holds too. The test: would this term still apply on a different product? Yes → here.
12
+
13
+ ## Layers
14
+
15
+ | Term | Means |
16
+ |---|---|
17
+ | **Corpus** | The four layers below. `_bmad-output/` is not part of it |
18
+ | **`.constitution/`** | How we work. Rules. Rarely changes |
19
+ | **`.control/`** | What currently holds and what has been decided. Facts. Changes often. Equals `{project_knowledge}` |
20
+ | **`.what/`** | What was promised |
21
+ | **`.how/`** | How it is built |
22
+ | **Workspace** | `_bmad-output/` — work in progress, committed but not curated |
23
+ | **Placement test** | Is this file still true after its spec has passed? Yes → corpus. No → workspace |
24
+
25
+ ## Units
26
+
27
+ | Term | Means |
28
+ |---|---|
29
+ | **Product Component** (PC) | A domain slice — the box at C4 L3. Lives in `.what/<pc>/` and `.how/<pc>/`, and its `containers:` says where it runs |
30
+ | **Logical Component** (LC) | One addressable unit of build. Registered in `components.yaml` with `type` · `container` · `owner` · `area` |
31
+ | **Container** | Something that **runs its own code or stores its own data** and can be replaced without rebuilding another one. The C4 L2 term, and the value of an LC's `container` field. Two questions decide it and both MUST be yes — `architecture-guide.md` owns the test. Shipping two containers in one release does **not** merge them |
32
+ | **`built`** | A container's one boolean. `true` when we write what is inside it, `false` when we deploy someone else's implementation. It decides whether the container gets an L3, an `LC`, and a heading in the codebase map — `container-built` checks all three |
33
+ | **External system** | Something the product talks to whose **runtime we do not deploy**. It belongs at C4 L1, and MUST NOT be a container, an `LC`'s `container`, or a heading in the codebase map. What we lean on it for lives in `cross-cutting.md` or an integration contract |
34
+ | **`_platform`** | **Not a component.** The home for what belongs to no Product Component: the spine, the C4 set, `cross-cutting.md`, the three inventories, and whatever it **owns**. A legitimate value in every position that asks which component owns something — `platform_owns`, an inventory row, an `LC`. Four kinds today: data · endpoint · job · screen. It carries no `mode`, no `risk_accepted`, and no G4 |
35
+ | **Library** | An includable artifact — compiled into or imported by something else, never run on its own. Not a container |
36
+ | **Slot** | A numbered sub-folder of a PC. `.what/<pc>/` 02–05 is reading order; `.how/<pc>/` 01–06 is ABCE classification. The two numberings do **not** mean the same thing |
37
+ | **Kernel** | The `SRS-<pc>.md` or `SDD-<pc>.md` file itself. Content SHOULD stay in it until it grows past roughly 400 lines — a suggestion, not a threshold |
38
+
39
+ A PC is not a container: a container runs, a PC is a domain. One container holds several PCs, and
40
+ one PC may appear in more than one container. Because they cross, neither list implies the other, and
41
+ the crossing MUST be written down rather than inferred — that is the PC × container matrix at C4 L2,
42
+ rendered from each PC's `containers:`.
43
+
44
+ ## Flow
45
+
46
+ There is no "stage". The five gates are the only flow vocabulary, and the word **area** is not used at all —
47
+ there is only Product Component.
48
+
49
+ | Term | Means |
50
+ |---|---|
51
+ | **Gate** | One of five decision points: G1 Problem · G2 Product · G3 Blueprint · G4 Component · G5 Release. Named after what is decided there, not after the work before it |
52
+ | **`mode`** | The one knob for **document depth**, and nothing else. `catalog` · `outline` · `guarded` · `deep`, default `catalog`. Two scopes — global in `index.yaml`, per component in `components.yaml`, and the per-component one wins. **No third scope** |
53
+ | **`risk_accepted`** | The one field for **review intensity**, and nothing else. `low` · `medium` · `high`. It MUST NOT be derived from `mode`, nor `mode` from it |
54
+ | **Blueprint** | The whole-product portrait decided once at G3: one line per use case, table, endpoint, and screen, plus entities, actors, the spine, C4, and cross-component rules. It names a **gate and a skill**, never a document or a folder |
55
+ | **Spec** | One unit of delivered work: the tickets that reach one outcome. Opens at G4 or G5, closes at G5, recorded in `specs.yaml`. Its document is `SPEC.md`, **optional at size `S`** — one term, one entry, and the document is named after the unit the way a `DEC-` file is named after its decision |
56
+ | **Release** | What a PRD promises. One release MAY span several specs; the relation is data, never inferred from numbering |
57
+ | **Spec size** | `S` ≤3 tickets no new FR · `M` 4–12 · `L` >12 or a new container. MAY be raised mid-flight, MUST NOT be lowered. It does **not** choose which gates are active — that is `mode`. It decides two things: whether G4 and G5 merge into one session (`S`), and whether `SPEC.md` is written at all (`M` and up) |
58
+ | **Fast Path** | A fix that skips all gates: ≤1 ticket, no FR/UC/`AD-N`/domain-model change, no money, personal data, or third-party integration |
59
+ | **Step** | One of the five points inside `wdi-build`'s ship pipeline — plan · build · panel · publish · CI. It is **not** a gate and not a stage, and the word MUST NOT be used for anything at gate altitude |
60
+
61
+ ## Artifacts
62
+
63
+ | Term | Means |
64
+ |---|---|
65
+ | **Brief** | One problem, one user, one measure. Singleton, spans releases |
66
+ | **PRD** | What is promised for **one initiative**, across every release it touches. FR and NFR numbered from the registry |
67
+ | **SRS** | Per PC: what the system must do. `.what/` — slices space, while the PRD slices initiative. It **exists at every `mode`**, carrying the actor list and the use case catalogue |
68
+ | **SDD** | Per PC: how it is built. `.how/`. At `mode: catalog` it is a skeleton, and that is a finished state |
69
+ | **Inventory** | One of three living registers at product level — tables, endpoints, screens. Written as a plan when there is no code, **derived** from code once there is |
70
+ | **Architecture spine** | `ARCHITECTURE-SPINE.md` — invariants as `AD-N`, each carrying Binds · Prevents · Rule. It constrains; it does not describe |
71
+ | **C4** | L1 system context · L2 containers · L3 components, one file per container. L1+L2 together are what other methods call the HLD |
72
+ | **`DESIGN.md`** | UX per PC, in `.how/<pc>/01-ux/` |
73
+ | **`EXPERIENCE.md`** | The user-facing journey, in `.what/<pc>/04-usecases/` |
74
+ | **`DEC-`** | One decision worth remembering, numbered globally. Lives in `.control/decisions/`. Recording is **not mandatory**; it freezes at `applied`, not at `accepted` |
75
+ | **SPEC** | The document of **one spec**: a projection of `.what/` + `.how/` that MUST NOT contain anything new. Not read by humans, and **not written at size `S`** — there the tickets are the contract |
76
+ | **Ticket** | One unit of build: a tracer-bullet vertical slice, complete through every layer, verifiable on its own, sized to one fresh context window. Carries the tickets that **block** it. Status is read from the ticket itself, never copied elsewhere |
77
+ | **Structure map** | `.control/structure-codebase.md` and `structure-document.md` — where things actually are today |
78
+ | **Mandate** | A `DEC-` of `type: mandate`: the owner's one dated, expiring delegation that lets `wdi-autopilot` decide what the skills would otherwise have asked. Its parameters live on its registry row only; every decision taken under it is a row in the ledger, `.control/memlog/autopilot-<date>.md` |
79
+ | **Memlog** | The record of *why* while an artifact was written. Never copied into a document; a source when writing a `DEC-`. It is a **run log**, and it MUST NOT be searched as an index of decisions — `.control/generated/decisions.md` is that |
80
+
81
+ ## Identifiers
82
+
83
+ | Code | For |
84
+ |---|---|
85
+ | `BG-` · `FR-` · `NFR-` · `UJ-` | Business goal · functional requirement · non-functional requirement · user journey |
86
+ | `UC-` | Use case |
87
+ | `AD-` | An invariant in the architecture spine |
88
+ | `DEC-` | A decision |
89
+ | `LC-` | A Logical Component |
90
+ | `OQ-` | An open question |
91
+ | `status: withdrawn` | On a `BG` · `CAP` · `FR` · `NFR` · `UC` row: the product stopped promising it, and the row stays so every old reference still resolves and its id is never reused. It names the `DEC-` that withdrew it in `withdrawn_by`. Still **defined**, no longer **promised** — `corpus-guide.md` owns the rule |
92
+ | `RTR-` | **Retired.** It was an archived retrospective in `.control/reports/`; the retrospective step and `V19` went together. A frozen `RTR-` file stays where it is |
93
+ | `CAP-` | A capability the planning unit |
94
+ | `NT-` | A non-technical fact |
95
+ | `BUG-` · `HOT-` | A defect · a hotfix |
96
+ | `goal-has-fr`–`container-built` | Validators. `V10` fell and its number is not reused |
97
+
98
+ IDs are allocated **globally** and never restart per document, per component, or per release. The chain
99
+ that must hold end to end: `BG → FR → UC → ticket → test`.
100
+
101
+ `ADR-` is **retired**. It was renamed to `DEC-` on 2026-08-18 with the numbers unchanged, so `ADR-004`
102
+ inside a document frozen before that date is an alias for `DEC-004`, and those documents MUST NOT be
103
+ rewritten for the prefix.
104
+
105
+ ## Registry and generated
106
+
107
+ | Term | Means |
108
+ |---|---|
109
+ | **Registry** | `.control/registry/` — the source of truth for IDs and plans. Written through `wdi-*` skills |
110
+ | **Generated** | `.control/generated/` derived from the registry. Written by the generator only, by hand **never** |
111
+ | **RTM** | Requirements traceability matrix. Generated, never hand-maintained |
112
+ | **Validator** | A script that answers what can be counted. It does not replace a gate checklist, which answers what must be judged |
113
+
114
+ ## BMad terms
115
+
116
+ BMad terms live in a WDI glossary because WDI runs on BMad. Each is defined **as WDI uses it**;
117
+ where BMad's own meaning is wider, the narrower one here wins.
118
+
119
+ | Term | Means |
120
+ |---|---|
121
+ | **Stories mode** | **Retired.** It was BMad's route `SPEC.md` + `stories.yaml` produced by `bmad-spec`. The engine layer below G5 no longer runs it; `bmad-guide.md` says what replaced it |
122
+ | **Skill class** | `A` living document, straight to the corpus · `B` living but wrongly granular, lands neutral then is placed · `C` spent after its work · `D` no artifact |
123
+ | **Companion** | A side file a BMad skill produces next to its main output. The lasting ones are promoted by the distillation table |
124
+ | **Distillation** | Promoting what is durable out of `_bmad-output/` before a spec closes. What is not promoted dies with the folder |
125
+ | **`persistent_facts`** | Files a skill always reads. Routing alone does not achieve this |
126
+ | **`doc_standards`** | Rule files a skill checks its output against. Facts MUST NOT be installed here, and neither MUST anything at `status: Reference` |
127
+ | **`{project_knowledge}`** | The config variable pointing at `.control/` |
128
+ | **`_bmad/custom/`** | Where every BMad override lives. `.claude/skills/bmad-*/customize.toml` MUST NOT be edited — it is overwritten on update |
129
+
130
+ ## Retired — MUST NOT be used as current
131
+
132
+ | Retired | Instead |
133
+ |---|---|
134
+ | `ADR-` | `DEC-` same numbers, new prefix |
135
+ | `ANX-`, and the annex concept | Nothing. Zero annexes were ever born |
136
+ | `SCP-` | A `DEC-` of `type: course-correction` |
137
+ | `layer:` on a decision | `touches:`, filled from what actually changed |
138
+ | "Stage 1"…"Stage 5" as flow vocabulary | The five gates |
139
+ | `epics.md` · `sprint-status.yaml` · `bmad-sprint-planning` · `bmad-create-epics-and-stories` · `stories.yaml` | Tickets, each carrying its own status and its blocking edges |
140
+ | Validator `V10` | Nothing. Its number is not reused |
141
+ | `bmad-help` as the answer to "where am I" | `wdi-help` |
142
+ | The skills `wdi-analysis` · `wdi-architecture` · `wdi-design` · `wdi-glossary` · `wdi-structure` · `wdi-apply` · `wdi-correct-course` · `wdi-wave` · `wdi-ship-story` · `wdi-product-brief` · `wdi-meeting` · `wdi-project-log` | The eighteen in `why/README.md`. `why/rationale.md` says which absorbed which, and why |
143
+ | An Indonesian synonym for a `mode` value — *ringkas*, *terjaga*, *katalog* as prose | The English value, used as written: `catalog` · `outline` · `guarded` · `deep` |
144
+
145
+ ## Synonyms that MUST NOT be coined
146
+
147
+ A synonym for a term that already has an entry is drift, and `wdi-reconcile` hunts for it.
148
+
149
+ | Do not say | Say | Because |
150
+ |---|---|---|
151
+ | application · app · service, for a deployable | **container** | The term is already defined at C4 L2 and carried by every LC |
152
+ | infrastructure · third-party · dependency, for something inside the boundary | **container** with `built: false` | Calling it something else is how a container ends up with no row, no owner, and no NFR |
153
+ | container, for something whose runtime we do not deploy | **external system** | It has no `built`, no L3, and no heading. Registering it as a container promises a section of the codebase map that will never exist |
154
+ | module · package, for a unit of build | **Logical Component** | `components.yaml` names it, and `lc-registered` resolves against that name |
155
+ | epic · sprint, for a batch of work | **spec** | Both belong to the sprint route this method dropped |
156
+ | **wave**, for a unit of delivered work | **spec** | Retired when the engine layer below G5 changed. See the retired-alias rule below |
157
+ | **story**, for a unit of build | **ticket** | Same retirement. A ticket is a vertical slice that blocks and is blocked; a story was a row in a file BMad owned |
158
+ | area, for a domain slice | **Product Component** | The word "area" is not used anywhere in this method |
159
+ | platform, for a Product Component | **`_platform`**, and only for what is not one | Registering `_platform` as a PC gives it a `mode`, an SRS, and a G4 it has no use for |
160
+ | profile · tier · level, for document depth | **`mode`** | One knob, four values, and no matrix behind it |
161
+ | feature, for a domain slice | **Product Component** | A feature is a promise; a PC is a folder pair with an owner |
162
+ | requirements document | **PRD** or **SRS** | They cut different axes — time versus space — and merging the names merges the documents |
163
+
164
+ ## Retired terms keep their frozen documents
165
+
166
+ `wave` and `story` are **retired aliases** of `spec` and `ticket`. A `wave` or a `story` appearing in a
167
+ document frozen before the change a closed `DEC-`, an `RTR-`, minutes, `why/rationale.md`'s record of what
168
+ happened reads as its replacement, and those documents **MUST NOT be rewritten for the term.** This is the
169
+ same rule Article 6 already applies to `ADR-NNN` `DEC-NNN`, and for the same reason: a frozen record that
170
+ cites a name is evidence, and rewriting evidence to tidy a vocabulary destroys the thing that made it useful.
171
+
172
+ What MUST use the new term: every `Accepted` guide, every skill, every registry, and anything written from
173
+ here on.
174
+
175
+ ## Rules
176
+
177
+ - A new method term MUST be added here in the same pass it first appears, not defined where it is
178
+ used. Adding one is a change to the **method itself** `wdi-blueprint` MAY propose it and MUST NOT
179
+ write it, because a method term binds every project the method is installed in.
180
+ - One term MUST NOT have two entries. Two meanings mean two terms.
181
+ - A domain term MUST go to `.control/product-glossary.md` instead. If it is unclear which, ask whether the
182
+ term would survive being applied to a different product: yes here, no there.
183
+ - Common technical terms stay in English when the industry name is the one that matches the code,
184
+ the error message, or the reader's expectation.
@@ -185,6 +185,27 @@ def git(root: Path, *args: str) -> str | None:
185
185
  REQUIREMENT_KEYS = ("goals", "capabilities", "functional", "nonfunctional", "journeys")
186
186
 
187
187
 
188
+ def is_withdrawn(row: dict) -> bool:
189
+ """`status: withdrawn` — the product stopped promising this, and the row STAYED.
190
+
191
+ Deleting it is what a repo used to do, and the cost was measured: two capabilities withdrawn by
192
+ decision, their rows removed, and twelve `refs-resolve` findings — eight `DEC-` rows still named
193
+ them, six of which genuinely served them at the time. `corpus-guide.md` forbids the other repair:
194
+ a retired name in a record of what happened is a fact about the past, and a `DEC-` is exactly
195
+ that record.
196
+
197
+ So a withdrawn row is read TWO ways. It is still **defined** — every old reference resolves and
198
+ `id-allocated-once` still refuses the number to anyone else. It is no longer **promised** — no UC
199
+ is owed, no ticket, no RTM row, and `promise_progress` is not dragged down by something nobody
200
+ promises.
201
+ """
202
+ return str(row.get("status") or "").strip().lower() == "withdrawn"
203
+
204
+
205
+ def promised(items: list[dict]) -> list[dict]:
206
+ return [row for row in items if not is_withdrawn(row)]
207
+
208
+
188
209
  @dataclass
189
210
  class Corpus:
190
211
  root: Path
@@ -253,23 +274,23 @@ class Corpus:
253
274
  # --- shortcuts used repeatedly
254
275
  @property
255
276
  def goals(self) -> list[dict]:
256
- return rows(self.requirements, "goals")
277
+ return promised(rows(self.requirements, "goals"))
257
278
 
258
279
  @property
259
280
  def caps(self) -> list[dict]:
260
- return rows(self.requirements, "capabilities")
281
+ return promised(rows(self.requirements, "capabilities"))
261
282
 
262
283
  @property
263
284
  def frs(self) -> list[dict]:
264
- return rows(self.requirements, "functional")
285
+ return promised(rows(self.requirements, "functional"))
265
286
 
266
287
  @property
267
288
  def nfrs(self) -> list[dict]:
268
- return rows(self.requirements, "nonfunctional")
289
+ return promised(rows(self.requirements, "nonfunctional"))
269
290
 
270
291
  @property
271
292
  def ucs(self) -> list[dict]:
272
- return rows(self.usecases, "usecases")
293
+ return promised(rows(self.usecases, "usecases"))
273
294
 
274
295
  @property
275
296
  def decs(self) -> list[dict]:
@@ -282,6 +303,15 @@ class Corpus:
282
303
  return own
283
304
  return str(self.index.get("mode") or "").strip() or "catalog"
284
305
 
306
+ @property
307
+ def withdrawn_rows(self) -> list[dict]:
308
+ """Every withdrawn requirement row, with the key it came from — the id side of the split."""
309
+ out = []
310
+ for key in REQUIREMENT_KEYS:
311
+ out += [(key, row) for row in rows(self.requirements, key) if is_withdrawn(row)]
312
+ out += [("usecases", row) for row in rows(self.usecases, "usecases") if is_withdrawn(row)]
313
+ return [row for _, row in out]
314
+
285
315
  @property
286
316
  def lcs(self) -> list[dict]:
287
317
  return rows(self.components, "logical_components")
@@ -341,6 +371,18 @@ def _legacy_tickets(spec: dict) -> list[dict]:
341
371
  yet scheduled, and a closed wave has nothing left to schedule.
342
372
  """
343
373
  sid = str(spec.get("id") or "")
374
+
375
+ def scoped(raw: str) -> str:
376
+ """`W7-S2` under wave `W7` stays `W7-S2`, not `W7-W7-S2`.
377
+
378
+ The prefix exists so two waves both naming a story `"1"` cannot collide into one node. A
379
+ story already scoped to its wave has nothing to collide with, and prefixing it again produces
380
+ an id that matches no file, no memlog line, and nothing a person would search for. Measured on
381
+ a live repo whose RTM read `W7-W7-S2`.
382
+ """
383
+ sub = str(raw)
384
+ return sub if sid and sub.startswith(f"{sid}-") else f"{sid}-{sub}"
385
+
344
386
  out = []
345
387
  for epic in spec.get("epics") or []:
346
388
  if not isinstance(epic, dict):
@@ -349,8 +391,8 @@ def _legacy_tickets(spec: dict) -> list[dict]:
349
391
  if not isinstance(story, dict):
350
392
  continue
351
393
  ticket = {k: v for k, v in story.items() if k not in ("id", "depends_on")}
352
- ticket["id"] = f"{sid}-{story.get('id')}"
353
- ticket["blocked_by"] = [f"{sid}-{d}" for d in (story.get("depends_on") or [])]
394
+ ticket["id"] = scoped(story.get("id"))
395
+ ticket["blocked_by"] = [scoped(d) for d in (story.get("depends_on") or [])]
354
396
  # The story's OWN id is kept because the file on disk is named after it, not after the
355
397
  # synthesized ticket id — see `_ticket_files`.
356
398
  ticket["_legacy_story_id"] = str(story.get("id") or "")
@@ -461,6 +503,8 @@ def refs_resolve(c: Corpus, r: Result) -> None: # was V6
461
503
  defined.add(str(spec.get("id")))
462
504
  for _, ticket in c.tickets():
463
505
  defined.add(str(ticket.get("id")))
506
+ # Withdrawn, therefore still defined. This is the whole point of keeping the row.
507
+ defined |= {str(row.get("id")) for row in c.withdrawn_rows if row.get("id") is not None}
464
508
 
465
509
  refs: list[tuple[str, str]] = []
466
510
  for cap in c.caps:
@@ -482,9 +526,21 @@ def refs_resolve(c: Corpus, r: Result) -> None: # was V6
482
526
  refs += [(str(ticket.get("id")), u) for u in listy(ticket, "satisfies")]
483
527
  refs += [(str(ticket.get("id")), b) for b in listy(ticket, "blocked_by")]
484
528
 
529
+ # A promise's id going missing has one likely cause and one wrong-looking-obvious repair. The
530
+ # cause: the row was DELETED when the product stopped promising it. The wrong repair: edit the
531
+ # reference — which `corpus-guide.md` refuses, because a `DEC-` records what happened and it did
532
+ # serve that promise at the time. One repo carried twelve of these before anyone worked out that
533
+ # the row was meant to stay, so the route travels with the finding.
534
+ promise_id = re.compile(r"^(BG|CAP|FR|NFR|UC)-\d+$")
485
535
  for owner, target in sorted(set(refs)):
486
536
  if target and target not in defined:
487
- r.fail("refs-resolve", owner, f"points to `{target}` which does not exist in any registry")
537
+ hint = ""
538
+ if promise_id.match(target):
539
+ hint = (" — if it was withdrawn, the row STAYS with `status: withdrawn` and a "
540
+ "`withdrawn_by`, and deleting it is what broke this reference (corpus-guide.md). "
541
+ "Editing the reference instead rewrites a record of the past")
542
+ r.fail("refs-resolve", owner,
543
+ f"points to `{target}` which does not exist in any registry{hint}")
488
544
 
489
545
 
490
546
  def _cycles(graph: dict[str, list[str]]) -> list[str]:
@@ -1619,6 +1675,47 @@ def engines_invocable(c: Corpus, r: Result) -> None:
1619
1675
  f"update` restores it; `npx wdi-method engines --fix` strips it back out")
1620
1676
 
1621
1677
 
1678
+ def withdrawn_recorded(c: Corpus, r: Result) -> None:
1679
+ """Two things, and without either one `withdrawn` is just a word that quiets a validator.
1680
+
1681
+ **It names the decision.** Withdrawing a promise is decision-worthy on the method's own terms —
1682
+ `corpus-guide.md` lists "no `BG`/`CAP`/`FR`/`NFR`/`UC`/`LC` id is born, renamed, or retired" as a
1683
+ test for whether something is a `DEC-`. So `withdrawn_by` MUST name one that exists.
1684
+
1685
+ **It does not orphan what is left.** A live `FR` whose capability is withdrawn still promises
1686
+ something whose capability nobody promises any more. Withdrawal that takes half a chain with it
1687
+ silently is worse than the deletion this replaced, because at least deletion went red.
1688
+ """
1689
+ dec_ids = {str(d.get("id")) for d in c.decs}
1690
+ for row in c.withdrawn_rows:
1691
+ rid = str(row.get("id") or "")
1692
+ by = str(row.get("withdrawn_by") or "").strip()
1693
+ if not by:
1694
+ r.fail("withdrawn-recorded", rid, "is `status: withdrawn` and names no `withdrawn_by`. "
1695
+ "Withdrawing a promise is a decision — name the `DEC-` that took it, or the "
1696
+ "word is only silencing a validator")
1697
+ elif by not in dec_ids:
1698
+ r.fail("withdrawn-recorded", rid, f"names `withdrawn_by: {by}`, which is not a decision in "
1699
+ f"`decisions.yaml`")
1700
+
1701
+ withdrawn_ids = {str(row.get("id")) for row in c.withdrawn_rows}
1702
+ if not withdrawn_ids:
1703
+ return
1704
+ for row, parent_key, what in ([(x, "goal", "goal") for x in c.caps]
1705
+ + [(x, "capability", "capability") for x in c.frs]
1706
+ + [(x, "capability", "capability") for x in c.nfrs]):
1707
+ parent = str(row.get(parent_key) or "").strip()
1708
+ if parent and parent in withdrawn_ids:
1709
+ r.fail("withdrawn-recorded", str(row.get("id")),
1710
+ f"is live, and the {what} it hangs off (`{parent}`) is withdrawn. Withdraw this row "
1711
+ f"too, or move it under something still promised")
1712
+ for uc in c.ucs:
1713
+ for fr in listy(uc, "satisfies"):
1714
+ if fr in withdrawn_ids:
1715
+ r.fail("withdrawn-recorded", str(uc.get("id")),
1716
+ f"is live and satisfies `{fr}`, which is withdrawn")
1717
+
1718
+
1622
1719
  def id_allocated_once(c: Corpus, r: Result) -> None: # was V28
1623
1720
  """One id, one row — across every file the requirement registry is split into.
1624
1721
 
@@ -1653,7 +1750,7 @@ def run_checks(c: Corpus, asof: dt.date) -> Result:
1653
1750
  # no two copies left to compare.
1654
1751
  # V19 is REPEALED. It checked one line item — an `RTR-` file in .control/reports/ — and the
1655
1752
  # retrospective it archived was the only thing spec size `L` ever decided. Both went together.
1656
- for fn in (goal_has_fr, fr_has_uc, uc_scheduled, ticket_has_test, nfr_has_enforcer, refs_resolve, no_cycles, applied_dec_touches, locked_gate_passed, parallel_tickets_blocked, lc_registered, review_trace, chain_links, memlog_home, spec_names_release_prd, ticket_status_one_home, defect_root_cause, entity_one_writer, spec_after_g4, high_risk_named, mandate_accept, cites_resolve, container_built, custom_room_declared, corpus_in_git, engines_invocable, id_allocated_once):
1753
+ for fn in (goal_has_fr, fr_has_uc, uc_scheduled, ticket_has_test, nfr_has_enforcer, refs_resolve, no_cycles, applied_dec_touches, locked_gate_passed, parallel_tickets_blocked, lc_registered, review_trace, chain_links, memlog_home, spec_names_release_prd, ticket_status_one_home, defect_root_cause, entity_one_writer, spec_after_g4, high_risk_named, mandate_accept, cites_resolve, container_built, custom_room_declared, corpus_in_git, engines_invocable, withdrawn_recorded, id_allocated_once):
1657
1754
  fn(c, r)
1658
1755
  plan_dates(c, r, asof)
1659
1756
  return r
@@ -1856,7 +1953,7 @@ def gen_status(c: Corpus, rtm: dict, result: Result) -> dict:
1856
1953
  per_spec.append({"spec": wid, "status": spec.get("status"),
1857
1954
  "tickets_done": done, "tickets_total": len(items),
1858
1955
  "work_progress": _pct(done, len(items))})
1859
- applicable = 27 # goal-has-fr..id-allocated-once minus V10 and V19, both repealed
1956
+ applicable = 28 # goal-has-fr..id-allocated-once minus V10 and V19, both repealed
1860
1957
  return {
1861
1958
  "promise_progress": _pct(green, len(counted)),
1862
1959
  "rtm_rows": {"green": green, "counted": len(counted),
@@ -119,6 +119,28 @@ A changed promise changes what other documents can still claim. Check, and **rep
119
119
  Then run the change-control matrix in `delivery-flow-guide.md` and **report** which gates reopen. You MUST
120
120
  NOT reopen one yourself.
121
121
 
122
+ ### Withdrawing a promise — the row stays
123
+
124
+ A `BG` · `CAP` · `FR` · `NFR` the product stops promising is **marked, never deleted**. Deleting it is
125
+ how a repo ended up with twelve `refs-resolve` findings: two capabilities were withdrawn by decision,
126
+ their rows removed, and eight `DEC-` rows still named them — six of the eight having genuinely served
127
+ them at the time, which `corpus-guide.md` forbids editing away.
128
+
129
+ 1. The withdrawal is a **decision first**. Route to `wdi-decision`; you MUST NOT withdraw a promise on
130
+ your own authority, and `withdrawn_by` needs that `DEC-` id to point at.
131
+ 2. Then mark the row, in place, in its own `requirements-<slug>.yaml`:
132
+ `status: withdrawn` and `withdrawn_by: DEC-NNN`. Everything else on the row is left as it was — it
133
+ is a record of what was promised, not a draft.
134
+ 3. Withdraw **down the chain in the same pass**: an `FR` under a withdrawn `CAP`, an `NFR` under it,
135
+ a `UC` satisfying a withdrawn `FR`. `withdrawn-recorded` reports a live row left hanging off a
136
+ withdrawn one, and that finding is the whole point — a half-withdrawn chain still promises half of
137
+ something.
138
+ 4. The id is **spent**. `id-allocated-once` counts a withdrawn row, so the number is never handed to
139
+ anything else.
140
+
141
+ What you MUST NOT do: delete the row, renumber around the gap, or edit a `DEC-` that served it.
142
+ `corpus-guide.md` § *A withdrawn promise STAYS in the registry* owns the rule.
143
+
122
144
  ## Rules
123
145
 
124
146
  - You MUST NOT write a second PRD for an area that already has one. The reader test decides, and its answer
@@ -43,7 +43,7 @@ before an earlier one lands content in a file that the earlier item is about to
43
43
  | # | Probe | Old shape | New home |
44
44
  |---|---|---|---|
45
45
  | 1 | `.control/registry/requirements.yaml` exists | one file for `BG` · `CAP` · `FR` · `NFR` · `UJ` | `goals.yaml` (`BG`) · `requirements-<slug>.yaml` per PRD (`CAP` · `FR` · `NFR` · `UJ`) |
46
- | 2 | `specs.yaml` has `W<n>` ids, or `epics:` / `stories:` keys | pre-rename plan | re-cut through `wdi-build` — **not this skill**; report it and move on |
46
+ | 2 | a spec with a `W<n>` id, or carrying `epics:` / `stories:`, that is **not `closed`** | pre-rename plan | flattened in place — **this skill's**, and § *The pre-rename plan* below is the mapping. A `closed` spec is left alone: `Corpus.tickets()` reads it correctly and its ticket files are already allowed to be gone |
47
47
  | 3 | `brief.md` has `## Executive Summary`, `## Vision`, `## Assumptions`, or `## Prerequisites`; or `## Goals` lists `BG-` statements | 14-section brief | 8 sections: `Why` merges Summary + Vision; Goals is a pointer, its rows in `goals.yaml`; Assumptions → `questions/assumptions.md`; Prerequisites → `questions/external.md` |
48
48
  | 4 | any `prd.md` has a section **named** Document Purpose, Glossary, Non-Goals, Open Questions, or Assumptions Index — under whatever number that kit gave it — or `**Proof of done:**` under a feature | 12-section PRD with `FR` blocks | 7 sections; `FR`/`NFR` text → `requirements-<slug>.yaml`, the PRD keeps `Realizes:` ids; Glossary → `product-glossary.md`; §8/§9 → `questions/`; §1 becomes a delta |
49
49
  | 5 | any `SRS-<pc>.md` `## UC Catalogue` has `\| UC-` rows | catalogue copied from `usecases.yaml` | one pointer line; the rows live in `usecases.yaml` |
@@ -53,7 +53,8 @@ before an earlier one lands content in a file that the earlier item is about to
53
53
  | 9 | `.what-rendered/` or `.how-rendered/` absent | no reader's tree yet | born by `render` |
54
54
  | 10 | any `.md` outside `.constitution/` cites `.control/generated/brief.md`, `blueprint.md`, or `prd-<slug>.md` | a pointer at a page that moved | `.what-rendered/_product-brief/brief.md` · `.how-rendered/blueprint.md` · `.what-rendered/_prd/<slug>/prd.md` — `cites-resolve` fails until it is repointed |
55
55
  | 11 | `docs/agents/issue-tracker.md` does not contain the words `seeded by ``wdi-method``` | the engines' config as `/setup-matt-pocock-skills` wrote it: everything in `.scratch/` with no registry behind it, `specs.yaml` never mentioned | the method's own answer. **`npx wdi-method engines --fix`** rewrites it and keeps the old text as `issue-tracker.md.bak`. Two of four live repos still had upstream's, which is why their tickets landed wherever the engine guessed |
56
- | 12 | any `spec_folder` in `specs.yaml` is outside `.scratch/`, or its leaf does not begin with the spec's own id | spec folders under `_bmad-output/specs/`, leaf named freely — four repos wrote it four ways, one of them all four inside itself | `.scratch/<spec-id>-<slug>/`. Move the directory, rewrite the `spec_folder` row, then repoint every cite — `cites-resolve` is red until you do, and it is how you find them all |
56
+ | 12 | a spec that is **not `closed`** has a `spec_folder` outside `.scratch/`, or a leaf that does not begin with its own id | spec folders under `_bmad-output/specs/`, leaf named freely — four repos wrote it four ways, one of them all four inside itself | `.scratch/<spec-id>-<slug>/`. Move the directory, rewrite the `spec_folder` row, then repoint every cite — `cites-resolve` is red until you do, and it is how you find them all. **A `closed` spec is left alone**: its folder is a record, moving it churns finished work, and its ticket files are already allowed to be gone |
57
+ | 13 | `validate.py` reports `refs-resolve` on a `BG-` · `CAP-` · `FR-` · `NFR-` · `UC-` id — the finding itself is the probe | the row was **deleted** when the product stopped promising it, leaving every `DEC-` that served it pointing at nothing | the row comes BACK, marked `status: withdrawn` with `withdrawn_by` naming the decision that took it. Recover its text from git rather than retyping it — `git log -S"id: CAP-8" -- .control/registry/` finds the commit that held it. You MUST NOT edit the references instead: a `DEC-` records what happened, and it did serve that promise at the time. `corpus-guide.md` § *A withdrawn promise STAYS in the registry* owns the rule, `wdi-product` the procedure |
57
58
 
58
59
  Items 11 and 12 are the engines' half of a version jump, and they come FIRST when both are hit:
59
60
  item 11 writes where a spec's files belong, item 12 moves them there. Doing 12 first means moving
@@ -81,6 +82,25 @@ and its markup: when it holds `: ` or `#` or starts with a quote, wrap the value
81
82
  owner names it. When every row has moved, delete `requirements.yaml`; `id-allocated-once` fails if a
82
83
  row was copied instead of moved.
83
84
 
85
+ **2 — the pre-rename plan.** A spec still shaped `epics: → stories:` is flattened into the one
86
+ `tickets:` list the validator reads. Every part of this is a mapping; nothing here is a judgment, and
87
+ nothing is invented:
88
+
89
+ | Old | New |
90
+ |---|---|
91
+ | `waves:` as the file's top-level key | `specs:`. The rows below it do not otherwise change shape from this rename alone |
92
+ | the spec's `W<n>` id | **unchanged.** It is a retired alias, and every memlog, `DEC-`, report and RTM row that names it MUST keep resolving. Renaming it to `SPEC-<n>` is the one thing this step MUST NOT do |
93
+ | `epics:` → `stories:` nesting | one flat `tickets:` list, in the order the stories appear, epic by epic. The `epics` level is repealed: it grouped rows and bought nothing |
94
+ | a story's own id (`"1"`, `"1-2"`) | `<spec-id>-<NN>`, renumbered from `01` in dependency order — `W3-01`, `W3-02`. A story id only ever promised uniqueness inside one epic, and this method keys tickets globally |
95
+ | `depends_on: ["1-1"]` on a story | `blocked_by: [W3-01]` — the same edge, the new key, pointing at the new id |
96
+ | `{spec_folder}/stories/1-2-<slug>.md` | `{spec_folder}/issues/<NN>-<slug>.md`, the `<NN>` matching the ticket's new id. `git mv`, so the file's history follows it |
97
+ | a story's `touches:` | kept as `touches:`. A story never carried `component:`, and this step MUST NOT invent one — `wdi-init` owns that field |
98
+
99
+ Two things stay untouched even here. The **status** of each ticket is read from its file and MUST NOT
100
+ be copied into `specs.yaml` — `ticket-status-one-home` is what refuses that. And a `closed` spec is
101
+ skipped whole: `validate.py` already flattens it in memory on every run, so rewriting the file changes
102
+ no reading and only churns the record of finished work.
103
+
84
104
  Run `validate.py --check`. Green here means the registry is whole before any document starts pointing
85
105
  at it.
86
106
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wdi-method",
3
- "version": "0.6.8",
3
+ "version": "0.6.15",
4
4
  "description": "WDI Method — software delivery method that wraps BMad",
5
5
  "type": "module",
6
6
  "bin": {