pan-wizard 3.13.1 → 3.15.0

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.
Files changed (39) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +4 -5
  3. package/commands/pan/audit-deployment.md +384 -384
  4. package/commands/pan/focus-auto.md +683 -683
  5. package/commands/pan/focus-doc-audit.md +530 -530
  6. package/commands/pan/focus-drift-walking.md +525 -525
  7. package/commands/pan/git.md +1 -1
  8. package/commands/pan/hud.md +3 -2
  9. package/commands/pan/report.md +70 -0
  10. package/hooks/dist/pan-check-update.js +62 -62
  11. package/hooks/dist/pan-context-monitor.js +134 -122
  12. package/hooks/dist/pan-statusline.js +7 -1
  13. package/package.json +5 -5
  14. package/pan-wizard-core/bin/lib/config.cjs +14 -1
  15. package/pan-wizard-core/bin/lib/core.cjs +6 -2
  16. package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
  17. package/pan-wizard-core/bin/lib/focus.cjs +48 -2
  18. package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
  19. package/pan-wizard-core/bin/lib/hud.cjs +202 -17
  20. package/pan-wizard-core/bin/lib/knowledge.cjs +2 -2
  21. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  22. package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
  23. package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
  24. package/pan-wizard-core/bin/lib/phase.cjs +4 -4
  25. package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
  26. package/pan-wizard-core/bin/lib/utils.cjs +171 -171
  27. package/pan-wizard-core/bin/lib/verify.cjs +172 -61
  28. package/pan-wizard-core/bin/pan-tools.cjs +1499 -1463
  29. package/pan-wizard-core/references/checkpoints.md +776 -776
  30. package/pan-wizard-core/references/continuation-format.md +249 -249
  31. package/pan-wizard-core/references/questioning.md +145 -145
  32. package/pan-wizard-core/references/tdd.md +263 -263
  33. package/pan-wizard-core/references/ui-brand.md +160 -160
  34. package/pan-wizard-core/templates/config.json +38 -38
  35. package/pan-wizard-core/workflows/exec-phase.md +14 -0
  36. package/scripts/build-hooks.js +51 -51
  37. package/scripts/git-hooks/pre-commit +0 -0
  38. package/scripts/release-check.js +53 -47
  39. package/scripts/run-tests.cjs +44 -0
@@ -3,15 +3,16 @@
3
3
  * release-check.js — Pre-publish validation gate.
4
4
  *
5
5
  * Wired into `prepublishOnly` so `npm publish` fails BEFORE upload if any
6
- * gate is red. Runs five checks in order; first failure aborts.
6
+ * gate is red. Runs seven checks in order; first failure aborts.
7
7
  *
8
8
  * 1. build:hooks — hook scripts copy/build cleanly
9
9
  * 2. test:all — full test suite (unit + scenario) passes
10
10
  * 3. npm audit — no known vulnerabilities in production deps
11
- * (we have zero runtime deps, but esbuild dev-dep is checked)
11
+ * (we have zero runtime deps, but the dev-deps are checked)
12
12
  * 4. doc-lint counts — no drift-prone count violations in user-facing docs
13
- * 5. npm pack dry-run — package builds; size is sane
14
- * 6. smoke install — npm pack + install into temp dir + run pan-tools list
13
+ * 5. links validate — doc↔code link graph resolves (no broken references)
14
+ * 6. npm pack dry-run — package builds; size is sane
15
+ * 7. smoke install — npm pack + install into temp dir + run pan-tools list
15
16
  * catches "ships but doesn't actually work" failures
16
17
  *
17
18
  * Usage:
@@ -56,7 +57,7 @@ function run(cmd, args, opts = {}) {
56
57
  }
57
58
 
58
59
  // Gate 1: build:hooks
59
- process.stderr.write('\n[release-check] Gate 1/6: build:hooks\n');
60
+ process.stderr.write('\n[release-check] Gate 1/7: build:hooks\n');
60
61
  {
61
62
  const r = run('npm', ['run', 'build:hooks']);
62
63
  logGate('build:hooks', r.status === 0, r.status !== 0 ? `exit ${r.status}` : '');
@@ -64,7 +65,7 @@ process.stderr.write('\n[release-check] Gate 1/6: build:hooks\n');
64
65
  }
65
66
 
66
67
  // Gate 2: test:all
