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.
Files changed (40) hide show
  1. package/docs/brainstorm-plan-contract.md +64 -0
  2. package/docs/plan-execute-contract.md +123 -0
  3. package/docs/revision-mode.md +115 -0
  4. package/docs/sillyspec/file-lifecycle.md +13 -4
  5. package/docs/workflow-contract-regression.md +106 -0
  6. package/package.json +1 -1
  7. package/packages/dashboard/dist/assets/{index-DpLHK4jv.js → index-Bq_Z2hne.js} +568 -568
  8. package/packages/dashboard/dist/assets/{index-BcM2J-hv.css → index-O2W5RV4z.css} +1 -1
  9. package/packages/dashboard/dist/index.html +16 -16
  10. package/packages/dashboard/src/components/PipelineStage.vue +22 -2
  11. package/packages/dashboard/src/components/PipelineView.vue +10 -2
  12. package/packages/dashboard/src/components/StageBadge.vue +17 -3
  13. package/packages/dashboard/src/components/StepCard.vue +7 -2
  14. package/src/change-risk-profile.js +167 -0
  15. package/src/contract-matrix.js +278 -0
  16. package/src/db.js +6 -0
  17. package/src/endpoint-extractor.js +315 -0
  18. package/src/index.js +53 -6
  19. package/src/init.js +31 -4
  20. package/src/knowledge-match.js +130 -0
  21. package/src/progress.js +464 -11
  22. package/src/run.js +287 -7
  23. package/src/scan-postcheck.js +34 -2
  24. package/src/stage-contract.js +86 -6
  25. package/src/stages/brainstorm.js +23 -0
  26. package/src/stages/execute.js +158 -4
  27. package/src/stages/plan.js +82 -0
  28. package/src/stages/scan.js +40 -0
  29. package/src/stages/verify.js +63 -2
  30. package/src/worktree.js +264 -35
  31. package/test/brainstorm-plan-contract.test.mjs +273 -0
  32. package/test/contract-artifacts.test.mjs +323 -0
  33. package/test/knowledge-match.test.mjs +231 -0
  34. package/test/plan-execute-contract.test.mjs +330 -0
  35. package/test/platform-failure-samples.test.mjs +4 -0
  36. package/test/revision-v1.test.mjs +1145 -0
  37. package/test/scan-knowledge.test.mjs +175 -0
  38. package/test/scan-postcheck.test.mjs +3 -0
  39. package/test/spec-dir.test.mjs +8 -3
  40. package/test/stage-definitions.test.mjs +1 -1
package/src/index.js CHANGED
@@ -31,7 +31,9 @@ SillySpec CLI — 规范驱动开发工具包
31
31
  --done --output "..." 完成当前步骤
32
32
  --skip 跳过可选步骤
33
33
  --status 查看阶段进度
34
- --reset 重置阶段
34
+ --reset 重置阶段(从头开始)
35
+ --reopen 重新打开已完成阶段进入修订模式
36
+ --from-step <index|name> 配合 --reopen:从指定步骤开始修订
35
37
  --change <name> 设置当前变更名
36
38
  --spec-dir <path> 指定规范目录(默认 <项目>/.sillyspec)
37
39
  --runtime-root <path> 平台模式:运行时产物根路径
@@ -43,6 +45,10 @@ SillySpec CLI — 规范驱动开发工具包
43
45
  scan, brainstorm, plan, execute, verify, archive
44
46
  quick, explore, status, doctor
45
47
 
48
+ Revision mode:
49
+ 已完成阶段不能直接重跑。使用 --reopen --from-step 进入受控修订。
50
+ 重开会使下游阶段自动标记为 stale,但不修改已有产物文件。
51
+
46
52
  sillyspec progress <cmd> 进度记录(轻量,不强制顺序)
47
53
  init 初始化项目数据库
48
54
  show 查看当前进度
@@ -50,6 +56,8 @@ SillySpec CLI — 规范驱动开发工具包
50
56
  add-step <stage> <name> 添加步骤
51
57
  update-step <s> <n> --status <st> [--output <t>]
52
58
  complete-stage <stage> 标记阶段完成
59
+ check 状态一致性检查(只报告,不修复)
60
+ repair [--apply] 修复状态元数据(默认 dry-run,--apply 才修改)
53
61
  validate 校验并修复
