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.
- package/LICENSE +21 -21
- package/README.md +4 -5
- package/commands/pan/audit-deployment.md +384 -384
- package/commands/pan/focus-auto.md +683 -683
- package/commands/pan/focus-doc-audit.md +530 -530
- package/commands/pan/focus-drift-walking.md +525 -525
- package/commands/pan/git.md +1 -1
- package/commands/pan/hud.md +3 -2
- package/commands/pan/report.md +70 -0
- package/hooks/dist/pan-check-update.js +62 -62
- package/hooks/dist/pan-context-monitor.js +134 -122
- package/hooks/dist/pan-statusline.js +7 -1
- package/package.json +5 -5
- package/pan-wizard-core/bin/lib/config.cjs +14 -1
- package/pan-wizard-core/bin/lib/core.cjs +6 -2
- package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
- package/pan-wizard-core/bin/lib/focus.cjs +48 -2
- package/pan-wizard-core/bin/lib/frontmatter.cjs +442 -442
- package/pan-wizard-core/bin/lib/hud.cjs +202 -17
- package/pan-wizard-core/bin/lib/knowledge.cjs +2 -2
- package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
- package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
- package/pan-wizard-core/bin/lib/phase-report.cjs +723 -0
- package/pan-wizard-core/bin/lib/phase.cjs +4 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
- package/pan-wizard-core/bin/lib/utils.cjs +171 -171
- package/pan-wizard-core/bin/lib/verify.cjs +172 -61
- package/pan-wizard-core/bin/pan-tools.cjs +1499 -1463
- package/pan-wizard-core/references/checkpoints.md +776 -776
- package/pan-wizard-core/references/continuation-format.md +249 -249
- package/pan-wizard-core/references/questioning.md +145 -145
- package/pan-wizard-core/references/tdd.md +263 -263
- package/pan-wizard-core/references/ui-brand.md +160 -160
- package/pan-wizard-core/templates/config.json +38 -38
- package/pan-wizard-core/workflows/exec-phase.md +14 -0
- package/scripts/build-hooks.js +51 -51
- package/scripts/git-hooks/pre-commit +0 -0
- package/scripts/release-check.js +53 -47
- 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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
354
|
-
for (const exp of
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1288
|
-
|
|
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,
|