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/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>
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
}
|