sillyspec 3.28.0 → 3.28.2
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/package.json +1 -1
- package/src/archive-delta.js +419 -0
- package/src/design-facts.js +354 -0
- package/src/docs-check.js +79 -8
- 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 +213 -4
- package/src/modules.js +41 -12
- package/src/progress/change-registry.js +20 -0
- package/src/progress/consistency-doctor.js +124 -5
- package/src/progress/stage-machine.js +13 -2
- package/src/progress.js +3 -0
- package/src/quicklog.js +51 -0
- package/src/run/command.js +70 -1
- package/src/run/complete-handlers.js +53 -4
- package/src/run/complete.js +46 -1
- package/src/run/gates.js +333 -2
- package/src/run/prompt.js +70 -0
- package/src/run/scan-profile.js +98 -6
- package/src/run/shared.js +12 -1
- package/src/run/stage.js +7 -1
- package/src/scan-diff.js +54 -1
- package/src/scan-facts.js +173 -0
- package/src/scan-postcheck.js +110 -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.js +10 -10
- package/src/stages/execute.js +15 -13
- package/src/stages/plan-postcheck.js +194 -1
- package/src/stages/plan.js +6 -2
- package/src/stages/scan.js +44 -102
- package/src/stages/verify.js +9 -7
- package/src/taskcard.js +8 -1
- package/src/verify-postcheck.js +610 -2
- 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/package.json
CHANGED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-delta — 归档变更 delta 聚合器(Before/Delta/After 三段式 delta.md 纯函数)
|
|
3
|
+
* (change: 2026-09-07-ir-stage-p3d,task-01,D-001@v1/D-002@v1/D-003@v1)
|
|
4
|
+
*
|
|
5
|
+
* 职责(设计文档「总体方案」的五源采集 fail-soft + 三段式渲染,CLI/archive 接线在 task-02):
|
|
6
|
+
* 1. collectDeltaSources({ changeDir, specRoot, project, runtimeRoot, cwd }):五源采集——
|
|
7
|
+
* ①reconcile:runtimeRoot/verify-runs/<ts>/reconcile-result.json 按 ts 倒序、change 字段
|
|
8
|
+
* 过滤后取最新(R-01 跨变更串台防护:不同变更的 run 混存于同一 verify-runs/);
|
|
9
|
+
* 全无命中 → runtimeRoot/apply-pathspec-<change>.txt 逐行交付清单兜底(D-003,reconcile=null)。
|
|
10
|
+
* ②verifyFacts:changeDir/verify-facts.json(JSON.parse fail-soft → null)。
|
|
11
|
+
* ③moduleMap:复用 design-facts loadModuleMap;归属推导复用 deriveActualModules(P3d 导出)。
|
|
12
|
+
* ④decisions:changeDir/decisions.md 文本 → parseDecisionDomains(null-safe,文件缺 → null)。
|
|
13
|
+
* ⑤endpointBaseline + currentEndpoints(change: 2026-09-07-endpoint-baseline,task-03):
|
|
14
|
+
* 基线 runtimeRoot/endpoint-baselines/<change>.json(readJsonSafe fail-soft → null);
|
|
15
|
+
* 现算 scanBackendEndpoints(scanRoot)——capture 同口径但不落盘(scanRoot=cwd 显式传入,
|
|
16
|
+
* 缺省 dirname(specRoot):本地模式 specRoot=<repoRoot>/.sillyspec → 即主仓根),异常 → null。
|
|
17
|
+
* 2. buildDeltaReport(...):三段式 md(D-002)——Before(受影响模块 map 注册摘要 + 声明域并集)/
|
|
18
|
+
* Delta(交付文件×模块归属表 + missing/undeclared 差集附注 + 决策清单 + 探针 metrics 摘要)/
|
|
19
|
+
* After(module-impact.md「更新结果」表引用 + scan 刷新建议 + 端点基线提示节)。
|
|
20
|
+
* 缺源逐段降级注记(「(无 X:原因)」),不因缺源失败(兼容策略:存量变更仍生成)。
|
|
21
|
+
*
|
|
22
|
+
* 端点基线提示节(2026-09-07-endpoint-baseline Wave2):门控沿用 probe5 backendEndpoints>0
|
|
23
|
+
* (Gap-4:无端点变更不收噪音注记,条件不动防断言面扩大),节标题沿用「### 端点基线提示」;
|
|
24
|
+
* 节内三态——基线+现算可得 → diffEndpointSets 增删表(added/removed 各自格行,changed 不配对
|
|
25
|
+
* 天然呈独立行;空 → 「无增删」一行);基线缺失 → 「无基线(变更未拍 baseline)」降级注记
|
|
26
|
+
* (替代 P3d 旧「独立立项」提示行);基线在但现算不可得 → 现算失败降级注记。
|
|
27
|
+
*
|
|
28
|
+
* 依赖方向(单向防环):archive-delta → design-facts / modules / endpoint-baseline /
|
|
29
|
+
* endpoint-extractor,被依赖者均不反向 import 本模块。
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
32
|
+
import { basename, dirname, join } from 'node:path'
|
|
33
|
+
import { parseDecisionDomains, loadModuleMap, deriveActualModules } from './design-facts.js'
|
|
34
|
+
import { parseModuleMapSimple } from './modules.js'
|
|
35
|
+
import { scanBackendEndpoints } from './endpoint-extractor.js'
|
|
36
|
+
import { diffEndpointSets } from './endpoint-baseline.js'
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// 小工具(全部 fail-soft:读不到/解析失败 → null / [],不抛出)
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/** 读 JSON 文件 → 对象;不存在/解析失败 → null */
|
|
43
|
+
function readJsonSafe(p) {
|
|
44
|
+
if (!p || !existsSync(p)) return null
|
|
45
|
+
try { return JSON.parse(readFileSync(p, 'utf8')) } catch { return null }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 读文本文件 → 字符串;不存在/读失败 → null */
|
|
49
|
+
function readTextSafe(p) {
|
|
50
|
+
if (!p || !existsSync(p)) return null
|
|
51
|
+
try { return readFileSync(p, 'utf8') } catch { return null }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** verify-runs/ 下 ts 形目录名(YYYYMMDDHHMMSS 纯数字)倒序列表 */
|
|
55
|
+
function listRunTsDescending(runsDir) {
|
|
56
|
+
try {
|
|
57
|
+
return readdirSync(runsDir).filter(n => /^\d+$/.test(n)).sort().reverse()
|
|
58
|
+
} catch { return [] }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 单文件 × prefixPairs 前缀匹配 → 全部命中模块 id(design-facts deriveActualModules 同款
|
|
63
|
+
* 语义:全等或目录前缀——但取全部命中而非首命中即断,供归属表展示一文件多模块的真实形态)。
|
|
64
|
+
* @returns {string[]} 命中模块 id(prefixPairs 顺序去重)
|
|
65
|
+
*/
|
|
66
|
+
function matchFileModules(file, prefixPairs) {
|
|
67
|
+
const t = String(file || '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
68
|
+
const ids = []
|
|
69
|
+
if (!t) return ids
|
|
70
|
+
for (const { id, path: p } of prefixPairs) {
|
|
71
|
+
if ((t === p || t.startsWith(p + '/')) && !ids.includes(id)) ids.push(id)
|
|
72
|
+
}
|
|
73
|
+
return ids
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 读 _module-map.yaml → Map<id, status>(Before 注册摘要的 status 列;loadModuleMap 只回
|
|
77
|
+
* ids/prefixPairs 不含标量字段,此处复用 modules.js canonical 解析再读一遍——只读不改) */
|
|
78
|
+
function loadModuleStatuses(specRoot, project) {
|
|
79
|
+
if (!specRoot || !project) return null
|
|
80
|
+
const text = readTextSafe(join(specRoot, 'docs', project, 'modules', '_module-map.yaml'))
|
|
81
|
+
if (text === null) return null
|
|
82
|
+
let parsed = {}
|
|
83
|
+
try { parsed = parseModuleMapSimple(text) || {} } catch { return null }
|
|
84
|
+
const st = new Map()
|
|
85
|
+
for (const [id, m] of Object.entries(parsed)) st.set(id, (m && m.status) || '未标注')
|
|
86
|
+
return st
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 提取 module-impact.md「## 更新结果」小节正文(不含标题行本身,到下一 `## ` 标题或文末;
|
|
90
|
+
* 引用正文嵌入 After 段 H3 小节下,原 H2 标题混入会破坏标题层级);无文件/无小节 → null */
|
|
91
|
+
function extractUpdateResultSection(mdPath) {
|
|
92
|
+
const text = readTextSafe(mdPath)
|
|
93
|
+
if (text === null) return null
|
|
94
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
95
|
+
const start = lines.findIndex(l => /^##\s+更新结果/.test(l))
|
|
96
|
+
if (start < 0) return null
|
|
97
|
+
const out = []
|
|
98
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
99
|
+
if (/^##\s+/.test(lines[i])) break
|
|
100
|
+
out.push(lines[i])
|
|
101
|
+
}
|
|
102
|
+
const body = out.join('\n').trim()
|
|
103
|
+
return body || null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** verifyFacts 探针 metrics 一行渲染:`key=value` 以「 / 」连接;probe5 形态随演进(P3b
|
|
107
|
+
* fail-soft 原则)——metrics 缺失/空时整行降级注记而非报错 */
|
|
108
|
+
function renderProbeLine(name, probe) {
|
|
109
|
+
const metrics = (probe && probe.metrics) || {}
|
|
110
|
+
const kv = Object.entries(metrics).map(([k, v]) => `${k}=${String(v)}`)
|
|
111
|
+
return kv.length > 0 ? `- ${name}:${kv.join(' / ')}` : `- ${name}:(无 metrics)`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const probe5BackendEndpoints = (verifyFacts) => {
|
|
115
|
+
const m = (verifyFacts && verifyFacts.probes && verifyFacts.probes.probe5
|
|
116
|
+
&& verifyFacts.probes.probe5.metrics) || {}
|
|
117
|
+
return typeof m.backendEndpoints === 'number' ? m.backendEndpoints : null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// 五源采集(Gap-2 兜底:deliverables 仅在 reconcile 全无命中时填充;
|
|
122
|
+
// ⑤ endpointBaseline/currentEndpoints 为 2026-09-07-endpoint-baseline task-03 第五源)
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 纯函数:聚合变更 delta 的五源数据(fail-soft,任一/全部缺源不抛出)。
|
|
127
|
+
* @param {{ changeDir?: string, specRoot?: string, project?: string, runtimeRoot?: string, cwd?: string }} opts
|
|
128
|
+
* changeDir=变更目录(basename 即 changeName,active changes/ 或 archive/ 下均可);
|
|
129
|
+
* specRoot=规范根(.sillyspec);project=子项目名;runtimeRoot=运行时根(.sillyspec/.runtime);
|
|
130
|
+
* cwd=⑤现算扫描根(缺省 dirname(specRoot)——本地模式即主仓根,capture 同口径锚定)
|
|
131
|
+
* @returns {{ reconcile: object|null, verifyFacts: object|null, moduleMap: object|null,
|
|
132
|
+
* decisions: Array<{id, domains}>|null, deliverables: string[],
|
|
133
|
+
* endpointBaseline: object|null, currentEndpoints: Array<{method,path,source,line}>|null }}
|
|
134
|
+
* reconcile=按 change 过滤取最新的对账产物;verifyFacts=探针底稿;moduleMap=loadModuleMap
|
|
135
|
+
* 返回形态;decisions=当前版本 D 条目模块域(decisions.md 缺 → null);deliverables=
|
|
136
|
+
* reconcile 全无命中时的 apply-pathspec 兜底清单(否则 []);endpointBaseline=基线文件
|
|
137
|
+
* payload(缺/损坏 → null);currentEndpoints=scanBackendEndpoints 现算端点集(不落盘,
|
|
138
|
+
* 扫描异常/无扫描根 → null)
|
|
139
|
+
*/
|
|
140
|
+
export function collectDeltaSources({ changeDir, specRoot, project, runtimeRoot, cwd } = {}) {
|
|
141
|
+
const change = changeDir ? basename(changeDir) : ''
|
|
142
|
+
|
|
143
|
+
// ① reconcile(R-01:先按 change 字段过滤再取最新,跨变更 run 不串台)
|
|
144
|
+
let reconcile = null
|
|
145
|
+
if (runtimeRoot && change) {
|
|
146
|
+
for (const ts of listRunTsDescending(join(runtimeRoot, 'verify-runs'))) {
|
|
147
|
+
const data = readJsonSafe(join(runtimeRoot, 'verify-runs', ts, 'reconcile-result.json'))
|
|
148
|
+
if (data && data.change === change) { reconcile = data; break }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 兜底(Gap-2/D-003):全无 reconcile → apply-pathspec-<change>.txt 逐行路径作交付清单
|
|
153
|
+
let deliverables = []
|
|
154
|
+
if (!reconcile && runtimeRoot && change) {
|
|
155
|
+
const text = readTextSafe(join(runtimeRoot, `apply-pathspec-${change}.txt`))
|
|
156
|
+
if (text !== null) {
|
|
157
|
+
deliverables = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ② verifyFacts(JSON.parse fail-soft null)
|
|
162
|
+
const verifyFacts = changeDir
|
|
163
|
+
? readJsonSafe(join(changeDir, 'verify-facts.json'))
|
|
164
|
+
: null
|
|
165
|
+
|
|
166
|
+
// ③ moduleMap(design-facts 单一真相源)
|
|
167
|
+
const moduleMap = loadModuleMap(specRoot, project)
|
|
168
|
+
|
|
169
|
+
// ④ decisions(null-safe:文件缺 → null;存在但无条目 → [])
|
|
170
|
+
let decisions = null
|
|
171
|
+
if (changeDir) {
|
|
172
|
+
const text = readTextSafe(join(changeDir, 'decisions.md'))
|
|
173
|
+
if (text !== null) decisions = parseDecisionDomains(text)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ⑤ endpointBaseline(2026-09-07-endpoint-baseline task-03):基线 JSON fail-soft → null
|
|
177
|
+
const endpointBaseline = (runtimeRoot && change)
|
|
178
|
+
? readJsonSafe(join(runtimeRoot, 'endpoint-baselines', `${change}.json`))
|
|
179
|
+
: null
|
|
180
|
+
|
|
181
|
+
// ⑤ currentEndpoints:归档时现算(capture 同口径 scanBackendEndpoints,不落盘)。
|
|
182
|
+
// 扫描根三择一:显式 cwd > dirname(specRoot)(本地模式 specRoot=<repoRoot>/.sillyspec →
|
|
183
|
+
// 主仓根)> 无(→ null);扫描异常 fail-soft → null(报告端降级注记,不阻断)。
|
|
184
|
+
const scanRoot = cwd || (specRoot ? dirname(specRoot) : null)
|
|
185
|
+
let currentEndpoints = null
|
|
186
|
+
if (scanRoot) {
|
|
187
|
+
try { currentEndpoints = scanBackendEndpoints(scanRoot) } catch { currentEndpoints = null }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { reconcile, verifyFacts, moduleMap, decisions, deliverables, endpointBaseline, currentEndpoints }
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// 三段式渲染(Before/Delta/After,缺源逐段降级注记)
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 纯函数:生成 delta.md 全文(三段式,D-002;不落盘——写盘/幂等由 CLI task-02 负责)。
|
|
199
|
+
* @param {{ changeDir?: string, specRoot?: string, project?: string, runtimeRoot?: string, cwd?: string, now?: string }} opts
|
|
200
|
+
* 前五项同 collectDeltaSources(cwd=⑤现算扫描根,缺省 dirname(specRoot));now=生成时刻
|
|
201
|
+
* ISO(缺省当前时刻,测试可注入)
|
|
202
|
+
* @returns {string} delta.md 全文(LF 行尾,单一尾换行)
|
|
203
|
+
*/
|
|
204
|
+
export function buildDeltaReport({ changeDir, specRoot, project, runtimeRoot, cwd, now } = {}) {
|
|
205
|
+
const change = changeDir ? basename(changeDir) : '<变更名>'
|
|
206
|
+
const {
|
|
207
|
+
reconcile, verifyFacts, moduleMap, decisions, deliverables,
|
|
208
|
+
endpointBaseline, currentEndpoints,
|
|
209
|
+
} = collectDeltaSources({ changeDir, specRoot, project, runtimeRoot, cwd })
|
|
210
|
+
const statuses = loadModuleStatuses(specRoot, project)
|
|
211
|
+
|
|
212
|
+
// 差集路径归一(missing={task,path,isNew?} / undeclared={path,suspectTask?|string}——
|
|
213
|
+
// 生产端 verify-postcheck.js :2087/:2088 钉死的形态;字符串形态按存量容忍)
|
|
214
|
+
const undeclaredPaths = (reconcile && Array.isArray(reconcile.undeclared))
|
|
215
|
+
? reconcile.undeclared.map(u => (u && typeof u === 'object' ? u.path : u)).filter(Boolean)
|
|
216
|
+
: []
|
|
217
|
+
const affectedFiles = reconcile
|
|
218
|
+
? [...(Array.isArray(reconcile.matched) ? reconcile.matched : []), ...undeclaredPaths]
|
|
219
|
+
: deliverables
|
|
220
|
+
const deliveredFiles = reconcile
|
|
221
|
+
? (Array.isArray(reconcile.matched) ? reconcile.matched : [])
|
|
222
|
+
: deliverables
|
|
223
|
+
|
|
224
|
+
// 受影响模块集 + 未匹配文件(R-02:未命中任何模块 paths 的文件显式列出不猜)
|
|
225
|
+
const affectedModules = moduleMap ? [...deriveActualModules(affectedFiles, moduleMap.prefixPairs)] : null
|
|
226
|
+
const unmatchedFiles = moduleMap
|
|
227
|
+
? affectedFiles.filter(f => matchFileModules(f, moduleMap.prefixPairs).length === 0)
|
|
228
|
+
: [...affectedFiles]
|
|
229
|
+
|
|
230
|
+
// 声明域并集(NEW: 前缀 → 「(新模块)」标记;decisions.md 的模块域可选字段)
|
|
231
|
+
const declaredDomains = []
|
|
232
|
+
if (Array.isArray(decisions)) {
|
|
233
|
+
for (const { domains } of decisions) {
|
|
234
|
+
for (const d of domains || []) if (!declaredDomains.includes(d)) declaredDomains.push(d)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const L = []
|
|
239
|
+
|
|
240
|
+
// ---- frontmatter(generated_at + 来源四项命中状态)----
|
|
241
|
+
L.push('---')
|
|
242
|
+
L.push(`generated_at: ${String(now || '').trim() || new Date().toISOString()}`)
|
|
243
|
+
L.push(`sources_reconcile: ${reconcile
|
|
244
|
+
? `命中(ran_at=${reconcile.ran_at || '?'},verify-runs 按 change 过滤取最新)`
|
|
245
|
+
: `未命中(apply-pathspec 兜底,${deliverables.length} 项)`}`)
|
|
246
|
+
L.push(`sources_verify_facts: ${verifyFacts ? '命中' : '缺失'}`)
|
|
247
|
+
L.push(`sources_module_map: ${moduleMap ? '命中' : '缺失'}`)
|
|
248
|
+
L.push(`sources_decisions: ${decisions === null ? '缺失' : '命中'}`)
|
|
249
|
+
L.push('---')
|
|
250
|
+
L.push('')
|
|
251
|
+
L.push(`# 变更 Delta — ${change}`)
|
|
252
|
+
L.push('')
|
|
253
|
+
|
|
254
|
+
// ---- Before(变更前状态)----
|
|
255
|
+
L.push('## Before(变更前状态)')
|
|
256
|
+
L.push('')
|
|
257
|
+
L.push('### 受影响模块(module-map 注册摘要)')
|
|
258
|
+
L.push('')
|
|
259
|
+
if (!moduleMap) {
|
|
260
|
+
L.push(`(无 module-map:docs/${project || '?'}/modules/_module-map.yaml 不存在或不可解析——模块归属推导跳过,交付文件全部按未匹配列出)`)
|
|
261
|
+
L.push('')
|
|
262
|
+
} else if (affectedModules.length === 0) {
|
|
263
|
+
L.push('(受影响模块集为空——交付/差集文件均未命中任何模块 paths)')
|
|
264
|
+
L.push('')
|
|
265
|
+
} else {
|
|
266
|
+
L.push('| 模块 | status | paths+core_files 条目数 |')
|
|
267
|
+
L.push('|---|---|---|')
|
|
268
|
+
for (const id of affectedModules) {
|
|
269
|
+
const status = (statuses && statuses.get(id)) || '—'
|
|
270
|
+
const count = moduleMap.prefixPairs.filter(p => p.id === id).length
|
|
271
|
+
L.push(`| ${id} | ${status} | ${count} |`)
|
|
272
|
+
}
|
|
273
|
+
L.push('')
|
|
274
|
+
}
|
|
275
|
+
if (unmatchedFiles.length > 0) {
|
|
276
|
+
L.push(`未匹配文件(不归属任何模块 paths,人工裁量):${unmatchedFiles.join('、')}`)
|
|
277
|
+
L.push('')
|
|
278
|
+
}
|
|
279
|
+
L.push('### 声明域并集(decisions.md 模块域)')
|
|
280
|
+
L.push('')
|
|
281
|
+
if (decisions === null) {
|
|
282
|
+
L.push(`(无 decisions.md:${join(changeDir || '?', 'decisions.md')} 不存在——声明域缺位)`)
|
|
283
|
+
} else if (declaredDomains.length === 0) {
|
|
284
|
+
L.push(`(decisions.md 解析出 ${decisions.length} 条当前版本决策,均未填写模块域——声明域为空)`)
|
|
285
|
+
} else {
|
|
286
|
+
L.push(declaredDomains.map(d => (d.startsWith('NEW:') ? `${d}(新模块)` : d)).join('、'))
|
|
287
|
+
}
|
|
288
|
+
L.push('')
|
|
289
|
+
|
|
290
|
+
// ---- Delta(做了什么)----
|
|
291
|
+
L.push('## Delta(做了什么)')
|
|
292
|
+
L.push('')
|
|
293
|
+
L.push('### 交付文件 × 模块归属')
|
|
294
|
+
L.push('')
|
|
295
|
+
if (!reconcile && deliveredFiles.length === 0) {
|
|
296
|
+
L.push(`(无 reconcile 产物且无 apply-pathspec-${change}.txt——交付文件清单不可得,本节缺位)`)
|
|
297
|
+
L.push('')
|
|
298
|
+
} else {
|
|
299
|
+
if (!reconcile) {
|
|
300
|
+
L.push(`(无 reconcile 产物(变更先于 P3a 或 verify 未落盘),清单取 apply-pathspec——文件级,无 missing/undeclared 差集)`)
|
|
301
|
+
L.push('')
|
|
302
|
+
}
|
|
303
|
+
L.push('| 交付文件 | 模块归属 |')
|
|
304
|
+
L.push('|---|---|')
|
|
305
|
+
for (const f of deliveredFiles) {
|
|
306
|
+
const ids = moduleMap ? matchFileModules(f, moduleMap.prefixPairs) : []
|
|
307
|
+
const cell = !moduleMap ? '—(无 module-map)' : (ids.length > 0 ? ids.join('、') : '—(未匹配)')
|
|
308
|
+
L.push(`| ${f} | ${cell} |`)
|
|
309
|
+
}
|
|
310
|
+
L.push('')
|
|
311
|
+
}
|
|
312
|
+
if (reconcile) {
|
|
313
|
+
L.push(`- 对账基线:status=${reconcile.status || '?'} / form=${reconcile.form || '?'} / sources=${(Array.isArray(reconcile.sources) ? reconcile.sources : []).join('、') || '?'}`)
|
|
314
|
+
const missing = Array.isArray(reconcile.missing) ? reconcile.missing : []
|
|
315
|
+
if (missing.length > 0) {
|
|
316
|
+
L.push(`- missing(声明未落盘,${missing.length} 项):${missing.map(m => (m && typeof m === 'object'
|
|
317
|
+
? `${m.task}:${m.path}${m.isNew ? '(NEW: 声明,路径已剥前缀)' : ''}`
|
|
318
|
+
: String(m))).join(';')}`)
|
|
319
|
+
} else {
|
|
320
|
+
L.push('- missing(声明未落盘):无')
|
|
321
|
+
}
|
|
322
|
+
if (undeclaredPaths.length > 0) {
|
|
323
|
+
const items = (Array.isArray(reconcile.undeclared) ? reconcile.undeclared : [])
|
|
324
|
+
.map(u => (u && typeof u === 'object' ? `${u.path}${u.suspectTask ? `(疑似归因 ${u.suspectTask})` : ''}` : String(u)))
|
|
325
|
+
L.push(`- undeclared(落盘未声明,${undeclaredPaths.length} 项):${items.join(';')}`)
|
|
326
|
+
} else {
|
|
327
|
+
L.push('- undeclared(落盘未声明):无')
|
|
328
|
+
}
|
|
329
|
+
L.push('')
|
|
330
|
+
}
|
|
331
|
+
L.push('### 决策清单(id × 模块域)')
|
|
332
|
+
L.push('')
|
|
333
|
+
if (decisions === null) {
|
|
334
|
+
L.push(`(无 decisions.md:${join(changeDir || '?', 'decisions.md')} 不存在——决策清单缺位)`)
|
|
335
|
+
L.push('')
|
|
336
|
+
} else if (decisions.length === 0) {
|
|
337
|
+
L.push('(decisions.md 无当前版本 D 条目——决策清单为空)')
|
|
338
|
+
L.push('')
|
|
339
|
+
} else {
|
|
340
|
+
L.push('| 决策 | 模块域 |')
|
|
341
|
+
L.push('|---|---|')
|
|
342
|
+
for (const { id, domains } of decisions) {
|
|
343
|
+
L.push(`| ${id} | ${(domains || []).join('、') || '(未填写)'} |`)
|
|
344
|
+
}
|
|
345
|
+
L.push('')
|
|
346
|
+
}
|
|
347
|
+
L.push('### 探针 metrics 摘要(验证结论表的机器半边)')
|
|
348
|
+
L.push('')
|
|
349
|
+
if (!verifyFacts || !verifyFacts.probes) {
|
|
350
|
+
L.push(`(无 verify-facts.json:${join(changeDir || '?', 'verify-facts.json')} 不存在或不可解析——探针指标快照缺位,可跑 sillyspec verify-probes --change ${change} --init 补)`)
|
|
351
|
+
} else {
|
|
352
|
+
if (verifyFacts.generatedAt) L.push(`快照时刻:${verifyFacts.generatedAt}`)
|
|
353
|
+
const order = ['probe1', 'probe3', 'probe5', 'probe6']
|
|
354
|
+
const names = [...order, ...Object.keys(verifyFacts.probes).filter(n => !order.includes(n))]
|
|
355
|
+
for (const n of names) L.push(renderProbeLine(n, verifyFacts.probes[n]))
|
|
356
|
+
}
|
|
357
|
+
L.push('')
|
|
358
|
+
|
|
359
|
+
// ---- After(建议动作)----
|
|
360
|
+
L.push('## After(建议动作)')
|
|
361
|
+
L.push('')
|
|
362
|
+
L.push('### 模块卡同步状态(module-impact.md「更新结果」)')
|
|
363
|
+
L.push('')
|
|
364
|
+
const updateResult = changeDir ? extractUpdateResultSection(join(changeDir, 'module-impact.md')) : null
|
|
365
|
+
if (updateResult) {
|
|
366
|
+
L.push('(引自变更目录 module-impact.md,人工维护为准)')
|
|
367
|
+
L.push('')
|
|
368
|
+
L.push(updateResult)
|
|
369
|
+
} else if (changeDir && existsSync(join(changeDir, 'module-impact.md'))) {
|
|
370
|
+
L.push('(module-impact.md 存在但无「## 更新结果」小节——模块卡同步状态引用缺位)')
|
|
371
|
+
} else {
|
|
372
|
+
L.push(`(无 module-impact.md:${join(changeDir || '?', 'module-impact.md')} 不存在——模块卡同步状态引用缺位)`)
|
|
373
|
+
}
|
|
374
|
+
L.push('')
|
|
375
|
+
L.push('### scan 刷新建议')
|
|
376
|
+
L.push('')
|
|
377
|
+
if (affectedModules && affectedModules.length > 0) {
|
|
378
|
+
L.push(`- \`sillyspec scan facts\` 下次刷新重点关注:${affectedModules.join('、')}(共 ${affectedModules.length} 个模块)`)
|
|
379
|
+
} else if (!moduleMap) {
|
|
380
|
+
L.push(`- (无 module-map:docs/${project || '?'}/modules/_module-map.yaml 不存在——受影响模块无法推导,建议先跑 sillyspec modules 同步补索引)`)
|
|
381
|
+
} else {
|
|
382
|
+
L.push('- (受影响模块集为空——无重点刷新面)')
|
|
383
|
+
}
|
|
384
|
+
if (unmatchedFiles.length > 0) {
|
|
385
|
+
L.push(`- 未匹配文件补录提示:以下文件未命中任何模块 paths——建议补录 _module-map.yaml(新文件)或核对归属(人工裁量):${unmatchedFiles.join('、')}`)
|
|
386
|
+
}
|
|
387
|
+
L.push('')
|
|
388
|
+
// 端点基线提示(2026-09-07-endpoint-baseline task-03):门控沿用 probe5 backendEndpoints>0
|
|
389
|
+
// 不动(Gap-4:无端点变更不收噪音注记),节标题沿用防断言面扩大;节内三态(基线×现算 →
|
|
390
|
+
// diffEndpointSets 增删表 / 基线缺失 → 降级注记 / 现算不可得 → 降级注记)。
|
|
391
|
+
const backendEndpoints = probe5BackendEndpoints(verifyFacts)
|
|
392
|
+
if (backendEndpoints !== null && backendEndpoints > 0) {
|
|
393
|
+
L.push('### 端点基线提示')
|
|
394
|
+
L.push('')
|
|
395
|
+
const baselineEps = (endpointBaseline && Array.isArray(endpointBaseline.endpoints))
|
|
396
|
+
? endpointBaseline.endpoints
|
|
397
|
+
: null
|
|
398
|
+
if (!baselineEps) {
|
|
399
|
+
L.push(`- 无基线(变更未拍 baseline):${join(runtimeRoot || '?', 'endpoint-baselines', `${change}.json`)} 不存在或不可解析——端点增删不可比(backendEndpoints=${backendEndpoints}(>0))`)
|
|
400
|
+
} else {
|
|
401
|
+
const endpointDiff = diffEndpointSets(baselineEps, currentEndpoints)
|
|
402
|
+
if (!endpointDiff) {
|
|
403
|
+
L.push(`- 基线已拍(${baselineEps.length} 端点)但现算不可得(scanBackendEndpoints 扫描失败)——端点增删不可比`)
|
|
404
|
+
} else if (endpointDiff.added.length === 0 && endpointDiff.removed.length === 0) {
|
|
405
|
+
L.push(`- 端点增删:无增删(基线 ${baselineEps.length} 端点 × 现算 ${currentEndpoints.length} 端点,method+归一 path 全一致)`)
|
|
406
|
+
} else {
|
|
407
|
+
L.push(`- 端点 diff:基线 ${baselineEps.length} 端点 × 现算 ${currentEndpoints.length} 端点(method+归一 path 集合运算;changed 不配对,天然呈独立行)`)
|
|
408
|
+
L.push('')
|
|
409
|
+
L.push('| 增删 | method | path | source |')
|
|
410
|
+
L.push('|---|---|---|---|')
|
|
411
|
+
for (const e of endpointDiff.added) L.push(`| + 新增 | ${e.method} | ${e.path} | ${e.source} |`)
|
|
412
|
+
for (const e of endpointDiff.removed) L.push(`| - 删除 | ${e.method} | ${e.path} | ${e.source} |`)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
L.push('')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return L.join('\n')
|
|
419
|
+
}
|