wdi-method 0.6.30 → 0.6.31
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 +68 -0
- package/README.md +3 -0
- package/bin/wdi-method.js +121 -14
- package/kit/.constitution/method/document/architecture-guide.md +217 -209
- package/kit/.constitution/method/document/corpus-guide.md +522 -517
- package/kit/.constitution/method/document/decision-guide.md +236 -216
- package/kit/.constitution/method/document/delivery-flow-guide.md +20 -0
- package/kit/.constitution/method/document/prd-guide.md +245 -245
- package/kit/.constitution/method/document/templates/design-system.md +96 -66
- package/kit/.constitution/method/document/templates/experience.md +62 -0
- package/kit/.constitution/method/document/templates/structure-codebase.md +131 -129
- package/kit/.constitution/method/document/templates/ux.md +78 -76
- package/kit/.constitution/method/document/ux-guide.md +161 -115
- package/kit/.constitution/method/method-glossary.md +3 -0
- package/kit/.constitution/method/scripts/validate.py +3310 -3200
- package/kit/.constitution/method/structure-guide.md +204 -202
- package/kit/.constitution/method/why/artifact-map.md +158 -157
- package/kit/skills/wdi-blueprint/SKILL.md +271 -264
- package/kit/skills/wdi-component/SKILL.md +179 -174
- package/kit/skills/wdi-decision/SKILL.md +206 -203
- package/kit/skills/wdi-help/SKILL.md +127 -125
- package/kit/skills/wdi-init/SKILL.md +9 -4
- package/kit/skills/wdi-problem/SKILL.md +114 -108
- package/kit/skills/wdi-product/SKILL.md +167 -162
- package/kit/skills/wdi-reconcile/SKILL.md +170 -169
- package/kit/skills/wdi-upgrade/SKILL.md +234 -215
- package/kit/skills/wdi-ux/SKILL.md +187 -169
- package/kit-overlay/AGENTS.md +3 -1
- package/package.json +1 -1
- package/scaffold/.control/registry/index.yaml +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,74 @@ version contains every fix below it.
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [0.6.31] - 2026-09-28
|
|
14
|
+
|
|
15
|
+
A patch by the owner's choice, though it carries behaviour changes — read the whole entry.
|
|
16
|
+
|
|
17
|
+
**Read this before you update.** Two validator rules are new or stricter, so a repo that is green today
|
|
18
|
+
can come back red. Most of it is something that was already wrong and went unchecked; two parts are new
|
|
19
|
+
obligations — the ban on citing a UX run from the corpus, and `landed_from` on every landed UX document.
|
|
20
|
+
|
|
21
|
+
### Changed: behaviour
|
|
22
|
+
|
|
23
|
+
- **New validator `ux-landed`, checked per run.** Every landed UX document names the run file(s) it came
|
|
24
|
+
from in a new frontmatter field, `landed_from`. Once Product Components exist, every active run
|
|
25
|
+
`DESIGN.md` / `EXPERIENCE.md` in `_bmad-output/ux/` MUST be named by some landed document — a
|
|
26
|
+
component's, or `.what/experience.md` — and `.what/` and `.how/` MUST NOT cite the run anywhere else.
|
|
27
|
+
`landed_from` is provenance: the run may be deleted later and it stays valid. `.control/decisions/` may
|
|
28
|
+
still cite the run. A run file without frontmatter is recognised by its filename. UX stays optional: the rule
|
|
29
|
+
is silent with no run, silent before any component exists, silent on a run marked `superseded` or
|
|
30
|
+
`withdrawn`, and no `mode` or `risk_accepted` value changes it.
|
|
31
|
+
- **`container-built` understands a product split across repositories.** A new optional field `repo:` on
|
|
32
|
+
a container names the repository its code lives in when that is NOT this one. Such a container keeps
|
|
33
|
+
`built: true` and its C4 place, but gets no heading in this repo's code map — the template, the
|
|
34
|
+
structure guide, and the validator now say the same thing. Absent `repo:` means this repository, so a
|
|
35
|
+
single-repo product sees no change. `repo:` naming this repository — compared with the `origin`
|
|
36
|
+
remote; without one the comparison is reported as skipped — or `repo:` on a `built: false` container,
|
|
37
|
+
is red.
|
|
38
|
+
- **`cites-resolve` recognises installed skill copies under every host.** `.<host>/skills/bmad-*` and
|
|
39
|
+
`.<host>/skills/wdi-*` are skipped by pattern instead of a three-host list, so a repo installed for
|
|
40
|
+
Kiro, Cline, Trae, or any other supported host no longer carries permanent findings on files it may not
|
|
41
|
+
edit. `.work/` is skipped too — scratch is not authority, so a pasted validator line is not a claim.
|
|
42
|
+
- **`update` records what `wdi-upgrade` still owes.** The probes now write `upgrade_pending` into
|
|
43
|
+
`.control/wdi-method.yaml`, numbered by the rows of `wdi-upgrade`'s checklist, and the field is absent
|
|
44
|
+
when nothing is owed. `npx wdi-method upgrade-check` re-probes at any time and rewrites it (exit 1 while
|
|
45
|
+
anything is pending); `wdi-upgrade` ends by running it, and `wdi-help` routes on the field instead of on
|
|
46
|
+
whether `update` "just ran". The probes no longer report the method's own skill text under a non-Claude
|
|
47
|
+
host, or a scratch paper, as stale product content.
|
|
48
|
+
|
|
49
|
+
### Changed: method
|
|
50
|
+
|
|
51
|
+
- **UX has a product level.** New `.what/experience.md` (template `templates/experience.md`) holds the
|
|
52
|
+
experience every component keeps — foundation, information architecture, voice and tone, the flow map,
|
|
53
|
+
journeys that cross components, shared edge cases. `design-system.md` grows the build side: state
|
|
54
|
+
patterns, interaction primitives, how accessibility is met, surfaces that are not screens.
|
|
55
|
+
`ux-guide.md` § *Product level* maps each `bmad-ux` section to one of the two, and both land at G2
|
|
56
|
+
because neither path contains a `<pc>`. A flow zoom-in lands with the component owning its screens,
|
|
57
|
+
never with the owner of a shared composite drawn inside it.
|
|
58
|
+
- **Completing `touches` on an applied decision is allowed**, append-only, for a file the applying commit
|
|
59
|
+
really changed within what the Decision says. A change outside it is a finding, not a trace.
|
|
60
|
+
- **Gates get recorded.** `wdi-problem`, `wdi-product`, `wdi-blueprint`, and `wdi-component` end by asking
|
|
61
|
+
the owner whether their gate passed and write `gates_passed` / `g4_passed` only on an explicit yes.
|
|
62
|
+
Skills that need a passed gate read the record and ask when it is missing; `wdi-reconcile` reports
|
|
63
|
+
downstream work whose gate was never recorded. `delivery-flow-guide.md` § *Recording a gate that passed*.
|
|
64
|
+
- **Session papers from outside tools live in `.work/<tool>/<slug>/`**, distilled into `DEC-` / memlog /
|
|
65
|
+
`.control/reports/` and deleted when the session closes. `corpus-guide.md` no longer asks for a `DEC-`
|
|
66
|
+
before scratch is deleted — the two guides disagreed.
|
|
67
|
+
|
|
68
|
+
**What a repo that already has the method installed does about it.** Run
|
|
69
|
+
`npx wdi-method@latest update --yes`, then read the `upgrade` line or `upgrade_pending`. Expect, where
|
|
70
|
+
they apply: cross-component UX sections parked in `design-system.md` (item 15), SRS or landed UX
|
|
71
|
+
documents citing the UX run (item 16), containers whose code lives in another repository (item 17 —
|
|
72
|
+
fill `repo:` there, drop it where it names this repo, and re-run `wdi-init` intent `structure`), and
|
|
73
|
+
landed UX documents with no `landed_from` (item 18). Gate skills now record `gates_passed`; a repo whose
|
|
74
|
+
gates passed before that is asked once per gate by `wdi-upgrade`, and nothing is written without a yes.
|
|
75
|
+
|
|
76
|
+
**`wdi-upgrade`:** needed when `upgrade_pending` lists anything — items 15–18 are new in this version —
|
|
77
|
+
or when `wdi-help` shows a passed gate that was never recorded. Not needed otherwise.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
13
81
|
## [0.6.30] - 2026-09-27
|
|
14
82
|
|
|
15
83
|
A patch. No behaviour changes.
|
package/README.md
CHANGED
|
@@ -73,6 +73,9 @@ Inside your coding agent, run:
|
|
|
73
73
|
```
|
|
74
74
|
`wdi-help` reads `.control/registry/` and tells you the gate your project is at, the open specs, and the next skill, without guessing from the conversation.
|
|
75
75
|
|
|
76
|
+
### Updating Later: Do I Need `/wdi-upgrade`?
|
|
77
|
+
You never have to work it out from the version number. `npx wdi-method@latest update` checks your repo's content for anything still in an older shape and writes what it found into `upgrade_pending` in `.control/wdi-method.yaml` — absent means nothing is owed. `/wdi-help` reads that field and tells you to run `/wdi-upgrade` first when it is there. `npx wdi-method upgrade-check` re-checks at any time. Every [`CHANGELOG.md`](CHANGELOG.md) entry also ends with a `wdi-upgrade: needed / not needed` line.
|
|
78
|
+
|
|
76
79
|
---
|
|
77
80
|
|
|
78
81
|
## Three Workflow Options
|
package/bin/wdi-method.js
CHANGED
|
@@ -170,6 +170,7 @@ function usage() {
|
|
|
170
170
|
update [dir] update (TUI unless --yes)
|
|
171
171
|
verify [dir]
|
|
172
172
|
engines [dir] [--fix] report the six engines, their invocation state, and the BMad G5 ban
|
|
173
|
+
upgrade-check [dir] re-probe what wdi-upgrade still owes; rewrites upgrade_pending (exit 1 if any)
|
|
173
174
|
promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
|
|
174
175
|
|
|
175
176
|
--yes non-interactive
|
|
@@ -214,7 +215,7 @@ function parseArgs(argv) {
|
|
|
214
215
|
return args;
|
|
215
216
|
}
|
|
216
217
|
const first = rest[0];
|
|
217
|
-
if (["install", "update", "verify", "promote", "engines"].includes(first)) {
|
|
218
|
+
if (["install", "update", "verify", "promote", "engines", "upgrade-check"].includes(first)) {
|
|
218
219
|
args.cmd = rest.shift();
|
|
219
220
|
} else if (first.startsWith("-")) {
|
|
220
221
|
args.cmd = "wizard";
|
|
@@ -1266,6 +1267,43 @@ function seedEmptyLayers(target, { first }) {
|
|
|
1266
1267
|
}
|
|
1267
1268
|
}
|
|
1268
1269
|
|
|
1270
|
+
// What `wdi-upgrade` still owes, written where it is committed. The summary line alone was the only
|
|
1271
|
+
// record, so a repo whose owner missed that one screen could not find out again — and `wdi-help` can
|
|
1272
|
+
// only route to `wdi-upgrade` from something it can read. Absent when nothing is pending: an empty
|
|
1273
|
+
// list would read as a question nobody has to answer.
|
|
1274
|
+
const PENDING_HEADER = "# What wdi-upgrade still has to move. Re-probed by `npx wdi-method upgrade-check`; absent = nothing.";
|
|
1275
|
+
|
|
1276
|
+
function pendingBlock(items) {
|
|
1277
|
+
if (!items.length) return "";
|
|
1278
|
+
return `${PENDING_HEADER}\nupgrade_pending:\n${items.map((i) => ` - ${JSON.stringify(i)}\n`).join("")}`;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// Re-probe and rewrite ONLY the pending block, leaving the rest of the stamp as `update` wrote it.
|
|
1282
|
+
// This is how `wdi-upgrade` closes: the field disappears when the probes come back clean.
|
|
1283
|
+
function upgradeCheck(target) {
|
|
1284
|
+
const file = path.join(target, ".control", "wdi-method.yaml");
|
|
1285
|
+
if (!fs.existsSync(file)) die("no .control/wdi-method.yaml — the method is not installed here");
|
|
1286
|
+
const items = pendingUpgrades(target);
|
|
1287
|
+
let text = fs.readFileSync(file, "utf8")
|
|
1288
|
+
.replace(/^# What wdi-upgrade still has to move\..*\r?\n/m, "")
|
|
1289
|
+
.replace(/^upgrade_pending:\r?\n(?:[ \t]+- .*\r?\n)*/m, "");
|
|
1290
|
+
const block = pendingBlock(items);
|
|
1291
|
+
if (block) {
|
|
1292
|
+
text = /^installed_at:/m.test(text)
|
|
1293
|
+
? text.replace(/^installed_at:/m, `${block}installed_at:`)
|
|
1294
|
+
: `${text.replace(/\s*$/, "\n")}${block}`;
|
|
1295
|
+
}
|
|
1296
|
+
fs.writeFileSync(file, text, "utf8");
|
|
1297
|
+
console.log("");
|
|
1298
|
+
if (!items.length) {
|
|
1299
|
+
ok("nothing pending — wdi-upgrade owes nothing here");
|
|
1300
|
+
return 0;
|
|
1301
|
+
}
|
|
1302
|
+
console.log(` upgrade ${items.length} item${items.length === 1 ? "" : "s"} still in the OLD shape — run the wdi-upgrade skill`);
|
|
1303
|
+
for (const item of items) console.log(` ${DIM}·${RESET} ${item}`);
|
|
1304
|
+
return 1;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1269
1307
|
function writeStamp(target) {
|
|
1270
1308
|
const control = path.join(target, ".control");
|
|
1271
1309
|
if (!fs.existsSync(control)) return;
|
|
@@ -1293,6 +1331,8 @@ function writeStamp(target) {
|
|
|
1293
1331
|
lines.push("engines:");
|
|
1294
1332
|
lines.push(" source: none # none in this repo — G5 cannot run until they are installed");
|
|
1295
1333
|
}
|
|
1334
|
+
const pending = pendingBlock(pendingUpgrades(target));
|
|
1335
|
+
if (pending) lines.push(pending.trimEnd());
|
|
1296
1336
|
lines.push(`installed_at: ${today()}`);
|
|
1297
1337
|
lines.push("");
|
|
1298
1338
|
const stamp = lines.join("\n");
|
|
@@ -1427,17 +1467,17 @@ function pendingUpgrades(target) {
|
|
|
1427
1467
|
});
|
|
1428
1468
|
};
|
|
1429
1469
|
const items = [];
|
|
1430
|
-
if (has(".control", "registry", "requirements.yaml")) items.push("requirements.yaml → goals.yaml + requirements-<slug>.yaml");
|
|
1470
|
+
if (has(".control", "registry", "requirements.yaml")) items.push("#1 requirements.yaml → goals.yaml + requirements-<slug>.yaml");
|
|
1431
1471
|
// The file the engines actually read. `/setup-matt-pocock-skills` writes its own answer here — no
|
|
1432
1472
|
// `specs.yaml`, no predefined path — and `seedAgentDocs` will not overwrite a file the product owns,
|
|
1433
1473
|
// so without this probe the repo never learns why its tickets scatter.
|
|
1434
1474
|
if (has("docs", "agents", "issue-tracker.md")
|
|
1435
1475
|
&& !read("docs", "agents", "issue-tracker.md").includes("seeded by `wdi-method`")) {
|
|
1436
|
-
items.push("docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
|
|
1476
|
+
items.push("#11 docs/agents/issue-tracker.md is not the method's answer (npx wdi-method engines --fix)");
|
|
1437
1477
|
}
|
|
1438
1478
|
const strays = specsOutsideScratch(read(".control", "registry", "specs.yaml"));
|
|
1439
1479
|
if (strays.length) {
|
|
1440
|
-
items.push(
|
|
1480
|
+
items.push(`#12 spec_folder outside .scratch/<spec-id>-<slug>/ on ${strays.join(", ")} `
|
|
1441
1481
|
+ `(the folder moves, then its cites)`);
|
|
1442
1482
|
}
|
|
1443
1483
|
// Reported only where it is still WORK. A closed pre-rename wave is read correctly (0.6.7 taught
|
|
@@ -1451,46 +1491,109 @@ function pendingUpgrades(target) {
|
|
|
1451
1491
|
// that would answer "not mine" and stop.
|
|
1452
1492
|
const legacyOpen = specsInLegacyShape(read(".control", "registry", "specs.yaml"));
|
|
1453
1493
|
if (legacyOpen.length) {
|
|
1454
|
-
items.push(
|
|
1494
|
+
items.push(`#2 ${legacyOpen.join(", ")} still in the W<n>/epics/stories shape and not closed `
|
|
1455
1495
|
+ `(flattened into tickets, id kept as its retired alias)`);
|
|
1456
1496
|
}
|
|
1457
|
-
if (/^## (Executive Summary|Vision|Assumptions|Prerequisites)\s*$/m.test(read(".what", "_product-brief", "brief.md"))) items.push("brief.md in the 14-section shape");
|
|
1497
|
+
if (/^## (Executive Summary|Vision|Assumptions|Prerequisites)\s*$/m.test(read(".what", "_product-brief", "brief.md"))) items.push("#3 brief.md in the 14-section shape");
|
|
1458
1498
|
// Sections by NAME: the numbers moved between kits (Non-Goals was §7 in one, §5 in the next).
|
|
1459
|
-
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");
|
|
1499
|
+
if (anyIn(".what/_prd", "prd.md", /^## (\d+\.\s*)?(Document Purpose|Glossary|Non-Goals|Open Questions|Assumptions Index)\b|\*\*Proof of done:\*\*/m)) items.push("#4 a prd.md in the 12-section shape, or with FR blocks");
|
|
1460
1500
|
const whatDir = path.join(target, ".what");
|
|
1461
1501
|
if (fs.existsSync(whatDir)) {
|
|
1462
1502
|
for (const pc of fs.readdirSync(whatDir)) {
|
|
1463
1503
|
if (pc.startsWith("_")) continue;
|
|
1464
1504
|
const srs = read(".what", pc, `SRS-${pc}.md`);
|
|
1465
|
-
if (/^\|\s*UC-\d+\s*\|/m.test(srs)) { items.push("an SRS with a UC Catalogue table (now a pointer)"); break; }
|
|
1505
|
+
if (/^\|\s*UC-\d+\s*\|/m.test(srs)) { items.push("#5 an SRS with a UC Catalogue table (now a pointer)"); break; }
|
|
1466
1506
|
}
|
|
1467
1507
|
}
|
|
1468
1508
|
const howDir = path.join(target, ".how");
|
|
1469
1509
|
if (fs.existsSync(howDir)) {
|
|
1470
1510
|
for (const pc of fs.readdirSync(howDir)) {
|
|
1471
1511
|
if (pc.startsWith("_")) continue;
|
|
1472
|
-
if (/\|\s*Quoted rule\s*\||Quoted verbatim from/.test(read(".how", pc, `SDD-${pc}.md`))) { items.push("an SDD quoting AD-N text (now ids only)"); break; }
|
|
1512
|
+
if (/\|\s*Quoted rule\s*\||Quoted verbatim from/.test(read(".how", pc, `SDD-${pc}.md`))) { items.push("#6 an SDD quoting AD-N text (now ids only)"); break; }
|
|
1473
1513
|
}
|
|
1474
1514
|
}
|
|
1475
|
-
if (/\|\s*Container\s*\|\s*Product Components living in it\s*\|/.test(read(".how", "_platform", "c4-l2-containers.md"))) items.push("c4-l2 with a PC x container table (now a pointer)");
|
|
1476
|
-
if (has(".control", "generated", "brief.md") || has(".control", "generated", "blueprint.md")) items.push("human pages still in .control/generated/ (render clears them)");
|
|
1477
|
-
if (has(".what", "_product-brief", "brief.md") && !has(".what-rendered")) items.push("no .what-rendered/ yet (render creates it)");
|
|
1515
|
+
if (/\|\s*Container\s*\|\s*Product Components living in it\s*\|/.test(read(".how", "_platform", "c4-l2-containers.md"))) items.push("#7 c4-l2 with a PC x container table (now a pointer)");
|
|
1516
|
+
if (has(".control", "generated", "brief.md") || has(".control", "generated", "blueprint.md")) items.push("#8 human pages still in .control/generated/ (render clears them)");
|
|
1517
|
+
if (has(".what", "_product-brief", "brief.md") && !has(".what-rendered")) items.push("#9 no .what-rendered/ yet (render creates it)");
|
|
1478
1518
|
// Skipped: what the validator never reads (kit copies, rendered output, dependencies) and what it
|
|
1479
1519
|
// treats as a record of the PAST — memlog, decisions, reports, _bmad-output. A stale path in a log
|
|
1480
1520
|
// is history, not a finding, and repointing it would falsify the record.
|
|
1481
1521
|
const SKIP = new Set([".git", "node_modules", "target", ".constitution", ".claude", ".agents", ".agent",
|
|
1482
1522
|
".what-rendered", ".how-rendered", "dist", "build", "memlog", "decisions", "reports", "meetings", "_bmad-output", ".work"]);
|
|
1483
1523
|
const OLD_PAGE = /\.control\/generated\/(brief|blueprint|prd-[a-z0-9-]+)\.md/;
|
|
1524
|
+
// At the root, every dot-folder except the corpus layers is a tool's: an agent host's skill copies
|
|
1525
|
+
// (`.kiro/skills/wdi-upgrade` names the OLD paths on purpose, to probe for them), editor state, CI.
|
|
1526
|
+
// Listing hosts by name missed every one outside the first three, and each such repo was told to run
|
|
1527
|
+
// `wdi-upgrade` on every update with nothing to move.
|
|
1528
|
+
const CORPUS_DOT = new Set([".control", ".what", ".how"]);
|
|
1484
1529
|
const citesOldPage = (dir, depth) => {
|
|
1485
1530
|
if (depth > 8) return false;
|
|
1486
1531
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1532
|
+
if (e.isDirectory() && depth === 0 && e.name.startsWith(".") && !CORPUS_DOT.has(e.name)) continue;
|
|
1487
1533
|
if (e.isDirectory()) { if (!SKIP.has(e.name) && citesOldPage(path.join(dir, e.name), depth + 1)) return true; continue; }
|
|
1488
1534
|
if (e.name === "answered.md") continue;
|
|
1489
1535
|
if (e.name.endsWith(".md") && OLD_PAGE.test(fs.readFileSync(path.join(dir, e.name), "utf8"))) return true;
|
|
1490
1536
|
}
|
|
1491
1537
|
return false;
|
|
1492
1538
|
};
|
|
1493
|
-
if (citesOldPage(target, 0)) items.push("a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
|
|
1539
|
+
if (citesOldPage(target, 0)) items.push("#10 a document cites .control/generated/brief|blueprint|prd-*.md (pages moved to the rendered trees)");
|
|
1540
|
+
// Cross-component EXPERIENCE parked in the product's design system. Before `.what/experience.md`
|
|
1541
|
+
// existed it had no home, so a repo put it in the one product-level UX file there was — a promise
|
|
1542
|
+
// in the build layer. The section names are bmad-ux's own, which is where the content comes from.
|
|
1543
|
+
// The names are bmad-ux's own and the product-level template's, in either spelling — a probe narrower
|
|
1544
|
+
// than the template it migrates into reports "nothing owed" while the content is still in the wrong layer.
|
|
1545
|
+
if (/^## (Foundation|Information architecture|Voice and tone|Flow map|Onboarding|Journeys|Shared edge cases|Promises every surface keeps|Cross-component (behaviou?r|journeys?))\b/mi
|
|
1546
|
+
.test(read(".how", "_platform", "design-system.md"))) {
|
|
1547
|
+
items.push("#15 cross-component experience in design-system.md (moves to .what/experience.md)");
|
|
1548
|
+
}
|
|
1549
|
+
// #16 and #18 follow `ux-landed`: nothing about a UX run is owed before a Product Component exists.
|
|
1550
|
+
const components = read(".control", "registry", "components.yaml");
|
|
1551
|
+
const hasPcs = /^product_components:[ \t]*\r?\n[ \t]+-/m.test(components);
|
|
1552
|
+
const LANDED_FROM = /^landed_from:.*(?:\r?\n[ \t]+-.*)*/m; // provenance, not a citation
|
|
1553
|
+
const UX_RUN = /_bmad-output\/ux\/[A-Za-z0-9_./-]*?(DESIGN|EXPERIENCE|design-system)\.md/;
|
|
1554
|
+
const citesUxRun = (dir, depth) => {
|
|
1555
|
+
if (depth > 8 || !fs.existsSync(dir)) return false;
|
|
1556
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1557
|
+
const full = path.join(dir, e.name);
|
|
1558
|
+
if (e.isDirectory()) { if (citesUxRun(full, depth + 1)) return true; continue; }
|
|
1559
|
+
if (/\.(md|ya?ml)$/.test(e.name) && UX_RUN.test(fs.readFileSync(full, "utf8").replace(LANDED_FROM, ""))) return true;
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1562
|
+
};
|
|
1563
|
+
if (hasPcs && (citesUxRun(path.join(target, ".what"), 0) || citesUxRun(path.join(target, ".how"), 0))) {
|
|
1564
|
+
items.push("#16 the corpus cites a UX run in _bmad-output/ux/ (repoint at what landed; provenance to landed_from)");
|
|
1565
|
+
}
|
|
1566
|
+
// #17 — a container whose `repo:` sends its code elsewhere but still has a heading here, or a `repo:` on
|
|
1567
|
+
// a container nobody writes. The self-naming case needs the origin remote and is left to `container-built`.
|
|
1568
|
+
const ctrSection = (components.match(/^containers:[^\n]*\n((?:[ \t].*(?:\r?\n|$)|\r?\n)*)/m) || [, ""])[1];
|
|
1569
|
+
const headings = new Set([...(read(".control", "structure-codebase.md").split(/^## Containers\s*$/m)[1] || "")
|
|
1570
|
+
.split(/^## (?!#)/m)[0].matchAll(/^###\s+(.+?)\s*$/gm)].map((m) => m[1]));
|
|
1571
|
+
const repoDebt = ctrSection.split(/^[ \t]*-[ \t]+id:[ \t]*/m).slice(1).some((block) => {
|
|
1572
|
+
const id = block.split(/\r?\n/)[0].trim().replace(/^["']|["']$/g, "");
|
|
1573
|
+
const repo = /^[ \t]+repo:[ \t]*["']?([^"'\r\n]+)/m.exec(block)?.[1].trim();
|
|
1574
|
+
if (!repo) return false;
|
|
1575
|
+
return /^[ \t]+built:[ \t]*false\b/m.test(block) || headings.has(id);
|
|
1576
|
+
});
|
|
1577
|
+
if (repoDebt) {
|
|
1578
|
+
items.push("#17 a container's repo: disagrees with the code map (heading for code kept elsewhere, or repo: on built: false)");
|
|
1579
|
+
}
|
|
1580
|
+
// #18 — landed UX documents written before `landed_from` existed. Without it `ux-landed` cannot tell
|
|
1581
|
+
// which run a landing discharged, so every run in `_bmad-output/ux/` reads as still owed.
|
|
1582
|
+
const uxRoot = path.join(target, "_bmad-output", "ux");
|
|
1583
|
+
const hasRun = fs.existsSync(uxRoot) && fs.readdirSync(uxRoot, { recursive: true })
|
|
1584
|
+
.some((f) => /(^|[\\/])(DESIGN|EXPERIENCE)\.md$/.test(String(f)));
|
|
1585
|
+
const landedDocs = [];
|
|
1586
|
+
for (const [layer, slot, name] of [[".how", "01-ux", "DESIGN.md"], [".what", "04-usecases", "EXPERIENCE.md"]]) {
|
|
1587
|
+
const base = path.join(target, layer);
|
|
1588
|
+
if (!fs.existsSync(base)) continue;
|
|
1589
|
+
for (const pc of fs.readdirSync(base)) {
|
|
1590
|
+
const f = path.join(base, pc, slot, name);
|
|
1591
|
+
if (fs.existsSync(f)) landedDocs.push(f);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
if (hasPcs && hasRun && landedDocs.some((f) => !/^landed_from:/m.test(fs.readFileSync(f, "utf8")))) {
|
|
1595
|
+
items.push("#18 a landed UX document has no landed_from (name the run file(s) it came from)");
|
|
1596
|
+
}
|
|
1494
1597
|
return items;
|
|
1495
1598
|
}
|
|
1496
1599
|
|
|
@@ -2181,7 +2284,7 @@ function runNonInteractive(args) {
|
|
|
2181
2284
|
|
|
2182
2285
|
async function main() {
|
|
2183
2286
|
const args = parseArgs(process.argv);
|
|
2184
|
-
if (!["wizard", "install", "update", "verify", "promote", "engines"].includes(args.cmd)) {
|
|
2287
|
+
if (!["wizard", "install", "update", "verify", "promote", "engines", "upgrade-check"].includes(args.cmd)) {
|
|
2185
2288
|
usage();
|
|
2186
2289
|
process.exit(2);
|
|
2187
2290
|
}
|
|
@@ -2208,6 +2311,10 @@ async function main() {
|
|
|
2208
2311
|
enginesCommand(requireTarget(args.dir), { fix: Boolean(args.fix) });
|
|
2209
2312
|
return;
|
|
2210
2313
|
}
|
|
2314
|
+
if (args.cmd === "upgrade-check") {
|
|
2315
|
+
process.exitCode = upgradeCheck(requireTarget(args.dir));
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2211
2318
|
const wantTui = !args.yes && args.cmd !== "verify" && process.stdin.isTTY && process.stdout.isTTY;
|
|
2212
2319
|
if (wantTui) {
|
|
2213
2320
|
await runWizard(args);
|