wdi-method 0.5.0 → 0.5.2

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
@@ -228,19 +228,26 @@ It prints the version it replaced, what it wrote, what it kept, and what to do n
228
228
 
229
229
  ---
230
230
 
231
- ## Carrying a change back into this package
231
+ ## Changing the method
232
232
 
233
- The published source is this repository. A product repo that holds a newer working copy of the method
234
- promotes it here before the change counts as published:
233
+ **This repository is where a method change is authored** a guide, a template, a skill wrapper, a
234
+ validator. It is proven here before publishing, against a fixture corpus the three registry scripts
235
+ actually run against:
235
236
 
236
237
  ```bash
237
- npx wdi-method promote /path/to/the/product-repo
238
- npm test
239
- git commit && git push
238
+ npm test # includes validate.py, timeline.py and inventory.py over tests/fixture/
240
239
  ```
241
240
 
242
- `promote` copies the portable method, replaces product-named files with their generic versions, scrubs
243
- initiative slugs, and **skips `.constitution/project/`** so a product's own rules can never be published.
241
+ The fixture is small but complete, and kept **green**, so a new finding is a regression rather than
242
+ noise. One test plants a defect in a copy and requires the matching validator to name it — a green
243
+ baseline is worthless if it is green because every check is broken.
244
+
245
+ A consuming repo then takes the change with `npx wdi-method update`, and that is where the judgement
246
+ half gets tested: whether a guide actually helps a person at G3 is only provable in use.
247
+
248
+ `promote` — pulling the method back out of a consumer — is a **rescue tool**, not the workflow. It
249
+ overwrites the whole kit from one copy, so it refuses to run without `--rescue`.
250
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) records why the direction was reversed and what it cost.
244
251
 
245
252
  **Patch releases are routine; minor and major are the maintainer's call.** This package overwrites files
246
253
  in repos that already hold months of work, and the version is the only signal a reader has for how
