wdi-method 0.6.8 → 0.6.18

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/CHANGELOG.md ADDED
@@ -0,0 +1,213 @@
1
+ # Changelog
2
+
3
+ What changed in `wdi-method`, newest first. Every entry says what it means for a repo that already has
4
+ the method installed — most often nothing beyond `npx wdi-method@latest update`.
5
+
6
+ A version here is a **git tag**. `npm publish` is a separate, deliberate step (see
7
+ [`CONTRIBUTING.md`](CONTRIBUTING.md)), so some versions exist as a tag and a GitHub release without ever
8
+ having been served from npm. The registry always holds the newest published version, and a published
9
+ version contains every fix below it.
10
+
11
+ ---
12
+
13
+ ## 0.6.18 — 2026-09-08
14
+
15
+ - **This file.** The version history was only readable as `git log`, which is the wrong place to look for
16
+ it when you are deciding how carefully to read an `update` diff.
17
+ - **The README had the validator count wrong.** It said twenty-seven; `validate.py` has run twenty-nine
18
+ named checks since `withdrawn-recorded` arrived in 0.6.14. It also now says what the validators treat as
19
+ corpus, which 0.6.17 changed.
20
+
21
+ ## 0.6.17 — 2026-09-08
22
+
23
+ - **The corpus walker honours `.gitignore`.** The walk pruned only a hardcoded folder list, so a vendored
24
+ dependency tree inside an ignored folder was read as this product's corpus — one repo saw 172
25
+ `cites-resolve` findings from source code it had deliberately ignored. The ignored set is now computed
26
+ once per run (`git ls-files --others --ignored --exclude-standard --directory`), and the hardcoded list
27
+ stays as the fallback for a checkout that is not a git repository. `.gitignore` still cannot hide real
28
+ corpus: `corpus-in-git` fails when a folder the method commits is ignored.
29
+ - **`timeline.py` no longer crashes on a capability that has tickets.** `cap_tickets()` returns
30
+ `(spec, ticket)` pairs and the span helper read each item as a ticket row, so generating the timeline
31
+ raised `AttributeError` and `/wdi-report progress` could not publish at all.
32
+
33
+ **For a consuming repo:** `update`, then regenerate — a `timeline.md` or `report.md` written before this
34
+ is dated and must not be read as current.
35
+
36
+ ## 0.6.16 — 2026-09-07
37
+
38
+ - **A superseded mandate still stands for what it accepted.** `mandate-accept` asked a question about the
39
+ past — was there a mandate on the day this decision was accepted — and answered it with the mandate's
40
+ status *today*. Superseding a mandate, which `wdi-autopilot`'s own instructions call for when a run
41
+ ends, therefore turned every decision that run had accepted permanently red; and an `applied` decision
42
+ is frozen, so there was no repair the corpus was allowed to make. The check now reads the mandate's
43
+ state at the delegation date, and bounds the window by the **earlier** of `mandate.expires` and the date
44
+ of the decision that superseded it — a revoked delegation ends when it was revoked, whatever its
45
+ `expires` still says. `superseded_by` is read from the decision file's frontmatter as well as the
46
+ registry row, because the template puts it there.
47
+
48
+ ## 0.6.15 — 2026-09-07
49
+
50
+ - The dangling-promise finding carries its own repair. `withdrawn-recorded` named the problem without
51
+ saying what to write, and a finding nobody can act on is a finding that gets switched off.
52
+
53
+ ## 0.6.14 — 2026-09-07
54
+
55
+ - **New validator `withdrawn-recorded`:** a promise that was withdrawn stays in the registry and says who
56
+ withdrew it. Deleting the row loses the fact that it was ever promised.
57
+
58
+ ## 0.6.13 — 2026-09-07
59
+
60
+ - A story id already scoped to its wave is not scoped twice — the pre-0.6 rename was double-prefixing ids
61
+ that had been written correctly.
62
+
63
+ ## 0.6.12 — 2026-09-07
64
+
65
+ - Flattening a pre-rename wave is `wdi-upgrade`'s work, not a re-cut. The skill was inventing a new
66
+ decomposition where it should have been moving one that already existed.
67
+
68
+ ## 0.6.11 — 2026-09-07
69
+
70
+ - `update` leaves a closed spec's folder where it is. Moving a finished spec's folder rewrote history for
71
+ nothing.
72
+
73
+ ## 0.6.10 — 2026-09-07
74
+
75
+ - `update` reports the two things `wdi-upgrade` now has to move, instead of leaving them for a reader to
76
+ discover.
77
+
78
+ ## 0.6.9 — 2026-09-07
79
+
80
+ - The TUI refuses a missing engine too, in step 2's place. The CLI had checked since 0.6.8; the
81
+ interactive path had not, and that is the path a first install actually takes.
82
+
83
+ ## 0.6.8 — 2026-09-07
84
+
85
+ - **The G5 engines are the repo's own, invocable, and BMad's replacements are locked out.** `install` and
86
+ `update` refuse until `to-spec`, `to-tickets`, `implement`, `tdd`, `code-review` and `domain-modeling`
87
+ are in the repo; `wdi-build` invokes them itself, so `wdi-autopilot` can finish a spec unattended; and
88
+ thirteen BMad skills retired at G5 are denied model invocation. A spec also gets one predefined home,
89
+ `.scratch/<spec-id>-<slug>/`.
90
+
91
+ **For a consuming repo:** the largest jump in the 0.6 line. The README section *"Moving a repo from 0.6.7
92
+ or earlier to 0.6.8"* walks all four changes, and `npx wdi-method engines --fix` repairs what can be
93
+ repaired without touching anything you wrote.
94
+
95
+ ## 0.6.7 — 2026-09-06
96
+
97
+ - `plan-dates` reported nothing at all, because it raised before it could report; and a legacy story file
98
+ was looked for in the wrong folder.
99
+ - A closed pre-rename wave stays visible to the RTM instead of disappearing from it.
100
+
101
+ ## 0.6.6 — 2026-09-06
102
+
103
+ - **New validator `corpus-in-git`:** a folder the method commits MUST NOT be gitignored. A corpus git
104
+ cannot see is a corpus the clone lacks.
105
+ - The git rule is stated where the bootstrap reads it, not only where the detail lives.
106
+
107
+ ## 0.6.5 — 2026-09-05
108
+
109
+ - The README said three things that 0.6.2 through 0.6.4 had made untrue.
110
+
111
+ ## 0.6.4 — 2026-09-05
112
+
113
+ - The test suite depended on the author's machine, and 0.6.3 failed CI because of it.
114
+
115
+ ## 0.6.3 — 2026-09-05
116
+
117
+ - The ticket engines are required, and their config ships pre-answered — so `/setup-matt-pocock-skills` is
118
+ not part of getting started.
119
+ - *"What now"* is answered after install and after update, in the README and in `wdi-help`.
120
+ - Three real migrations, none of them `wdi-upgrade`'s: a mechanical rename, a printed warning, and a skill
121
+ that heals its own ledger.
122
+
123
+ ## 0.6.2 — 2026-09-05
124
+
125
+ - **New skill `wdi-autopilot`:** one mandate, then every `FR` in scope delivered unattended, every answer
126
+ recorded in one ledger. It stops at one of three places — done, at capacity, or blocked — and one run
127
+ lands as one pull request. An adversarial review closed ten findings before release, two of them in the
128
+ validator.
129
+
130
+ **For a consuming repo:** `update` renames a pre-0.6.2 ledger to `autopilot-<mandate-id>.md`. The content
131
+ is never rewritten.
132
+
133
+ ## 0.6.1 — 2026-09-03
134
+
135
+ - **New skill `wdi-explain-to-me`:** a decision briefing the owner reads, written in the owner's language.
136
+
137
+ ## 0.6.0 — 2026-09-03
138
+
139
+ - **Two trees, one home per fact.** The working set points and the rendered page answers, so a human gets
140
+ a complete document while the corpus keeps no copies. This is the release the validators' rule against
141
+ comparing two copies of one fact comes from.
142
+ - The units are renamed: wave and story are retired, `waves.yaml` becomes `specs.yaml`, and size decides
143
+ the spec.
144
+ - The BMad engine layer below G5 is retired in favour of `to-spec`, `to-tickets` and `implement`;
145
+ `wdi-blueprint` wraps `domain-modeling` as its engine.
146
+ - UX runs and **lands** at G2, breaking a deadlock where the container was never in its path.
147
+ - A decision's first home is the document it governs, and no `DEC-` is mandatory.
148
+ - The corpus is present tense, and stale is not a finding.
149
+ - `wdi-review` stopped being a treadmill; gate *shape* and checklist *length* became separate knobs.
150
+
151
+ **For a consuming repo:** run `wdi-upgrade` after updating, before any other skill. The validators read
152
+ the new shape, and a corpus half in the old one answers them wrongly. The untagged 0.5.13 and 0.5.14
153
+ bumps — the review panel dropping its two-CLI-family requirement, and the platform picker aligning with
154
+ BMad — are included here.
155
+
156
+ ## 0.5.12 — 2026-08-22
157
+
158
+ - Blueprint actor headings lose the orphaned separator a nameless actor left behind.
159
+
160
+ ## 0.5.11 — 2026-08-22
161
+
162
+ - Version bump only; no method change.
163
+
164
+ ## 0.5.10 — 2026-08-19
165
+
166
+ - The inventory engine littered the room it was asked to read.
167
+
168
+ ## 0.5.9 — 2026-08-19
169
+
170
+ - The package ships no stack at all: a skeleton, and a skill that writes it. A stack baked into a generic
171
+ package is an assumption every consumer inherits.
172
+
173
+ ## 0.5.8 — 2026-08-19
174
+
175
+ - `inventory.py`: the engine stays in the package, the stack moves to the room that owns it.
176
+
177
+ ## 0.5.7 — 2026-08-19
178
+
179
+ - The method assumed a stack, and a path was only the visible half of that assumption.
180
+
181
+ ## 0.5.6 — 2026-08-19
182
+
183
+ - The guard a real component actually needed, which the retired V24 could never have been.
184
+
185
+ ## 0.5.5 — 2026-08-19
186
+
187
+ - V24 was unsatisfiable for every consumer, and the language sweep had missed nineteen strings.
188
+
189
+ ## 0.5.4 — 2026-08-19
190
+
191
+ - `portability.md` was right about the path and wrong about everything else.
192
+
193
+ ## 0.5.3 — 2026-08-19
194
+
195
+ - The 0.5.0 split was unreachable for the repos that needed it.
196
+ - Python bytecode is kept out of the tarball — 0.5.2 shipped 123 kB of it, and a `.pyc` embeds the
197
+ absolute path it was compiled from.
198
+
199
+ ## 0.5.2 — 2026-08-19
200
+
201
+ - **The direction was reversed:** a method change is authored *here* and proven against a fixture corpus
202
+ the registry scripts actually run against, instead of being promoted out of a live product. `promote`
203
+ became a rescue tool that refuses to run without `--rescue`.
204
+ - An external audit of 0.5.0 found the `AGENTS.md` block calling the whole kit non-binding.
205
+ - Bahasa Indonesia is swept out of the generic package; the installer's interface is English.
206
+ - Included here: `.constitution/` becoming exactly two folders, `method/` and `project/` (0.5.0), and the
207
+ 0.5.1 republish of a kit that had shipped pre-fix overlay content.
208
+
209
+ ## 0.3.0 — 2026-08-18
210
+
211
+ - First public release: an interactive installer, a replaceable `AGENTS.md` method block, two language
212
+ settings asked separately, and `.constitution/project/` as the custom room `update` never overwrites and
213
+ `promote` never publishes.
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
 
