sillyspec 3.26.13 → 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.
Files changed (56) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +2 -0
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-commit/SKILL.md +18 -60
  4. package/.claude/skills/sillyspec-continue/SKILL.md +2 -0
  5. package/.claude/skills/sillyspec-execute/SKILL.md +10 -4
  6. package/.claude/skills/sillyspec-plan/SKILL.md +10 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +2 -2
  8. package/.claude/skills/sillyspec-resume/SKILL.md +2 -0
  9. package/.claude/skills/sillyspec-verify/SKILL.md +2 -0
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +7 -1
  11. package/package.json +1 -1
  12. package/src/commit-suggest.js +202 -0
  13. package/src/config-cat.js +88 -0
  14. package/src/docs-check.js +48 -12
  15. package/src/doctor-diagnostics.js +49 -0
  16. package/src/hooks/worktree-guard.js +20 -1
  17. package/src/index.js +547 -21
  18. package/src/init.js +1 -1
  19. package/src/local-register.js +98 -0
  20. package/src/module-impact.js +146 -0
  21. package/src/progress/change-registry.js +43 -4
  22. package/src/progress/stage-machine.js +10 -5
  23. package/src/progress.js +18 -3
  24. package/src/quicklog.js +106 -1
  25. package/src/run/command.js +151 -38
  26. package/src/run/complete-handlers.js +62 -4
  27. package/src/run/complete.js +10 -8
  28. package/src/run/concurrent-detect.js +3 -1
  29. package/src/run/gates.js +86 -4
  30. package/src/run/multi-repo-context.js +3 -1
  31. package/src/run/next.js +128 -0
  32. package/src/run/prompt.js +28 -6
  33. package/src/run/scan-profile.js +35 -23
  34. package/src/run/shared.js +28 -7
  35. package/src/scan-postcheck.js +142 -2
  36. package/src/stage-contract-spec.js +1 -1
  37. package/src/stage-review.js +55 -7
  38. package/src/stages/archive.js +1 -1
  39. package/src/stages/brainstorm.js +26 -12
  40. package/src/stages/doctor.js +2 -4
  41. package/src/stages/execute.js +110 -26
  42. package/src/stages/knowledge.js +21 -5
  43. package/src/stages/plan-postcheck.js +24 -10
  44. package/src/stages/plan.js +14 -9
  45. package/src/stages/quick.js +14 -6
  46. package/src/stages/scan.js +1 -0
  47. package/src/stages/verify.js +7 -49
  48. package/src/sync.js +19 -7
  49. package/src/task-review.js +256 -10
  50. package/src/verify-probes.js +264 -0
  51. package/src/workspace.js +125 -0
  52. package/src/worktree-apply.js +15 -1
  53. package/src/worktree.js +58 -8
  54. package/templates/claude-instruction.md +1 -1
  55. package/templates/prompts/taskcard-rules.md +1 -1
  56. package/templates/prompts/verify-probes.md +13 -47
@@ -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
+ }
@@ -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
- details.push(`branch kept: ${reviewRefs.length} 个 task review.json 引用分支上的 commit(base/head 审计保护)`);
914
- console.log(`🔗 分支保留:${branch} 上有 ${reviewRefs.length} 个 task review.json 引用其 commit(apply 只复制文件内容,ref 删除后 base/head 悬空无法复核)。确要删除:git branch -D ${branch}`);
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
- // staged 变更
1401
- const staged = gitQuiet(mainCwd, ['diff', '--cached', '--name-only'], { timeout: 30000 }) || '';
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 手动精修**:CLI 只写骨架,`--done` 后手动补语义化标题 / 文件多行带括注 / 结果四段。
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
- - 写完后用 Write tool 保存到文件
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
- **探针 1:未实现标记扫描(仅变更文件)**
5
- 只扫描本次变更涉及的文件,不要全项目扫描——历史 TODO 与本次变更无关,徒增噪音与 token。变更文件 = design.md「文件变更清单」列出的源码文件(你已在「加载规范」步骤读取 design.md,glob 路径需展开为具体文件):
6
- ```bash
7
- grep -n "尚未实现\|TODO\|FIXME\|HACK\|XXX" <design 清单中的源码文件>
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
- 1. 读取变更目录下的 tasks.md,提取所有 checkbox 任务
21
- 2. 对每个 task,**递归**检查对应模块目录及其**所有子目录**下是否存在测试文件(*test*、*spec*、*Test*、*Spec*)——包括模块内 co-located 的 tests/ 目录(如 `backend/app/modules/*/tests/`),**不要只看顶层 `tests/`**,否则会把测试就在模块内部的项目误判为“无测试”
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:API Contract Parity Check(跨前后端契约对账)**
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
- 静默删除代码是 verify 的盲区——agent 删一段它看不懂的旧代码,只要路径合规、不碰风险关键词,其他探针都不会响。用 git 事实客观对账,不要凭记忆:
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 的最大盲区。