package/bin/wdi-method.js CHANGED
@@ -89,7 +89,7 @@ function usage() {
89
89
  install [dir] first install (TUI unless --yes)
90
90
  update [dir] update (TUI unless --yes)
91
91
  verify [dir]
92
- promote <live-dir>
92
+ promote <live-dir> --rescue pull a method change back out of a consumer (not the normal flow)
93
93
 
94
94
  --yes non-interactive
95
95
  --agents a,b claude,cursor,codex,antigravity
@@ -109,6 +109,7 @@ function parseArgs(argv) {
109
109
  dir: null,
110
110
  agents: null,
111
111
  skipBmad: false,
112
+ rescue: false,
112
113
  yes: false,
113
114
  product: null,
114
115
  client: null,
@@ -136,6 +137,7 @@ function parseArgs(argv) {
136
137
  while (rest.length) {
137
138
  const t = rest.shift();
138
139
  if (t === "--skip-bmad-check") args.skipBmad = true;
140
+ else if (t === "--rescue") args.rescue = true;
139
141
  else if (t === "--yes" || t === "-y") args.yes = true;
140
142
  else if (t === "--agents") {
141
143
  const raw = rest.shift();
@@ -324,6 +326,77 @@ function mv(from, to) {
324
326
  fs.renameSync(from, to);
325
327
  }
326
328
 
329
+ /** Article numbers that belong to the method half. The product keeps 1, 2, and 5. */
330
+ const METHOD_ARTICLES = [3, 4, 6, 7];
331
+
332
+ /**
333
+ * Cut the method's articles out of a product's constitution.md, and repoint its relative links.
334
+ *
335
+ * Returns {cut, kept, relinked}, or null when the file does not look like a constitution at all —
336
+ * in which case it is left ALONE rather than guessed at.
337
+ *
338
+ * 0.5.0 moved the file whole and printed "delete Articles 3, 4, 6, 7 yourself", on the grounds that
339
+ * no script can tell an edited copy from the original. That reasoning was wrong in the way that
340
+ * matters: the split does not need to know whether a section was edited, only which article numbers
341
+ * are the method's — and the file states them in its own headings. Leaving it whole left every
342
+ * migrated repo carrying those articles in TWO files, one of them frozen and drifting, plus relative
343
+ * links that no longer resolve one level down. It is all in git, so cutting is reversible; not
344
+ * cutting is what nobody notices.
345
+ */
346
+ function splitProductConstitution(file) {
347
+ const raw = fs.readFileSync(file, "utf8");
348
+ const crlf = raw.includes("\r\n");
349
+ const text = crlf ? raw.replaceAll("\r\n", "\n") : raw;
350
+ const marks = [...text.matchAll(/^## Article (\d+)\b.*$/gm)];
351
+ if (marks.length < 2) return null; // not the shape we know; do not touch it
352
+
353
+ const kept = [];
354
+ const cut = [];
355
+ let out = text.slice(0, marks[0].index);
356
+ for (let i = 0; i < marks.length; i += 1) {
357
+ const n = Number(marks[i][1]);
358
+ const end = i + 1 < marks.length ? marks[i + 1].index : text.length;
359
+ if (METHOD_ARTICLES.includes(n)) cut.push(n);
360
+ else {
361
+ kept.push(n);
362
+ out += text.slice(marks[i].index, end);
363
+ }
364
+ }
365
+ if (!cut.length) return { cut, kept, relinked: 0 };
366
+
367
+ // The file sits one level deeper than it did, and its former siblings moved into method/. A link
368
+ // left as `repo-guide.md` now resolves to .constitution/project/repo-guide.md, which does not exist.
369
+ let relinked = 0;
370
+ const bump = (re, to) => {
371
+ out = out.replace(re, (m, ...rest) => {
372
+ relinked += 1;
373
+ return typeof to === "function" ? to(m, ...rest) : to + m;
374
+ });
375
+ };
376
+ for (const name of ["repo-guide.md", "structure-guide.md", "language-guide.md",
377
+ "method-glossary.md"]) {
378
+ bump(new RegExp(`(?<![\\w./-])${name.replace(".", "\\.")}`, "g"), "../method/");
379
+ }
380
+ bump(/(?<![\w./-])document\//g, "../method/");
381
+ bump(/(?<![\w./-])codebase\/([a-z]+)-guide\.md/g, (_m, kind) => `codebase-${kind}-guide.md`);
382
+ out = out.replaceAll("../method/../method/", "../method/");
383
+
384
+ const banner = [
385
+ "",
386
+ `> **Articles ${cut.join(", ")} were removed from this file on migration to the two-folder layout.**`,
387
+ "> They are the method's and live in [`../method/constitution.md`](../method/constitution.md), which",
388
+ `> \`update\` replaces. Only Articles ${kept.join(", ")} are yours. The removed text is in git.`,
389
+ "",
390
+ ].join("\n");
391
+ const firstArticle = out.search(/^## Article /m);
392
+ out = firstArticle === -1
393
+ ? out + banner
394
+ : out.slice(0, firstArticle) + banner.trimStart() + "\n" + out.slice(firstArticle);
395
+
396
+ fs.writeFileSync(file, crlf ? out.replaceAll("\n", "\r\n") : out, "utf8");
397
+ return { cut, kept, relinked };
398
+ }
399
+
327
400
  function migrateToTwoFolders(target) {
328
401
  const c = path.join(target, ".constitution");
329
402
  if (!fs.existsSync(c)) return false; // a first install has nothing to migrate
@@ -375,11 +448,18 @@ function migrateToTwoFolders(target) {
375
448
  }
376
449
  // 6. The product's constitution.md moves WHOLE into the room, so its Articles 1, 2, and 5 survive
377
450
  // exactly as written. The generic half then arrives fresh at method/constitution.md.
378
- let split = false;
451
+ let split = null;
379
452
  if (fs.existsSync(at("constitution.md")) && !fs.existsSync(at("project", "constitution.md"))) {
380
453
  mv(at("constitution.md"), at("project", "constitution.md"));
381
- note(" moved constitution.md → project/constitution.md (your Articles 1, 2, 5)");
382
- split = true;
454
+ note(" moved constitution.md → project/constitution.md");
455
+ split = splitProductConstitution(at("project", "constitution.md"));
456
+ if (split && split.cut.length) {
457
+ note(` kept Articles ${split.kept.join(", ")}, removed ${split.cut.join(", ")} `
458
+ + "(the method's — they arrive in method/constitution.md)");
459
+ if (split.relinked) note(` repointed ${split.relinked} relative links one level up`);
460
+ } else if (split === null) {
461
+ note(" it does not carry `## Article N` headings, so it was moved but NOT split — yours to check");
462
+ }
383
463
  }
384
464
  // Anything else loose at the root is a file this product ADDED. It is NOT moved: it may be routed
385
465
  // from AGENTS.md by its current path, and guessing a destination would break that silently.
@@ -390,6 +470,8 @@ function migrateToTwoFolders(target) {
390
470
  : [];
391
471
  if (stray.length) {
392
472
  note(` left at .constitution/ root, yours to place: ${stray.join(", ")}`);
473
+ note(" a file you added belongs in project/ — but moving it would break any pointer that");
474
+ note(" names its current path, so the choice is yours. repo-guide.md states the rule.");
393
475
  }
394
476
  return split;
395
477
  }
@@ -745,10 +827,14 @@ function apply(target, agents,
745
827
  const splitConstitution = migrateToTwoFolders(target);
746
828
  const { written, skipped } = syncConstitution(target);
747
829
  note(`constitution wrote ${written}, kept ${skipped}`);
830
+ // A migrated repo also carries derived output stamped against the OLD layout: .control/generated/*
831
+ // still names the pre-0.5.0 script path, and the two structure maps still draw the old tree. The
832
+ // installer MUST NOT write either — one is generated, the other is re-derived by a skill — so it
833
+ // says so instead of leaving them to be found by whoever trusts them next.
748
834
  if (splitConstitution) {
749
- note(" your constitution.md still carries Articles 3, 4, 6, 7 now also in method/constitution.md.");
750
- note(" Delete them from project/constitution.md; only 1, 2, 5 are yours. Not automated: your");
751
- note(" copy may have been edited, and no script can tell an edit from the original.");
835
+ note(" derived output still describes the OLD layout, and neither is mine to write:");
836
+ note(" uv run .constitution/method/scripts/validate.py --generate → .control/generated/");
837
+ note(" then the wdi-init skill, intent `structure` → the two structure maps");
752
838
  }
753
839
  const skills = syncSkills(target, agents);
754
840
  note(`skills ${skills.files} files`);
@@ -1100,6 +1186,20 @@ async function main() {
1100
1186
  }
1101
1187
  if (args.cmd === "promote") {
1102
1188
  if (!args.dir) die("promote needs a path to the working copy");
1189
+ // `promote` used to BE the workflow: author a rule in a product repo, run it, carry it here.
1190
+ // It is now a rescue tool, and the flag is what makes that structural rather than a paragraph
1191
+ // nobody rereads. Running it by habit overwrites the whole kit with one consumer's copy —
1192
+ // silently reverting every change made here since that repo last updated.
1193
+ if (!args.rescue) {
1194
+ die([
1195
+ "promote overwrites the whole kit from a consumer's copy, and this package is now where a",
1196
+ " method change is authored — see CONTRIBUTING.md. If a change really was made in a",
1197
+ " product repo by mistake and needs rescuing, say so:",
1198
+ "",
1199
+ " npx wdi-method promote <dir> --rescue",
1200
+ ].join("\n"));
1201
+ }
1202
+ note("--rescue: pulling the method back out of a consumer. Read the diff before committing.");
1103
1203
  promote(args.dir);
1104
1204
  return;
1105
1205
  }
@@ -2,31 +2,37 @@
2
2
  status: Reference
3
3
  ---
4
4
 
5
- # `.constitution/` — index
5
+ # `.constitution/method/` — index
6
6
 
7
- Method files arrive from the public WDI Method package via `npx wdi-method install`
8
- or `update`. Load [`constitution.md`](constitution.md) before acting. Guides are
9
- loaded **lazily** only when the task matches, and every guide states when on its
10
- own **Loaded when:** line.
7
+ `.constitution/` holds **exactly two folders**, and the folder is what says who owns a file:
8
+
9
+ | Folder | Owner | `update` | `promote` |
10
+ |---|---|---|---|
11
+ | `method/` — you are in it | the method | **overwritten** in full | carries it into the package |
12
+ | [`../project/`](../project/) | this product | **never touched** — seeded once when absent | never carries it, so your rules cannot be published |
13
+
14
+ Load [`constitution.md`](constitution.md) and [`../project/constitution.md`](../project/constitution.md)
15
+ before acting: Articles 3, 4, 6, 7 are here, Articles 1, 2, 5 are yours. Guides are loaded **lazily** —
16
+ only when the task matches, and every guide states when on its own **Loaded when:** line.
11
17
 
12
18
  Every file here carries a `status:` — Article 4 owns the five values. Only `Accepted` binds;
13
19
  `Reference` explains and MUST NOT be cited to reject a change. A template carries no status of its
14
20
  own, because its frontmatter belongs to the artifact it produces.
15
21
 
16
- A file this product added (one that is not in the snapshot) stays here across `update` and MUST be
17
- listed from `constitution.md` Article 2 or from `AGENTS.md` routing — this index is overwritten on
18
- every update.
22
+ **A file this product adds MUST go in [`../project/`](../project/), not here** anything in `method/`
23
+ is replaced on the next update, without warning, because that is what `method/` means. This index is
24
+ overwritten too.
19
25
 
20
- ## `method/` — the explanation, `status: Reference`
26
+ ## `why/` — the explanation, `status: Reference`
21
27
 
22
28
  Never a rule. When it disagrees with a guide, the guide wins and the disagreement is a defect.
23
29
 
24
30
  | File | Opened when |
25
31
  |---|---|
26
- | [`method/README.md`](method/README.md) | You want the whole shape in five minutes — five gates, two settings, fifteen skills, WDI ↔ BMad |
27
- | [`method/artifact-map.md`](method/artifact-map.md) | "Where does this file go", or "does this document exist at my `mode`" |
28
- | [`method/rationale.md`](method/rationale.md) | Before changing a rule, to know what you would break |
29
- | [`method/portability.md`](method/portability.md) | Which files are the method and which are the product; how promote and install move them |
32
+ | [`why/README.md`](why/README.md) | You want the whole shape in five minutes — five gates, two settings, fifteen skills, WDI ↔ BMad |
33
+ | [`why/artifact-map.md`](why/artifact-map.md) | "Where does this file go", or "does this document exist at my `mode`" |
34
+ | [`why/rationale.md`](why/rationale.md) | Before changing a rule, to know what you would break |
35
+ | [`why/portability.md`](why/portability.md) | Which files are the method and which are the product; how promote and install move them |
30
36
 
31
37
  ## Cross-domain
32
38
 
@@ -51,12 +57,13 @@ Never a rule. When it disagrees with a guide, the guide wins and the disagreemen
51
57
  | [`decision-guide.md`](document/decision-guide.md) | `DEC-`: the one test for recording at all, shape, global numbering, the `draft → accepted → applied` ladder, supersession |
52
58
  | [`templates/`](document/templates/) | Templates, one per kind of document; they MUST be copied, and MUST NOT be reproduced from memory |
53
59
 
54
- ## `codebase/`code rules
60
+ ## Code rules in the room, not here
55
61
 
56
- All three are written by the **project**, not the kit. While `Draft`, their contents MAY be read as
57
- guidance but MUST NOT be used to reject a change.
62
+ All three are written by the **product**, so they live in [`../project/`](../project/) and no update
63
+ touches them at any `status:`. While `Draft`, their contents MAY be read as guidance but MUST NOT be
64
+ used to reject a change.
58
65
 
59
- [`stack-guide.md`](codebase/stack-guide.md) · [`conventions-guide.md`](codebase/conventions-guide.md) · [`brownfield-guide.md`](codebase/brownfield-guide.md)
66
+ [`stack-guide.md`](../project/codebase-stack-guide.md) · [`conventions-guide.md`](../project/codebase-conventions-guide.md) · [`brownfield-guide.md`](../project/codebase-brownfield-guide.md)
60
67
 
61
68
  ## `scripts/`
62
69