sillyspec 3.26.14 → 3.26.15
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/.claude/skills/sillyspec-archive/SKILL.md +2 -0
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
- package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
- package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
- package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
- package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
- package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
- package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
- package/package.json +1 -1
- package/src/commit-suggest.js +202 -0
- package/src/config-cat.js +88 -0
- package/src/docs-check.js +48 -12
- package/src/doctor-diagnostics.js +49 -0
- package/src/hooks/worktree-guard.js +20 -1
- package/src/index.js +508 -6
- package/src/init.js +1 -1
- package/src/local-register.js +98 -0
- package/src/module-impact.js +146 -0
- package/src/progress/change-registry.js +43 -4
- package/src/progress/stage-machine.js +10 -5
- package/src/progress.js +13 -1
- package/src/quicklog.js +106 -1
- package/src/run/command.js +95 -1
- package/src/run/complete-handlers.js +56 -3
- package/src/run/complete.js +4 -7
- package/src/run/gates.js +62 -4
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +20 -2
- package/src/run/shared.js +2 -2
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +15 -6
- package/src/stages/archive.js +1 -1
- package/src/stages/brainstorm.js +1 -0
- package/src/stages/execute.js +105 -26
- package/src/stages/knowledge.js +3 -2
- package/src/stages/quick.js +14 -6
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +7 -49
- package/src/task-review.js +218 -4
- package/src/verify-probes.js +264 -0
- package/src/workspace.js +125 -0
- package/src/worktree-apply.js +15 -1
- package/src/worktree.js +58 -8
- package/templates/claude-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +1 -1
- package/templates/prompts/verify-probes.md +13 -47
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-probes.js — `sillyspec verify-probes` 的机械探针实现 + verify-result.md 骨架生成
|
|
3
|
+
* (2026-08-21 agent-手工产出审计第三批 H1/H3/H5/H7/F9)。
|
|
4
|
+
*
|
|
5
|
+
* verify-probes.md 模板定义六个探针,agent 此前逐条手跑 grep/递归查找/git 对账再手工拼表格。
|
|
6
|
+
* 本模块把纯机械的四个探针命令化(语义判断的留 agent,输出里显式标注):
|
|
7
|
+
* 探针1 未实现标记扫描:design §6 清单的具体文件逐行 grep TODO/FIXME/尚未实现 等
|
|
8
|
+
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/ 陷阱)
|
|
9
|
+
* 探针5 API 契约对账:复用 contract-matrix.verifyApiParity(endpoints.json × 前端调用)+ 表格渲染
|
|
10
|
+
* 探针6 删除对账:git diff --name-status HEAD 的 D/R × design 声明操作三态判定
|
|
11
|
+
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
|
+
*
|
|
13
|
+
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
14
|
+
* 结论章节留「待填」——extractVerifyConclusion 找不到 PASS/FAIL 关键词即判不过,骨架不能
|
|
15
|
+
* 直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
18
|
+
import { join, dirname, basename } from 'path'
|
|
19
|
+
import { gitQuiet } from './git-helper.js'
|
|
20
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
21
|
+
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
22
|
+
import { verifyApiParity } from './contract-matrix.js'
|
|
23
|
+
import { resolveSpecDir, resolveRuntimeRoot } from './run/shared.js'
|
|
24
|
+
|
|
25
|
+
const TODO_MARKER_RE = /尚未实现|TODO|FIXME|HACK|XXX/
|
|
26
|
+
const TEST_FILE_RE = /test|spec/i
|
|
27
|
+
const PROBE1_MAX_MATCHES = 200
|
|
28
|
+
|
|
29
|
+
function listTaskIds(tasksPath) {
|
|
30
|
+
if (!existsSync(tasksPath)) return []
|
|
31
|
+
const ids = []
|
|
32
|
+
for (const line of readFileSync(tasksPath, 'utf8').split('\n')) {
|
|
33
|
+
const m = line.match(/^[-*]\s*\[[ xX]\]\s*(task-\d+)\b/)
|
|
34
|
+
if (m) ids.push(m[1])
|
|
35
|
+
}
|
|
36
|
+
return ids
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 递归找测试文件(排除 node_modules/.git/dist 等;返回 posix 相对 cwd 路径,封顶展示) */
|
|
40
|
+
function findTestFiles(rootDir, cwd, cap = 10) {
|
|
41
|
+
const found = []
|
|
42
|
+
const skip = new Set(['node_modules', '.git', '.gradle', '__pycache__', '.venv', 'dist', 'build', 'target', 'out'])
|
|
43
|
+
const walk = (d) => {
|
|
44
|
+
let entries
|
|
45
|
+
try {
|
|
46
|
+
entries = readdirSync(d, { withFileTypes: true })
|
|
47
|
+
} catch {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const full = join(d, e.name)
|
|
52
|
+
if (e.isDirectory()) {
|
|
53
|
+
if (!skip.has(e.name)) walk(full)
|
|
54
|
+
} else if (e.isFile() && TEST_FILE_RE.test(basename(e.name))) {
|
|
55
|
+
found.push(full.split('\\').join('/').replace(cwd.split('\\').join('/').replace(/\/$/, '') + '/', ''))
|
|
56
|
+
if (found.length >= cap) return
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(rootDir)) walk(rootDir)
|
|
61
|
+
return found
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 跑四个机械探针。
|
|
66
|
+
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
67
|
+
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object }}
|
|
68
|
+
*/
|
|
69
|
+
export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
70
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
71
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
72
|
+
const designPath = join(changeDir, 'design.md')
|
|
73
|
+
const detailed = existsSync(designPath) ? parseFileChangeListDetailed(designPath) : []
|
|
74
|
+
const designOps = new Map(detailed.map(e => [e.path, e.operation]))
|
|
75
|
+
|
|
76
|
+
// ── 探针 1:未实现标记扫描(design 清单具体文件;glob 项列出让 agent 展开)──
|
|
77
|
+
const probe1 = { matches: [], globEntries: [], skippedFiles: [] }
|
|
78
|
+
for (const e of detailed) {
|
|
79
|
+
if (e.path.startsWith('.sillyspec/')) continue
|
|
80
|
+
if (/[*?[\]]/.test(e.path)) {
|
|
81
|
+
probe1.globEntries.push(e.path)
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const abs = join(cwd, e.path)
|
|
85
|
+
if (!existsSync(abs)) {
|
|
86
|
+
probe1.skippedFiles.push(e.path)
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const lines = readFileSync(abs, 'utf8').split('\n')
|
|
91
|
+
lines.forEach((line, i) => {
|
|
92
|
+
if (probe1.matches.length >= PROBE1_MAX_MATCHES) return
|
|
93
|
+
if (TODO_MARKER_RE.test(line)) probe1.matches.push({ file: e.path, line: i + 1, content: line.trim().slice(0, 160) })
|
|
94
|
+
})
|
|
95
|
+
} catch {
|
|
96
|
+
probe1.skippedFiles.push(e.path)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── 探针 3:验收标准测试覆盖(task 卡 allowed_paths → 模块目录递归找测试文件)──
|
|
101
|
+
const tasksPath = join(changeDir, 'tasks.md')
|
|
102
|
+
const probe3 = { tasks: [], note: '集成盲区(路由/跨模块装配)与断言有效性抽查是语义判断,留给 agent 逐 task 标注 ⚠️' }
|
|
103
|
+
for (const taskId of listTaskIds(tasksPath)) {
|
|
104
|
+
const cardPath = join(changeDir, 'tasks', `${taskId}.md`)
|
|
105
|
+
let allowed = []
|
|
106
|
+
if (existsSync(cardPath)) allowed = parseAllowedPaths(readFileSync(cardPath, 'utf8'))
|
|
107
|
+
const moduleDirs = [...new Set(allowed.map(p => dirname(p.split('\\').join('/'))).filter(d => d && d !== '.'))]
|
|
108
|
+
const testFiles = []
|
|
109
|
+
for (const d of moduleDirs) {
|
|
110
|
+
for (const f of findTestFiles(join(cwd, d), cwd)) {
|
|
111
|
+
if (!testFiles.includes(f)) testFiles.push(f)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
probe3.tasks.push({
|
|
115
|
+
task: taskId,
|
|
116
|
+
moduleDirs,
|
|
117
|
+
testFiles: testFiles.slice(0, 10),
|
|
118
|
+
testFileCount: testFiles.length,
|
|
119
|
+
hasTest: testFiles.length > 0,
|
|
120
|
+
located: moduleDirs.length > 0,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── 探针 5:API 契约对账(复用 verifyApiParity:endpoints.json × 前端调用)──
|
|
125
|
+
const runtimeRoot = resolveRuntimeRoot({ specRoot: specDir }, specBase)
|
|
126
|
+
const probe5 = verifyApiParity(specBase, cwd, runtimeRoot, changeName)
|
|
127
|
+
|
|
128
|
+
// ── 探针 6:代码删除对账(git diff --name-status HEAD 的 D/R × design 声明三态)──
|
|
129
|
+
const probe6 = { deletions: [], note: '以 git 事实为准(真实 > 声明);是否 FAIL blocker 由 agent 诚实判定' }
|
|
130
|
+
const nsOut = gitQuiet(cwd, ['diff', '--name-status', 'HEAD']) || ''
|
|
131
|
+
for (const row of nsOut.split('\n').filter(Boolean)) {
|
|
132
|
+
const [status, ...paths] = row.split('\t')
|
|
133
|
+
const st = (status || '').trim().toUpperCase()
|
|
134
|
+
if (!/^[DRC]/.test(st)) continue
|
|
135
|
+
// R/C 的旧路径等价删除(paths[0]);D 的唯一路径
|
|
136
|
+
const target = (st.startsWith('D') ? paths[0] : paths[0]) || ''
|
|
137
|
+
const posix = target.split('\\').join('/')
|
|
138
|
+
if (!posix || posix.startsWith('.sillyspec/') || posix === 'meta.json' || basename(posix) === 'meta.json') continue
|
|
139
|
+
const op = designOps.get(posix)
|
|
140
|
+
let verdict
|
|
141
|
+
if (op && /删除|delete|del/i.test(op)) verdict = '✅ 合规(design 声明删除)'
|
|
142
|
+
else if (op) verdict = `❌ 高风险(design 声明「${op}」却整文件删除)`
|
|
143
|
+
else verdict = '⚠️ 未声明删除(design 清单未列出)'
|
|
144
|
+
probe6.deletions.push({ path: posix, status: st, designOp: op || null, verdict })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { probe1, probe3, probe5, probe6 }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 渲染探针结果为 markdown(可直接粘进 verify-result.md「探针结果」章节)。
|
|
152
|
+
*/
|
|
153
|
+
export function renderVerifyProbesReport(result) {
|
|
154
|
+
const L = []
|
|
155
|
+
const { probe1, probe3, probe5, probe6 } = result
|
|
156
|
+
|
|
157
|
+
L.push('#### 探针 1:未实现标记扫描(design 清单文件)')
|
|
158
|
+
if (probe1.matches.length === 0) {
|
|
159
|
+
L.push('- ✅ 无 TODO/FIXME/尚未实现 标记命中')
|
|
160
|
+
} else {
|
|
161
|
+
for (const m of probe1.matches) L.push(`- ⚠️ \`${m.file}:${m.line}\` ${m.content}`)
|
|
162
|
+
}
|
|
163
|
+
if (probe1.globEntries.length > 0) L.push(`- ℹ️ glob 项未展开(agent 手动展开扫描):${probe1.globEntries.join('、')}`)
|
|
164
|
+
if (probe1.skippedFiles.length > 0) L.push(`- ℹ️ 清单文件不存在(跳过):${probe1.skippedFiles.join('、')}`)
|
|
165
|
+
L.push('')
|
|
166
|
+
|
|
167
|
+
L.push('#### 探针 2:设计关键词覆盖')
|
|
168
|
+
L.push('<!--TODO: 半语义探针——从 design 提取能力关键词逐个 grep 确认实现(agent 执行)-->')
|
|
169
|
+
L.push('')
|
|
170
|
+
|
|
171
|
+
L.push('#### 探针 3:验收标准测试覆盖')
|
|
172
|
+
if (probe3.tasks.length === 0) {
|
|
173
|
+
L.push('- ℹ️ tasks.md 无 checkbox 任务')
|
|
174
|
+
} else {
|
|
175
|
+
for (const t of probe3.tasks) {
|
|
176
|
+
if (!t.located) {
|
|
177
|
+
L.push(`- ⚠️ ${t.task}: 无 task 卡/allowed_paths,无法定位模块目录(agent 手查)`)
|
|
178
|
+
} else if (t.hasTest) {
|
|
179
|
+
L.push(`- ✅ ${t.task}: 模块目录(${t.moduleDirs.join('、')})找到 ${t.testFileCount} 个测试文件(${t.testFiles.slice(0, 5).join('、')}${t.testFileCount > 5 ? ' …' : ''})`)
|
|
180
|
+
} else {
|
|
181
|
+
L.push(`- ⚠️ ${t.task}: 模块目录(${t.moduleDirs.join('、')})递归未找到测试文件(含 co-located tests/)`)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
L.push(`- ℹ️ ${probe3.note}`)
|
|
186
|
+
L.push('')
|
|
187
|
+
|
|
188
|
+
L.push('#### 探针 4:决策追踪覆盖')
|
|
189
|
+
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->')
|
|
190
|
+
L.push('')
|
|
191
|
+
|
|
192
|
+
L.push('#### 探针 5:API Contract Parity')
|
|
193
|
+
L.push(`- ${probe5.summary || `backend ${probe5.backendCount ?? 0} 端点 / frontend ${probe5.frontendCount ?? 0} 调用`}`)
|
|
194
|
+
if ((probe5.missingBackend || []).length > 0) {
|
|
195
|
+
L.push('')
|
|
196
|
+
L.push('| 状态 | 前端调用 | 后端端点 | 文件 |')
|
|
197
|
+
L.push('|---|---|---|---|')
|
|
198
|
+
for (const m of probe5.missingBackend) {
|
|
199
|
+
L.push(`| ❌ missing | ${m.method} ${m.path} | — | ${m.consumerFile || '?'}${m.consumerLine ? ':' + m.consumerLine : ''} |`)
|
|
200
|
+
}
|
|
201
|
+
L.push('')
|
|
202
|
+
L.push('- ❌ contract gap 是真实集成缺陷——诚实判 FAIL 并回 execute 补端点(CLI 仅 advisory 不硬阻断)')
|
|
203
|
+
}
|
|
204
|
+
if ((probe5.unusedBackend || []).length > 0) {
|
|
205
|
+
L.push(`- ⚠️ ${probe5.unusedBackend.length} 个后端端点前端未调用(warning 不阻断):${probe5.unusedBackend.slice(0, 5).map(u => `${u.method} ${u.path}`).join('、')}${probe5.unusedBackend.length > 5 ? ' …' : ''}`)
|
|
206
|
+
}
|
|
207
|
+
L.push('')
|
|
208
|
+
|
|
209
|
+
L.push('#### 探针 6:代码删除对账')
|
|
210
|
+
if (probe6.deletions.length === 0) {
|
|
211
|
+
L.push('- ✅ git diff 无整文件删除(D/R/C)记录')
|
|
212
|
+
} else {
|
|
213
|
+
for (const d of probe6.deletions) {
|
|
214
|
+
L.push(`- ${d.verdict} \`${d.path}\`(git 状态 ${d.status})`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
L.push(`- ℹ️ ${probe6.note}`)
|
|
218
|
+
return L.join('\n')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
223
|
+
* 结论章节留「待填」——extractVerifyConclusion 无 PASS/FAIL 关键词即判不过,骨架不能直接过门。
|
|
224
|
+
* @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
|
|
225
|
+
*/
|
|
226
|
+
export function generateVerifyResultSkeleton(result) {
|
|
227
|
+
const L = [
|
|
228
|
+
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
229
|
+
'',
|
|
230
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论必须写明 PASS / FAIL**——',
|
|
231
|
+
'> 留「待填」会被 gate 判不过(fail-closed)。',
|
|
232
|
+
'',
|
|
233
|
+
'## 结论:<待填:PASS 或 FAIL(+一句话理由)>',
|
|
234
|
+
'',
|
|
235
|
+
'## 任务完成度',
|
|
236
|
+
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
|
237
|
+
'',
|
|
238
|
+
'## 设计一致性',
|
|
239
|
+
'<!--TODO: 实现与 design.md 的偏差(无偏差也显式写「一致」)-->',
|
|
240
|
+
'',
|
|
241
|
+
'## 探针结果(CLI 机械预填)',
|
|
242
|
+
renderVerifyProbesReport(result),
|
|
243
|
+
'',
|
|
244
|
+
'## 测试结果',
|
|
245
|
+
'<!--TODO: 测试命令 + 结果(通过数/失败数;known_failures 豁免逐条注明)-->',
|
|
246
|
+
'',
|
|
247
|
+
'## 决策追踪矩阵(如存在 decisions.md;无则删本节)',
|
|
248
|
+
'<!--TODO: | 决策 ID | FR | Task | Evidence | 状态 |(D-xxx@vN → FR-xxx → task → 证据回指闭环)-->',
|
|
249
|
+
'',
|
|
250
|
+
'## 技术债务',
|
|
251
|
+
'<!--TODO: TODO/FIXME/HACK 统计(探针 1 的命中已预填在上方探针结果)-->',
|
|
252
|
+
'',
|
|
253
|
+
'## 变更风险等级',
|
|
254
|
+
'<!--TODO: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,写明「显式声明 = <等级>」+ 理由(豁免可审计)-->',
|
|
255
|
+
'',
|
|
256
|
+
'## Runtime Evidence',
|
|
257
|
+
'<!--TODO: 关键命令输出/时间戳/commit hash 证据链;integration/deployment-critical 必填,按实际触碰的运行时组件写(启动命令/端点/请求响应/日志片段/生命周期终态断言/失败模式排除),未涉及的行写「不涉及」-->',
|
|
258
|
+
'',
|
|
259
|
+
'## 代码审查',
|
|
260
|
+
'<!--TODO: 问题列表 + 总体评价-->',
|
|
261
|
+
'',
|
|
262
|
+
]
|
|
263
|
+
return L.join('\n')
|
|
264
|
+
}
|
package/src/workspace.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace.js — `sillyspec workspace add/remove/status` 的多项目工作区管理
|
|
3
|
+
* (2026-08-21 agent-手工产出审计第四批 C-1)。
|
|
4
|
+
*
|
|
5
|
+
* workspace skill 此前纯手写:`.sillyspec/projects/<name>.yaml` 5 字段 YAML 手编、remove 手删
|
|
6
|
+
* 文件手 commit、status 用 bash for-loop + grep/sed 解析 YAML 逐项目 cd 探测——skill 明言
|
|
7
|
+
* 「CLI 没有 workspace 命令」。本模块命令化全部机械面:
|
|
8
|
+
* - add:YAML 外科写入(name/path/status 必填 + role/repo 可选追加,已有字段保留)、路径校验
|
|
9
|
+
* - remove:删 yaml(--commit 时顺带 git add 该删除)
|
|
10
|
+
* - status:逐项目探测(yaml 解析 + .sillyspec 产物存在性),输出 skill 原格式的三态表
|
|
11
|
+
* init 语义对齐(skill L75):init 只写 name/path/status 三字段,role/repo 是 add 专属追加。
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, mkdirSync } from 'fs'
|
|
14
|
+
import { join, basename, resolve } from 'path'
|
|
15
|
+
import { resolveSpecDir } from './run/shared.js'
|
|
16
|
+
|
|
17
|
+
function parseSimpleYaml(text) {
|
|
18
|
+
const out = {}
|
|
19
|
+
for (const line of String(text || '').split('\n')) {
|
|
20
|
+
const m = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/)
|
|
21
|
+
if (m) out[m[1]] = m[2].trim()
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readProjectYaml(specBase, name) {
|
|
27
|
+
const p = join(specBase, 'projects', `${name}.yaml`)
|
|
28
|
+
if (!existsSync(p)) return null
|
|
29
|
+
return parseSimpleYaml(readFileSync(p, 'utf8'))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 新增/更新子项目登记。
|
|
34
|
+
* @param {{ cwd: string, name: string, path: string, role?: string|null, repo?: string|null, specDir?: string|null }} opts
|
|
35
|
+
* @returns {{ file: string, created: boolean, fieldsKept: string[] }}
|
|
36
|
+
* @throws {Error} name 非法 / path 不存在
|
|
37
|
+
*/
|
|
38
|
+
export function workspaceAdd({ cwd, name, path: projPath, role = null, repo = null, specDir = null }) {
|
|
39
|
+
if (!name || !/^[A-Za-z0-9_.\-]+$/.test(name)) {
|
|
40
|
+
throw new Error(`子项目名非法: "${name}"(限字母数字 . _ -,作 yaml 文件名)`)
|
|
41
|
+
}
|
|
42
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
43
|
+
const abs = resolve(cwd, projPath)
|
|
44
|
+
if (!existsSync(abs)) {
|
|
45
|
+
throw new Error(`路径不存在: ${abs}`)
|
|
46
|
+
}
|
|
47
|
+
const yamlPath = join(specBase, 'projects', `${name}.yaml`)
|
|
48
|
+
const existed = existsSync(yamlPath)
|
|
49
|
+
const prev = existed ? parseSimpleYaml(readFileSync(yamlPath, 'utf8')) : {}
|
|
50
|
+
// init 三字段 + add 追加字段;已有值优先保留(幂等重跑不覆盖手调内容)
|
|
51
|
+
const fields = {
|
|
52
|
+
name,
|
|
53
|
+
path: projPath.replace(/\\/g, '/'),
|
|
54
|
+
status: prev.status || 'active',
|
|
55
|
+
}
|
|
56
|
+
if (role) fields.role = role
|
|
57
|
+
else if (prev.role) fields.role = prev.role
|
|
58
|
+
if (repo) fields.repo = repo
|
|
59
|
+
else if (prev.repo) fields.repo = prev.repo
|
|
60
|
+
const fieldsKept = Object.keys(prev).filter(k => fields[k] === prev[k] && k !== 'name' && k !== 'path')
|
|
61
|
+
mkdirSync(join(specBase, 'projects'), { recursive: true })
|
|
62
|
+
// 追加不认识的手写字段(防整文件重序列化丢内容)
|
|
63
|
+
for (const k of Object.keys(prev)) {
|
|
64
|
+
if (!(k in fields)) fields[k] = prev[k]
|
|
65
|
+
}
|
|
66
|
+
const text = Object.entries(fields).map(([k, v]) => `${k}: ${v}`).join('\n') + '\n'
|
|
67
|
+
writeFileSync(yamlPath, text)
|
|
68
|
+
return { file: yamlPath, created: !existed, fieldsKept }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 移除子项目登记(删 yaml;git add 由调用方/CLI 层做)。
|
|
73
|
+
* @returns {{ file: string }} @throws {Error} 不存在
|
|
74
|
+
*/
|
|
75
|
+
export function workspaceRemove({ cwd, name, specDir = null }) {
|
|
76
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
77
|
+
const yamlPath = join(specBase, 'projects', `${name}.yaml`)
|
|
78
|
+
if (!existsSync(yamlPath)) throw new Error(`子项目不存在: ${yamlPath}`)
|
|
79
|
+
unlinkSync(yamlPath)
|
|
80
|
+
return { file: yamlPath }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 工作区状态探测(skill 3d 的命令化):逐项目 yaml 解析 + 三态探测。
|
|
85
|
+
* @returns {{ projects: Array<{name, path, role, state, detail}>, sharedDocs: string[] }}
|
|
86
|
+
* state ∈ initialized(有 .sillyspec)/ scanned(docs/<name>/scan 有文档)/ unregistered(无 .sillyspec)
|
|
87
|
+
*/
|
|
88
|
+
export function workspaceStatus({ cwd, specDir = null }) {
|
|
89
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
90
|
+
const projects = []
|
|
91
|
+
const projDir = join(specBase, 'projects')
|
|
92
|
+
if (existsSync(projDir)) {
|
|
93
|
+
for (const f of readdirSync(projDir).filter(f => f.endsWith('.yaml')).sort()) {
|
|
94
|
+
const y = parseSimpleYaml(readFileSync(join(projDir, f), 'utf8'))
|
|
95
|
+
const name = y.name || basename(f, '.yaml')
|
|
96
|
+
const projAbs = resolve(cwd, y.path || '.')
|
|
97
|
+
let state
|
|
98
|
+
let detail
|
|
99
|
+
if (!existsSync(projAbs)) {
|
|
100
|
+
state = 'missing'
|
|
101
|
+
detail = '路径不存在(yaml 的 path 失效)'
|
|
102
|
+
} else if (!existsSync(join(projAbs, '.sillyspec'))) {
|
|
103
|
+
state = 'unregistered'
|
|
104
|
+
detail = '未初始化(无 .sillyspec)'
|
|
105
|
+
} else {
|
|
106
|
+
const scanDir = join(projAbs, '.sillyspec', 'docs', name, 'scan')
|
|
107
|
+
let scanCount = 0
|
|
108
|
+
try { scanCount = readdirSync(scanDir).filter(x => x.endsWith('.md')).length } catch {}
|
|
109
|
+
if (scanCount > 0) {
|
|
110
|
+
state = 'scanned'
|
|
111
|
+
detail = `已扫描(${scanCount} 份文档)`
|
|
112
|
+
} else {
|
|
113
|
+
state = 'initialized'
|
|
114
|
+
detail = '已初始化(未扫描)'
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
projects.push({ name, path: y.path || '?', role: y.role || '', state, detail })
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
let sharedDocs = []
|
|
121
|
+
try {
|
|
122
|
+
sharedDocs = readdirSync(join(specBase, 'shared')).filter(f => f.endsWith('.md')).sort()
|
|
123
|
+
} catch {}
|
|
124
|
+
return { projects, sharedDocs }
|
|
125
|
+
}
|
package/src/worktree-apply.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 8. 成功后自动 cleanup
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync, readFileSync } from 'fs';
|
|
15
|
+
import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync, readFileSync, mkdirSync } from 'fs';
|
|
16
16
|
import { join, resolve } from 'path';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
18
|
import { WorktreeManager } from './worktree.js';
|
|
@@ -694,6 +694,20 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
694
694
|
|
|
695
695
|
result.ok = true;
|
|
696
696
|
|
|
697
|
+
// --- 7.5 提交复用 pathspec(坑 apply-commit-pathspec-sweep,2026-08-21 实证)---
|
|
698
|
+
// apply 后主仓常混有无关未提交文件(他者会话/并行 quick),agent 习惯 `git add <目录>/`
|
|
699
|
+
// 会把无关文件扫进暂存需手工剔除。落盘本变更精确 pathspec 供提交直接复用:
|
|
700
|
+
// 文件 + result 字段(CLI 输出可照抄 git add -- … / 长清单用 --pathspec-from-file)。
|
|
701
|
+
try {
|
|
702
|
+
const commitFiles = [...new Set(patchFiles)].sort().filter(Boolean);
|
|
703
|
+
result.commitPathspec = commitFiles;
|
|
704
|
+
const runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
|
|
705
|
+
mkdirSync(runtimeRoot, { recursive: true });
|
|
706
|
+
const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`);
|
|
707
|
+
writeFileSync(pathspecFile, commitFiles.join('\n') + '\n');
|
|
708
|
+
result.pathspecFile = pathspecFile;
|
|
709
|
+
} catch { /* pathspec 落盘失败不影响 apply 结果 */ }
|
|
710
|
+
|
|
697
711
|
// --- 8. 成功后自动 cleanup(失败不影响整体结果) ---
|
|
698
712
|
try {
|
|
699
713
|
wm.cleanup(changeName, { force: true });
|
package/src/worktree.js
CHANGED
|
@@ -907,11 +907,32 @@ export class WorktreeManager {
|
|
|
907
907
|
|
|
908
908
|
// 4. 删除分支(fail-closed:task review 引用可达性校验。force 也不绕过——force 语义 =
|
|
909
909
|
// 丢弃内容,不含丢弃审计链;确要删用 git branch -D 手动执行)
|
|
910
|
+
// 升级(坑 cleanup-branch-review-anchor-tag,2026-08-21 实证):分支被 review.json 引用
|
|
911
|
+
// 时不再只能手动保留——打 `sillyspec-audit/<branch>` 轻量 tag 锚定分支 tip(ref 前缀独立
|
|
912
|
+
// 于 sillyspec/* 分支族,doctor 孤儿分支扫描不误伤),commit 经 tag 保持可达(gc 安全),
|
|
913
|
+
// 随后正常删分支。tag 创建失败 → 回退保留(宁保留勿丢审计链)。
|
|
910
914
|
try {
|
|
911
915
|
const reviewRefs = this._branchReviewReferences(branch);
|
|
912
916
|
if (reviewRefs.length > 0) {
|
|
913
|
-
|
|
914
|
-
|
|
917
|
+
const tagName = 'sillyspec-audit/' + branch;
|
|
918
|
+
let tagOk = false;
|
|
919
|
+
try {
|
|
920
|
+
if (!gitQuiet(this.cwd, ['rev-parse', '--verify', '--quiet', `refs/tags/${tagName}`])) {
|
|
921
|
+
git(this.cwd, ['tag', tagName, branch]);
|
|
922
|
+
}
|
|
923
|
+
tagOk = true;
|
|
924
|
+
} catch (e) {
|
|
925
|
+
details.push(`audit tag creation failed: ${e.message}`);
|
|
926
|
+
}
|
|
927
|
+
if (tagOk) {
|
|
928
|
+
gitQuiet(this.cwd, ['branch', '-D', branch]);
|
|
929
|
+
details.push(`branch deleted after anchoring tip to tag ${tagName} (audit 链可达)`);
|
|
930
|
+
console.log(`🔗 审计锚定:${branch} 被 ${reviewRefs.length} 个 task review.json 引用其 commit——已打 tag ${tagName} 锚定(commit 保持可达,gc 安全)并删除分支 ref。`);
|
|
931
|
+
console.log(` 确认不再需要该审计链后可删 tag:git tag -d ${tagName}`);
|
|
932
|
+
} else {
|
|
933
|
+
details.push(`branch kept: ${reviewRefs.length} 个 task review.json 引用分支上的 commit(base/head 审计保护)`);
|
|
934
|
+
console.log(`🔗 分支保留:${branch} 上有 ${reviewRefs.length} 个 task review.json 引用其 commit(apply 只复制文件内容,ref 删除后 base/head 悬空无法复核)。确要删除:git branch -D ${branch}`);
|
|
935
|
+
}
|
|
915
936
|
} else {
|
|
916
937
|
gitQuiet(this.cwd, ['branch', '-D', branch]);
|
|
917
938
|
details.push('branch deleted');
|
|
@@ -1388,6 +1409,13 @@ export class WorktreeManager {
|
|
|
1388
1409
|
* 将主工作区未提交变更同步到 worktree(dirty baseline overlay)
|
|
1389
1410
|
* 覆盖 staged + unstaged 的文件变更,以及 untracked 文件。
|
|
1390
1411
|
* 使用 git diff + git apply 确保正确处理删除/rename/binary。
|
|
1412
|
+
*
|
|
1413
|
+
* .sillyspec/ 隔离(坑 baseline-overlay-cross-change-contamination,2026-08-21 实证):
|
|
1414
|
+
* overlay 原样全量吸收主仓未提交改动——多变更并行时其他变更的 spec 文档
|
|
1415
|
+
* (changes/<他变更>/、ROADMAP.md、quicklog 等 39 个文件实测)被 checkpoint 进本变更
|
|
1416
|
+
* baseline,apply 回 main 即随本变更交付,需人工隔离。spec 文档不参与 worktree 内
|
|
1417
|
+
* 构建/测试(execute 的 spec 读写经 specDriftAnchor 锚回主仓),排除零功能损失;
|
|
1418
|
+
* 排除清单显式打印保持可见(哪些跨变更文件被隔离、去哪了)。
|
|
1391
1419
|
* @param {string} mainCwd - 主工作区路径
|
|
1392
1420
|
* @param {string} worktreePath - worktree 路径
|
|
1393
1421
|
* @returns {Array<string>} overlay 的文件列表
|
|
@@ -1395,15 +1423,26 @@ export class WorktreeManager {
|
|
|
1395
1423
|
_overlayBaseline(mainCwd, worktreePath) {
|
|
1396
1424
|
const files = [];
|
|
1397
1425
|
const errors = [];
|
|
1426
|
+
// git pathspec 排除(execFileSync 数组传参不经 shell,字面安全):.sillyspec/ 整目录
|
|
1427
|
+
const EXCLUDE_PATHSPEC = ':(exclude).sillyspec';
|
|
1428
|
+
const collectExcluded = (raw, excluded) => {
|
|
1429
|
+
for (const f of raw.split('\n').filter(Boolean)) {
|
|
1430
|
+
const norm = f.replace(/\\/g, '/')
|
|
1431
|
+
if (norm === '.sillyspec' || norm.startsWith('.sillyspec/')) excluded.push(f)
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1398
1434
|
|
|
1399
1435
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1436
|
+
const excludedSpecFiles = [];
|
|
1437
|
+
// staged 变更(pathspec 排除 .sillyspec/——跨变更 spec 文档不进 baseline)
|
|
1438
|
+
const staged = gitQuiet(mainCwd, ['diff', '--cached', '--name-only', '--', '.', EXCLUDE_PATHSPEC], { timeout: 30000 }) || ''
|
|
1439
|
+
const stagedAll = gitQuiet(mainCwd, ['diff', '--cached', '--name-only'], { timeout: 30000 }) || ''
|
|
1440
|
+
collectExcluded(stagedAll, excludedSpecFiles)
|
|
1402
1441
|
if (staged) {
|
|
1403
1442
|
try {
|
|
1404
1443
|
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏
|
|
1405
1444
|
// QUAL-01 收口:git-helper 新增 encoding:'buffer' 支持(二进制输出专用,跳过 trim)
|
|
1406
|
-
const patchBuf = git(mainCwd, ['diff', '--cached', '--binary'], { encoding: 'buffer', timeout: 30000 });
|
|
1445
|
+
const patchBuf = git(mainCwd, ['diff', '--cached', '--binary', '--', '.', EXCLUDE_PATHSPEC], { encoding: 'buffer', timeout: 30000 });
|
|
1407
1446
|
if (patchBuf && patchBuf.length > 0) {
|
|
1408
1447
|
const patchFile = join(worktreePath, '.sillyspec-baseline-staged.patch');
|
|
1409
1448
|
try {
|
|
@@ -1421,12 +1460,14 @@ export class WorktreeManager {
|
|
|
1421
1460
|
files.push(...staged.split('\n').filter(Boolean));
|
|
1422
1461
|
}
|
|
1423
1462
|
|
|
1424
|
-
// unstaged
|
|
1425
|
-
const unstaged = gitQuiet(mainCwd, ['diff', '--name-only'], { timeout: 30000 }) || ''
|
|
1463
|
+
// unstaged 变更(同上 pathspec 排除)
|
|
1464
|
+
const unstaged = gitQuiet(mainCwd, ['diff', '--name-only', '--', '.', EXCLUDE_PATHSPEC], { timeout: 30000 }) || ''
|
|
1465
|
+
const unstagedAll = gitQuiet(mainCwd, ['diff', '--name-only'], { timeout: 30000 }) || ''
|
|
1466
|
+
collectExcluded(unstagedAll, excludedSpecFiles)
|
|
1426
1467
|
if (unstaged) {
|
|
1427
1468
|
try {
|
|
1428
1469
|
// 用 Buffer 模式读取,避免二进制 patch 被 UTF-8 解码损坏(QUAL-01 收口,同上)
|
|
1429
|
-
const patchBuf = git(mainCwd, ['diff', '--binary'], { encoding: 'buffer', timeout: 30000 });
|
|
1470
|
+
const patchBuf = git(mainCwd, ['diff', '--binary', '--', '.', EXCLUDE_PATHSPEC], { encoding: 'buffer', timeout: 30000 });
|
|
1430
1471
|
if (patchBuf && patchBuf.length > 0) {
|
|
1431
1472
|
const patchFile = join(worktreePath, '.sillyspec-baseline-unstaged.patch');
|
|
1432
1473
|
try {
|
|
@@ -1448,6 +1489,8 @@ export class WorktreeManager {
|
|
|
1448
1489
|
const skippedDirs = [];
|
|
1449
1490
|
if (untracked) {
|
|
1450
1491
|
for (const f of untracked.split('\n').filter(Boolean)) {
|
|
1492
|
+
const norm = f.replace(/\\/g, '/')
|
|
1493
|
+
if (norm === '.sillyspec' || norm.startsWith('.sillyspec/')) { excludedSpecFiles.push(f); continue }
|
|
1451
1494
|
const r = copyUntrackedEntry(join(mainCwd, f), join(worktreePath, f));
|
|
1452
1495
|
if (r.status === 'copied') files.push(f);
|
|
1453
1496
|
else if (r.status === 'skipped-dir') skippedDirs.push(f);
|
|
@@ -1457,6 +1500,13 @@ export class WorktreeManager {
|
|
|
1457
1500
|
if (skippedDirs.length > 0) {
|
|
1458
1501
|
console.log(`ℹ️ baseline overlay 跳过 ${skippedDirs.length} 个 untracked 目录(不读目录避免 EISDIR,坑 execute-worktree-overlay-untracked-dir-eisdir)`);
|
|
1459
1502
|
}
|
|
1503
|
+
// 跨变更 spec 文档隔离可见性:列出被排除的 .sillyspec/ 文件(去重,截断展示),
|
|
1504
|
+
// 让「ROADMAP 被谁改动、为何不在本 baseline」在 create 时刻可查,不需事后考古
|
|
1505
|
+
const excludedUnique = [...new Set(excludedSpecFiles)]
|
|
1506
|
+
if (excludedUnique.length > 0) {
|
|
1507
|
+
const preview = excludedUnique.slice(0, 8).join(', ') + (excludedUnique.length > 8 ? ` …(共 ${excludedUnique.length} 个)` : '')
|
|
1508
|
+
console.log(`🧹 baseline overlay 已隔离 ${excludedUnique.length} 个 .sillyspec/ 未提交文件(跨变更 spec 文档不进本变更 baseline,留在主仓各自归属):${preview}`)
|
|
1509
|
+
}
|
|
1460
1510
|
|
|
1461
1511
|
if (files.length > 0) {
|
|
1462
1512
|
console.log(`📁 baseline overlay: ${files.length} 个未提交文件已同步到 worktree`);
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
12. **hook 拦截提交时禁止跳过**(`.husky/pre-push`),修复问题后再提交。
|
|
19
19
|
13. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|
|
20
20
|
14. **任务记录隔离**:永不重置 / reset / 清零已存在的 change;多个活跃 change 各自 `--change <名>` 隔离不重叠;quick 同一 QUICKLOG 按 ql-ID 条目追加,不冲突。
|
|
21
|
-
15. **quicklog
|
|
21
|
+
15. **quicklog 结构化落盘**:末步 `--done` 用四参数 `--req/--cause/--solution/--result`(CLI 合成结构化 output 并自动提取标题/四段分行);文件括注用 `--file-notes "path::注 || path2"`。骨架由 CLI 接管,`--done` 后按需核对即可,勿手拼模板。
|
|
22
22
|
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
23
23
|
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
|
@@ -25,4 +25,4 @@ TaskCard 格式规则(必须严格遵守):
|
|
|
25
25
|
- **规范约定字段**(应填但不硬校验,缺失不阻断完成、只影响规范性):author、created_at、priority、depends_on、blocks
|
|
26
26
|
- allowed_paths 非空(至少一个真实源文件路径;回归类 task 无源码改动时填被验证的关键入口文件)
|
|
27
27
|
- acceptance 与 verify 都在 frontmatter 内(漏写会被 plan-postcheck 报「缺少验收标准」/「缺少验证步骤」)
|
|
28
|
-
-
|
|
28
|
+
- **生成方式**:用 `sillyspec taskcard <change-name> --task task-NN [--all]` 生成安全骨架(LF 行尾 + frontmatter 必闭合 + 九个硬校验字段齐全 + author/created_at 自动填),再往骨架里填语义内容——**勿用 Write tool 整文件手写**(CRLF / 漏闭合 `---` / 漏字段的根源,postcheck 报错大多由此来);已存在的卡命令会跳过不覆盖,局部修改用 Edit 定点改
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
### 自动探针(必须先执行)
|
|
2
|
-
在检查前,依次运行以下六个探针,将结果作为验证输入:
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
```
|
|
9
|
-
记录每个匹配的文件、行号和内容。
|
|
3
|
+
> 🔧 **先跑 CLI 机械探针(一条命令)**:`sillyspec verify-probes --change <change-name> [--init]`
|
|
4
|
+
> 输出已包含四个纯机械探针的结果(可直接进验证报告):**探针 1** 未实现标记扫描(design 清单文件逐行 TODO/FIXME 命中 + 行号)、**探针 3** 测试文件递归查找(含 co-located tests/,逐 task 覆盖)、**探针 5** API 契约对账表(endpoints.json × 前端调用 diff)、**探针 6** 删除对账三态(git 事实 × design 声明)。`--init` 顺带生成 verify-result.md 九章节骨架(探针结果已预填,`<!--TODO-->` 占位替换即可)。
|
|
5
|
+
>
|
|
6
|
+
> 下列条目中,只有**半语义部分**需要你执行:探针 2(关键词提取)、探针 3.4 集成盲区标注、探针 3.5 断言有效性抽查、探针 4(决策追踪闭环判定)。CLI 已跑的部分勿重复手跑——对 CLI 输出的 warning 项做语义复核即可。
|
|
10
7
|
|
|
11
|
-
**探针 2
|
|
8
|
+
**探针 2:设计关键词覆盖探针(你执行——关键词提取是语义)**
|
|
12
9
|
1. 读取 design.md,从中提取所有能力关键词(如"登录"、"导出"、"批量"、"删除"、"搜索"等动作词)
|
|
13
10
|
2. 对每个关键词,在源码目录中 grep 确认是否有对应的实现代码:
|
|
14
11
|
```bash
|
|
@@ -16,14 +13,11 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
16
13
|
```
|
|
17
14
|
3. 如果某个关键词在源码中完全没有匹配,标记为 ⚠️ 可能未实现
|
|
18
15
|
|
|
19
|
-
**探针 3
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
3. 没有测试文件的 task 标记为 ⚠️ 缺少测试
|
|
23
|
-
4. **集成盲区提示**:测试文件存在 ≠ 集成正确——路由/layout 守卫重定向、跨模块装配这类集成 bug 组件单测覆盖不到,只有集成/冒烟/E2E 才暴露。对路由/layout/跨进程装配敏感的 task,额外检查是否有集成冒烟覆盖;无则标 ⚠️ 集成层未验证(需人工或部署确认)
|
|
24
|
-
5. **断言有效性抽查(与 execute「测试用例设计」闭环)**:测试文件存在 ≠ 测试有效。对最能代表本次变更行为的 2-3 个核心测试做抽查:① 断言验证真实输出/副作用,不是只"不抛错"的空断言、只测 getter/setter;② 覆盖边界/异常分支,不只业务正例;③ 走公开 API 测行为,不测实现细节(重构后功能不变测试不应失败)。不达标标 ⚠️ 断言薄弱或无效(advisory:CLI 不阻断,是否 FAIL 由你诚实判定;execute 已按「测试用例设计」写的,这里只抽查核验,不重审全量)
|
|
16
|
+
**探针 3 补充(CLI 已查存在性,你补语义)**
|
|
17
|
+
- **集成盲区提示**:测试文件存在 ≠ 集成正确——路由/layout 守卫重定向、跨模块装配这类集成 bug 组件单测覆盖不到,只有集成/冒烟/E2E 才暴露。对路由/layout/跨进程装配敏感的 task,额外检查是否有集成冒烟覆盖;无则标 ⚠️ 集成层未验证(需人工或部署确认)
|
|
18
|
+
- **断言有效性抽查(与 execute「测试用例设计」闭环)**:测试文件存在 ≠ 测试有效。对最能代表本次变更行为的 2-3 个核心测试做抽查:① 断言验证真实输出/副作用,不是只"不抛错"的空断言、只测 getter/setter;② 覆盖边界/异常分支,不只业务正例;③ 走公开 API 测行为,不测实现细节(重构后功能不变测试不应失败)。不达标标 ⚠️ 断言薄弱或无效(advisory:CLI 不阻断,是否 FAIL 由你诚实判定;execute 已按「测试用例设计」写的,这里只抽查核验,不重审全量)
|
|
25
19
|
|
|
26
|
-
**探针 4:决策追踪覆盖探针(如存在 decisions.md
|
|
20
|
+
**探针 4:决策追踪覆盖探针(如存在 decisions.md——闭环图机械,证据真伪是语义)**
|
|
27
21
|
1. 从 decisions.md 提取所有当前版本 D-xxx@vN
|
|
28
22
|
2. 检查 requirements.md 是否引用每个 D-xxx@vN,并映射到 FR-xxx
|
|
29
23
|
3. 检查 plan.md 或 tasks/task-NN.md 是否引用每个 FR-xxx/D-xxx@vN
|
|
@@ -31,36 +25,8 @@ grep -rl "<关键词>" <源码目录>/ --include="*.java" --include="*.js" --inc
|
|
|
31
25
|
5. 任意 D-xxx@vN 无下游覆盖时标记为 ⚠️ 决策未闭环
|
|
32
26
|
6. 任意 P0/P1 unresolved/blocking 决策标记为 FAIL blocker
|
|
33
27
|
|
|
34
|
-
**探针 5
|
|
35
|
-
|
|
36
|
-
- 存在 \.sillyspec/.runtime/contract-artifacts/ 目录(说明 execute 阶段生成了 endpoint artifact)
|
|
37
|
-
- 或者项目同时有 backend/ 和 frontend/ 目录
|
|
38
|
-
|
|
39
|
-
执行步骤:
|
|
40
|
-
1. 收集所有 provider endpoint artifacts:
|
|
41
|
-
- 读取 .sillyspec/.runtime/contract-artifacts/*/endpoints.json
|
|
42
|
-
- 汇总为 backend 端点清单
|
|
43
|
-
2. 扫描前端 API 调用:
|
|
44
|
-
- 在 frontend/ 目录中搜索 apiFetch/request/axios/fetch 调用
|
|
45
|
-
- 提取所有 API 路径(归一化动态参数为 {param})
|
|
46
|
-
3. Diff 对账:
|
|
47
|
-
- 前端调用路径在 backend 端点清单中找不到 → **❌ Missing backend endpoint**(advisory:CLI 复核后仅 warn,不硬阻断归档;但 contract gap 是真实集成缺陷,应诚实标 FAIL 并回 execute 补端点)
|
|
48
|
-
- backend 端点在前端无调用 → ⚠️ Unused backend endpoint(warning,不阻断)
|
|
49
|
-
4. 输出对账结果表格(示例用通用占位路径,实际填本仓真实文件):
|
|
50
|
-
|
|
51
|
-
| 状态 | 前端调用 | 后端端点 | 文件 |
|
|
52
|
-
|---|---|---|---|
|
|
53
|
-
| ❌ missing | GET /api/<域>/<资源>/<param> | — | <前端调用所在文件,如 src/lib/<模块>.ts> |
|
|
54
|
-
|
|
55
|
-
如果发现 Missing backend endpoint,在验证报告中标记为 ❌ contract gap(CLI 仅 advisory 提示、不硬阻断归档;但缺口真实存在——诚实判 FAIL 并回 execute 补齐端点,勿因 CLI 不拦而放行)。
|
|
28
|
+
**探针 5 补充(CLI 已出对账表,你做诚实判定)**
|
|
29
|
+
CLI 的 missing backend endpoint 是 advisory(不硬阻断归档)——但 contract gap 是真实集成缺陷,应诚实标 FAIL 并回 execute 补端点,勿因 CLI 不拦而放行。后端 router 的端点清单可用 `sillyspec endpoints extract --change <变更名> --task <task-NN>` 重新静态提取(勿手扫装饰器)。
|
|
56
30
|
|
|
57
|
-
**探针 6
|
|
58
|
-
|
|
59
|
-
1. 运行 `git diff --name-status HEAD`(本次变更已 apply 到主仓、未 commit,删除的文件在工作树消失但仍在 HEAD,显示 `D`)
|
|
60
|
-
2. 筛出状态以 `D` 开头的行(删除);`R`/`C` 开头的旧路径等价删除
|
|
61
|
-
3. 对每个删除文件,核对 design.md「文件变更清单」声明的操作:
|
|
62
|
-
- design 声明「删除」→ ✅ 合规(预期删除)
|
|
63
|
-
- design 声明「新增/修改」却整文件删除 → ❌ 高风险(声明与事实矛盾)
|
|
64
|
-
- design 清单未列出 → ⚠️ 未声明删除
|
|
65
|
-
4. 排除 `.sillyspec/` 与 `meta.json`(文档 churn 不算删除信号)
|
|
66
|
-
5. verify --done 时 CLI 会用同一 git 事实 advisory 复核(打印警告,不硬阻断归档)——是否 FAIL blocker 由你诚实判定,务必如实记录
|
|
31
|
+
**探针 6 补充(CLI 已出三态判定,你做终审)**
|
|
32
|
+
以 git 事实为准(真实 > 声明);CLI 的三态(合规/高风险/未声明)是机械判定,是否构成 FAIL blocker 由你诚实判定,务必如实记录——静默删除代码是 verify 的最大盲区。
|