54
62
  reset [--stage X] 重置进度
55
63
 
@@ -174,6 +182,14 @@ async function main() {
174
182
  case 'show':
175
183
  pm.show(dir, progChangeName);
176
184
  break;
185
+ case 'check':
186
+ await pm.checkConsistency(dir, progChangeName);
187
+ break;
188
+ case 'repair': {
189
+ const repairApply = filteredArgs.includes('--apply');
190
+ await pm.repairConsistency(dir, { apply: repairApply, changeName: progChangeName });
191
+ break;
192
+ }
177
193
  case 'validate':
178
194
  await pm.validate(dir, progChangeName);
179
195
  break;
@@ -313,7 +329,8 @@ SillySpec worktree — git worktree 隔离管理
313
329
  sillyspec worktree create <change-name> [--base <branch>] 创建隔离 worktree
314
330
  sillyspec worktree apply <change-name> [--check-only] 校验并应用变更到主工作区
315
331
  sillyspec worktree list 列出所有活跃 worktree
316
- sillyspec worktree cleanup <change-name> 强制清理 worktree
332
+ sillyspec worktree cleanup <change-name> [--force] 强制清理 worktree
333
+ sillyspec worktree doctor [--fix] [--stale-hours N] 健康检查 + 修复
317
334
 
318
335
  选项:
319
336
  --base <branch> create: 指定基础分支(默认当前 HEAD)
@@ -413,11 +430,13 @@ SillySpec worktree — git worktree 隔离管理
413
430
  const forceFlag = args.includes('--force');
414
431
  try {
415
432
  const result = wm.cleanup(wtName, { force: forceFlag });
416
- if (result.result === 'cleaned') {
433
+ if (result.result === 'cleaned' || result.result === 'force-cleaned') {
417
434
  console.log(`✅ worktree 已清理: ${wtName} (mode: ${result.mode})`);
418
- } else if (result.result === 'force-cleaned') {
419
- console.log(`⚠️ worktree 已强制清理: ${wtName} (mode: ${result.mode})`);
420
- console.log(` 原因: git worktree remove 失败,通过直接删除目录完成`);
435
+ if (result.details?.length > 0) {
436
+ for (const d of result.details) {
437
+ if (d.startsWith('⚠️')) console.log(` ${d}`);
438
+ }
439
+ }
421
440
  } else if (result.result === 'skipped') {
422
441
  console.log(`⏭️ worktree 跳过清理: ${wtName} (mode: ${result.mode})`);
423
442
  console.log(` 原因: in-place 模式没有隔离目录需要清理`);
@@ -430,6 +449,34 @@ SillySpec worktree — git worktree 隔离管理
430
449
  }
431
450
  break;
432
451
  }
452
+ case 'doctor': {
453
+ const fixFlag = args.includes('--fix');
454
+ const staleIdx = args.indexOf('--stale-hours');
455
+ const staleHours = staleIdx !== -1 && args[staleIdx + 1] ? parseInt(args[staleIdx + 1], 10) : 24;
456
+ const diag = wm.doctor({ fix: fixFlag, staleHours });
457
+ if (diag.issues.length === 0) {
458
+ console.log('✅ worktree 健康检查通过,无异常');
459
+ } else {
460
+ console.log(`🔍 发现 ${diag.issues.length} 个问题:\n`);
461
+ for (const issue of diag.issues) {
462
+ const icon = issue.fixable ? '⚠️' : '❌';
463
+ console.log(` ${icon} [${issue.type}] ${issue.name}: ${issue.detail}`);
464
+ }
465
+ if (fixFlag) {
466
+ console.log(`\n🔧 修复完成:`);
467
+ for (const f of diag.fixed) console.log(` ✅ ${f}`);
468
+ if (diag.unfixable.length > 0) {
469
+ for (const u of diag.unfixable) console.log(` ❌ ${u}`);
470
+ }
471
+ if (diag.fixed.length === 0 && diag.unfixable.length === 0) {
472
+ console.log(' 无需修复');
473
+ }
474
+ } else {
475
+ console.log(`\n💡 运行 sillyspec worktree doctor --fix 自动修复`);
476
+ }
477
+ }
478
+ break;
479
+ }
433
480
  default:
434
481
  console.error(`❌ 未知子命令: worktree ${wtSubCmd}`);
435
482
  console.log(' 运行 sillyspec worktree --help 查看帮助');
package/src/init.js CHANGED
@@ -108,12 +108,39 @@ async function doInstall(projectDir, tools, subprojects = [], specDir = null) {
108
108
  // projectDir: 源码项目根目录(用于工具检测、指令注入、.gitignore)
109
109
  const spec = specDir || join(projectDir, '.sillyspec');
110
110
 
111
- // 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)
111
+ // 外部 specDir 时清理旧版本残留的 cwd/.sillyspec/(防止源码污染)。
112
+ // ⚠️ 必须保护真实资产:若本地 .sillyspec 含 changes/(非空)、projects/(非空)
113
+ // 或 sillyspec.db(进度库),说明该项目本身就用 SillySpec 管理,整体删除会丢资产。
114
+ // 此时只清运行时残留,拒绝整删;确无资产时才视为旧残留清理。
112
115
  const legacyDir = join(projectDir, '.sillyspec');
113
116
  if (specDir && existsSync(legacyDir)) {
114
- try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
115
- if (!existsSync(legacyDir)) console.log('🧹 已清理旧版本残留的源码 .sillyspec/ 目录');
116
- else console.error('⚠️ 清理残留 .sillyspec/ 失败');
117
+ let hasChanges = false;
118
+ try {
119
+ const changesDir = join(legacyDir, 'changes');
120
+ if (existsSync(changesDir)) hasChanges = readdirSync(changesDir).length > 0;
121
+ } catch {}
122
+ let hasProjects = false;
123
+ try {
124
+ const projectsDir = join(legacyDir, 'projects');
125
+ if (existsSync(projectsDir)) hasProjects = readdirSync(projectsDir).length > 0;
126
+ } catch {}
127
+ const hasDb = existsSync(join(legacyDir, 'sillyspec.db'));
128
+
129
+ if (hasChanges || hasProjects || hasDb) {
130
+ // 真实资产存在:拒绝整体删除,仅清理运行时残留
131
+ console.error('❌ [sillyspec] 拒绝删除源码目录的 .sillyspec/:检测到真实资产(changes/、projects/ 或 sillyspec.db)。');
132
+ console.error(' 该项目似乎本身就用 SillySpec 管理。如需改用外部 spec 目录,请先手动迁移/备份。');
133
+ console.error(' 本次仅清理运行时残留(.runtime/、local.yaml、codebase/)。');
134
+ for (const residue of ['.runtime', 'local.yaml', 'codebase']) {
135
+ const p = join(legacyDir, residue);
136
+ if (existsSync(p)) { try { rmSync(p, { recursive: true, force: true }) } catch {} }
137
+ }
138
+ } else {
139
+ // 无真实资产:确属旧版本残留,安全删除
140
+ try { rmSync(legacyDir, { recursive: true, force: true }) } catch {}
141
+ if (!existsSync(legacyDir)) console.log('🧹 已清理旧版本残留的源码 .sillyspec/ 目录');
142
+ else console.error('⚠️ 清理残留 .sillyspec/ 失败');
143
+ }
117
144
  }
118
145
 
119
146
  // 创建基础目录
@@ -0,0 +1,130 @@
1
+ /**
2
+ * knowledge-match.js — knowledge 关键词匹配引擎
3
+ * 从 INDEX.md 解析知识条目,按任务上下文匹配并生成 hit report
4
+ */
5
+
6
+ import { existsSync, readFileSync } from 'fs'
7
+ import { join } from 'path'
8
+
9
+ /**
10
+ * 从 INDEX.md 解析所有知识条目
11
+ * @param {string} indexDir - knowledge 目录路径
12
+ * @returns {{ category: string, keywords: string[], file: string, anchor: string, display: string, line: string }[]}
13
+ */
14
+ export function parseKnowledgeIndex(indexDir) {
15
+ const indexPath = join(indexDir, 'INDEX.md')
16
+ if (!existsSync(indexPath)) return []
17
+
18
+ const content = readFileSync(indexPath, 'utf8')
19
+ const entries = []
20
+ let currentCategory = ''
21
+
22
+ for (const line of content.split('\n')) {
23
+ // 匹配 ## Category 行
24
+ const catMatch = line.match(/^##\s+(.+)/)
25
+ if (catMatch) {
26
+ currentCategory = catMatch[1].trim()
27
+ continue
28
+ }
29
+
30
+ // 匹配条目行:- 关键词1|关键词2 → [显示名](文件名#锚点)
31
+ const entryMatch = line.match(/^-\s+(.+?)\s*→\s*\[(.+?)\]\(([^#)]+)(?:#([^)]+))?\)/)
32
+ if (entryMatch) {
33
+ const keywords = entryMatch[1].split('|').map(k => k.trim()).filter(Boolean)
34
+ const display = entryMatch[2].trim()
35
+ const file = entryMatch[3].trim()
36
+ const anchor = entryMatch[4] ? entryMatch[4].trim() : ''
37
+ entries.push({ category: currentCategory, keywords, file, anchor, display, line })
38
+ }
39
+ }
40
+
41
+ return entries
42
+ }
43
+
44
+ function escapeRegex(s) {
45
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
46
+ }
47
+
48
+ /**
49
+ * 单个关键词是否命中上下文。
50
+ * - 过短关键词(<2 字符)视为噪音,不参与匹配
51
+ * - 非 ASCII(中文等)用子串匹配
52
+ * - ASCII 关键词用词边界匹配,避免子串误命中(如 "DB" 不命中 "dashboard")
53
+ */
54
+ function keywordMatchesContext(keyword, contextLower) {
55
+ const kw = keyword.toLowerCase().trim()
56
+ if (kw.length < 2) return false
57
+ if (/[^\x00-\x7f]/.test(kw)) return contextLower.includes(kw)
58
+ return new RegExp(`(^|[^a-z0-9])${escapeRegex(kw)}([^a-z0-9]|$)`).test(contextLower)
59
+ }
60
+
61
+ /**
62
+ * 用任务上下文匹配知识条目
63
+ * @param {string} indexDir - knowledge 目录路径
64
+ * @param {string} taskContext - 任务上下文(task 名称 + 描述,用于关键词匹配)
65
+ * @returns {{ matched: boolean, entries: Array, report: string, json: object }}
66
+ */
67
+ export function matchKnowledge(indexDir, taskContext) {
68
+ const indexPath = join(indexDir, 'INDEX.md')
69
+
70
+ // INDEX.md 不存在
71
+ if (!existsSync(indexPath)) {
72
+ return {
73
+ matched: false,
74
+ entries: [],
75
+ report: 'Status: no matches (INDEX.md not found)',
76
+ json: { matched: false, entry_count: 0, entries: [] }
77
+ }
78
+ }
79
+
80
+ const allEntries = parseKnowledgeIndex(indexDir)
81
+ if (allEntries.length === 0 || !taskContext) {
82
+ return {
83
+ matched: false,
84
+ entries: [],
85
+ report: 'Status: no matches',
86
+ json: { matched: false, entry_count: 0, entries: [] }
87
+ }
88
+ }
89
+
90
+ const contextLower = taskContext.toLowerCase()
91
+ const matched = allEntries.filter(entry => {
92
+ return entry.keywords.some(kw => keywordMatchesContext(kw, contextLower))
93
+ })
94
+
95
+ if (matched.length === 0) {
96
+ return {
97
+ matched: false,
98
+ entries: [],
99
+ report: 'Status: no matches',
100
+ json: { matched: false, entry_count: 0, entries: [] }
101
+ }
102
+ }
103
+
104
+ const sources = matched.map(e => {
105
+ const base = e.anchor ? `${e.file}#${e.anchor}` : e.file
106
+ return ` - ${base}`
107
+ }).join('\n')
108
+
109
+ const report = [
110
+ 'Knowledge Context',
111
+ '─────────────────',
112
+ `Status: matched`,
113
+ `Entries: ${matched.length}`,
114
+ 'Sources:',
115
+ sources
116
+ ].join('\n')
117
+
118
+ const json = {
119
+ matched: true,
120
+ entry_count: matched.length,
121
+ entries: matched.map(e => ({
122
+ file: e.file,
123
+ anchor: e.anchor,
124
+ keywords: e.keywords,
125
+ category: e.category
126
+ }))
127
+ }
128
+
129
+ return { matched: true, entries: matched, report, json }
130
+ }