@@ -380,12 +379,17 @@ A `DEC-` freezes when it is applied. A change of mind produces a new one; it nev
380
379
 
381
380
  ## The mechanical half
382
381
 
383
- `validate.py` runs twenty-seven named validators — `goal-has-fr`, `cites-resolve`, `no-cycles`,
382
+ `validate.py` runs twenty-nine named validators — `goal-has-fr`, `cites-resolve`, `no-cycles`,
384
383
  `id-allocated-once`, and the rest, each named for the thing it checks — over the registries and the
385
384
  corpus, and `inventory.py` derives the three inventories from code and reports the difference against the
386
385
  plan without patching either side. There is no validator that compares two copies of one fact, because
387
386
  the corpus keeps no copies.
388
387
 
388
+ **The corpus is what git tracks.** A vendored dependency tree inside a gitignored folder is not this
389
+ product's writing, and since 0.6.17 the walk skips what the repo ignores. The other half of that rule is
390
+ `corpus-in-git`: a folder the method commits MUST NOT be ignored, so `.gitignore` cannot be used to quiet
391
+ a finding about a file that really is yours.
392
+
389
393
  The validators exist because prose that nothing checks is prose that gets contradicted by the first
390
394
  person in a hurry. Every one of them also states **the state in which it does not apply** — a rule that
