sillyspec 3.18.2 → 3.18.4
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/docs/brainstorm-plan-contract.md +64 -0
- package/docs/plan-execute-contract.md +123 -0
- package/docs/revision-mode.md +115 -0
- package/docs/sillyspec/file-lifecycle.md +13 -4
- package/docs/workflow-contract-regression.md +106 -0
- package/package.json +1 -1
- package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
- package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
- package/packages/dashboard/dist/index.html +16 -16
- package/packages/dashboard/src/components/PipelineStage.vue +22 -2
- package/packages/dashboard/src/components/PipelineView.vue +10 -2
- package/packages/dashboard/src/components/StageBadge.vue +17 -3
- package/packages/dashboard/src/components/StepCard.vue +7 -2
- package/src/change-risk-profile.js +167 -0
- package/src/contract-matrix.js +278 -0
- package/src/db.js +6 -0
- package/src/endpoint-extractor.js +315 -0
- package/src/index.js +53 -6
- package/src/init.js +31 -4
- package/src/knowledge-match.js +130 -0
- package/src/progress.js +464 -11
- package/src/run.js +287 -7
- package/src/scan-postcheck.js +34 -2
- package/src/stage-contract.js +86 -6
- package/src/stages/brainstorm.js +23 -0
- package/src/stages/execute.js +158 -4
- package/src/stages/plan.js +82 -0
- package/src/stages/scan.js +40 -0
- package/src/stages/verify.js +63 -2
- package/src/worktree.js +264 -35
- package/test/brainstorm-plan-contract.test.mjs +273 -0
- package/test/contract-artifacts.test.mjs +323 -0
- package/test/knowledge-match.test.mjs +231 -0
- package/test/plan-execute-contract.test.mjs +330 -0
- package/test/platform-failure-samples.test.mjs +4 -0
- package/test/revision-v1.test.mjs +1145 -0
- package/test/scan-knowledge.test.mjs +175 -0
- package/test/scan-postcheck.test.mjs +3 -0
- package/test/spec-dir.test.mjs +8 -3
- package/test/stage-definitions.test.mjs +1 -1
package/src/worktree.js
CHANGED
|
@@ -239,6 +239,8 @@ export class WorktreeManager {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// 5.5 自动同步远程最新代码(防止 worktree 基于过时的 commit)
|
|
242
|
+
let syncStatus = 'ok';
|
|
243
|
+
let syncError = null;
|
|
242
244
|
try {
|
|
243
245
|
// 先 fetch origin
|
|
244
246
|
gitQuiet(worktreePath, 'fetch origin');
|
|
@@ -262,8 +264,10 @@ export class WorktreeManager {
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
}
|
|
265
|
-
} catch {
|
|
266
|
-
|
|
267
|
+
} catch (e) {
|
|
268
|
+
syncStatus = 'failed';
|
|
269
|
+
syncError = e.message || String(e);
|
|
270
|
+
console.warn(`⚠️ worktree 远程同步失败:${syncError}`);
|
|
267
271
|
}
|
|
268
272
|
|
|
269
273
|
// 5.6 Dirty baseline overlay:将主工作区未提交变更同步到 worktree
|
|
@@ -290,6 +294,8 @@ export class WorktreeManager {
|
|
|
290
294
|
baselineFiles,
|
|
291
295
|
baselineCommit,
|
|
292
296
|
baselineHash,
|
|
297
|
+
syncStatus,
|
|
298
|
+
...(syncError ? { syncError } : {}),
|
|
293
299
|
};
|
|
294
300
|
|
|
295
301
|
const metaPath = join(worktreePath, META_FILE);
|
|
@@ -412,18 +418,22 @@ export class WorktreeManager {
|
|
|
412
418
|
|
|
413
419
|
/**
|
|
414
420
|
* 清理 worktree(仅限 SillySpec 创建的临时 worktree)
|
|
421
|
+
* 幂等:重复调用不报错。
|
|
422
|
+
* 三重清理:git worktree 注册 + worktree 目录 + meta 目录。
|
|
415
423
|
* @param {string} changeName
|
|
416
|
-
* @param {{ force?: boolean }} opts
|
|
417
|
-
* @
|
|
418
|
-
* @returns {{ result: 'cleaned'|'skipped'|'kept', mode: string }}
|
|
424
|
+
* @param {{ force?: boolean, maxRetries?: number }} opts
|
|
425
|
+
* @returns {{ result: 'cleaned'|'force-cleaned'|'skipped'|'kept', mode: string|null, details: string[] }}
|
|
419
426
|
*/
|
|
420
|
-
cleanup(changeName, { force = false } = {}) {
|
|
427
|
+
cleanup(changeName, { force = false, maxRetries = 3 } = {}) {
|
|
421
428
|
const name = validateChangeName(changeName);
|
|
422
429
|
const meta = this.getMeta(name);
|
|
423
430
|
const worktreePath = this.getWorktreePath(name);
|
|
431
|
+
const metaDir = join(this.worktreeBase, name);
|
|
432
|
+
const details = [];
|
|
424
433
|
|
|
425
|
-
|
|
426
|
-
|
|
434
|
+
// 幂等:什么都不存在 → 直接跳过
|
|
435
|
+
if (!meta && !existsSync(worktreePath) && !existsSync(metaDir)) {
|
|
436
|
+
return { result: 'skipped', mode: null, details };
|
|
427
437
|
}
|
|
428
438
|
|
|
429
439
|
const mode = meta?.mode || 'worktree';
|
|
@@ -431,50 +441,269 @@ export class WorktreeManager {
|
|
|
431
441
|
// 安全检查:只有 SillySpec 创建的 worktree 才允许删除
|
|
432
442
|
if (!force) {
|
|
433
443
|
if (mode === 'native-worktree') {
|
|
434
|
-
|
|
435
|
-
`当前 worktree 是外部/原生隔离环境(mode: native-worktree),SillySpec 不允许删除。\n` +
|
|
436
|
-
`此 worktree 不是由 SillySpec 创建的,请手动管理。\n` +
|
|
437
|
-
`如需强制清理,使用 --force 标志。`
|
|
438
|
-
);
|
|
444
|
+
return { result: 'kept', mode, details: ['native-worktree: 外部隔离环境,跳过清理'] };
|
|
439
445
|
}
|
|
440
446
|
if (mode === 'in-place-fallback') {
|
|
441
|
-
return { result: 'skipped', mode };
|
|
447
|
+
return { result: 'skipped', mode, details: ['in-place-fallback: 无隔离目录,跳过清理'] };
|
|
442
448
|
}
|
|
443
449
|
}
|
|
444
450
|
|
|
445
|
-
// 1. 尝试 git worktree remove
|
|
446
|
-
let gitRemoveOk = true;
|
|
447
|
-
try {
|
|
448
|
-
git(this.cwd, `worktree remove ${worktreePath} --force`);
|
|
449
|
-
} catch (e) {
|
|
450
|
-
gitRemoveOk = false;
|
|
451
|
-
}
|
|
452
451
|
const branch = (meta && meta.branch) || BRANCH_PREFIX + name;
|
|
453
452
|
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
|
|
453
|
+
// 1. git worktree remove(带 retry)
|
|
454
|
+
let gitRemoveOk = false;
|
|
455
|
+
if (existsSync(worktreePath)) {
|
|
456
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
457
|
+
try {
|
|
458
|
+
git(this.cwd, `worktree remove ${worktreePath} --force`);
|
|
459
|
+
gitRemoveOk = true;
|
|
460
|
+
details.push(`git worktree remove succeeded (attempt ${attempt})`);
|
|
461
|
+
break;
|
|
462
|
+
} catch (e) {
|
|
463
|
+
details.push(`git worktree remove attempt ${attempt}/${maxRetries} failed: ${e.message}`);
|
|
464
|
+
if (attempt < maxRetries) {
|
|
465
|
+
// 短暂等待后重试
|
|
466
|
+
execSync('sleep 0.5', { stdio: 'pipe' });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 2. fallback: 确保 worktree 目录已删除
|
|
473
|
+
if (existsSync(worktreePath)) {
|
|
474
|
+
try {
|
|
457
475
|
rmSync(worktreePath, { recursive: true, force: true });
|
|
476
|
+
details.push('worktree directory force-removed (fallback)');
|
|
477
|
+
} catch (e) {
|
|
478
|
+
details.push(`worktree directory force-remove failed: ${e.message}`);
|
|
458
479
|
}
|
|
459
|
-
} catch (e) {
|
|
460
|
-
throw new Error(`清理 worktree 目录失败: ${e.message}`);
|
|
461
480
|
}
|
|
462
481
|
|
|
463
|
-
// 3.
|
|
464
|
-
|
|
482
|
+
// 3. git worktree prune(清理 git 内部注册信息)
|
|
483
|
+
try {
|
|
484
|
+
gitQuiet(this.cwd, 'worktree prune');
|
|
485
|
+
} catch {
|
|
486
|
+
// prune 失败不阻断
|
|
487
|
+
}
|
|
465
488
|
|
|
466
|
-
// 4.
|
|
467
|
-
|
|
468
|
-
|
|
489
|
+
// 4. 删除分支(忽略分支不存在的错误)
|
|
490
|
+
try {
|
|
491
|
+
gitQuiet(this.cwd, `branch -D ${branch}`);
|
|
492
|
+
details.push('branch deleted');
|
|
493
|
+
} catch {
|
|
494
|
+
// 分支可能已被删除,幂等跳过
|
|
469
495
|
}
|
|
470
496
|
|
|
471
|
-
// 5. 清除 meta
|
|
472
|
-
const metaDir = join(this.worktreeBase, name);
|
|
497
|
+
// 5. 清除 meta 目录
|
|
473
498
|
if (existsSync(metaDir)) {
|
|
474
|
-
|
|
499
|
+
try {
|
|
500
|
+
rmSync(metaDir, { recursive: true, force: true });
|
|
501
|
+
details.push('meta directory cleaned');
|
|
502
|
+
} catch (e) {
|
|
503
|
+
details.push(`meta directory cleanup failed: ${e.message}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 6. 最终验证:确认三重清理完成
|
|
508
|
+
const residual = [];
|
|
509
|
+
if (existsSync(worktreePath)) residual.push(`worktree dir: ${worktreePath}`);
|
|
510
|
+
if (existsSync(metaDir)) residual.push(`meta dir: ${metaDir}`);
|
|
511
|
+
if (gitQuiet(this.cwd, `worktree list`)?.includes(worktreePath)) {
|
|
512
|
+
residual.push('git worktree list still references this worktree');
|
|
513
|
+
}
|
|
514
|
+
if (residual.length > 0) {
|
|
515
|
+
details.push(`⚠️ 残留: ${residual.join('; ')}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { result: gitRemoveOk ? 'cleaned' : 'force-cleaned', mode, details };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* worktree 健康检查 + 可选修复
|
|
523
|
+
* 检查项:
|
|
524
|
+
* - git worktree list 中的孤儿条目(目录不存在)
|
|
525
|
+
* - worktree 目录存在但 git 不认识
|
|
526
|
+
* - meta 存在但 worktree 目录不存在
|
|
527
|
+
* - worktree 目录存在但 meta 不存在(幽灵目录)
|
|
528
|
+
* - SillySpec 分支残留(sillyspec/* 但无对应 meta)
|
|
529
|
+
* - 超过指定小时的过期 worktree
|
|
530
|
+
*
|
|
531
|
+
* @param {{ fix?: boolean, staleHours?: number }} opts
|
|
532
|
+
* @returns {{ issues: Array<{ type: string, name: string, detail: string, fixable: boolean }>, fixed: string[], unfixable: string[] }}
|
|
533
|
+
*/
|
|
534
|
+
doctor({ fix = false, staleHours = 24 } = {}) {
|
|
535
|
+
const issues = [];
|
|
536
|
+
const fixed = [];
|
|
537
|
+
const unfixable = [];
|
|
538
|
+
|
|
539
|
+
// 1. 列出 git worktree list 中的条目
|
|
540
|
+
let gitWorktreeList = [];
|
|
541
|
+
try {
|
|
542
|
+
const raw = execSync(`git worktree list --porcelain`, { cwd: this.cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] });
|
|
543
|
+
const entries = raw.split(/\n\n/).filter(Boolean);
|
|
544
|
+
for (const entry of entries) {
|
|
545
|
+
const lines = entry.split('\n');
|
|
546
|
+
const wtPath = lines.find(l => l.startsWith('worktree '))?.replace('worktree ', '');
|
|
547
|
+
if (wtPath && wtPath !== this.cwd) { // 排除主工作区
|
|
548
|
+
gitWorktreeList.push({ path: wtPath, raw: entry });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
// git worktree 不可用,跳过
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 2. 列出 SillySpec meta 条目
|
|
556
|
+
const metaEntries = this.list();
|
|
557
|
+
const metaNames = new Set(metaEntries.map(m => m.changeName));
|
|
558
|
+
|
|
559
|
+
// 3. 检查 git worktree list 中的孤儿条目
|
|
560
|
+
for (const wt of gitWorktreeList) {
|
|
561
|
+
if (!existsSync(wt.path)) {
|
|
562
|
+
const name = this._pathToChangeName(wt.path);
|
|
563
|
+
issues.push({ type: 'orphan-git-entry', name: name || wt.path, detail: `git worktree 引用存在但目录不存在: ${wt.path}`, fixable: true });
|
|
564
|
+
if (fix) {
|
|
565
|
+
try { gitQuiet(this.cwd, 'worktree prune'); fixed.push(`pruned orphan: ${wt.path}`); } catch { unfixable.push(`prune failed for: ${wt.path}`); }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 4. 扫描 worktreeBase 目录,检查幽灵目录和孤儿 meta
|
|
571
|
+
if (existsSync(this.worktreeBase)) {
|
|
572
|
+
const entries = readdirSync(this.worktreeBase, { withFileTypes: true });
|
|
573
|
+
for (const entry of entries) {
|
|
574
|
+
if (!entry.isDirectory()) continue;
|
|
575
|
+
const name = entry.name;
|
|
576
|
+
const dirPath = join(this.worktreeBase, name);
|
|
577
|
+
const hasMeta = existsSync(join(dirPath, META_FILE));
|
|
578
|
+
const meta = hasMeta ? this.getMeta(name) : null;
|
|
579
|
+
|
|
580
|
+
// meta 存在但 worktree 目录不存在
|
|
581
|
+
if (meta && meta.worktreePath && !existsSync(meta.worktreePath)) {
|
|
582
|
+
issues.push({ type: 'meta-no-dir', name, detail: `meta 存在但 worktree 目录不存在: ${meta.worktreePath}`, fixable: true });
|
|
583
|
+
if (fix) {
|
|
584
|
+
try { rmSync(dirPath, { recursive: true, force: true }); fixed.push(`cleaned orphan meta: ${name}`); } catch { unfixable.push(`cleanup failed for: ${name}`); }
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// worktree 目录存在但 meta 不存在(幽灵目录)
|
|
589
|
+
if (!hasMeta && existsSync(dirPath)) {
|
|
590
|
+
// 可能是 in-place 模式的 meta-only 目录,或者真正的幽灵
|
|
591
|
+
const files = readdirSync(dirPath);
|
|
592
|
+
if (files.length === 0 || (files.length === 1 && files[0] === META_FILE)) {
|
|
593
|
+
issues.push({ type: 'ghost-dir', name, detail: `空目录/幽灵目录: ${dirPath}`, fixable: true });
|
|
594
|
+
if (fix) {
|
|
595
|
+
try { rmSync(dirPath, { recursive: true, force: true }); fixed.push(`removed ghost dir: ${name}`); } catch { unfixable.push(`remove failed for: ${name}`); }
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
issues.push({ type: 'ghost-dir-with-files', name, detail: `目录存在但无 meta.json: ${dirPath} (含 ${files.length} 文件)`, fixable: false });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 检查过期 worktree
|
|
603
|
+
if (meta && meta.createdAt) {
|
|
604
|
+
const ageMs = Date.now() - new Date(meta.createdAt).getTime();
|
|
605
|
+
const ageHours = ageMs / (1000 * 60 * 60);
|
|
606
|
+
if (ageHours > staleHours) {
|
|
607
|
+
issues.push({ type: 'stale', name, detail: `worktree 已存在 ${Math.round(ageHours)} 小时(超过 ${staleHours}h 阈值)`, fixable: true });
|
|
608
|
+
if (fix && meta.mode !== 'native-worktree') {
|
|
609
|
+
try {
|
|
610
|
+
const result = this.cleanup(name);
|
|
611
|
+
if (result.result === 'cleaned' || result.result === 'force-cleaned') {
|
|
612
|
+
fixed.push(`cleaned stale: ${name}`);
|
|
613
|
+
} else {
|
|
614
|
+
unfixable.push(`cleanup skipped: ${name}`);
|
|
615
|
+
}
|
|
616
|
+
} catch { unfixable.push(`cleanup failed: ${name}`); }
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 5. 检查 SillySpec 分支残留
|
|
624
|
+
try {
|
|
625
|
+
const branches = execSync(`git branch --list '${BRANCH_PREFIX}*'`, { cwd: this.cwd, encoding: 'utf8', stdio: ['pipe','pipe','pipe'] }).trim();
|
|
626
|
+
if (branches) {
|
|
627
|
+
for (const line of branches.split('\n').filter(Boolean)) {
|
|
628
|
+
const branch = line.replace(/^\*?\s+/, '').trim();
|
|
629
|
+
const name = branch.replace(BRANCH_PREFIX, '');
|
|
630
|
+
if (!metaNames.has(name)) {
|
|
631
|
+
issues.push({ type: 'orphan-branch', name, detail: `分支残留(无对应 meta): ${branch}`, fixable: true });
|
|
632
|
+
if (fix) {
|
|
633
|
+
try { gitQuiet(this.cwd, `branch -D ${branch}`); fixed.push(`deleted orphan branch: ${branch}`); } catch { unfixable.push(`branch delete failed: ${branch}`); }
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch {}
|
|
639
|
+
|
|
640
|
+
return { issues, fixed, unfixable };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* 检查 worktree 是否有未 apply 到主工作区的变更
|
|
645
|
+
* @param {string} changeName
|
|
646
|
+
* @returns {{ hasChanges: boolean, changedFiles: string[], reason?: string }}
|
|
647
|
+
*/
|
|
648
|
+
hasUnappliedChanges(changeName) {
|
|
649
|
+
const name = validateChangeName(changeName);
|
|
650
|
+
const meta = this.getMeta(name);
|
|
651
|
+
if (!meta) return { hasChanges: false, changedFiles: [], reason: 'no meta' };
|
|
652
|
+
|
|
653
|
+
const worktreePath = meta.worktreePath;
|
|
654
|
+
if (!worktreePath || !existsSync(worktreePath)) {
|
|
655
|
+
return { hasChanges: false, changedFiles: [], reason: 'worktree dir not found' };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// in-place 模式没有隔离目录,不算有未 apply 的变更
|
|
659
|
+
if (meta.mode === 'in-place-fallback') {
|
|
660
|
+
return { hasChanges: false, changedFiles: [], reason: 'in-place mode' };
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const diffBase = meta.baselineCommit || meta.baseHash;
|
|
664
|
+
if (!diffBase) {
|
|
665
|
+
return { hasChanges: false, changedFiles: [], reason: 'no diff base' };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
// tracked 文件变更
|
|
670
|
+
const statusRaw = gitQuiet(worktreePath, `diff --name-status ${diffBase}`) || '';
|
|
671
|
+
const statusFiles = new Set();
|
|
672
|
+
if (statusRaw) {
|
|
673
|
+
for (const line of statusRaw.split('\n').filter(Boolean)) {
|
|
674
|
+
const parts = line.split('\t');
|
|
675
|
+
if (parts.length >= 2) statusFiles.add(parts[parts.length - 1]);
|
|
676
|
+
if (parts.length >= 3) statusFiles.add(parts[parts.length - 2]);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// untracked 文件
|
|
681
|
+
const untrackedRaw = gitQuiet(worktreePath, `ls-files --others --exclude-standard`) || '';
|
|
682
|
+
const untrackedFiles = untrackedRaw
|
|
683
|
+
? untrackedRaw.split('\n').filter(Boolean).filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json')
|
|
684
|
+
: [];
|
|
685
|
+
|
|
686
|
+
const changedFiles = [...new Set([...statusFiles, ...untrackedFiles])];
|
|
687
|
+
return { hasChanges: changedFiles.length > 0, changedFiles };
|
|
688
|
+
} catch (e) {
|
|
689
|
+
// 检测失败时保守处理:视为有变更
|
|
690
|
+
return { hasChanges: true, changedFiles: [], reason: `diff failed: ${e.message}` };
|
|
475
691
|
}
|
|
692
|
+
}
|
|
476
693
|
|
|
477
|
-
|
|
694
|
+
/**
|
|
695
|
+
* 从 worktree 路径反推 changeName
|
|
696
|
+
* @private
|
|
697
|
+
*/
|
|
698
|
+
_pathToChangeName(wtPath) {
|
|
699
|
+
try {
|
|
700
|
+
const resolved = resolve(wtPath);
|
|
701
|
+
const baseResolved = resolve(this.worktreeBase);
|
|
702
|
+
if (resolved.startsWith(baseResolved + '/')) {
|
|
703
|
+
return resolved.slice(baseResolved.length + 1);
|
|
704
|
+
}
|
|
705
|
+
} catch {}
|
|
706
|
+
return null;
|
|
478
707
|
}
|
|
479
708
|
|
|
480
709
|
/**
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brainstorm → Plan Contract v1 测试
|
|
3
|
+
*
|
|
4
|
+
* 验证 design.md 到 plan 的输入契约:
|
|
5
|
+
* 1. 合法 design 通过
|
|
6
|
+
* 2. 缺关键章节失败
|
|
7
|
+
* 3. warning 不阻断
|
|
8
|
+
*/
|
|
9
|
+
import { validateDesignForPlan } from '../src/stages/plan.js'
|
|
10
|
+
|
|
11
|
+
let failed = 0
|
|
12
|
+
const failures = []
|
|
13
|
+
|
|
14
|
+
function assert(condition, msg) {
|
|
15
|
+
if (!condition) {
|
|
16
|
+
failed++
|
|
17
|
+
failures.push(msg)
|
|
18
|
+
console.log(` ❌ FAIL: ${msg}`)
|
|
19
|
+
} else {
|
|
20
|
+
console.log(` ✅ PASS: ${msg}`)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log('=== Brainstorm → Plan Contract v1 测试 ===\n')
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────
|
|
27
|
+
// Case 1: valid design 通过
|
|
28
|
+
// ─────────────────────────────────────────
|
|
29
|
+
console.log('--- Case 1: valid design 通过 ---')
|
|
30
|
+
{
|
|
31
|
+
const design = `# Design: 用户认证系统
|
|
32
|
+
|
|
33
|
+
## 背景
|
|
34
|
+
需要实现用户认证。
|
|
35
|
+
|
|
36
|
+
## 设计目标
|
|
37
|
+
- 支持 OAuth2
|
|
38
|
+
- 支持手机号登录
|
|
39
|
+
|
|
40
|
+
## 非目标
|
|
41
|
+
- 不做 SSO
|
|
42
|
+
|
|
43
|
+
## 总体方案
|
|
44
|
+
使用 JWT + Refresh Token。
|
|
45
|
+
|
|
46
|
+
## 决策
|
|
47
|
+
- D-001@v1: 选择 JWT 而非 Session
|
|
48
|
+
|
|
49
|
+
## 约束
|
|
50
|
+
- 必须兼容现有 API
|
|
51
|
+
|
|
52
|
+
## 文件变更清单
|
|
53
|
+
| 操作 | 文件路径 | 说明 |
|
|
54
|
+
|------|---------|------|
|
|
55
|
+
| 新增 | src/auth.js | 认证模块 |
|
|
56
|
+
`
|
|
57
|
+
const result = validateDesignForPlan(design)
|
|
58
|
+
assert(result.ok, '完整 design 应校验通过')
|
|
59
|
+
assert(result.errors.length === 0, '不应有 errors')
|
|
60
|
+
assert(result.warnings.length === 0, '不应有 warnings')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────
|
|
64
|
+
// Case 2: empty design 失败
|
|
65
|
+
// ─────────────────────────────────────────
|
|
66
|
+
console.log('\n--- Case 2: empty design 失败 ---')
|
|
67
|
+
{
|
|
68
|
+
assert(!validateDesignForPlan('').ok, '空字符串应失败')
|
|
69
|
+
assert(!validateDesignForPlan(null).ok, 'null 应失败')
|
|
70
|
+
assert(!validateDesignForPlan(' ').ok, '纯空格应失败')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────
|
|
74
|
+
// Case 3: missing goal 失败
|
|
75
|
+
// ─────────────────────────────────────────
|
|
76
|
+
console.log('\n--- Case 3: missing goal/背景 失败 ---')
|
|
77
|
+
{
|
|
78
|
+
const design = `# Design
|
|
79
|
+
|
|
80
|
+
## 总体方案
|
|
81
|
+
用 JWT。
|
|
82
|
+
|
|
83
|
+
## 决策
|
|
84
|
+
D-001@v1: 选 JWT
|
|
85
|
+
`
|
|
86
|
+
const result = validateDesignForPlan(design)
|
|
87
|
+
assert(!result.ok, '缺目标/背景应失败')
|
|
88
|
+
assert(result.errors.some(e => e.includes('目标') || e.includes('背景')), '错误应提到目标/背景')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────
|
|
92
|
+
// Case 4: missing scope/方案 失败
|
|
93
|
+
// ─────────────────────────────────────────
|
|
94
|
+
console.log('\n--- Case 4: missing scope/方案 失败 ---')
|
|
95
|
+
{
|
|
96
|
+
const design = `# Design
|
|
97
|
+
|
|
98
|
+
## 背景
|
|
99
|
+
需要认证。
|
|
100
|
+
|
|
101
|
+
## 决策
|
|
102
|
+
D-001@v1: 选 JWT
|
|
103
|
+
`
|
|
104
|
+
const result = validateDesignForPlan(design)
|
|
105
|
+
assert(!result.ok, '缺范围/方案应失败')
|
|
106
|
+
assert(result.errors.some(e => e.includes('范围') || e.includes('方案')), '错误应提到范围/方案')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────────────────
|
|
110
|
+
// Case 5: missing decisions 失败
|
|
111
|
+
// ─────────────────────────────────────────
|
|
112
|
+
console.log('\n--- Case 5: missing decisions 失败 ---')
|
|
113
|
+
{
|
|
114
|
+
const design = `# Design
|
|
115
|
+
|
|
116
|
+
## 背景
|
|
117
|
+
需要认证。
|
|
118
|
+
|
|
119
|
+
## 总体方案
|
|
120
|
+
用 JWT。
|
|
121
|
+
`
|
|
122
|
+
const result = validateDesignForPlan(design)
|
|
123
|
+
assert(!result.ok, '缺决策应失败')
|
|
124
|
+
assert(result.errors.some(e => e.includes('决策')), '错误应提到决策')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─────────────────────────────────────────
|
|
128
|
+
// Case 6: decisions.md 引用也算决策
|
|
129
|
+
// ─────────────────────────────────────────
|
|
130
|
+
console.log('\n--- Case 6: decisions.md 引用算决策 ---')
|
|
131
|
+
{
|
|
132
|
+
const design = `# Design
|
|
133
|
+
|
|
134
|
+
## 背景
|
|
135
|
+
需要认证。
|
|
136
|
+
|
|
137
|
+
## 总体方案
|
|
138
|
+
用 JWT。详见 decisions.md。
|
|
139
|
+
|
|
140
|
+
## 文件变更清单
|
|
141
|
+
| 操作 | 文件 | 说明 |
|
|
142
|
+
`
|
|
143
|
+
const result = validateDesignForPlan(design)
|
|
144
|
+
assert(result.ok, 'decisions.md 引用应满足决策检查')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────
|
|
148
|
+
// Case 7: missing non-goals 只有 warning
|
|
149
|
+
// ─────────────────────────────────────────
|
|
150
|
+
console.log('\n--- Case 7: missing non-goals warning ---')
|
|
151
|
+
{
|
|
152
|
+
const design = `# Design
|
|
153
|
+
|
|
154
|
+
## 背景
|
|
155
|
+
需要认证。
|
|
156
|
+
|
|
157
|
+
## 总体方案
|
|
158
|
+
用 JWT。
|
|
159
|
+
|
|
160
|
+
## 决策
|
|
161
|
+
D-001@v1: 选 JWT
|
|
162
|
+
|
|
163
|
+
## 约束
|
|
164
|
+
必须兼容现有 API。
|
|
165
|
+
`
|
|
166
|
+
const result = validateDesignForPlan(design)
|
|
167
|
+
assert(result.ok, '缺非目标不应阻断')
|
|
168
|
+
assert(result.warnings.some(w => w.includes('非目标') || w.includes('Non-goals')), '应有非目标 warning')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────
|
|
172
|
+
// Case 8: missing constraints 只有 warning
|
|
173
|
+
// ─────────────────────────────────────────
|
|
174
|
+
console.log('\n--- Case 8: missing constraints warning ---')
|
|
175
|
+
{
|
|
176
|
+
const design = `# Design
|
|
177
|
+
|
|
178
|
+
## 目标
|
|
179
|
+
实现认证。
|
|
180
|
+
|
|
181
|
+
## 设计方案
|
|
182
|
+
用 JWT。
|
|
183
|
+
|
|
184
|
+
## 决策
|
|
185
|
+
选择 JWT。
|
|
186
|
+
|
|
187
|
+
## 非目标
|
|
188
|
+
不做 SSO。
|
|
189
|
+
`
|
|
190
|
+
const result = validateDesignForPlan(design)
|
|
191
|
+
assert(result.ok, '缺约束不应阻断')
|
|
192
|
+
assert(result.warnings.some(w => w.includes('约束') || w.includes('风险') || w.includes('Trade-off')), '应有约束/风险 warning')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─────────────────────────────────────────
|
|
196
|
+
// Case 9: missing 文件变更清单 warning
|
|
197
|
+
// ─────────────────────────────────────────
|
|
198
|
+
console.log('\n--- Case 9: missing 文件变更清单 warning ---')
|
|
199
|
+
{
|
|
200
|
+
const design = `# Design
|
|
201
|
+
|
|
202
|
+
## 背景
|
|
203
|
+
需要认证。
|
|
204
|
+
|
|
205
|
+
## 总体方案
|
|
206
|
+
用 JWT。
|
|
207
|
+
|
|
208
|
+
## 决策
|
|
209
|
+
D-001@v1: 选 JWT
|
|
210
|
+
`
|
|
211
|
+
const result = validateDesignForPlan(design)
|
|
212
|
+
assert(result.ok, '缺文件变更清单不应阻断')
|
|
213
|
+
assert(result.warnings.some(w => w.includes('文件变更')), '应有文件变更 warning')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─────────────────────────────────────────
|
|
217
|
+
// Case 10: 英文 design 通过
|
|
218
|
+
// ─────────────────────────────────────────
|
|
219
|
+
console.log('\n--- Case 10: 英文 design 通过 ---')
|
|
220
|
+
{
|
|
221
|
+
const design = `# Design: Auth System
|
|
222
|
+
|
|
223
|
+
## Background
|
|
224
|
+
We need authentication.
|
|
225
|
+
|
|
226
|
+
## Solution
|
|
227
|
+
Use JWT + Refresh Token.
|
|
228
|
+
|
|
229
|
+
## Decision
|
|
230
|
+
D-001@v1: Choose JWT over Session
|
|
231
|
+
|
|
232
|
+
## Non-goals
|
|
233
|
+
No SSO.
|
|
234
|
+
|
|
235
|
+
## Constraints
|
|
236
|
+
Must be backwards compatible.
|
|
237
|
+
`
|
|
238
|
+
const result = validateDesignForPlan(design)
|
|
239
|
+
assert(result.ok, '英文 design 应校验通过')
|
|
240
|
+
assert(result.errors.length === 0, '不应有 errors')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────
|
|
244
|
+
// Case 11: 最小合法 design
|
|
245
|
+
// ─────────────────────────────────────────
|
|
246
|
+
console.log('\n--- Case 11: 最小合法 design ---')
|
|
247
|
+
{
|
|
248
|
+
const design = `# Design
|
|
249
|
+
|
|
250
|
+
## 目标
|
|
251
|
+
修 bug。
|
|
252
|
+
|
|
253
|
+
## 方案
|
|
254
|
+
改代码。
|
|
255
|
+
|
|
256
|
+
## 决策
|
|
257
|
+
D-001@v1: 直接改。
|
|
258
|
+
`
|
|
259
|
+
const result = validateDesignForPlan(design)
|
|
260
|
+
assert(result.ok, '最小合法 design 应通过')
|
|
261
|
+
// 可能有 warning 但 ok
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── 结果 ──
|
|
265
|
+
console.log(`\n${'='.repeat(50)}`)
|
|
266
|
+
console.log(`✅ 通过: ${11 - failed} ❌ 失败: ${failed}`)
|
|
267
|
+
if (failures.length > 0) {
|
|
268
|
+
console.log(`失败项:`)
|
|
269
|
+
failures.forEach(f => console.log(` - ${f}`))
|
|
270
|
+
}
|
|
271
|
+
console.log(`${'='.repeat(50)}`)
|
|
272
|
+
|
|
273
|
+
if (failed > 0) process.exit(1)
|