67
- process.stderr.write('\n[release-check] Gate 2/6: test:all\n');
68
+ process.stderr.write('\n[release-check] Gate 2/7: test:all\n');
68
69
  {
69
70
  const r = run('npm', ['run', 'test:all']);
70
71
  logGate('test:all', r.status === 0, r.status !== 0 ? `exit ${r.status}` : '');
@@ -73,9 +74,9 @@ process.stderr.write('\n[release-check] Gate 2/6: test:all\n');
73
74
 
74
75
  // Gate 3: npm audit (production deps only)
75
76
  if (SKIP_AUDIT) {
76
- process.stderr.write('\n[release-check] Gate 3/6: npm audit (SKIPPED)\n');
77
+ process.stderr.write('\n[release-check] Gate 3/7: npm audit (SKIPPED)\n');
77
78
  } else {
78
- process.stderr.write('\n[release-check] Gate 3/6: npm audit --omit=dev\n');
79
+ process.stderr.write('\n[release-check] Gate 3/7: npm audit --omit=dev\n');
79
80
  const r = run('npm', ['audit', '--omit=dev', '--audit-level=high'], { capture: true });
80
81
  // npm audit exits non-zero on findings. We tolerate moderate; fail on high+.
81
82
  const ok = r.status === 0;
@@ -87,7 +88,7 @@ if (SKIP_AUDIT) {
87
88
  }
88
89
 
89
90
  // Gate 4: doc-lint counts on user-facing docs (count-SSoT enforcement)
90
- process.stderr.write('\n[release-check] Gate 4/6: doc-lint counts docs/\n');
91
+ process.stderr.write('\n[release-check] Gate 4/7: doc-lint counts docs/\n');
91
92
  {
92
93
  const tools = path.join(REPO_ROOT, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
93
94
  const docsDir = path.join(REPO_ROOT, 'docs');
@@ -100,62 +101,67 @@ process.stderr.write('\n[release-check] Gate 4/6: doc-lint counts docs/\n');
100
101
  }
101
102
  }
102
103
 
103
-
104
- // npm runs lifecycle scripts (prepare) before pack; any of their stdout noise
105
- // lands ahead of the --json payload. npm pretty-prints the JSON array starting
106
- // on its own line — parse from there.
107
- function parseNpmJson(stdout) {
108
- try { return JSON.parse(stdout); } catch { /* fall through to extraction */ }
109
- const m = stdout.search(/^[[{]s*$/m);
110
- if (m === -1) throw new Error('no JSON payload found in npm output');
111
- return JSON.parse(stdout.slice(m));
104
+ // Gate 5: doc↔code link graph resolves (anti-fake — a doc cannot reference a
105
+ // code anchor that doesn't exist; deterministic, self-enforcing exit 1).
106
+ process.stderr.write('\n[release-check] Gate 5/7: links validate\n');
107
+ {
108
+ const tools = path.join(REPO_ROOT, 'pan-wizard-core', 'bin', 'pan-tools.cjs');
109
+ const r = run('node', [tools, 'links', 'validate', '--raw'], { capture: true });
110
+ const ok = r.status === 0;
111
+ logGate('links validate', ok, ok ? 'doc↔code link graph resolves' : 'broken doc↔code references');
112
+ if (!ok) {
113
+ process.stderr.write((r.stdout || '') + '\n');
114
+ process.exit(1);
115
+ }
112
116
  }
113
117
 
114
- // Gate 5: npm pack dry-run
115
- process.stderr.write('\n[release-check] Gate 5/6: npm pack --dry-run\n');
118
+
119
+ // NOTE: we deliberately do NOT parse `npm pack --json` stdout. Under
120
+ // `npm publish` the runner routes the child pack's lifecycle-script output onto
121
+ // stdout (foreground-scripts), and that noise can include a decoy JSON object —
122
+ // any string heuristic then picks the wrong payload (Gate 7 crashed on
123
+ // packJson[0].filename with "0 files"). Instead, pack into a temp dir and read
124
+ // the .tgz npm actually wrote: a filesystem op that stdout noise cannot corrupt.
125
+
126
+ // Gate 6: npm pack — produces a non-empty, sanely-sized tarball (read the file,
127
+ // never parse stdout)
128
+ process.stderr.write('\n[release-check] Gate 6/7: npm pack (size sanity)\n');
116
129
  {
117
- const r = run('npm', ['pack', '--dry-run', '--json'], { capture: true });
118
- if (r.status !== 0) {
119
- logGate('npm pack dry-run', false, `exit ${r.status}`);
130
+ const tmp6 = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-release-pack-'));
131
+ const r = run('npm', ['pack', '--pack-destination', tmp6], { capture: true });
132
+ const tgz = r.status === 0 ? fs.readdirSync(tmp6).find(f => f.endsWith('.tgz')) : null;
133
+ const size = tgz ? fs.statSync(path.join(tmp6, tgz)).size : 0;
134
+ const sizeMB = (size / 1024 / 1024).toFixed(2);
135
+ // Sane = a non-empty tarball under 50MB (large for a zero-runtime-dep tool)
136
+ const ok = r.status === 0 && !!tgz && size > 0 && size < 50 * 1024 * 1024;
137
+ logGate('npm pack', ok, tgz ? `${sizeMB}MB tarball` : `no tarball (exit ${r.status})`);
138
+ fs.rmSync(tmp6, { recursive: true, force: true });
139
+ if (!ok) {
120
140
  process.stderr.write((r.stderr || '') + '\n');
121
141
  process.exit(1);
122
142
  }
123
- // Parse the JSON output to check size
124
- try {
125
- const parsed = parseNpmJson(r.stdout);
126
- const entry = Array.isArray(parsed) ? parsed[0] : parsed;
127
- const size = entry.size || 0;
128
- const fileCount = entry.files ? entry.files.length : 0;
129
- const sizeMB = (size / 1024 / 1024).toFixed(2);
130
- // Flag if pack size exceeds 50MB — large for a zero-runtime-dep tool
131
- const ok = size < 50 * 1024 * 1024;
132
- logGate('npm pack dry-run', ok, `${sizeMB}MB, ${fileCount} files`);
133
- if (!ok) process.exit(1);
134
- } catch (err) {
135
- logGate('npm pack dry-run', false, 'JSON parse failed: ' + err.message);
136
- process.exit(1);
137
- }
138
143
  }
139
144
 
140
- // Gate 6: smoke install — pack and install into temp dir, run pan-tools
145
+ // Gate 7: smoke install — pack and install into temp dir, run pan-tools
141
146
  if (SKIP_SMOKE) {
142
- process.stderr.write('\n[release-check] Gate 6/6: smoke install (SKIPPED)\n');
147
+ process.stderr.write('\n[release-check] Gate 7/7: smoke install (SKIPPED)\n');
143
148
  } else {
144
- process.stderr.write('\n[release-check] Gate 6/6: smoke install (npm pack + install + sanity)\n');
149
+ process.stderr.write('\n[release-check] Gate 7/7: smoke install (npm pack + install + sanity)\n');
145
150
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pan-release-smoke-'));
146
151
  try {
147
- // Pack
148
- const pack = run('npm', ['pack', '--pack-destination', tmpDir, '--json'], { capture: true });
152
+ // Pack — read the .tgz npm writes to tmpDir; never parse its stdout (see note).
153
+ const pack = run('npm', ['pack', '--pack-destination', tmpDir], { capture: true });
149
154
  if (pack.status !== 0) {
150
155
  logGate('smoke install (pack)', false, `exit ${pack.status}`);
156
+ process.stderr.write((pack.stderr || '') + '\n');
151
157
  process.exit(1);
152
158
  }
153
- const packJson = parseNpmJson(pack.stdout);
154
- const tarball = path.join(tmpDir, packJson[0].filename);
155
- if (!fs.existsSync(tarball)) {
156
- logGate('smoke install (pack)', false, `tarball not found at ${tarball}`);
159
+ const tgz = fs.readdirSync(tmpDir).find(f => f.endsWith('.tgz'));
160
+ if (!tgz) {
161
+ logGate('smoke install (pack)', false, `no .tgz produced in ${tmpDir}`);
157
162
  process.exit(1);
158
163
  }
164
+ const tarball = path.join(tmpDir, tgz);
159
165
  // Install into a separate fake project dir
160
166
  const installDir = path.join(tmpDir, 'install-target');
161
167
  fs.mkdirSync(installDir, { recursive: true });
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Glob-free test runner. `node --test tests/*.test.cjs` relies on shell glob
4
+ * expansion: bash provides it (linux/macOS) and Node >=22 expands test-path
5
+ * globs itself, but Windows PowerShell does neither on Node 18/20 — the
6
+ * literal pattern "tests/*.test.cjs" matches no file and the run exits 1.
7
+ * This script expands the pattern deterministically on every platform.
8
+ *
9
+ * Usage: node scripts/run-tests.cjs <dir> [<dir> ...]
10
+ * Runs every *.test.cjs DIRECTLY inside each listed directory (no recursion,
11
+ * so `tests` and `tests/scenarios` stay separately addressable).
12
+ */
13
+
14
+ const { spawnSync } = require('child_process');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const dirs = process.argv.slice(2);
19
+ if (dirs.length === 0) {
20
+ console.error('Usage: node scripts/run-tests.cjs <dir> [<dir> ...]');
21
+ process.exit(1);
22
+ }
23
+
24
+ const files = [];
25
+ for (const dir of dirs) {
26
+ let entries;
27
+ try {
28
+ entries = fs.readdirSync(dir);
29
+ } catch (e) {
30
+ console.error(`run-tests: cannot read directory ${dir}: ${e.message}`);
31
+ process.exit(1);
32
+ }
33
+ for (const name of entries.sort()) {
34
+ if (name.endsWith('.test.cjs')) files.push(path.join(dir, name));
35
+ }
36
+ }
37
+
38
+ if (files.length === 0) {
39
+ console.error(`run-tests: no *.test.cjs files found in: ${dirs.join(', ')}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ const result = spawnSync(process.execPath, ['--test', ...files], { stdio: 'inherit' });
44
+ process.exit(result.status ?? 1);