391
395
  demands a trace before the trace can exist is a rule that gets switched off, and a validator nobody
@@ -514,6 +518,9 @@ Open an [issue](https://github.com/wiradigitalid/wdi-method/issues) for a bug or
514
518
  [`CONTRIBUTING.md`](CONTRIBUTING.md) before sending a pull request — it explains where a change belongs,
515
519
  how versioning works here, and what to check before publishing.
516
520
 
521
+ [`CHANGELOG.md`](CHANGELOG.md) is what changed in each version, and what each change means for a repo
522
+ that already has the method installed. Read it before an `update` that crosses more than a patch.
523
+
517
524
  ## License
518
525
 
519
526
  MIT — see [LICENSE](LICENSE). Requires Node 20+ and [uv](https://docs.astral.sh/uv/) for the Python
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,47 @@ 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
+ **What git ignores is not corpus.** A vendored upstream checkout kept for reading, a scratch download, a
67
+ build cache — if the product excludes it from git it is in no clone, nobody curates it, and the validators
68
+ do not read it. The other half of that rule is `corpus-in-git`: a folder the method itself keeps MUST NOT be
69
+ excluded, and the two lock together — material is either in git and checked, or ignored and not corpus. What
70
+ `.gitignore` MUST NOT be used for is quieting a finding about a file that really is this product's.
71
+
72
+ ### A withdrawn promise STAYS in the registry
73
+
74
+ A `BG` · `CAP` · `FR` · `NFR` · `UC` the product stops promising is marked, never deleted:
75
+
76
+ ```yaml
77
+ - id: CAP-8
78
+ title: "Publish an order as a public page"
79
+ status: withdrawn
80
+ withdrawn_by: DEC-026
81
+ ```
82
+
83
+ **Why the row stays.** One repo deleted two withdrawn capabilities and paid for it in twelve
84
+ `refs-resolve` findings: eight `DEC-` rows still named them in `serves:`, and six of those eight
85
+ genuinely served them at the time. The other repair — editing those decisions — is refused by the
86
+ section above: a `DEC-` is a record of what happened, and a retired name inside one is a fact about
87
+ the past.
88
+
89
+ **So a withdrawn row is read two ways, and both matter.** It is still **defined**: every old
90
+ reference resolves, and `id-allocated-once` still refuses the number to anything else — an id is
91
+ allocated once, withdrawal included. It is no longer **promised**: no `UC` is owed, no ticket, no RTM
92
+ row, and `promise_progress` is not dragged down by something nobody promises any more.
93
+
94
+ **Two rules keep it honest**, and `withdrawn-recorded` is what enforces both:
95
+
96
+ - `withdrawn_by` MUST name a `DEC-` that exists. Retiring an id is decision-worthy on this method's
97
+ own terms — the **ID chain** row under § *Landing that MUST be confirmed first* says so outright —
98
+ and without the pointer `withdrawn` is only a word that quiets a validator.
99
+ - A live row MUST NOT hang off a withdrawn one. An `FR` under a withdrawn `CAP` still promises
100
+ something whose capability nobody promises: withdraw it too, or move it under something live.
101
+ Withdrawal that takes half a chain with it silently is worse than the deletion it replaced, because
102
+ deletion at least went red.
103
+
104
+ `wdi-product` owns the edit, because it owns the row. The withdrawal itself goes through
105
+ `wdi-decision` first — the `DEC-` is what `withdrawn_by` points at.
106
+
66
107
  ## Who lands what
67
108
 
68
109
  There is no separate placement skill. A skill lands the output of the layer **it owns**, and the landing is