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
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execFileSync } = require('child_process');
8
- const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error } = require('./core.cjs');
8
+ const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error, escapeRegex } = require('./core.cjs');
9
9
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
10
10
  const { writeStateMd, readStateSafe } = require('./state.cjs');
11
11
  const {
@@ -317,32 +317,22 @@ function cmdVerifyCommits(cwd, hashes, raw) {
317
317
  * @param {boolean} raw - If true, output raw value instead of JSON
318
318
  * @returns {void}
319
319
  */
320
- function cmdVerifyArtifacts(cwd, planFilePath, raw) {
321
- if (!planFilePath) { error('plan file path required'); }
322
- const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
323
- const content = safeReadFile(fullPath);
324
- if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
325
-
326
- const artifacts = parseMustHavesBlock(content, 'artifacts');
327
- if (artifacts.length === 0) {
328
- output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
329
- return;
330
- }
331
-
320
+ /**
321
+ * Pure substance check of must_haves.artifacts against disk — no output/exit,
322
+ * so `verify reconcile` can compose it. Returns {all_passed, passed, total, artifacts}.
323
+ */
324
+ function checkArtifacts(cwd, planContent) {
325
+ const artifacts = parseMustHavesBlock(planContent, 'artifacts');
332
326
  const results = [];
333
327
  for (const artifact of artifacts) {
334
328
  if (typeof artifact === 'string') continue; // skip simple string items
335
329
  const artPath = artifact.path;
336
330
  if (!artPath) continue;
337
-
338
- const artFullPath = path.join(cwd, artPath);
339
- const fileContent = safeReadFile(artFullPath);
331
+ const fileContent = safeReadFile(path.join(cwd, artPath));
340
332
  const exists = fileContent !== null;
341
333
  const check = { path: artPath, exists, issues: [], passed: false };
342
-
343
334
  if (exists) {
344
335
  const lineCount = fileContent.split('\n').length;
345
-
346
336
  if (artifact.min_lines && lineCount < artifact.min_lines) {
347
337
  check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
348
338
  }
@@ -350,8 +340,8 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
350
340
  check.issues.push(`Missing pattern: ${artifact.contains}`);
351
341
  }
352
342
  if (artifact.exports) {
353
- const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
354
- for (const exp of exports) {
343
+ const exps = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
344
+ for (const exp of exps) {
355
345
  if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
356
346
  }
357
347
  }
@@ -359,43 +349,42 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
359
349
  } else {
360
350
  check.issues.push('File not found');
361
351
  }
362
-
363
352
  results.push(check);
364
353
  }
365
-
366
354
  const passed = results.filter(r => r.passed).length;
367
- output({
368
- all_passed: passed === results.length,
369
- passed,
370
- total: results.length,
371
- artifacts: results,
372
- }, raw, passed === results.length ? 'valid' : 'invalid');
355
+ return { all_passed: passed === results.length, passed, total: results.length, artifacts: results };
373
356
  }
374
357
 
375
- /**
376
- * Verify must_haves.key_links from a plan.md: source-to-target references and patterns.
377
- * @param {string} cwd - Working directory path
378
- * @param {string} planFilePath - Path to the plan.md file containing key link specs
379
- * @param {boolean} raw - If true, output raw value instead of JSON
380
- * @returns {void}
381
- */
382
- function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
358
+ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
383
359
  if (!planFilePath) { error('plan file path required'); }
384
360
  const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
385
361
  const content = safeReadFile(fullPath);
386
362
  if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
387
-
388
- const keyLinks = parseMustHavesBlock(content, 'key_links');
389
- if (keyLinks.length === 0) {
390
- output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
363
+ const r = checkArtifacts(cwd, content);
364
+ if (r.total === 0) {
365
+ output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
391
366
  return;
392
367
  }
368
+ output(r, raw, r.all_passed ? 'valid' : 'invalid');
369
+ }
393
370
 
371
+ /**
372
+ * Verify must_haves.key_links from a plan.md: source-to-target references and patterns.
373
+ * @param {string} cwd - Working directory path
374
+ * @param {string} planFilePath - Path to the plan.md file containing key link specs
375
+ * @param {boolean} raw - If true, output raw value instead of JSON
376
+ * @returns {void}
377
+ */
378
+ /**
379
+ * Pure wiring check of must_haves.key_links against disk — no output/exit.
380
+ * Returns {all_verified, verified, total, links}.
381
+ */
382
+ function checkKeyLinks(cwd, planContent) {
383
+ const keyLinks = parseMustHavesBlock(planContent, 'key_links');
394
384
  const results = [];
395
385
  for (const link of keyLinks) {
396
386
  if (typeof link === 'string') continue;
397
387
  const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
398
-
399
388
  const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
400
389
  if (!sourceContent) {
401
390
  check.detail = 'Source file not found';
@@ -415,29 +404,142 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
415
404
  }
416
405
  }
417
406
  } catch {
418
- // Regex compilation failed -- report the invalid pattern to the caller
419
407
  check.detail = `Invalid regex pattern: ${link.pattern}`;
420
408
  }
409
+ } else if (sourceContent.includes(link.to || '')) {
410
+ check.verified = true;
411
+ check.detail = 'Target referenced in source';
421
412
  } else {
422
- // No pattern: just check source references target
423
- if (sourceContent.includes(link.to || '')) {
424
- check.verified = true;
425
- check.detail = 'Target referenced in source';
426
- } else {
427
- check.detail = 'Target not referenced in source';
428
- }
413
+ check.detail = 'Target not referenced in source';
429
414
  }
430
-
431
415
  results.push(check);
432
416
  }
433
-
434
417
  const verified = results.filter(r => r.verified).length;
435
- output({
436
- all_verified: verified === results.length,
437
- verified,
438
- total: results.length,
439
- links: results,
440
- }, raw, verified === results.length ? 'valid' : 'invalid');
418
+ return { all_verified: verified === results.length, verified, total: results.length, links: results };
419
+ }
420
+
421
+ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
422
+ if (!planFilePath) { error('plan file path required'); }
423
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
424
+ const content = safeReadFile(fullPath);
425
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
426
+ const r = checkKeyLinks(cwd, content);
427
+ if (r.total === 0) {
428
+ output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
429
+ return;
430
+ }
431
+ output(r, raw, r.all_verified ? 'valid' : 'invalid');
432
+ }
433
+
434
+ // ─── Reconcile: cross-check a written verification verdict against the
435
+ // mechanical signals (ADR-0036 review — closes the rubber-stamp gap). ─────────
436
+
437
+ function findPhaseDir(cwd, phaseNum) {
438
+ const base = phasesPath(cwd);
439
+ let entries;
440
+ try { entries = fs.readdirSync(base, { withFileTypes: true }); } catch { return null; }
441
+ const re = new RegExp('^0*' + String(phaseNum).replace(/[^0-9A-Za-z.]/g, '') + '-');
442
+ for (const e of entries) {
443
+ if (e.isDirectory() && re.test(e.name)) return path.join(base, e.name);
444
+ }
445
+ return null;
446
+ }
447
+
448
+ /**
449
+ * Re-derive the mechanical signals for a phase and check them against the
450
+ * verdict written in its -verification.md. A verification that CLAIMS a pass
451
+ * while artifacts fail substance checks or key-links are unwired is a
452
+ * contradiction (a rubber stamp) — reported deterministically, never trusted.
453
+ * When no must_haves are declared there are no mechanical signals to reconcile,
454
+ * so the verdict is passed through (reconciled: true, with a note).
455
+ */
456
+ function reconcilePhase(cwd, phaseNum) {
457
+ const base = { phase: String(phaseNum), found: false, reconciled: true, contradictions: [] };
458
+ const dir = findPhaseDir(cwd, phaseNum);
459
+ if (!dir) return { ...base, note: 'phase directory not found' };
460
+ let files;
461
+ try { files = fs.readdirSync(dir); } catch { return { ...base, note: 'phase directory unreadable' }; }
462
+ const verFile = files.find(f => isVerificationFile(f));
463
+ if (!verFile) return { ...base, note: 'no verification.md — absence is covered by the verification gate, not reconcile' };
464
+ const verRaw = safeReadFile(path.join(dir, verFile)) || '';
465
+ const sm = verRaw.match(/^status:\s*([A-Za-z_-]+)/m);
466
+ const status = sm ? sm[1].toLowerCase() : 'unknown';
467
+ const claimsPass = /^(pass|passed|verified|complete|verified_pass)$/.test(status);
468
+ const planFile = files.find(f => isPlanFile(f));
469
+ const planContent = planFile ? (safeReadFile(path.join(dir, planFile)) || '') : '';
470
+ const artifacts = checkArtifacts(cwd, planContent);
471
+ const keyLinks = checkKeyLinks(cwd, planContent);
472
+ const signals = artifacts.total + keyLinks.total;
473
+ const contradictions = [];
474
+ if (claimsPass) {
475
+ if (artifacts.total > 0 && !artifacts.all_passed) {
476
+ contradictions.push(`verification status "${status}" but ${artifacts.total - artifacts.passed}/${artifacts.total} artifact substance check(s) FAIL`);
477
+ }
478
+ if (keyLinks.total > 0 && !keyLinks.all_verified) {
479
+ contradictions.push(`verification status "${status}" but ${keyLinks.total - keyLinks.verified}/${keyLinks.total} key-link(s) UNWIRED`);
480
+ }
481
+ }
482
+ return {
483
+ phase: String(phaseNum), found: true, verification_status: status, claims_pass: claimsPass,
484
+ mechanical_signals: signals, artifacts, key_links: keyLinks, contradictions,
485
+ reconciled: contradictions.length === 0,
486
+ note: signals === 0 ? 'no must_haves declared — mechanical reconciliation unavailable; verdict trusted' : undefined,
487
+ };
488
+ }
489
+
490
+ function cmdVerifyReconcile(cwd, phaseNum, raw) {
491
+ if (!phaseNum) { error('Usage: verify reconcile <phase>'); }
492
+ const r = reconcilePhase(cwd, phaseNum);
493
+ output(r, raw, r.reconciled ? 'valid' : 'invalid');
494
+ process.exit(r.reconciled ? 0 : 1);
495
+ }
496
+
497
+ // ─── Stub / fake-return scanner (ADR-0036 review — closes the hardcoded
498
+ // "return {ok:true}" / "not implemented" gap the old anti-pattern grep missed,
499
+ // which only blocked the literal `return {}` and `placeholder`/`coming soon`). ─
500
+ const STUB_PATTERNS = [
501
+ { re: /\bnot[\s_-]?implemented\b/i, marker: 'not-implemented', severity: 'high' },
502
+ { re: /\bNotImplemented(Error)?\b/, marker: 'NotImplemented', severity: 'high' },
503
+ { re: /throw\s+new\s+\w*Error\s*\(\s*['"`][^'"`]*\b(unimplemented|not\s+implemented|stub|todo)\b/i, marker: 'throw-stub', severity: 'high' },
504
+ { re: /\bres(ponse)?\.status\(\s*501\s*\)/, marker: 'http-501', severity: 'high' },
505
+ { re: /\b(coming\s+soon|placeholder)\b/i, marker: 'placeholder', severity: 'medium' },
506
+ { re: /return\s*(\{\s*\}|\[\s*\])\s*;?\s*(\/\/.*)?$/, marker: 'empty-return', severity: 'medium' },
507
+ { re: /return\s*\{\s*ok\s*:\s*true\s*\}\s*;?\s*(\/\/.*)?$/, marker: 'fake-ok-return', severity: 'medium' },
508
+ { re: /\b(TODO|FIXME|XXX|HACK)\b/, marker: 'todo-marker', severity: 'low' },
509
+ ];
510
+ const STUB_CODE_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|py|go|rb|java|php|rs|c|cc|cpp|h|hpp|cs|kt|swift|scala)$/i;
511
+
512
+ /**
513
+ * Scan source files for stub / fake-implementation markers. Defaults to the
514
+ * git-changed files (so it gates a handoff), or a caller-supplied file list.
515
+ * `high`-severity markers are the blocking set; TODO markers are informational.
516
+ * @returns {{scanned, findings: Array, blocking: number, total: number}}
517
+ */
518
+ function scanStubs(cwd, opts = {}) {
519
+ let files = Array.isArray(opts.files) ? opts.files : getChangedFiles(cwd);
520
+ files = (files || []).filter(f => STUB_CODE_EXT.test(f));
521
+ const findings = [];
522
+ for (const rel of files) {
523
+ const content = safeReadFile(path.join(cwd, rel));
524
+ if (content === null) continue;
525
+ const lines = content.split(/\r?\n/);
526
+ for (let i = 0; i < lines.length; i++) {
527
+ for (const { re, marker, severity } of STUB_PATTERNS) {
528
+ if (re.test(lines[i])) {
529
+ findings.push({ file: toPosix(rel), line: i + 1, marker, severity, text: lines[i].trim().slice(0, 160) });
530
+ break; // one finding per line
531
+ }
532
+ }
533
+ }
534
+ }
535
+ const blocking = findings.filter(f => f.severity === 'high').length;
536
+ return { scanned: files.length, findings, blocking, total: findings.length };
537
+ }
538
+
539
+ function cmdVerifyStubs(cwd, opts = {}, raw) {
540
+ const r = scanStubs(cwd, opts);
541
+ output(r, raw, r.blocking === 0 ? 'valid' : 'invalid');
542
+ if (opts.gate) process.exit(r.blocking > 0 ? 1 : 0);
441
543
  }
442
544
 
443
545
  /**
@@ -995,12 +1097,12 @@ function syncRequirementCheckboxes(cwd) {
995
1097
  let fixed = 0;
996
1098
  for (const phaseNum of completedPhases) {
997
1099
  const reqMatch = roadmapContent.match(
998
- new RegExp(`Phase\\s+${phaseNum.replace(/\./g, '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
1100
+ new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
999
1101
  );
1000
1102
  if (!reqMatch) continue;
1001
1103
  const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
1002
1104
  for (const reqId of reqIds) {
1003
- const escaped = reqId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1105
+ const escaped = escapeRegex(reqId);
1004
1106
  const re = new RegExp(`(- \\[) (\\]\\s*\\*\\*${escaped}\\*\\*)`, 'gi');
1005
1107
  const before = reqContent;
1006
1108
  reqContent = reqContent.replace(re, '$1x$2');
@@ -1284,8 +1386,11 @@ function runFullTestCheck(cwd) {
1284
1386
  stdio: ['pipe', 'pipe', 'pipe'],
1285
1387
  encoding: 'utf-8',
1286
1388
  });
1287
- const testMatch = result.match(/# tests (\d+)/);
1288
- const passMatch = result.match(/# pass (\d+)/);
1389
+ // Match both TAP ("# tests N") and spec-reporter ("ℹ tests N") summaries —
1390
+ // modern node --test defaults to the spec reporter, which the old "# "-only
1391
+ // regex silently missed (returning tests: null).
1392
+ const testMatch = result.match(/[#ℹ]\s*tests\s+(\d+)/);
1393
+ const passMatch = result.match(/[#ℹ]\s*pass\s+(\d+)/);
1289
1394
  return {
1290
1395
  pass: true,
1291
1396
  exitCode: 0,
@@ -1344,6 +1449,12 @@ module.exports = {
1344
1449
  cmdVerifyCommits,
1345
1450
  cmdVerifyArtifacts,
1346
1451
  cmdVerifyKeyLinks,
1452
+ checkArtifacts,
1453
+ checkKeyLinks,
1454
+ reconcilePhase,
1455
+ cmdVerifyReconcile,
1456
+ scanStubs,
1457
+ cmdVerifyStubs,
1347
1458
  cmdValidateConsistency,
1348
1459
  cmdValidateHealth,
1349
1460
  cmdPreflight,