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.
- 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 +547 -21
- 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 +18 -3
- package/src/quicklog.js +106 -1
- package/src/run/command.js +151 -38
- package/src/run/complete-handlers.js +62 -4
- package/src/run/complete.js +10 -8
- package/src/run/concurrent-detect.js +3 -1
- package/src/run/gates.js +86 -4
- package/src/run/multi-repo-context.js +3 -1
- package/src/run/next.js +128 -0
- package/src/run/prompt.js +28 -6
- package/src/run/scan-profile.js +35 -23
- package/src/run/shared.js +28 -7
- package/src/scan-postcheck.js +142 -2
- package/src/stage-contract-spec.js +1 -1
- package/src/stage-review.js +55 -7
- package/src/stages/archive.js +1 -1
- package/src/stages/brainstorm.js +26 -12
- package/src/stages/doctor.js +2 -4
- package/src/stages/execute.js +110 -26
- package/src/stages/knowledge.js +21 -5
- package/src/stages/plan-postcheck.js +24 -10
- package/src/stages/plan.js +14 -9
- package/src/stages/quick.js +14 -6
- package/src/stages/scan.js +1 -0
- package/src/stages/verify.js +7 -49
- package/src/sync.js +19 -7
- package/src/task-review.js +256 -10
- 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
|
@@ -79,7 +79,9 @@ export class MultiRepoContext {
|
|
|
79
79
|
throw new Error(
|
|
80
80
|
`MultiRepoContext: 以下 repo 未在 local.yaml repos: 段注册:[${unregistered.join(', ')}]。` +
|
|
81
81
|
`当前已注册 repo:[${registered.join(', ')}]。` +
|
|
82
|
-
|
|
82
|
+
`一键注册(每个缺的 key 各跑一次,勿手编 YAML):` +
|
|
83
|
+
unregistered.map(k => `sillyspec local register-repo ${k} <${k} 仓根路径>`).join(';') +
|
|
84
|
+
`。补注册后再启动 execute(约束② fail-closed,跨仓 apply 走错仓=数据所有权事故)。`
|
|
83
85
|
);
|
|
84
86
|
}
|
|
85
87
|
|
package/src/run/next.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* next.js — `sillyspec next` 项目状态探测(吸收 continue/resume 两个 skill 的手工探测表,
|
|
3
|
+
* 2026-08-21 agent-手工产出审计第二批 G7/G8)。
|
|
4
|
+
*
|
|
5
|
+
* 两个 skill 各自维护一套「文件存在性状态机」推断表且内容重复,agent 每次手工跑
|
|
6
|
+
* ls/cat 探测再查表——纯机械探测是 CLI 的本职。本模块输出「状态描述 + 下一步命令 +
|
|
7
|
+
* 依据文件」,agent 按输出执行即可;skill 保留作兜底(CLI 不可用/输出异常时)。
|
|
8
|
+
*
|
|
9
|
+
* 纯 fs 探测(零 DB 依赖、零 token、快):进度库的权威状态归 `sillyspec progress show`,
|
|
10
|
+
* 本命令管「没有活跃进度时从产物文件反推该干什么」。决策表(两个 skill 合并):
|
|
11
|
+
*
|
|
12
|
+
* 1. HANDOFF.json 存在 → 交接恢复
|
|
13
|
+
* 2. 活跃变更(changes/ 非 archive)逐个推断:
|
|
14
|
+
* 无文件→补 proposal / 无 design.md→brainstorm / 无 tasks.md→brainstorm 末步 /
|
|
15
|
+
* 无 plan.md→plan / tasks.md 有未勾→execute / 全勾无 verify-result.md→verify /
|
|
16
|
+
* 有 verify-result.md→archive
|
|
17
|
+
* 3. 有 docs/<p>/scan/ 无活跃变更 → brainstorm(已扫描待开变更)
|
|
18
|
+
* 4. 有 REQUIREMENTS.md/ROADMAP.md 无变更 → brainstorm(绿地已有需求)
|
|
19
|
+
* 5. 什么都没有 → init(新项目)或 scan(棕地)
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
22
|
+
import { join } from 'path'
|
|
23
|
+
import { resolveSpecDir } from './shared.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 数 tasks.md checkbox:返回 { checked, total }(无文件 → {checked:0,total:0})
|
|
27
|
+
*/
|
|
28
|
+
function countTasks(tasksPath) {
|
|
29
|
+
if (!existsSync(tasksPath)) return { checked: 0, total: 0 }
|
|
30
|
+
let checked = 0
|
|
31
|
+
let total = 0
|
|
32
|
+
for (const line of readFileSync(tasksPath, 'utf8').split('\n')) {
|
|
33
|
+
const m = line.match(/^[-*]\s*\[([ xX])\]\s*task-\d+\b/)
|
|
34
|
+
if (!m) continue
|
|
35
|
+
total++
|
|
36
|
+
if (m[1] !== ' ') checked++
|
|
37
|
+
}
|
|
38
|
+
return { checked, total }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 单个活跃变更的产物推断:返回 { state, next, evidence }
|
|
43
|
+
*/
|
|
44
|
+
function inferChange(changeDir, name) {
|
|
45
|
+
const has = (f) => existsSync(join(changeDir, f))
|
|
46
|
+
const ev = (f) => join('.sillyspec', 'changes', name, f)
|
|
47
|
+
const t = countTasks(join(changeDir, 'tasks.md'))
|
|
48
|
+
|
|
49
|
+
if (!has('proposal.md') && !has('design.md') && !has('tasks.md') && !has('plan.md')) {
|
|
50
|
+
return { state: `${name}:变更目录为空`, next: 'sillyspec run brainstorm(完善 proposal)或清理该空目录', evidence: [ev('(无产物)')] }
|
|
51
|
+
}
|
|
52
|
+
if (!has('design.md')) {
|
|
53
|
+
return { state: `${name}:有 proposal 缺 design.md`, next: `sillyspec run brainstorm --change ${name}(推进到设计产物)`, evidence: [ev('proposal.md')] }
|
|
54
|
+
}
|
|
55
|
+
if (!has('tasks.md')) {
|
|
56
|
+
return { state: `${name}:有 design 缺 tasks.md`, next: `sillyspec run brainstorm --change ${name}(末步生成规范文件)`, evidence: [ev('design.md')] }
|
|
57
|
+
}
|
|
58
|
+
if (!has('plan.md')) {
|
|
59
|
+
return { state: `${name}:有 tasks.md 缺 plan.md`, next: `sillyspec run plan --change ${name}`, evidence: [ev('tasks.md')] }
|
|
60
|
+
}
|
|
61
|
+
if (t.total > 0 && t.checked < t.total) {
|
|
62
|
+
return {
|
|
63
|
+
state: `${name}:执行中(task ${t.checked}/${t.total})`,
|
|
64
|
+
next: `sillyspec run execute --change ${name}`,
|
|
65
|
+
evidence: [ev('plan.md'), ev('tasks.md')],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!has('verify-result.md')) {
|
|
69
|
+
return { state: `${name}:task 全勾,待验证`, next: `sillyspec run verify --change ${name}`, evidence: [ev('tasks.md'), ev('plan.md')] }
|
|
70
|
+
}
|
|
71
|
+
return { state: `${name}:已验证,待归档`, next: `sillyspec run archive --change ${name}`, evidence: [ev('verify-result.md')] }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 探测当前项目状态与下一步命令。
|
|
76
|
+
* @param {{ cwd: string, specDir?: string|null }} opts
|
|
77
|
+
* @returns {{ state: string, next: string, evidence: string[], activeChanges: Array<{name,checked,total}> }}
|
|
78
|
+
* activeChanges 附带各变更 task 勾选进度(供 --json 消费);evidence 为相对 spec 根的路径
|
|
79
|
+
*/
|
|
80
|
+
export function detectNextStep({ cwd, specDir = null }) {
|
|
81
|
+
const specBase = resolveSpecDir(cwd, { specDir })
|
|
82
|
+
const changesRoot = join(specBase, 'changes')
|
|
83
|
+
|
|
84
|
+
// 1. HANDOFF 交接
|
|
85
|
+
for (const handoff of [join(cwd, 'HANDOFF.json'), join(specBase, 'HANDOFF.json')]) {
|
|
86
|
+
if (existsSync(handoff)) {
|
|
87
|
+
return { state: '存在 HANDOFF.json(上次会话交接)', next: '读 HANDOFF.json 后从交接点继续(sillyspec progress show 看活跃进度)', evidence: [handoff], activeChanges: [] }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. 活跃变更(排除 archive)
|
|
92
|
+
let active = []
|
|
93
|
+
try {
|
|
94
|
+
active = readdirSync(changesRoot, { withFileTypes: true })
|
|
95
|
+
.filter(e => e.isDirectory() && e.name !== 'archive')
|
|
96
|
+
.map(e => e.name)
|
|
97
|
+
.sort()
|
|
98
|
+
} catch { /* 无 changes/ 目录 */ }
|
|
99
|
+
if (active.length > 0) {
|
|
100
|
+
// 多活跃时逐个推断全报告,next 取第一个(字典序最前)的;调用方输出含全部状态行
|
|
101
|
+
const inferences = active.map(name => inferChange(join(changesRoot, name), name))
|
|
102
|
+
const progress = active.map((name, i) => {
|
|
103
|
+
const t = countTasks(join(changesRoot, name, 'tasks.md'))
|
|
104
|
+
return { name, checked: t.checked, total: t.total, state: inferences[i].state }
|
|
105
|
+
})
|
|
106
|
+
return { state: inferences.map(i => i.state).join(';'), next: inferences[0].next, evidence: inferences[0].evidence, activeChanges: progress }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. 已扫描无变更
|
|
110
|
+
const docsDir = join(specBase, 'docs')
|
|
111
|
+
try {
|
|
112
|
+
const scanned = readdirSync(docsDir, { withFileTypes: true })
|
|
113
|
+
.filter(e => e.isDirectory() && existsSync(join(docsDir, e.name, 'scan')))
|
|
114
|
+
if (scanned.length > 0) {
|
|
115
|
+
return { state: `已扫描(${scanned.length} 个项目文档),无活跃变更`, next: 'sillyspec run brainstorm "你的想法"', evidence: [join('.sillyspec', 'docs', scanned[0].name, 'scan')], activeChanges: [] }
|
|
116
|
+
}
|
|
117
|
+
} catch { /* 无 docs/ */ }
|
|
118
|
+
|
|
119
|
+
// 4. 绿地已有需求
|
|
120
|
+
for (const f of ['REQUIREMENTS.md', 'ROADMAP.md']) {
|
|
121
|
+
if (existsSync(join(specBase, f))) {
|
|
122
|
+
return { state: `绿地项目已有 ${f},无活跃变更`, next: 'sillyspec run brainstorm "你的想法"', evidence: [join('.sillyspec', f)], activeChanges: [] }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 5. 什么都没有
|
|
127
|
+
return { state: '未初始化(无 .sillyspec 产物)', next: 'sillyspec init(新项目)或 sillyspec scan(棕地项目)', evidence: [], activeChanges: [] }
|
|
128
|
+
}
|
package/src/run/prompt.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { basename, join } from 'node:path'
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
23
23
|
import { stageRegistry } from '../stages/index.js'
|
|
24
|
-
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE } from './shared.js'
|
|
24
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
25
25
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
26
26
|
import { parseModuleMapSimple } from '../modules.js'
|
|
27
27
|
import { REVIEW_SCHEMA_VERSION, isValidExecuteRunId } from '../task-review.js'
|
|
@@ -183,8 +183,11 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
183
183
|
console.log(`project: ${projectName}`)
|
|
184
184
|
if (changeName) {
|
|
185
185
|
console.log(`change: ${changeName}`)
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// changeDir 根(坑 change-dir-base-mismatch):平台/漂移锚定用绝对根(specDriftAnchor 防
|
|
187
|
+
// worktree 副本错位),普通本地保持相对 '.sillyspec' 展示契约(test 钉死)。此前
|
|
188
|
+
// isPlatform 用 specRoot||runtimeRoot 判定、取值只拿 specRoot——runtimeRoot-only 平台组合
|
|
189
|
+
// join(null,...) 直接 TypeError。
|
|
190
|
+
const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
|
|
188
191
|
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
189
192
|
console.log(`changeDir: ${changeDir}`)
|
|
190
193
|
}
|
|
@@ -233,6 +236,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
233
236
|
promptText = promptText.replace(/<now-datetime>/g, nowDatetime)
|
|
234
237
|
promptText = promptText.replace(/<now-timestamp>/g, nowTimestamp)
|
|
235
238
|
promptText = promptText.replace(/<now-date>/g, nowDate)
|
|
239
|
+
// <now-iso-datetime>(scan frontmatter updated_at 用;与 scan-fix-headers 同款秒级格式)
|
|
240
|
+
promptText = promptText.replace(/<now-iso-datetime>/g, now.toISOString().slice(0, 19).replace('T', ' '))
|
|
241
|
+
// <git-head-short>(scan frontmatter source_commit 用;CLI 代查,agent 勿自跑 rev-parse)
|
|
242
|
+
if (promptText.includes('<git-head-short>')) {
|
|
243
|
+
let headShort = 'unknown'
|
|
244
|
+
try {
|
|
245
|
+
headShort = safeGit(cwd, ['rev-parse', '--short', 'HEAD']).value || 'unknown'
|
|
246
|
+
} catch { /* git 不可用 → unknown,scan-postcheck 失配时再核 */ }
|
|
247
|
+
promptText = promptText.replace(/<git-head-short>/g, headShort)
|
|
248
|
+
}
|
|
236
249
|
// 替换 {REVIEW_SCHEMA_VERSION} 占位符(task review 示例模板用,值=CLI 当前 REVIEW_SCHEMA_VERSION 常量,
|
|
237
250
|
// 避免 agent 照抄 design 目标版本与 CLI 写侧常量漂移;与 stage review 契约 renderReviewJsonContract 动态注入同源)
|
|
238
251
|
if (promptText.includes('{REVIEW_SCHEMA_VERSION}')) {
|
|
@@ -715,10 +728,19 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
715
728
|
console.log('- **平台模式:local.yaml 中的 commands 必须在 package.json scripts 中真实存在,不存在的标记 unavailable。**')
|
|
716
729
|
}
|
|
717
730
|
if (changeName) {
|
|
718
|
-
|
|
719
|
-
|
|
731
|
+
// changeDir 根与上方头部同源(坑 change-dir-base-mismatch):平台/漂移锚定绝对根,
|
|
732
|
+
// 普通本地保持相对 '.sillyspec' 展示;runtimeRoot-only 组合不再 join(null) 崩溃
|
|
733
|
+
const changeDirBase = platformOpts?.specRoot || platformOpts?.specDriftAnchor || '.sillyspec'
|
|
720
734
|
const changeDir = join(changeDirBase, 'changes', changeName)
|
|
721
|
-
|
|
735
|
+
// quick 会话(quick-<hex8>)按设计无实体变更目录(progress.js initChange 同款跳过),
|
|
736
|
+
// 纯代码 quick 根本不产 spec 文档——「所有变更文件必须写入 changes/<change>/」的硬规则
|
|
737
|
+
// 对它是误导(ql-20260821-011 实证:CLI 反复提示一个用不到的目录)。改述条件化:
|
|
738
|
+
// 仅「若本 quick 决定补文档」才落该目录,代码改动照常写源码目录。
|
|
739
|
+
if (QUICK_SID_RE.test(changeName)) {
|
|
740
|
+
console.log(`- **文件路径规则(quick 会话):纯代码改动直接写源码目录,无目录限制。仅当本 quick 需要落 spec 文档(design/plan 等,复杂任务建议升级完整流程)时才写 \`${changeDir}/\`;QUICKLOG/tasks.md 记录由 CLI 接管,不要手建。**`)
|
|
741
|
+
} else {
|
|
742
|
+
console.log(`- **文件路径规则:所有变更文件必须写入 \`${changeDir}/\` 目录下。不要自己拼接路径,直接使用 changeDir 值。示例:\`${changeDir}/proposal.md\`**`)
|
|
743
|
+
}
|
|
722
744
|
}
|
|
723
745
|
}
|
|
724
746
|
const changeFlag = changeName ? ` --change ${changeName}` : ''
|
package/src/run/scan-profile.js
CHANGED
|
@@ -105,21 +105,15 @@ export function computeScanProfile(cwd, platformOpts) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
109
|
-
* quick
|
|
110
|
-
*
|
|
108
|
+
* quick 档 3 步表构建(唯一真相):applyScanProfileSteps(首进程裁剪)与 getStageSteps
|
|
109
|
+
* (后续进程重取定义)共用。此前 quick 步骤表只在首进程 applyScanProfileSteps 里内联构建,
|
|
110
|
+
* getStageSteps('scan') 恒返回 11 步注册表 → 同进程 outputStep 用注册表 defSteps[1] 渲染错位
|
|
111
|
+
* prompt;跨进程 ensureStageSteps 见 3 vs 11 触发长度漂移重播种,quick 已完成步丢失、
|
|
112
|
+
* agent 从未做过的注册表步骤被 --done(坑 scan-quick-profile-step-mismatch)。
|
|
111
113
|
*/
|
|
112
|
-
export function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (mode === 'quick') {
|
|
117
|
-
const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
|
|
118
|
-
const projectName = basename(cwd)
|
|
119
|
-
const docsRoot = join(specBase, 'docs', projectName)
|
|
120
|
-
|
|
121
|
-
// Step 1: CLI preflight(不调 AI,自动完成)
|
|
122
|
-
const step1 = {
|
|
114
|
+
export function buildQuickScanSteps() {
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
123
117
|
name: '项目概览(自动探测)',
|
|
124
118
|
status: 'pending',
|
|
125
119
|
noAI: true,
|
|
@@ -127,9 +121,8 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
127
121
|
prompt: '',
|
|
128
122
|
outputHint: 'preflight 结果',
|
|
129
123
|
optional: false
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const step2 = {
|
|
124
|
+
},
|
|
125
|
+
{
|
|
133
126
|
name: '生成核心文档',
|
|
134
127
|
status: 'pending',
|
|
135
128
|
prompt: `## Quick Scan — 核心文档生成
|
|
@@ -156,13 +149,32 @@ export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
|
156
149
|
生成的文件列表`,
|
|
157
150
|
outputHint: '文件列表',
|
|
158
151
|
optional: false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
// 名字与注册表末步一致(按名匹配/迁移语义);prompt 清空、noAI postcheck 自动执行
|
|
155
|
+
name: '自检和提交',
|
|
156
|
+
status: 'pending',
|
|
157
|
+
prompt: '',
|
|
158
|
+
outputHint: '结果',
|
|
159
|
+
optional: false,
|
|
160
|
+
noAI: true,
|
|
161
|
+
_cliAction: 'scanPostcheck'
|
|
159
162
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 根据 scanProfile 裁剪步骤
|
|
168
|
+
* quick: 3 步 — CLI preflight / AI generate / CLI postcheck
|
|
169
|
+
* standard: 跳过续扫检测(4), 跳过可选步骤(9)
|
|
170
|
+
*/
|
|
171
|
+
export function applyScanProfileSteps(stageData, profile, cwd, platformOpts) {
|
|
172
|
+
const steps = stageData.steps
|
|
173
|
+
const mode = profile.mode
|
|
174
|
+
|
|
175
|
+
if (mode === 'quick') {
|
|
176
|
+
// 3 步表构建抽至 buildQuickScanSteps(getStageSteps 同源复用,防跨进程 3 vs 11 漂移)
|
|
177
|
+
stageData.steps = buildQuickScanSteps()
|
|
166
178
|
return
|
|
167
179
|
}
|
|
168
180
|
|
package/src/run/shared.js
CHANGED
|
@@ -367,8 +367,14 @@ export function writePlatformPointer(cwd, platformOpts, extra = {}) {
|
|
|
367
367
|
declaredAt: new Date().toISOString(),
|
|
368
368
|
}
|
|
369
369
|
try {
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
// platform-scan.json 只在 specRoot 存在时可写(坑 pointer-runtime-root-only-null-join):
|
|
371
|
+
// runtimeRoot-only 平台组合(command.js 明确允许)下 specRoot 为 null,此前 join(null,…)
|
|
372
|
+
// 抛 ERR_INVALID_ARG_TYPE 落入总 catch → 指针与接管声明也一并静默未写,双入口 fail-closed
|
|
373
|
+
// 对该组合整体失效。分步写:specRoot 缺失只跳过 platform-scan.json,指针/声明照写。
|
|
374
|
+
if (platformOpts.specRoot) {
|
|
375
|
+
mkdirSync(join(platformOpts.specRoot, '.runtime'), { recursive: true })
|
|
376
|
+
writeFileSync(join(platformOpts.specRoot, '.runtime', 'platform-scan.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
377
|
+
}
|
|
372
378
|
writeFileSync(join(cwd, '.sillyspec-platform.json'), JSON.stringify(payload, null, 2) + '\n')
|
|
373
379
|
writeFileSync(join(cwd, PLATFORM_MANAGED_FILENAME), JSON.stringify(declaration, null, 2) + '\n')
|
|
374
380
|
return true
|
|
@@ -454,8 +460,8 @@ async function raceWithAbort(op, timeoutMs = SYNC_TOTAL_TIMEOUT_MS) {
|
|
|
454
460
|
|
|
455
461
|
// quick 会话 ID 形态(quick-<hex8>)。与 run/command.js QUICK_SID_RE、progress.js
|
|
456
462
|
// initChange 的跳过实体目录判断同源;shared.js 不反向 import command.js(重模块),
|
|
457
|
-
//
|
|
458
|
-
const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
463
|
+
// 局部复制正则保持轻载——改形态时三处同改(prompt.js 经本导出消费,勿再复制一份)。
|
|
464
|
+
export const QUICK_SID_RE = /^quick-[0-9a-f]{8}$/
|
|
459
465
|
|
|
460
466
|
export async function triggerSync(cwd, changeName, platformOpts = {}, opts = {}) {
|
|
461
467
|
// 平台模式(SillyHub)走自己的回传链路,不走 CLI 内置 sync
|
|
@@ -913,13 +919,17 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
913
919
|
// 致 flag 对 status 失效。)
|
|
914
920
|
if ((!forceBaseline && result.baselineHit.length > 0) || (!allowDelete && result.deletedFiles.length > 0) || result.reasons.some(r => r.startsWith('危险') || r.startsWith('删除'))) {
|
|
915
921
|
result.status = 'blocked'
|
|
916
|
-
} else if ((!allowNew && result.newFiles.
|
|
922
|
+
} else if ((!allowNew && result.newFiles.some(f => !isQuickMetadata(f, guard.linkedChanges))) || (allowedFiles.length > 0 && result.reasons.some(r => r.startsWith('超出')))) {
|
|
923
|
+
// newFiles 按元数据过滤后判定(坑 quick-metadata-warning-no-reason):.sillyspec/ 被 git
|
|
924
|
+
// 跟踪的仓库里 quick 新建的 quicklog/*.md 是合法元数据,不过滤会 status=warning 而
|
|
925
|
+
// reasons 为空(reasons 侧已过滤),打印空原因的 WARNING 噪声。
|
|
917
926
|
result.status = 'warning'
|
|
918
927
|
}
|
|
919
928
|
|
|
920
|
-
// quicklog
|
|
929
|
+
// quicklog 存在性检查(specBase 感知:平台模式 quicklog 写在 specRoot 下,stage.js
|
|
930
|
+
// allocateQuicklogEntry 同源;硬编码 cwd/.sillyspec 会恒误报「目录不存在」抬 warning)
|
|
921
931
|
try {
|
|
922
|
-
const quicklogDir = join(cwd, '.sillyspec', 'quicklog')
|
|
932
|
+
const quicklogDir = join(specBase || join(cwd, '.sillyspec'), 'quicklog')
|
|
923
933
|
if (existsSync(quicklogDir)) {
|
|
924
934
|
const qlFiles = readdirSync(quicklogDir).filter(f => f.endsWith('.md'))
|
|
925
935
|
if (qlFiles.length === 0) {
|
|
@@ -1342,6 +1352,17 @@ export async function getStageSteps(stageName, cwd, progress, specDir = null) {
|
|
|
1342
1352
|
const changeDir = resolveChangeDir(cwd, progress, specDir)
|
|
1343
1353
|
return buildPlanSteps(changeDir)
|
|
1344
1354
|
}
|
|
1355
|
+
if (stageName === 'scan') {
|
|
1356
|
+
// quick 档感知(坑 scan-quick-profile-step-mismatch):scan 的 scanProfile 在首进程
|
|
1357
|
+
// runStage 里算出并持久化(stageData.scanProfile),quick 档把 11 步注册表裁成 3 步。
|
|
1358
|
+
// 此处恒返回注册表会让后续进程 ensureStageSteps 见 3 vs 11 触发漂移重播种(quick 已完成
|
|
1359
|
+
// 步丢失、从未执行的注册表步骤被 --done),同进程 outputStep 也用错位 defSteps 渲染。
|
|
1360
|
+
// quick 档 → 与 applyScanProfileSteps 同源返回 3 步表;standard/deep 与注册表同长度零影响。
|
|
1361
|
+
if (progress?.stages?.scan?.scanProfile?.mode === 'quick') {
|
|
1362
|
+
const { buildQuickScanSteps } = await import('./scan-profile.js')
|
|
1363
|
+
return buildQuickScanSteps()
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1345
1366
|
const def = stageRegistry[stageName]
|
|
1346
1367
|
return def ? def.steps : null
|
|
1347
1368
|
}
|
package/src/scan-postcheck.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* 平台模式下必须通过所有 check 才能 success,否则降级。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync } from 'fs'
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, renameSync, rmdirSync } from 'fs'
|
|
9
9
|
import { join, basename } from 'path'
|
|
10
10
|
import { SCAN_STATUS, CHECK_SEVERITY, SCAN_REQUIRED_DOCS, SCAN_REQUIRED_DOCS_QUICK } from './constants.js'
|
|
11
11
|
import { validateScriptCommands } from './stages/cmd-existence.js'
|
|
12
|
+
import { git } from './git-helper.js'
|
|
12
13
|
|
|
13
14
|
const REQUIRED_SCAN_DOCS = SCAN_REQUIRED_DOCS
|
|
14
15
|
|
|
@@ -112,7 +113,7 @@ export function runScanPostCheck({ cwd, specDir, outputText = '', scanMeta = {},
|
|
|
112
113
|
checks.push({
|
|
113
114
|
name: 'docs_missing_header',
|
|
114
115
|
severity: CHECK_SEVERITY.WARNING,
|
|
115
|
-
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}
|
|
116
|
+
detail: `${docsMissingHeader.length} 份文档缺少 author/created_at: ${docsMissingHeader.join(', ')}(sillyspec scan-fix-headers 一键补齐)`
|
|
116
117
|
})
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -369,3 +370,142 @@ export function printScanPostCheckResult(result) {
|
|
|
369
370
|
}
|
|
370
371
|
console.log(` 📋 最终状态: ${result.status}`)
|
|
371
372
|
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* scan 文档 header 幂等补填(2026-08-21 agent-手工产出审计第三批 E3)。
|
|
376
|
+
*
|
|
377
|
+
* runScanPostCheck 对缺 author/created_at 只 warning,agent 手改常忘——本函数补齐:
|
|
378
|
+
* author = git user.name(回退 unknown),created_at = 当前时间。已有两键的文件不动;
|
|
379
|
+
* 有 frontmatter(--- 开头)就地插缺失键,无则补一段 frontmatter。
|
|
380
|
+
* `sillyspec scan-fix-headers [--project <名>]` 入口;与 runScanPostCheck 的
|
|
381
|
+
* 「头部 512 字符含 author:/created_at:」判定口径一致。
|
|
382
|
+
*
|
|
383
|
+
* @param {{ cwd: string, specDir?: string|null, project?: string|null }} opts
|
|
384
|
+
* @returns {{ fixed: string[], skipped: string[] }}
|
|
385
|
+
*/
|
|
386
|
+
export function fixScanDocHeaders({ cwd, specDir = null, project = null }) {
|
|
387
|
+
const specBase = specDir ? specDir : join(cwd, '.sillyspec')
|
|
388
|
+
const docsRoot = join(specBase, 'docs')
|
|
389
|
+
const projects = project
|
|
390
|
+
? [project]
|
|
391
|
+
: (existsSync(docsRoot) ? readdirSync(docsRoot, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name) : [])
|
|
392
|
+
|
|
393
|
+
let author = 'unknown'
|
|
394
|
+
try {
|
|
395
|
+
author = git(cwd, ['config', 'user.name']) || 'unknown'
|
|
396
|
+
} catch { /* git 不可用 → unknown */ }
|
|
397
|
+
const now = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
398
|
+
|
|
399
|
+
const fixed = []
|
|
400
|
+
const skipped = []
|
|
401
|
+
for (const p of projects) {
|
|
402
|
+
const scanDir = join(docsRoot, p, 'scan')
|
|
403
|
+
if (!existsSync(scanDir)) continue
|
|
404
|
+
for (const f of readdirSync(scanDir)) {
|
|
405
|
+
if (!f.endsWith('.md')) continue
|
|
406
|
+
const filePath = join(scanDir, f)
|
|
407
|
+
let content
|
|
408
|
+
try {
|
|
409
|
+
content = readFileSync(filePath, 'utf8')
|
|
410
|
+
} catch {
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
const header = content.slice(0, 512)
|
|
414
|
+
const hasAuthor = /author\s*:/.test(header)
|
|
415
|
+
const hasCreated = /created_at\s*:/.test(header)
|
|
416
|
+
if (hasAuthor && hasCreated) {
|
|
417
|
+
skipped.push(filePath)
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
const inserts = []
|
|
421
|
+
if (!hasAuthor) inserts.push(`author: ${author}`)
|
|
422
|
+
if (!hasCreated) inserts.push(`created_at: ${now}`)
|
|
423
|
+
let newContent
|
|
424
|
+
if (content.startsWith('---\n') || content.startsWith('---\r\n')) {
|
|
425
|
+
// 有 frontmatter:插到开标签后(CRLF 归一写回,与其它 CLI 写入口径一致)
|
|
426
|
+
const normalized = content.replace(/\r\n?/g, '\n')
|
|
427
|
+
newContent = normalized.replace(/^---\n/, `---\n${inserts.join('\n')}\n`)
|
|
428
|
+
} else {
|
|
429
|
+
newContent = `---\n${inserts.join('\n')}\n---\n\n` + content
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
writeFileSync(filePath, newContent)
|
|
433
|
+
fixed.push(filePath)
|
|
434
|
+
} catch { /* 只读文件等写失败 → 跳过不抛 */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return { fixed, skipped }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* source_root 污染一键修复(2026-08-21 agent-手工产出审计第四批 C-3)。
|
|
442
|
+
*
|
|
443
|
+
* runScanPostCheck 对「agent 把产物写到 <source_root>/.sillyspec/ 而非平台 specRoot」只报
|
|
444
|
+
* FAILED 不修——agent 手工搬目录常搬错。本函数把检查枚举的同款清单(docs/projects/workflows/
|
|
445
|
+
* knowledge 目录 + manifest.json/local.yaml)搬到 specRoot 对应位置(已存在的不覆盖,报告
|
|
446
|
+
* skipped),搬完源目录若空则删除。与检查的路径口径逐字一致(pollutePaths/polluteFiles)。
|
|
447
|
+
*
|
|
448
|
+
* @param {{ cwd: string, specDir: string }} opts cwd=source_root,specDir=平台 spec_root 绝对路径
|
|
449
|
+
* @returns {{ moved: string[], skipped: string[], removedDirs: string[] }}
|
|
450
|
+
*/
|
|
451
|
+
export function fixSourceRootLeak({ cwd, specDir }) {
|
|
452
|
+
if (!cwd || !specDir) return { moved: [], skipped: [], removedDirs: [] }
|
|
453
|
+
const moved = []
|
|
454
|
+
const skipped = []
|
|
455
|
+
const removedDirs = []
|
|
456
|
+
const pollutePaths = ['docs', 'projects', 'workflows', 'knowledge']
|
|
457
|
+
const polluteFiles = ['manifest.json', 'local.yaml']
|
|
458
|
+
|
|
459
|
+
// 目标目录确保存在
|
|
460
|
+
try { mkdirSync(join(specDir, '.runtime'), { recursive: true }) } catch {}
|
|
461
|
+
|
|
462
|
+
for (const file of polluteFiles) {
|
|
463
|
+
const src = join(cwd, '.sillyspec', file)
|
|
464
|
+
const dst = join(specDir, file)
|
|
465
|
+
if (!existsSync(src)) continue
|
|
466
|
+
if (existsSync(dst)) {
|
|
467
|
+
skipped.push(`local.yaml 已存在于 specRoot(保留两份,人工合并凭据后删源)`.replace('local.yaml', file))
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
renameSync(src, dst)
|
|
472
|
+
moved.push(`.sillyspec/${file} → specRoot/${file}`)
|
|
473
|
+
} catch (e) {
|
|
474
|
+
skipped.push(`.sillyspec/${file} 搬移失败: ${e.message}`)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
for (const sub of pollutePaths) {
|
|
479
|
+
const srcDir = join(cwd, '.sillyspec', sub)
|
|
480
|
+
if (!existsSync(srcDir)) continue
|
|
481
|
+
let entries = []
|
|
482
|
+
try { entries = readdirSync(srcDir, { withFileTypes: true }) } catch { continue }
|
|
483
|
+
if (entries.length === 0) {
|
|
484
|
+
try { rmdirSync(srcDir); removedDirs.push(`.sillyspec/${sub}/(空目录)`) } catch {}
|
|
485
|
+
continue
|
|
486
|
+
}
|
|
487
|
+
const dstDir = join(specDir, sub)
|
|
488
|
+
mkdirSync(dstDir, { recursive: true })
|
|
489
|
+
let allMoved = true
|
|
490
|
+
for (const e of entries) {
|
|
491
|
+
const src = join(srcDir, e.name)
|
|
492
|
+
const dst = join(dstDir, e.name)
|
|
493
|
+
if (existsSync(dst)) {
|
|
494
|
+
skipped.push(`.sillyspec/${sub}/${e.name}(specRoot 已有同名,不覆盖——人工比对后择一保留)`)
|
|
495
|
+
allMoved = false
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
renameSync(src, dst)
|
|
500
|
+
moved.push(`.sillyspec/${sub}/${e.name} → specRoot/${sub}/${e.name}`)
|
|
501
|
+
} catch (err) {
|
|
502
|
+
skipped.push(`.sillyspec/${sub}/${e.name} 搬移失败: ${err.message}`)
|
|
503
|
+
allMoved = false
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (allMoved) {
|
|
507
|
+
try { rmdirSync(srcDir); removedDirs.push(`.sillyspec/${sub}/`) } catch {}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return { moved, skipped, removedDirs }
|
|
511
|
+
}
|
|
@@ -330,7 +330,7 @@ const PLAN_RULES = [
|
|
|
330
330
|
id: 'plan.task-plan-reconciliation', stage: 'plan', source: 'validateBlueprintConsistency', severity: 'error', kind: 'task-plan-reconciliation',
|
|
331
331
|
target: { root: 'change', path: 'tasks' },
|
|
332
332
|
data: {
|
|
333
|
-
messageMissingCards: 'plan.md 声明了 ${declared} 个任务,但 tasks/ 只有部分卡片。缺卡: ${missing}\n
|
|
333
|
+
messageMissingCards: 'plan.md 声明了 ${declared} 个任务,但 tasks/ 只有部分卡片。缺卡: ${missing}\n 无卡任务的 allowed_paths/review.json 都不覆盖,不进 Wave、不受 execute 审计 → 必然漏做(典型:尾部文档同步类任务)。\n 修复:跑 sillyspec taskcard <change> --all 一键补齐全部缺卡安全骨架(已存在的卡不动,之后填充语义字段),或从 tasks.md 删除该任务声明。',
|
|
334
334
|
messageOrphanCards: 'tasks/ 存在 plan.md 未声明的卡片: ${orphans}\n 这些任务不进任何 Wave,execute 不会执行。修复:在 plan.md 补声明,或删除孤儿卡片。',
|
|
335
335
|
},
|
|
336
336
|
spec: 'plan.md 中声明的每个 task-NN 必须有对应 tasks/task-NN.md 卡片(反向亦然),否则阻断——无卡片任务不受 execute 审计,必然漏做。',
|
package/src/stage-review.js
CHANGED
|
@@ -65,9 +65,12 @@ export function renderReviewJsonContract({ stage, changeDir, reviewRunId, tier }
|
|
|
65
65
|
'- `checklist`(可选): 扁平数组 —— 每项 { item: string, result: ∈ { ' + CHECKLIST_RESULTS.join(' / ') + ' }, note?: string }。注意是**扁平数组**,不是按层(定义/一致/可行性)嵌套对象',
|
|
66
66
|
'- `reviewerNotes`: 说明(verdict=fail 时写明阻断项)',
|
|
67
67
|
'',
|
|
68
|
-
'### docHash
|
|
69
|
-
|
|
70
|
-
'-
|
|
68
|
+
'### docHash(一键代填,勿手算)',
|
|
69
|
+
'- **推荐**:docHash 先占位(如 `"TODO"`),review.json 写完后跑',
|
|
70
|
+
' `sillyspec register-stage-review --change <变更名> --stage ' + stageLbl + ' --refresh-hash`',
|
|
71
|
+
' —— CLI 重算真实 sha256 代填,verdict/checklist 原样保留;主文档改版后同样跑这条',
|
|
72
|
+
' (多阶段联动用 `--all` 一次处理)。gate 拦 docHash 失配时也是同一条命令修复。',
|
|
73
|
+
'- 兜底手算口径(极端无 CLI 场景):node `crypto.createHash(\'sha256\').update(readFileSync(...))` (原始字节)/ `sha256sum <file>` 首列 hex。',
|
|
71
74
|
'- ⚠️ **review.json 写入后若 ' + mainDoc + ' 再被改,必须重算 docHash** —— gate 会重算 reviewedFiles[0] 的 sha256 比对,不符判伪造(历史翻车根因:design 改了 rev 后 docHash 仍是旧值)',
|
|
72
75
|
'',
|
|
73
76
|
'### 完整 JSON 示例(照抄改值)',
|
|
@@ -209,8 +212,9 @@ export function verifyStageReviewDocHash(review, searchDirs) {
|
|
|
209
212
|
if (claimed !== actualRaw.toLowerCase() && claimed !== actualLf.toLowerCase()) {
|
|
210
213
|
errors.push(
|
|
211
214
|
`docHash 与主审查文档 ${primaryRel} 的实际内容不匹配。` +
|
|
212
|
-
`若刚改过该文档(如 design
|
|
213
|
-
|
|
215
|
+
`若刚改过该文档(如 design 修订后忘重算),一键修复:` +
|
|
216
|
+
`sillyspec register-stage-review --change <变更名> --stage <阶段> --refresh-hash` +
|
|
217
|
+
`(CLI 重算 docHash 代填,verdict 保留;多阶段联动加 --all);` +
|
|
214
218
|
`若未改文档,则 review.json 疑似伪造(未真正读取文档)。`
|
|
215
219
|
)
|
|
216
220
|
return { ok: false, errors, warnings }
|
|
@@ -414,7 +418,7 @@ export function validateStageReview(opts) {
|
|
|
414
418
|
* @param {object} context - { stage, reviewRunId } 用于错误提示路径
|
|
415
419
|
*/
|
|
416
420
|
export function printStageReviewResult(result, context = {}) {
|
|
417
|
-
const { stage, reviewRunId, runtimeRoot } = context
|
|
421
|
+
const { stage, reviewRunId, runtimeRoot, changeName } = context
|
|
418
422
|
if (result.ok && result.warnings.length === 0) {
|
|
419
423
|
console.log(`\n✅ Stage Review Gate — ${stage} 阶段独立审查通过`)
|
|
420
424
|
return
|
|
@@ -435,6 +439,11 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
435
439
|
}
|
|
436
440
|
console.error(`\n 提示:tier=independent 要求独立审查子代理产出 review.json,补全后重新 --done`)
|
|
437
441
|
console.error(` 可用 sillyspec register-stage-review --change <名> --stage ${stage} [--from <已有review.json>] 一步生成 run 目录 + review.json 骨架(docHash 自动算)+ 写 marker + 自检,省掉手动建目录/写 marker`)
|
|
442
|
+
// docHash 失配精确指路(2026-08-21 agent-手工产出审计项②):改版后忘重算是最高频失败,
|
|
443
|
+
// 一键重算命令带上真实 change 名,agent 可直接照抄执行(--refresh-hash 保 verdict)。
|
|
444
|
+
if (result.errors.some(e => String(e).includes('docHash')) && changeName) {
|
|
445
|
+
console.error(` docHash 失配修复:sillyspec register-stage-review --change ${changeName} --stage ${stage} --refresh-hash(主文档改版多阶段联动用 --all)`)
|
|
446
|
+
}
|
|
438
447
|
}
|
|
439
448
|
|
|
440
449
|
if (result.warnings.length > 0) {
|
|
@@ -465,7 +474,7 @@ export function printStageReviewResult(result, context = {}) {
|
|
|
465
474
|
* @returns {{ ok: boolean, reviewRunId: string, reviewPath: string, markerPath: string, mode: 'skeleton'|'adopted', mainDoc: string, review: object }}
|
|
466
475
|
* @throws {Error} changeName 空 / stage 非法 / 主文档缺失 / --from 不存在或 schema 不过
|
|
467
476
|
*/
|
|
468
|
-
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {} }) {
|
|
477
|
+
export function registerStageReview({ changeName, stage, fromFile, cwd, platformOpts = {}, refreshHash = false }) {
|
|
469
478
|
if (!changeName) throw new Error('register-stage-review: changeName 不能为空')
|
|
470
479
|
if (!stage || !STAGE_MAIN_DOC[stage]) {
|
|
471
480
|
throw new Error(`register-stage-review: stage 无效 "${stage}"(应为 brainstorm|plan|execute)`)
|
|
@@ -486,6 +495,45 @@ export function registerStageReview({ changeName, stage, fromFile, cwd, platform
|
|
|
486
495
|
}
|
|
487
496
|
const reviewedFiles = [`changes/${changeName}/${mainDoc}`]
|
|
488
497
|
|
|
498
|
+
// ── refresh-hash 模式(坑 stage-review-dochash-manual-resync,2026-08-21 实证):改一版 design
|
|
499
|
+
// 要重算 2-3 个 stage review 的 docHash(brainstorm/execute 都锚 design.md),手工易漏、gate
|
|
500
|
+
// 报错才补。就地更新 marker 指向的已有 review.json——保留 verdict/checklist/requiredEvidence
|
|
501
|
+
// (结论是否仍适用由人确认),仅重算 docHash + reviewerNotes 追加刷新记录;不换 run 目录
|
|
502
|
+
// (换新 run 会把 marker 重定向到骨架,丢已审结论)。无既有 review → 报错(该用骨架模式生成)。
|
|
503
|
+
if (refreshHash) {
|
|
504
|
+
if (fromFile) throw new Error('register-stage-review: --refresh-hash 与 --from 互斥(refresh 就地更新既有 review,adopt 引入新文件)')
|
|
505
|
+
const existingRunId = getLatestStageReviewRunId(runtimeRoot, stage, changeName)
|
|
506
|
+
const existingPath = existingRunId
|
|
507
|
+
? join(runtimeRoot, 'stage-reviews', `${stage}-${existingRunId}`, 'review.json')
|
|
508
|
+
: null
|
|
509
|
+
if (!existingPath || !existsSync(existingPath)) {
|
|
510
|
+
throw new Error(`register-stage-review: --refresh-hash 找不到 ${stage} 的既有 review.json(marker ${existingRunId || '无'} 无对应 run)——先去掉 --refresh-hash 生成骨架,或派审查子代理产出 review`)
|
|
511
|
+
}
|
|
512
|
+
let existing
|
|
513
|
+
try {
|
|
514
|
+
existing = JSON.parse(readFileSync(existingPath, 'utf8'))
|
|
515
|
+
} catch (e) {
|
|
516
|
+
throw new Error(`register-stage-review: 既有 review.json 解析失败 ${existingPath}: ${e.message}`)
|
|
517
|
+
}
|
|
518
|
+
const refreshedAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
|
519
|
+
const review = {
|
|
520
|
+
...existing,
|
|
521
|
+
docHash,
|
|
522
|
+
reviewedFiles,
|
|
523
|
+
reviewerNotes: `${existing.reviewerNotes || ''}\ndocHash refreshed at ${refreshedAt}(主文档改版后机械重算;verdict 结论是否仍适用需人工确认)`.trim(),
|
|
524
|
+
}
|
|
525
|
+
writeFileSync(existingPath, JSON.stringify(review, null, 2) + '\n')
|
|
526
|
+
const schemaRecheck = validateStageReviewSchema(review)
|
|
527
|
+
if (!schemaRecheck.ok) {
|
|
528
|
+
throw new Error(`register-stage-review: refresh 后 schema 自检失败 — ${schemaRecheck.errors.join('; ')}(原 review 字段异常,先修原文件)`)
|
|
529
|
+
}
|
|
530
|
+
const hashRecheck = verifyStageReviewDocHash(review, [specBase, changeDir, cwd])
|
|
531
|
+
if (!hashRecheck.ok) {
|
|
532
|
+
throw new Error(`register-stage-review: refresh 后 docHash 自检失败(不应发生)— ${hashRecheck.errors.join('; ')}`)
|
|
533
|
+
}
|
|
534
|
+
return { ok: true, reviewRunId: existingRunId, reviewPath: existingPath, markerPath: stageReviewMarkerPath(runtimeRoot, stage, changeName), mode: 'refreshed', mainDoc, review }
|
|
535
|
+
}
|
|
536
|
+
|
|
489
537
|
let review
|
|
490
538
|
let mode
|
|
491
539
|
if (fromFile) {
|
package/src/stages/archive.js
CHANGED
|
@@ -36,7 +36,7 @@ export const definition = {
|
|
|
36
36
|
5. module-impact.md 第一行标题必须用中文:# 模块影响分析(Module Impact)— <变更简述>
|
|
37
37
|
|
|
38
38
|
### 降级(module-impact.md 不存在)
|
|
39
|
-
若缺失(如旧变更未经新 plan 流程,或 small
|
|
39
|
+
若缺失(如旧变更未经新 plan 流程,或 small 变更从未生成):先跑 \`sillyspec module-impact --change <change-name>\`——CLI 按 _module-map.yaml paths 前缀匹配预填「模块影响矩阵」骨架(文件×模块归属 + 未匹配文件清单,机械部分全代算),你只需逐行填「影响类型」(逻辑变更/数据结构变更/接口变更/调用关系变更/配置变更/新增)与 needs_review 标记,补首行中文标题 \`# 模块影响分析(Module Impact)— <变更简述>\`。骨架无从生成(无 module-map / 无 diff)时才按 archive-impact.yaml 的 impact-analyzer 角色规则全手写。
|
|
40
40
|
|
|
41
41
|
确认完成后,下一步 sync-module-docs 会读 module-impact.md 更新模块卡片文档。`,
|
|
42
42
|
outputHint: 'module-impact.md 终审结果 + 修正摘要',
|