sillyspec 3.28.0 → 3.28.3
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-auto/SKILL.md +54 -93
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
- package/.claude/skills/sillyspec-state/SKILL.md +25 -64
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
- package/package.json +1 -1
- package/src/archive-delta.js +462 -0
- package/src/constants.js +6 -0
- package/src/design-facts.js +418 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +197 -34
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +11 -2
- package/src/endpoint-baseline.js +109 -0
- package/src/git-helper.js +31 -0
- package/src/index.js +381 -22
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +39 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +5 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +199 -5
- package/src/run/complete-handlers.js +60 -4
- package/src/run/complete.js +105 -6
- package/src/run/gates.js +353 -2
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +193 -15
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +117 -7
- package/src/run/shared.js +22 -6
- package/src/run/stage.js +14 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +114 -3
- package/src/spec-sync.js +19 -2
- package/src/stage-review.js +62 -3
- package/src/stages/archive.js +5 -5
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +12 -20
- package/src/stages/execute.js +19 -21
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +8 -3
- package/src/stages/quick.js +4 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +18 -18
- package/src/task-review.js +200 -0
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +771 -29
- package/src/verify-probes.js +89 -10
- package/src/workflow.js +30 -0
- package/templates/agents-instruction.md +1 -1
- package/templates/prompts/taskcard-rules.md +5 -0
- package/templates/workflows/scan-docs.yaml +6 -0
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/scan-diff.js
CHANGED
|
@@ -26,6 +26,7 @@ import { safeGit } from './git-helper.js'
|
|
|
26
26
|
import { parseSourceCommit } from './scan-staleness.js'
|
|
27
27
|
import { matchFilesToModules } from './docs-debt.js'
|
|
28
28
|
import { parseModuleMapSimple } from './modules.js'
|
|
29
|
+
import { collectDocRefs } from './docs-check.js'
|
|
29
30
|
|
|
30
31
|
/** safeGit 超时(仿 docs-debt 的 GIT_TIMEOUT_MS 模式:大 diff 慢时降级不挂起) */
|
|
31
32
|
const GIT_TIMEOUT_MS = 10000
|
|
@@ -40,7 +41,8 @@ const DEFAULT_LIST_CAP = 5
|
|
|
40
41
|
* specBase / projectName:scan 文档与 module-map 定位(specBase/docs/<projectName>/...)
|
|
41
42
|
* base:基线 commit(短/长哈希、引用均可);缺省读 scan 文档 frontmatter 的 source_commit
|
|
42
43
|
* @returns {{ ok: true, base, baseSource, head, behindCommits, daysSinceScan, scope,
|
|
43
|
-
* added, deleted, modified, unknown, unmapped, outOfScope, renameMap, byModule,
|
|
44
|
+
* added, deleted, modified, unknown, unmapped, outOfScope, renameMap, byModule,
|
|
45
|
+
* staleRefs, warnings, driftCount }
|
|
44
46
|
* | { ok: false, error: string }}
|
|
45
47
|
* ok:false = 硬错误(参数缺失 / 无 source_commit / 无效 base / git diff 失败)
|
|
46
48
|
* added/deleted/modified/unknown:范围内变更文件(POSIX 相对路径;R/C 记新路径)
|
|
@@ -163,6 +165,40 @@ export function computeScanDiff({ projectRoot, specBase, projectName, base } = {
|
|
|
163
165
|
byModule[id] = { doc: entry.doc || '', added: a, deleted: d, modified: m, files: entry.files }
|
|
164
166
|
}
|
|
165
167
|
|
|
168
|
+
// ── 引用级漂移(archify 借鉴,2026-09-04):scan 文档显式 file:line 引用 × 基线后变更文件集。
|
|
169
|
+
// 变更集取 parseNameStatus 全量——**不经 module-map scope 过滤**(引用自带范围:指向「已变更
|
|
170
|
+
// 但范围外」文件的引用同样过时,advisory 信号不该被 scope 吞掉);rename 的旧路径也入集
|
|
171
|
+
//(文档写在 scan 时点,引用的是旧路径,重命名后引用必失效——最强失效信号)。
|
|
172
|
+
// best-effort(读不到文档/异常 → 空清单不阻断文件级结果),不计入 driftCount / 退出码
|
|
173
|
+
//(advisory:引用指向已变更文件 ≠ 引用失效,提示人工核对)。──
|
|
174
|
+
const staleRefs = []
|
|
175
|
+
try {
|
|
176
|
+
const scanDir = join(specBase, 'docs', projectName, 'scan')
|
|
177
|
+
if (existsSync(scanDir)) {
|
|
178
|
+
const fullChanged = new Map() // POSIX path → A/D/M/R
|
|
179
|
+
for (const item of parseNameStatus(diffRes.value)) {
|
|
180
|
+
if (item.status === 'A' || item.status === 'D') fullChanged.set(item.path, item.status)
|
|
181
|
+
else if (item.status === 'M' || item.status === 'C') fullChanged.set(item.path, 'M')
|
|
182
|
+
else if (item.status === 'R') { fullChanged.set(item.path, 'R'); fullChanged.set(item.oldPath, 'R') }
|
|
183
|
+
}
|
|
184
|
+
const hitChange = (p) => {
|
|
185
|
+
if (fullChanged.has(p)) return { file: p, change: fullChanged.get(p) }
|
|
186
|
+
// src/ 前缀双向归一:文档 src 相对写法 ↔ git 仓库根路径
|
|
187
|
+
const alt = p.startsWith('src/') ? p.slice(4) : `src/${p}`
|
|
188
|
+
return fullChanged.has(alt) ? { file: alt, change: fullChanged.get(alt) } : null
|
|
189
|
+
}
|
|
190
|
+
for (const f of readdirSync(scanDir)) {
|
|
191
|
+
if (!f.endsWith('.md')) continue
|
|
192
|
+
const content = readFileSync(join(scanDir, f), 'utf8')
|
|
193
|
+
for (const ref of collectDocRefs(content)) {
|
|
194
|
+
if (ref.repo !== null) continue
|
|
195
|
+
const hit = hitChange(ref.file.replace(/\\/g, '/'))
|
|
196
|
+
if (hit) staleRefs.push({ doc: f, ref: ref.ref, change: hit.change, file: hit.file })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch { /* 引用级漂移为 advisory,异常静默降级 */ }
|
|
201
|
+
|
|
166
202
|
return {
|
|
167
203
|
ok: true,
|
|
168
204
|
base: baseDisplay,
|
|
@@ -174,6 +210,7 @@ export function computeScanDiff({ projectRoot, specBase, projectName, base } = {
|
|
|
174
210
|
added, deleted, modified, unknown, unmapped, outOfScope,
|
|
175
211
|
renameMap,
|
|
176
212
|
byModule,
|
|
213
|
+
staleRefs,
|
|
177
214
|
warnings,
|
|
178
215
|
driftCount: added.length + deleted.length + modified.length,
|
|
179
216
|
}
|
|
@@ -240,6 +277,13 @@ function renderTerminal(r, { full }) {
|
|
|
240
277
|
if (r.unknown.length > 0) {
|
|
241
278
|
console.log(`无法归类(unknown,${r.unknown.length} 条):${r.unknown.slice(0, cap).join(', ')}`)
|
|
242
279
|
}
|
|
280
|
+
if (r.staleRefs && r.staleRefs.length > 0) {
|
|
281
|
+
console.log(`引用过时(scan 文档 file:line 引用指向基线后已变更的文件,${r.staleRefs.length} 条,advisory):`)
|
|
282
|
+
for (const s of r.staleRefs.slice(0, cap)) {
|
|
283
|
+
console.log(` - ${s.doc}: \`${s.ref}\`(${s.change === 'D' ? '文件已删除' : s.change === 'A' ? '文件为基线后新增' : s.change === 'R' ? '文件已重命名' : '文件已变更'})`)
|
|
284
|
+
}
|
|
285
|
+
if (r.staleRefs.length > cap) console.log(` ... 其余 ${r.staleRefs.length - cap} 条,--full 展开`)
|
|
286
|
+
}
|
|
243
287
|
console.log(`漂移合计:${r.driftCount} 条(A${r.added.length} / D${r.deleted.length} / M${r.modified.length})`)
|
|
244
288
|
}
|
|
245
289
|
|
|
@@ -304,6 +348,15 @@ function writeReport(r, { specBase, projectName }) {
|
|
|
304
348
|
for (const f of r.outOfScope) lines.push(`- \`${f}\``)
|
|
305
349
|
lines.push('')
|
|
306
350
|
}
|
|
351
|
+
if (r.staleRefs && r.staleRefs.length > 0) {
|
|
352
|
+
lines.push('## 引用过时(advisory,不计入漂移合计)', '')
|
|
353
|
+
lines.push('scan 文档 file:line 引用指向基线后已变更的文件——不必然失效,逐条核对:', '')
|
|
354
|
+
for (const s of r.staleRefs) {
|
|
355
|
+
const label = s.change === 'D' ? '文件已删除' : s.change === 'A' ? '文件为基线后新增' : s.change === 'R' ? '文件已重命名' : '文件已变更'
|
|
356
|
+
lines.push(`- ${s.doc}: \`${s.ref}\`(${label})`)
|
|
357
|
+
}
|
|
358
|
+
lines.push('')
|
|
359
|
+
}
|
|
307
360
|
if (r.warnings.length > 0) {
|
|
308
361
|
lines.push('## 提示', '')
|
|
309
362
|
for (const w of r.warnings) lines.push(`- ${w}`)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan-facts.js — scan 事实底稿(archify 借鉴:机械事实预咀嚼,2026-09-05)
|
|
3
|
+
*
|
|
4
|
+
* 定位:把 scan 子代理「各自从零 grep 探索」的机械部分(端点清单 / 依赖与脚本清单 /
|
|
5
|
+
* 源码规模 / git 基线)收敛为 CLI 一次确定性抽取的共享底稿——agent 从「发现」降级为
|
|
6
|
+
* 「解读」,探索 token 不再按子代理数量重复计费(为什么 archify 快的机制 2/7,分析见
|
|
7
|
+
* docs/sillyspec/archify-reference-analysis-2026-09-04.md)。
|
|
8
|
+
*
|
|
9
|
+
* 产物:docs/<project>/scan/_facts.md(下划线前缀 = 非必需文档,workflow check 与
|
|
10
|
+
* postcheck 的 7 份清单不查它)。底稿里的 file:line 引用天然进入 scan_doc_ref_invalid
|
|
11
|
+
* 核验与 scan-diff staleRefs 漂移追踪——这份底稿即 scan IR 的「机器半边」;
|
|
12
|
+
* agent 半边(per-doc claims sidecar)走正式 change 另行落地。
|
|
13
|
+
*
|
|
14
|
+
* 约束:fail-soft——任何一段抽取失败只跳过该段(md 里附注记),绝不抛出、绝不阻断 scan。
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'
|
|
17
|
+
import { join, relative } from 'node:path'
|
|
18
|
+
import { scanBackendEndpoints, scanFrontendApiCalls } from './endpoint-extractor.js'
|
|
19
|
+
import { estimateSourceSize } from './run/scan-profile.js'
|
|
20
|
+
import { safeGit } from './git-helper.js'
|
|
21
|
+
|
|
22
|
+
/** 底稿各清单的上限(防大仓把底稿撑爆反而变成新的 token 黑洞) */
|
|
23
|
+
const MAX_ENDPOINTS = 60
|
|
24
|
+
const MAX_FRONTEND_CALLS = 30
|
|
25
|
+
const MAX_DEPS = 40
|
|
26
|
+
const MAX_SCRIPTS = 20
|
|
27
|
+
|
|
28
|
+
/** 依赖与脚本清单:package.json 优先(结构化最稳),pyproject.toml 做行级 best-effort,其余清单记存在性 */
|
|
29
|
+
function readManifestFacts(root) {
|
|
30
|
+
const manifests = []
|
|
31
|
+
for (const f of ['package.json', 'pyproject.toml', 'pom.xml', 'go.mod', 'build.gradle', 'Cargo.toml', 'requirements.txt', 'Gemfile', 'composer.json']) {
|
|
32
|
+
if (existsSync(join(root, f))) manifests.push(f)
|
|
33
|
+
}
|
|
34
|
+
const deps = []
|
|
35
|
+
const scripts = []
|
|
36
|
+
let pkgName = null
|
|
37
|
+
if (existsSync(join(root, 'package.json'))) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
40
|
+
pkgName = pkg.name || null
|
|
41
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
42
|
+
for (const dep of Object.keys(pkg[key] || {})) deps.push(dep)
|
|
43
|
+
}
|
|
44
|
+
for (const [name, cmd] of Object.entries(pkg.scripts || {})) scripts.push(`${name} = ${cmd}`)
|
|
45
|
+
} catch { /* 坏 package.json 不阻断,仅缺该段 */ }
|
|
46
|
+
} else if (existsSync(join(root, 'pyproject.toml'))) {
|
|
47
|
+
try {
|
|
48
|
+
const lines = readFileSync(join(root, 'pyproject.toml'), 'utf8').split(/\r?\n/)
|
|
49
|
+
// 两种形态:PEP 621 `dependencies = ["pkg>=x", ...]` 数组;poetry `[tool.poetry.dependencies]` 段
|
|
50
|
+
let inArray = false
|
|
51
|
+
let section = ''
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
const sec = line.match(/^\[([^\]]+)\]\s*$/)
|
|
54
|
+
if (sec) { section = sec[1]; inArray = false; continue }
|
|
55
|
+
if (/^dependencies\s*=\s*\[/.test(line)) inArray = true
|
|
56
|
+
if (inArray) {
|
|
57
|
+
for (const m of line.matchAll(/["']([A-Za-z0-9_.-]+)\s*[\[<>=!~;]/g)) deps.push(m[1])
|
|
58
|
+
if (/\]\s*$/.test(line) && !/^dependencies/.test(line)) inArray = false
|
|
59
|
+
} else if (section === 'tool.poetry.dependencies') {
|
|
60
|
+
const m = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*["'{]/)
|
|
61
|
+
if (m && m[1] !== 'python') deps.push(m[1])
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch { /* best-effort */ }
|
|
65
|
+
}
|
|
66
|
+
return { manifests, pkgName, deps: [...new Set(deps)].slice(0, MAX_DEPS), depsTruncated: new Set(deps).size > MAX_DEPS, scripts: scripts.slice(0, MAX_SCRIPTS) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 端点与前端调用抽取(source 转 cwd 相对 POSIX 路径,引用形态可直接被 docs-check/staleRefs 消费) */
|
|
70
|
+
function collectEndpointFacts(cwd, root) {
|
|
71
|
+
let backend = []
|
|
72
|
+
let frontend = []
|
|
73
|
+
try { backend = scanBackendEndpoints(root) } catch { /* fail-soft */ }
|
|
74
|
+
try { frontend = scanFrontendApiCalls(root) } catch { /* fail-soft */ }
|
|
75
|
+
const rel = (abs) => relative(cwd, abs).replace(/\\/g, '/')
|
|
76
|
+
return {
|
|
77
|
+
backend: backend.slice(0, MAX_ENDPOINTS).map(e => ({ method: e.method, path: e.path, ref: `${rel(e.source)}:${e.line}` })),
|
|
78
|
+
backendTotal: backend.length,
|
|
79
|
+
frontend: frontend.slice(0, MAX_FRONTEND_CALLS).map(e => ({ method: e.method, path: e.path, ref: `${rel(e.source)}:${e.line}` })),
|
|
80
|
+
frontendTotal: frontend.length,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 确定性收集 scan 机械事实(纯同步,不写盘)。
|
|
86
|
+
* @param {{ cwd: string, projectPath?: string|null }} opts cwd:仓库根(引用解析锚);projectPath:子项目相对路径(缺省主项目)
|
|
87
|
+
* @returns {object} facts(renderScanFactsMd 的输入)
|
|
88
|
+
*/
|
|
89
|
+
export function collectScanFacts({ cwd, projectPath = null }) {
|
|
90
|
+
const root = projectPath ? join(cwd, projectPath) : cwd
|
|
91
|
+
const manifest = readManifestFacts(root)
|
|
92
|
+
const size = (() => { try { return estimateSourceSize(root) } catch { return { fileCount: 0, sourceBytes: 0 } } })()
|
|
93
|
+
const git = safeGit(cwd, ['rev-parse', '--short', 'HEAD'])
|
|
94
|
+
const type = manifest.manifests.includes('package.json') ? 'nodejs'
|
|
95
|
+
: (manifest.manifests.includes('pyproject.toml') || manifest.manifests.includes('requirements.txt')) ? 'python'
|
|
96
|
+
: manifest.manifests.includes('pom.xml') ? 'maven'
|
|
97
|
+
: manifest.manifests.includes('go.mod') ? 'go'
|
|
98
|
+
: manifest.manifests.includes('Cargo.toml') ? 'rust'
|
|
99
|
+
: 'generic'
|
|
100
|
+
return {
|
|
101
|
+
generatedAt: new Date().toISOString().replace('T', ' ').slice(0, 19),
|
|
102
|
+
projectPath,
|
|
103
|
+
type,
|
|
104
|
+
pkgName: manifest.pkgName,
|
|
105
|
+
gitHead: git && git.value ? git.value.trim() : null,
|
|
106
|
+
manifests: manifest.manifests,
|
|
107
|
+
deps: manifest.deps,
|
|
108
|
+
depsTruncated: manifest.depsTruncated,
|
|
109
|
+
scripts: manifest.scripts,
|
|
110
|
+
size,
|
|
111
|
+
endpoints: collectEndpointFacts(cwd, root),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 渲染事实底稿 markdown。引用一律 `cwd 相对路径:行号`(反引号),可直接被
|
|
117
|
+
* collectDocRefs / scan_doc_ref_invalid / staleRefs 消费。
|
|
118
|
+
* @param {object} facts collectScanFacts 返回值
|
|
119
|
+
* @returns {string} markdown 全文
|
|
120
|
+
*/
|
|
121
|
+
export function renderScanFactsMd(facts) {
|
|
122
|
+
const scope = facts.projectPath ? `${facts.projectPath}/` : '(主项目根)'
|
|
123
|
+
const lines = []
|
|
124
|
+
lines.push(`# scan 事实底稿(CLI 确定性抽取)`)
|
|
125
|
+
lines.push('')
|
|
126
|
+
lines.push(`> 生成:sillyspec scan facts · ${facts.generatedAt} · 范围:${scope} · git HEAD:${facts.gitHead || '(不可用)'}`)
|
|
127
|
+
lines.push('> 本文件由 CLI 机器生成(零 AI token)。子代理**禁止重新 grep 发现**下列机械事实,只做解读与成文;与源码冲突时以本底稿为准并标注差异。')
|
|
128
|
+
lines.push('')
|
|
129
|
+
lines.push('## 项目类型与清单')
|
|
130
|
+
lines.push('')
|
|
131
|
+
lines.push(`- 类型判定:${facts.type}${facts.pkgName ? `(包名 ${facts.pkgName})` : ''}`)
|
|
132
|
+
lines.push(`- 清单文件:${facts.manifests.length > 0 ? facts.manifests.join(' / ') : '(未发现)'}`)
|
|
133
|
+
lines.push(`- 源码规模:${facts.size.fileCount} 个源文件 / ${Math.round(facts.size.sourceBytes / 1024)}KB`)
|
|
134
|
+
lines.push('')
|
|
135
|
+
if (facts.deps.length > 0) {
|
|
136
|
+
lines.push(`## 依赖清单${facts.depsTruncated ? `(前 ${MAX_DEPS} 个,共更多)` : ''}`)
|
|
137
|
+
lines.push('')
|
|
138
|
+
lines.push(facts.deps.join(', '))
|
|
139
|
+
lines.push('')
|
|
140
|
+
}
|
|
141
|
+
if (facts.scripts.length > 0) {
|
|
142
|
+
lines.push('## package.json scripts')
|
|
143
|
+
lines.push('')
|
|
144
|
+
for (const s of facts.scripts) lines.push(`- ${s}`)
|
|
145
|
+
lines.push('')
|
|
146
|
+
}
|
|
147
|
+
lines.push(`## 后端端点(${facts.endpoints.backendTotal} 条${facts.endpoints.backendTotal > MAX_ENDPOINTS ? `,仅列前 ${MAX_ENDPOINTS}` : ''})`)
|
|
148
|
+
lines.push('')
|
|
149
|
+
if (facts.endpoints.backend.length === 0) lines.push('(未抽取到路由定义——非后端项目或框架不在支持列表)')
|
|
150
|
+
for (const e of facts.endpoints.backend) lines.push(`- \`${e.method} ${e.path}\` — \`${e.ref}\``)
|
|
151
|
+
lines.push('')
|
|
152
|
+
if (facts.endpoints.frontend.length > 0) {
|
|
153
|
+
lines.push(`## 前端 API 调用(${facts.endpoints.frontendTotal} 条${facts.endpoints.frontendTotal > MAX_FRONTEND_CALLS ? `,仅列前 ${MAX_FRONTEND_CALLS}` : ''})`)
|
|
154
|
+
lines.push('')
|
|
155
|
+
for (const e of facts.endpoints.frontend) lines.push(`- \`${e.method} ${e.path}\` — \`${e.ref}\``)
|
|
156
|
+
lines.push('')
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\n') + '\n'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 收集并落盘事实底稿(scan 步骤 3 / `sillyspec scan facts` CLI 共用入口)。
|
|
163
|
+
* @param {{ cwd: string, specDir: string, projectName: string, projectPath?: string|null }} opts
|
|
164
|
+
* @returns {{ outPath: string, facts: object }}
|
|
165
|
+
*/
|
|
166
|
+
export function buildAndWriteScanFacts({ cwd, specDir, projectName, projectPath = null }) {
|
|
167
|
+
const facts = collectScanFacts({ cwd, projectPath })
|
|
168
|
+
const outDir = join(specDir, 'docs', projectName, 'scan')
|
|
169
|
+
mkdirSync(outDir, { recursive: true })
|
|
170
|
+
const outPath = join(outDir, '_facts.md')
|
|
171
|
+
writeFileSync(outPath, renderScanFactsMd(facts), 'utf8')
|
|
172
|
+
return { outPath, facts }
|
|
173
|
+
}
|
package/src/scan-postcheck.js
CHANGED
|
@@ -11,9 +11,45 @@ import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUI
|
|
|
11
11
|
import { validateScriptCommands } from './stages/cmd-existence.js'
|
|
12
12
|
import { git } from './git-helper.js'
|
|
13
13
|
import { nowWallClock } from './datetime.js'
|
|
14
|
+
import { collectInvalidDocRefs } from './docs-check.js'
|
|
14
15
|
|
|
15
16
|
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* scan 文档引用事实核验(archify 借鉴 P0,2026-09-04):
|
|
20
|
+
* 对 scan 产物里显式写的 file:line 引用跑 docs-check 的联合核验(层1 存在性+行号边界 +
|
|
21
|
+
* 层2 引用行符号窗口断言,语义单一源 collectInvalidDocRefs)。
|
|
22
|
+
* repo:// 跨仓引用需要本机映射,跳过本地校验(防跨设备误报,与 docs-check 立场一致)。
|
|
23
|
+
* 只核验「显式写了引用的」事实;没写引用的散文不在范围(P2 的 scan IR 才覆盖)。
|
|
24
|
+
* WARNING 级:scan 刚写完窗口极小,失效多为笔误;docs-gate ratchet 负责长期增量收敛。
|
|
25
|
+
* @param {string} cwd 源码项目根(引用解析锚点)
|
|
26
|
+
* @param {string} scanDir scan 文档目录
|
|
27
|
+
* @param {string[]} docs 存在的文档文件名列表
|
|
28
|
+
* @returns {object|null} check 条目;无引用或全部有效时返回 null
|
|
29
|
+
*/
|
|
30
|
+
function checkScanDocRefs(cwd, scanDir, docs) {
|
|
31
|
+
let verified
|
|
32
|
+
try {
|
|
33
|
+
verified = collectInvalidDocRefs(cwd, docs.map(d => ({ name: d, absPath: join(scanDir, d) })))
|
|
34
|
+
} catch {
|
|
35
|
+
return null // 核验器自身故障不阻断 postcheck(fail-open,仅损失一层 advisory)
|
|
36
|
+
}
|
|
37
|
+
const { invalid, totalRefs, skippedCrossRepo } = verified
|
|
38
|
+
if (invalid.length === 0) return null
|
|
39
|
+
const sample = invalid.slice(0, 3).map(i => `${i.doc}: \`${i.ref}\`(${i.reason})`).join(';')
|
|
40
|
+
return {
|
|
41
|
+
name: 'scan_doc_ref_invalid',
|
|
42
|
+
severity: CHECK_SEVERITY.WARNING,
|
|
43
|
+
detail: `${invalid.length} 条 file:line 引用未通过核验(${totalRefs} 条中): ${sample}${invalid.length > 3 ? ` 等 ${invalid.length} 条` : ''}`,
|
|
44
|
+
evidence: { invalidCount: invalid.length, totalRefs, skippedCrossRepo, checkedDocs: docs.length, sample: invalid.slice(0, 3) },
|
|
45
|
+
supportedFixes: [
|
|
46
|
+
'sillyspec docs check --paths <该文档> --dry-run 查看候选锚点(fixable 条目会给出 newLine),确认后去 --dry-run 落盘重锚',
|
|
47
|
+
'sillyspec docs check --paths <该文档> --fix 对 fixable 引用自动重锚(回执附修复前后失效数对比)',
|
|
48
|
+
'先跑 sillyspec docs check --paths <该文档> 确认零候选锚点后,再删除确认无法核验的引用行',
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
17
53
|
/**
|
|
18
54
|
* @param {object} opts
|
|
19
55
|
* @param {string} opts.cwd - 源码项目根目录 (source_root)
|
|
@@ -47,6 +83,11 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {},
|
|
|
47
83
|
checks.push({ name: 'quick_profile_notice', severity: CHECK_SEVERITY.WARNING, detail: 'quick scan:仅生成 4 份核心文档(PROJECT/ARCHITECTURE/CONVENTIONS/STRUCTURE),深度扫描待补齐' })
|
|
48
84
|
}
|
|
49
85
|
|
|
86
|
+
// 引用事实核验(archify P0):本地模式与平台模式同规则,防「本地过平台挂」
|
|
87
|
+
const existingLocalDocs = requiredDocs.filter(f => existsSync(join(scanDir, f)))
|
|
88
|
+
const refCheck = checkScanDocRefs(cwd, scanDir, existingLocalDocs)
|
|
89
|
+
if (refCheck) checks.push(refCheck)
|
|
90
|
+
|
|
50
91
|
const hasWarning = checks.some(c => c.severity === 'warning')
|
|
51
92
|
return { status: hasWarning ? 'completed_with_warnings' : 'success', checks }
|
|
52
93
|
}
|
|
@@ -114,10 +155,16 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {},
|
|
|
114
155
|
checks.push({
|
|
115
156
|
name: 'docs_missing_header',
|
|
116
157
|
severity: CHECK_SEVERITY.WARNING,
|
|
117
|
-
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}(sillyspec scan-fix-headers
|
|
158
|
+
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}(sillyspec scan-fix-headers 一键补齐)`,
|
|
159
|
+
evidence: { count: docsMissingHeader.length, docs: docsMissingHeader },
|
|
160
|
+
supportedFixes: ['sillyspec scan-fix-headers'],
|
|
118
161
|
})
|
|
119
162
|
}
|
|
120
163
|
|
|
164
|
+
// 3.5 引用事实核验(archify 借鉴 P0):scan 文档里显式写的 file:line 引用逐条核验
|
|
165
|
+
const refCheck = checkScanDocRefs(cwd, specScanDir, existingDocs)
|
|
166
|
+
if (refCheck) checks.push(refCheck)
|
|
167
|
+
|
|
121
168
|
// 4. local.yaml 校验
|
|
122
169
|
const localYamlPath = join(specDir, 'local.yaml')
|
|
123
170
|
if (existsSync(localYamlPath)) {
|
|
@@ -132,7 +179,9 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {},
|
|
|
132
179
|
checks.push({
|
|
133
180
|
name: 'local_config_invalid',
|
|
134
181
|
severity: CHECK_SEVERITY.WARNING,
|
|
135
|
-
detail: `local.yaml 引用不存在的命令: ${invalid.map(i => `${i.cmd} (${i.reason})`).join('; ')}
|
|
182
|
+
detail: `local.yaml 引用不存在的命令: ${invalid.map(i => `${i.cmd} (${i.reason})`).join('; ')}`,
|
|
183
|
+
evidence: { invalid },
|
|
184
|
+
supportedFixes: ['修正 .sillyspec/local.yaml commands 中引用的 script 名(与 package.json scripts 对齐)'],
|
|
136
185
|
})
|
|
137
186
|
}
|
|
138
187
|
}
|
|
@@ -267,6 +316,9 @@ export function formatStructuredResult(result, meta = {}) {
|
|
|
267
316
|
name: c.name,
|
|
268
317
|
severity: c.severity === 'failed' ? 'critical' : c.severity,
|
|
269
318
|
detail: c.detail,
|
|
319
|
+
// 诊断信封升级(archify 借鉴 P1a,2026-09-04):additive 字段,机器可路由的修复建议
|
|
320
|
+
...(c.evidence !== undefined ? { evidence: c.evidence } : {}),
|
|
321
|
+
...(Array.isArray(c.supportedFixes) ? { supportedFixes: c.supportedFixes } : {}),
|
|
270
322
|
})),
|
|
271
323
|
}
|
|
272
324
|
|
|
@@ -285,7 +337,7 @@ export function formatStructuredResult(result, meta = {}) {
|
|
|
285
337
|
structured.failure_categories.missing_outputs.push(entry)
|
|
286
338
|
}
|
|
287
339
|
// 引用无效类
|
|
288
|
-
else if (check.name === 'local_config_invalid') {
|
|
340
|
+
else if (check.name === 'local_config_invalid' || check.name === 'scan_doc_ref_invalid') {
|
|
289
341
|
structured.failure_categories.bad_references.push(entry)
|
|
290
342
|
}
|
|
291
343
|
// AI 输出质量类
|
|
@@ -450,6 +502,65 @@ export function backfillFrontmatter(content, { author, createdAt }) {
|
|
|
450
502
|
return { changed: true, content: `---\n${inserts.join('\n')}\n---\n\n` + content.replace(/\r\n?/g, '\n') }
|
|
451
503
|
}
|
|
452
504
|
|
|
505
|
+
/**
|
|
506
|
+
* scan 文档 frontmatter CLI 原子注入(2026-09-05 ②,archify 借鉴:机器校验替代 agent 自检)。
|
|
507
|
+
*
|
|
508
|
+
* agent 只写正文,元数据由 CLI「盖章」:author / created_at / source_commit(HEAD 短哈希)/
|
|
509
|
+
* updated_at / generator: sillyspec-scan(quick 档另加 scan_depth: quick)。
|
|
510
|
+
* 只补缺失键、**绝不覆盖已有**(增量扫描时旧文档保留原 source_commit 溯源);
|
|
511
|
+
* 插入口径与 backfillFrontmatter 逐字一致(有 --- 就地插入、无则前置整块、CRLF 归一写回)。
|
|
512
|
+
* 幂等——三个调用路径(scanFinalize / quick postcheck / 平台收尾)重复执行无害。
|
|
513
|
+
* 消灭 agent 手抄 CLI 注入值的失败模式(scan-fix-headers 从「兜底」降级为「历史文档修复工具」)。
|
|
514
|
+
*
|
|
515
|
+
* @param {{ cwd: string, specDir?: string|null, project?: string|null, mode?: string|null }} opts
|
|
516
|
+
* @returns {{ fixed: string[], skipped: string[], stampedKeys: number }}
|
|
517
|
+
*/
|
|
518
|
+
export function stampScanDocHeaders({ cwd, specDir = null, project = null, mode = null }) {
|
|
519
|
+
const specBase = specDir ? specDir : join(cwd, '.sillyspec')
|
|
520
|
+
const docsRoot = join(specBase, 'docs')
|
|
521
|
+
const projects = project
|
|
522
|
+
? [project]
|
|
523
|
+
: (existsSync(docsRoot) ? readdirSync(docsRoot, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) : [])
|
|
524
|
+
|
|
525
|
+
let author = 'unknown'
|
|
526
|
+
try { author = git(cwd, ['config', 'user.name']) || 'unknown' } catch { /* git 不可用 → unknown */ }
|
|
527
|
+
let headShort = null
|
|
528
|
+
try { headShort = String(git(cwd, ['rev-parse', '--short', 'HEAD']) || '').trim() || null } catch { /* 非 git 仓 → 省略 source_commit */ }
|
|
529
|
+
const now = nowWallClock()
|
|
530
|
+
const updatedIso = new Date().toISOString()
|
|
531
|
+
|
|
532
|
+
const fixed = []
|
|
533
|
+
const skipped = []
|
|
534
|
+
let stampedKeys = 0
|
|
535
|
+
for (const p of projects) {
|
|
536
|
+
const scanDir = join(docsRoot, p, 'scan')
|
|
537
|
+
if (!existsSync(scanDir)) continue
|
|
538
|
+
for (const f of readdirSync(scanDir)) {
|
|
539
|
+
if (!f.endsWith('.md')) continue
|
|
540
|
+
const filePath = join(scanDir, f)
|
|
541
|
+
let content
|
|
542
|
+
try { content = readFileSync(filePath, 'utf8') } catch { continue }
|
|
543
|
+
const header = content.slice(0, 600)
|
|
544
|
+
const inserts = []
|
|
545
|
+
if (!/author\s*:/.test(header)) { inserts.push(`author: ${author}`); stampedKeys++ }
|
|
546
|
+
if (!/created_at\s*:/.test(header)) { inserts.push(`created_at: ${now}`); stampedKeys++ }
|
|
547
|
+
if (headShort && !/source_commit\s*:/.test(header)) { inserts.push(`source_commit: ${headShort}`); stampedKeys++ }
|
|
548
|
+
if (!/updated_at\s*:/.test(header)) { inserts.push(`updated_at: ${updatedIso}`); stampedKeys++ }
|
|
549
|
+
if (!/generator\s*:/.test(header)) { inserts.push(`generator: sillyspec-scan`); stampedKeys++ }
|
|
550
|
+
if (mode === 'quick' && !/scan_depth\s*:/.test(header)) { inserts.push(`scan_depth: quick`); stampedKeys++ }
|
|
551
|
+
if (inserts.length === 0) { skipped.push(filePath); continue }
|
|
552
|
+
const out = content.startsWith('---\n') || content.startsWith('---\r\n')
|
|
553
|
+
? content.replace(/\r\n?/g, '\n').replace(/^---\n/, `---\n${inserts.join('\n')}\n`)
|
|
554
|
+
: `---\n${inserts.join('\n')}\n---\n\n` + content.replace(/\r\n?/g, '\n')
|
|
555
|
+
try {
|
|
556
|
+
writeFileSync(filePath, out)
|
|
557
|
+
fixed.push(filePath)
|
|
558
|
+
} catch { /* 只读文件等写失败 → 跳过不抛 */ }
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return { fixed, skipped, stampedKeys }
|
|
562
|
+
}
|
|
563
|
+
|
|
453
564
|
/**
|
|
454
565
|
* source_root 污染一键修复(2026-08-21 agent-手工产出审计第四批 C-3)。
|
|
455
566
|
*
|
package/src/spec-sync.js
CHANGED
|
@@ -45,6 +45,23 @@ function debugLog(msg) {
|
|
|
45
45
|
if (process.env.SILLYSPEC_DEBUG_SYNC) console.warn(msg);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// 同步失败分类文案(坑 spec-sync-aborted-looks-exception,2026-09-07 平台执行环境实证):
|
|
49
|
+
// undici 被取消时 err.message 是英文原始串(AbortError=「This operation was aborted」、
|
|
50
|
+
// TimeoutError=「The operation was aborted due to timeout」),原样拼进 warn 看起来像未知
|
|
51
|
+
// 异常。按 err.name 翻译:AbortError=外部总熔断取消(run/shared.js raceWithAbort,预算经
|
|
52
|
+
// SILLYSPEC_SYNC_TIMEOUT_MS 可调,故不写死秒数);TimeoutError=单请求超时(清单 10s /
|
|
53
|
+
// 推送 30s 口径);其余错误维持原样。判别口径同 sync.js fetchJson 的 AbortError/
|
|
54
|
+
// TimeoutError 先例(外部 signal abort 抛 AbortError、AbortSignal.timeout 到期抛 TimeoutError)。
|
|
55
|
+
export function describeSyncError(err) {
|
|
56
|
+
if (err && err.name === 'AbortError') {
|
|
57
|
+
return '平台响应慢,本轮同步被总预算熔断让路(best-effort,下一条命令自动重试)';
|
|
58
|
+
}
|
|
59
|
+
if (err && err.name === 'TimeoutError') {
|
|
60
|
+
return '单请求超时(清单 10s / 推送 30s 口径,best-effort,下次自动重试)';
|
|
61
|
+
}
|
|
62
|
+
return err && err.message ? err.message : String(err);
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
function _hashBuffer(buf) {
|
|
49
66
|
return createHash('sha256').update(buf).digest('hex');
|
|
50
67
|
}
|
|
@@ -477,7 +494,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
477
494
|
const body = await res.json().catch(() => ({}));
|
|
478
495
|
serverManifest = body.files || {};
|
|
479
496
|
} catch (err) {
|
|
480
|
-
console.warn(`[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${changeName}: ${err
|
|
497
|
+
console.warn(`[spec-sync] 拉取清单异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`);
|
|
481
498
|
return { synced: 0 };
|
|
482
499
|
}
|
|
483
500
|
|
|
@@ -605,7 +622,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
605
622
|
writeBaseSnapshot(specRoot, localFiles);
|
|
606
623
|
return { synced: ops.length };
|
|
607
624
|
} catch (err) {
|
|
608
|
-
console.warn(`[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${err
|
|
625
|
+
console.warn(`[spec-sync] 同步异常(文件树本次未同步,下次自动重试): ${changeName}: ${describeSyncError(err)}`);
|
|
609
626
|
return { synced: 0 };
|
|
610
627
|
}
|
|
611
628
|
}
|
package/src/stage-review.js
CHANGED
|
@@ -69,9 +69,13 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier }
|
|
|
69
69
|
'- **推荐**:docHash 先占位(如 `"TODO"`),review.json 写完后跑',
|
|
70
70
|
' `sillyspec register-stage-review --change <变更名> --stage ' + stageLbl + ' --refresh-hash`',
|
|
71
71
|
' —— CLI 重算真实 sha256 代填,verdict/checklist 原样保留;主文档改版后同样跑这条',
|
|
72
|
-
' (多阶段联动用 `--all` 一次处理)。
|
|
72
|
+
' (多阶段联动用 `--all` 一次处理)。',
|
|
73
|
+
'- **忘了跑也没关系**:' + mainDoc + ' 改版后 --done 的 gate 检测到 docHash 失配会**自动机械重算放行**',
|
|
74
|
+
' (verdict 保留;结论是否仍适用于新文档需人工确认,reviewerNotes 会留审计行)。',
|
|
73
75
|
'- 兜底手算口径(极端无 CLI 场景):node `crypto.createHash(\'sha256\').update(readFileSync(...))` (原始字节)/ `sha256sum <file>` 首列 hex。',
|
|
74
|
-
'- ⚠️ **review.json 写入后若 ' + mainDoc + '
|
|
76
|
+
'- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,须重算 docHash** —— 忘算也没关系:gate 会自动机械重算放行,',
|
|
77
|
+
' 但那只是「机械对齐」;大幅改版(设计要点/接口变更)时审查结论可能已过时,应人工确认结论续用或重审,',
|
|
78
|
+
' 不要把自动放行当「新文档已审过」(历史翻车根因:design 改了 rev 后结论未复核)。',
|
|
75
79
|
'',
|
|
76
80
|
'### 完整 JSON 示例(照抄改值)',
|
|
77
81
|
'```json',
|
|
@@ -212,7 +216,8 @@ export function verifyStageReviewDocHash(review, searchDirs) {
|
|
|
212
216
|
if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
|
|
213
217
|
errors.push(
|
|
214
218
|
`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
|
|
215
|
-
`若刚改过该文档(如 design
|
|
219
|
+
`若刚改过该文档(如 design 修订后忘重算),--done gate 会自动机械重算放行(verdict 保留);` +
|
|
220
|
+
`本调用方未走自动刷新时可手动执行:` +
|
|
216
221
|
`sillyspec register-stage-review --change <变更名> --stage <阶段> --refresh-hash` +
|
|
217
222
|
`(CLI 重算 docHash 代填,verdict 保留;多阶段联动加 --all);` +
|
|
218
223
|
`若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
|
|
@@ -412,6 +417,60 @@ export function validateStageReview(opts) {
|
|
|
412
417
|
return { ok: errors.length === 0, errors, warnings, review: parsed }
|
|
413
418
|
}
|
|
414
419
|
|
|
420
|
+
/**
|
|
421
|
+
* validateStageReview 的 gate 时刻自动刷新包装(坑 stage-review-refresh-hash-manual-forget,
|
|
422
|
+
* 2026-09-04 用户实证③:design 每次改版都要手动重跑 register-stage-review --refresh-hash,
|
|
423
|
+
* 易忘——忘则 --done 被 docHash 失配拦一轮,纯机械损耗)。
|
|
424
|
+
*
|
|
425
|
+
* 语义与 --refresh-hash 完全同源:主文档改版后的 hash 机械重算,verdict/checklist/
|
|
426
|
+
* requiredEvidence 原样保留(结论是否仍适用由人工确认——reviewerNotes 追加审计行留痕)。
|
|
427
|
+
* 自动刷新不放宽防伪造边界:
|
|
428
|
+
* - 真正判伪造/错位的「主文档在候选基准下均不存在」不触发自动刷新(仍 fail-closed);
|
|
429
|
+
* - schema 失败 / verdict=fail 不碰(照常阻断);
|
|
430
|
+
* - 写盘失败回落原失败结果(fail-closed,不静默放行)。
|
|
431
|
+
*
|
|
432
|
+
* @param {object} opts - 同 validateStageReview,另加 opts.autoRefresh(true 才尝试自动刷新)
|
|
433
|
+
* @returns {{ result: object, autoRefreshed: boolean, refreshedPath?: string, previousErrors?: string[] }}
|
|
434
|
+
*/
|
|
435
|
+
export function validateStageReviewWithAutoRefresh(opts) {
|
|
436
|
+
const result = validateStageReview(opts)
|
|
437
|
+
if (result.ok) return { result, autoRefreshed: false }
|
|
438
|
+
if (!opts.autoRefresh) return { result, autoRefreshed: false }
|
|
439
|
+
// 仅「docHash 与主审查文档 …不匹配」类错误才自动刷新;「均不存在」(路径伪造/基准错位)
|
|
440
|
+
// 与 schema/verdict 错误一律原样返回
|
|
441
|
+
const mismatchOnly = result.review
|
|
442
|
+
&& Array.isArray(result.errors) && result.errors.length > 0
|
|
443
|
+
&& result.errors.every(e => String(e).includes('docHash 与主审查文档'))
|
|
444
|
+
if (!mismatchOnly) return { result, autoRefreshed: false }
|
|
445
|
+
|
|
446
|
+
const reviewDir = join(opts.runtimeRoot, 'stage-reviews', `${opts.stage}-${opts.reviewRunId}`)
|
|
447
|
+
const reviewPath = join(reviewDir, 'review.json')
|
|
448
|
+
// 定位主文档并重算 hash(与 verifyStageReviewDocHash 同法:reviewedFiles[0] × searchDirs)
|
|
449
|
+
const primaryRel = Array.isArray(result.review.reviewedFiles) ? result.review.reviewedFiles[0] : null
|
|
450
|
+
let docHash = null
|
|
451
|
+
if (primaryRel) {
|
|
452
|
+
for (const base of (opts.searchDirs || [])) {
|
|
453
|
+
const abs = join(base, primaryRel)
|
|
454
|
+
if (existsSync(abs)) { docHash = computeDocHash(abs); break }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (!docHash) return { result, autoRefreshed: false }
|
|
458
|
+
|
|
459
|
+
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
460
|
+
const refreshed = {
|
|
461
|
+
...result.review,
|
|
462
|
+
docHash,
|
|
463
|
+
reviewerNotes: `${result.review.reviewerNotes || ''}\ndocHash auto-refreshed at ${refreshedAt}(gate 检测主文档改版,机械重算;verdict 保留,结论是否仍适用需人工确认)`.trim(),
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
writeFileSync(reviewPath, JSON.stringify(refreshed, null, 2) + '\n')
|
|
467
|
+
} catch {
|
|
468
|
+
return { result, autoRefreshed: false } // 写失败回落原失败结果,不静默放行
|
|
469
|
+
}
|
|
470
|
+
const recheck = validateStageReview(opts)
|
|
471
|
+
return { result: recheck, autoRefreshed: recheck.ok, refreshedPath: reviewPath, previousErrors: result.errors }
|
|
472
|
+
}
|
|
473
|
+
|
|
415
474
|
/**
|
|
416
475
|
* 打印 stage review 校验结果
|
|
417
476
|
* @param {{ ok: boolean, errors: string[], warnings: string[] }} result
|
package/src/stages/archive.js
CHANGED
|
@@ -186,14 +186,14 @@ created_at: <now-datetime>
|
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
name: '更新路线图和提交',
|
|
189
|
-
prompt:
|
|
189
|
+
prompt: `更新路线图并确认归档。
|
|
190
190
|
|
|
191
191
|
### 操作
|
|
192
192
|
1. 如果 \`.sillyspec/ROADMAP.md\` 存在,标记对应 Phase 为已完成
|
|
193
|
-
2.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
2. 确认 sillyspec.db 中该变更已不再 active(确认归档步骤由 CLI 调用 unregisterChange)
|
|
194
|
+
|
|
195
|
+
### 说明
|
|
196
|
+
- **git 暂存已由 CLI 自动完成**(归档完成时自动 add:\`.sillyspec/changes/archive/\`、本次同步的模块文档、\`.sillyspec/knowledge/decisions/\`——未提交,由用户通过统一提交工具处理)——你不需要也不应该手动 git add
|
|
197
197
|
|
|
198
198
|
### 输出
|
|
199
199
|
归档完成确认 + 累积规范统计`,
|
|
@@ -28,7 +28,8 @@ export const definition = {
|
|
|
28
28
|
### 加载上下文
|
|
29
29
|
4. 读取项目总览 \`.sillyspec/docs/<project>/scan/PROJECT.md\`(如存在)+ 共享规范 + 子项目上下文
|
|
30
30
|
5. 加载项目信息:\`cat .sillyspec/projects/*.yaml 2>/dev/null\`
|
|
31
|
-
6.
|
|
31
|
+
6. 构建命令(CLI 自 local.yaml 注入,勿再读文件):
|
|
32
|
+
{LOCAL_COMMANDS}
|
|
32
33
|
7. 棕地项目:读取 scan 下的 STRUCTURE.md、CONVENTIONS.md、ARCHITECTURE.md
|
|
33
34
|
8. 加载模块索引:读取 \`.sillyspec/docs/<project>/modules/_module-map.yaml\`(如存在)
|
|
34
35
|
9. 查看进行中的变更:\`ls .sillyspec/changes/ | grep -v archive\`
|