sillyspec 3.28.10 → 3.28.12
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-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
package/src/verify-probes.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* verify-probes.md 模板定义六个探针,agent 此前逐条手跑 grep/递归查找/git 对账再手工拼表格。
|
|
6
6
|
* 本模块把纯机械的四个探针命令化(语义判断的留 agent,输出里显式标注):
|
|
7
|
-
* 探针1 未实现标记扫描:design §6 清单的具体文件逐行 grep TODO/FIXME/尚未实现 等
|
|
7
|
+
* 探针1 未实现标记扫描:design §6 清单的具体文件逐行 grep TODO/FIXME/尚未实现 等 probe1-noqa
|
|
8
8
|
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/ 陷阱
|
|
9
9
|
* + JVM src/test/<lang> 同包镜像根——Maven/Gradle 布局测试与 main 侧永不 co-located)
|
|
10
10
|
* 探针5 API 契约对账:复用 contract-matrix.verifyApiParity(endpoints.json × 前端调用)+ 表格渲染
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* 关键词命中提示(命中≠判定,不参与门禁;2026-09-14-acceptance-test-matrix)
|
|
15
15
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
16
16
|
*
|
|
17
|
-
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
17
|
+
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。 probe1-noqa
|
|
18
18
|
* 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
|
|
19
19
|
* 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
20
20
|
* P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
|
|
@@ -37,16 +37,16 @@ import { resolveSpecDir, resolveRuntimeRoot, detectWorktreeSpecDrift } from './r
|
|
|
37
37
|
|
|
38
38
|
// 探针1 未实现标记匹配(坑 probe1-literal-false-positive,2026-09-15 复盘实证:16 命中全
|
|
39
39
|
// 字面误报——TODO_FLAG_TODO 业务常量、「XXX完成处置」中文占位模板)。口径分三级:
|
|
40
|
-
// - 尚未实现:中文短语高置信,保持子串匹配;
|
|
41
|
-
// - TODO/FIXME/HACK:ASCII 标识符边界匹配——TODO_FLAG_TODO / parseHackArgs 等标识符内部不再
|
|
40
|
+
// - 尚未实现:中文短语高置信,保持子串匹配; /* probe1-noqa */
|
|
41
|
+
// - TODO/FIXME/HACK:ASCII 标识符边界匹配——TODO_FLAG_TODO / parseHackArgs 等标识符内部不再 /* probe1-noqa */
|
|
42
42
|
// 命中(两侧任一边贴 [A-Za-z0-9_] 即视为标识符成分);
|
|
43
|
-
// - XXX:边界匹配且前或后紧邻 CJK 表意字符即排除——「XXX完成处置」「订单XXX号」类中文占位
|
|
44
|
-
// 模板不再命中(独立代码注释 `XXX:` / `// XXX fix` 紧邻标点空白,仍命中)。
|
|
43
|
+
// - XXX:边界匹配且前或后紧邻 CJK 表意字符即排除——「XXX完成处置」「订单XXX号」类中文占位 /* probe1-noqa */
|
|
44
|
+
// 模板不再命中(独立代码注释 `XXX:` / `// XXX fix` 紧邻标点空白,仍命中)。 /* probe1-noqa */
|
|
45
45
|
const CJK_IDEOGRAPH_CLASS = '\\u4e00-\\u9fff\\u3400-\\u4dbf'
|
|
46
|
-
const TODO_ASCII_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_])(?:TODO|FIXME|HACK)(?![A-Za-z0-9_])`)
|
|
47
|
-
const XXX_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])XXX(?![A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])`)
|
|
46
|
+
const TODO_ASCII_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_])(?:TODO|FIXME|HACK)(?![A-Za-z0-9_])`) /* probe1-noqa */
|
|
47
|
+
const XXX_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])XXX(?![A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])`) /* probe1-noqa */
|
|
48
48
|
export function isUnimplementedMarkerLine(line) {
|
|
49
|
-
if (line.includes('尚未实现')) return true
|
|
49
|
+
if (line.includes('尚未实现')) return true /* probe1-noqa */
|
|
50
50
|
if (TODO_ASCII_MARKER_RE.test(line)) return true
|
|
51
51
|
if (XXX_MARKER_RE.test(line)) return true
|
|
52
52
|
return false
|
|
@@ -189,19 +189,205 @@ export function extractPayloadKeys(text) {
|
|
|
189
189
|
return keys
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// ── 探针 8 契约维度(task-01,2026-09-16 跨层契约探针扩展)──
|
|
193
|
+
// design.md 的契约类章节(接口定义/数据模型等)字段表是「文档契约面」——既有三面(Java 字段/
|
|
194
|
+
// SQL NOT NULL/前端载荷键)对不上「文档 vs 实现」漂移:契约改了代码没跟上(orphan)、契约必填
|
|
195
|
+
// 前端根本没送(EHS 实证小程序缺发 reportOrgId 必填被拒同族)。纯 advisory 不升硬门。
|
|
196
|
+
const PROBE8_CONTRACT_SECTION_RE = /接口定义|数据模型|接口契约|字段/
|
|
197
|
+
const PROBE8_FIELD_HEADER_RE = /字段|field/i
|
|
198
|
+
const PROBE8_REQUIRED_CELL_RE = /必填|required|※/i
|
|
199
|
+
const PROBE8_FIELD_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/
|
|
200
|
+
|
|
201
|
+
/** markdown 切章节(##/### 标题行;# 是题名、#### 归正文,均不切),返回 [{heading, lines}] */
|
|
202
|
+
function splitMdSections(text) {
|
|
203
|
+
const sections = []
|
|
204
|
+
let cur = null
|
|
205
|
+
for (const line of String(text || '').split('\n')) {
|
|
206
|
+
const h = line.match(/^#{2,3}\s+(.*?)\s*#*\s*$/)
|
|
207
|
+
if (h) {
|
|
208
|
+
cur = { heading: h[1].trim(), lines: [] }
|
|
209
|
+
sections.push(cur)
|
|
210
|
+
} else if (cur) cur.lines.push(line)
|
|
211
|
+
}
|
|
212
|
+
return sections
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 表格行切列(剥首尾竖线与空白) */
|
|
216
|
+
const splitMdRow = (line) => String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(c => c.trim())
|
|
217
|
+
|
|
218
|
+
/** markdown 表格分隔行(|---|---| 形态:剥竖线后仅剩 :/-/空白即认定) */
|
|
219
|
+
const isMdDividerRow = (line) => {
|
|
220
|
+
const s = String(line || '').replace(/\|/g, '').trim()
|
|
221
|
+
return s !== '' && /^[:\s-]+$/.test(s)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 契约字段首列清洗:剥反引号与类型注记尾缀——`` `fieldName` `` / `fieldName(String)` /
|
|
226
|
+
* `fieldName int` 等形态统一取标识符部分(类型混进字段名会破坏归一化比对)。
|
|
227
|
+
*/
|
|
228
|
+
const stripContractFieldName = (cell) => {
|
|
229
|
+
const s = String(cell || '').replace(/`/g, '').trim()
|
|
230
|
+
const paren = s.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*[((]/)
|
|
231
|
+
if (paren) return paren[1]
|
|
232
|
+
const tail = s.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+(?:String|Integer|Int|Long|Double|Float|Boolean|Date|BigDecimal|varchar|char|int|bigint|decimal|datetime|timestamp|text)\b/i)
|
|
233
|
+
if (tail) return tail[1]
|
|
234
|
+
return s
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* design.md 契约面解析(task-01):契约类章节(标题含 接口定义/数据模型/接口契约/字段 之一)内
|
|
239
|
+
* 表头首个数据列含「字段/Field」的表格 → {name=章节名, fields, required}。防误判:文件清单表
|
|
240
|
+
* (首列「操作」)/风险表(首列「#」)不入面;数据行不足 2 行的噪声表跳过;章节含
|
|
241
|
+
* `<!-- probe8-skip -->` 整章跳过计 skippedSections(非契约字段表的逃生门)。
|
|
242
|
+
* @param {string} designPath
|
|
243
|
+
* @returns {{contracts: Array<{name: string, fields: Set<string>, required: Set<string>}>, skippedSections: number}|null}
|
|
244
|
+
*/
|
|
245
|
+
export function parseDesignContracts(designPath) {
|
|
246
|
+
if (!existsSync(designPath)) return null
|
|
247
|
+
let text
|
|
248
|
+
try { text = readFileSync(designPath, 'utf8') } catch { return null }
|
|
249
|
+
const contracts = []
|
|
250
|
+
let skippedSections = 0
|
|
251
|
+
for (const sec of splitMdSections(text)) {
|
|
252
|
+
if (!PROBE8_CONTRACT_SECTION_RE.test(sec.heading)) continue
|
|
253
|
+
if (sec.lines.some(l => l.includes('<!-- probe8-skip -->'))) { skippedSections++; continue }
|
|
254
|
+
let i = 0
|
|
255
|
+
while (i < sec.lines.length) {
|
|
256
|
+
const line = sec.lines[i]
|
|
257
|
+
if (!line.includes('|') || !isMdDividerRow(sec.lines[i + 1] || '')) { i++; continue }
|
|
258
|
+
// 表头首个数据列(首个非空列,剥反引号/空白)含 字段/Field 才入面;「操作」/「#」首列
|
|
259
|
+
// (文件清单表/风险表)显式排除——防表头措辞漂移误收。
|
|
260
|
+
const headerFirst = (splitMdRow(line).find(c => c !== '') || '').replace(/`/g, '').trim()
|
|
261
|
+
let j = i + 2
|
|
262
|
+
const dataRows = []
|
|
263
|
+
while (j < sec.lines.length && sec.lines[j].includes('|') && sec.lines[j].trim() !== '') { dataRows.push(sec.lines[j]); j++ }
|
|
264
|
+
if (PROBE8_FIELD_HEADER_RE.test(headerFirst) && headerFirst !== '操作' && headerFirst !== '#'
|
|
265
|
+
&& dataRows.length >= 2) {
|
|
266
|
+
const fields = new Set()
|
|
267
|
+
const required = new Set()
|
|
268
|
+
for (const row of dataRows) {
|
|
269
|
+
const cells = splitMdRow(row)
|
|
270
|
+
const fname = stripContractFieldName(cells[0])
|
|
271
|
+
if (!fname || !PROBE8_FIELD_IDENT_RE.test(fname)) continue // 非标识符首列(子说明行)不收
|
|
272
|
+
fields.add(fname)
|
|
273
|
+
// required=该行任一列含 必填|required|※
|
|
274
|
+
if (cells.some(c => PROBE8_REQUIRED_CELL_RE.test(c))) required.add(fname)
|
|
275
|
+
}
|
|
276
|
+
if (fields.size > 0) contracts.push({ name: sec.heading, fields, required })
|
|
277
|
+
}
|
|
278
|
+
i = j
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { contracts, skippedSections }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** design 是否声明提交端点:「接口定义」章节内含 POST/PUT 行(纯查询契约不苛求前端送全必填) */
|
|
285
|
+
function designHasSubmitEndpoint(text) {
|
|
286
|
+
for (const sec of splitMdSections(text)) {
|
|
287
|
+
if (!/接口定义/.test(sec.heading)) continue
|
|
288
|
+
if (sec.lines.some(l => /\b(POST|PUT)\b/.test(l))) return true
|
|
289
|
+
}
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── 接口面解析 + 消费端归类(2026-09-17-api-coverage-smoke task-04 / FR-05 / D-005~D-007)──
|
|
294
|
+
// parseDesignApiTable:design.md 接口段 tolerant 解析(D-005:不做 normative 接口表格式硬
|
|
295
|
+
// 契约)。双重防线:段头宽收(含 接口/端点/API/REST 子串、不区分大小写的 ##/### 段——宁多
|
|
296
|
+
// 勿漏,R-01:漏端点→漏覆盖假绿比多端点更糟)+ 行级双条件紧守(R-02 误报防护):HTTP 方法
|
|
297
|
+
// token 词边界(GETTING/BUCKET 内嵌词形不误认)× 路径样式 token(/xxx 类路径或 {xxx}/:xxx
|
|
298
|
+
// 模板段,取行内首个类路径串;?query 尾自然截断——端点身份是 path)。非接口段(非目标/
|
|
299
|
+
// 先例引用段)内的表格/示例行靠段头过滤不计(「示例行跳过」的机制即此)。产物三面:
|
|
300
|
+
// endpoints [{method, path, rowIdx}](rowIdx = 命中段内端点产出序号,1-based);
|
|
301
|
+
// declared(「本变更接口面:N 端点」声明行提取的 N,全文首个命中,缺席 null——声明与
|
|
302
|
+
// 解析并存以解析为准并注记,渲染侧体现);
|
|
303
|
+
// sectionHint(命中的接口段头列表,审计注记)。
|
|
304
|
+
// 落盘面(数据通道=落盘即消费,stage-contract 零 import 纪律):backfillFactsFromMdAndTests
|
|
305
|
+
// 主路径写 facts.apiFace / facts.consumerHints,task-05 validator 经 facts 消费不经函数调用。
|
|
306
|
+
const API_FACE_SECTION_RE = /接口|端点|api|rest/i
|
|
307
|
+
const API_FACE_METHOD_RE = /(?:^|[^A-Za-z])(GET|POST|PUT|DELETE|PATCH)(?![A-Za-z])/
|
|
308
|
+
// 路径样式 token:/ 起始、前邻非字母数字(日期 2026/09/17 的 /09 前是数字段不认),
|
|
309
|
+
// 首字符字母数字/_/-/{,后续段字符含 {}/: 模板形态(/orders/{id}、/orders/:id 全串命中)
|
|
310
|
+
const API_FACE_PATH_RE = /(?:^|[^A-Za-z0-9])(\/[A-Za-z0-9_\-{}][\w\-./{}:]*)/
|
|
311
|
+
const API_FACE_DECLARED_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* design.md 接口段 tolerant 解析(task-04 / D-005)。纯函数、本地正则零新依赖;输入统一
|
|
315
|
+
* CRLF→LF 归一(parseRuntimeEndpointExcluded 同款,Windows/Linux/macOS 兼容)。
|
|
316
|
+
* @param {string} designMd design.md 全文
|
|
317
|
+
* @returns {{ endpoints: Array<{method: string, path: string, rowIdx: number}>,
|
|
318
|
+
* declared: number|null, sectionHint: string[] }}
|
|
319
|
+
*/
|
|
320
|
+
export function parseDesignApiTable(designMd) {
|
|
321
|
+
const text = String(designMd || '').replace(/\r\n/g, '\n')
|
|
322
|
+
const dm = text.match(API_FACE_DECLARED_RE)
|
|
323
|
+
const declared = dm ? parseInt(dm[1], 10) : null
|
|
324
|
+
const endpoints = []
|
|
325
|
+
const sectionHint = []
|
|
326
|
+
for (const sec of splitMdSections(text)) {
|
|
327
|
+
if (!API_FACE_SECTION_RE.test(sec.heading)) continue
|
|
328
|
+
sectionHint.push(sec.heading)
|
|
329
|
+
let rowIdx = 0
|
|
330
|
+
for (const line of sec.lines) {
|
|
331
|
+
const t = line.trim()
|
|
332
|
+
if (!t || t.startsWith('#')) continue
|
|
333
|
+
const mm = t.match(API_FACE_METHOD_RE)
|
|
334
|
+
if (!mm) continue // 缺方法列(有路径无方法 token)不认——双条件缺一不可
|
|
335
|
+
const pm = t.match(API_FACE_PATH_RE)
|
|
336
|
+
if (!pm) continue // 有方法无路径样式 token(说明行/纯文档行)不认
|
|
337
|
+
rowIdx++
|
|
338
|
+
endpoints.push({ method: mm[1], path: pm[1], rowIdx })
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { endpoints, declared, sectionHint }
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── 消费端归类启发式(task-04 / D-006 / FR-05,仅 advisory 提示面)──
|
|
345
|
+
// design 清单文件面路径段归类 web/mp/script:routes/pages/model 目录词 → web;mp/miniapp →
|
|
346
|
+
// mp;scripts/test → script。归类只用于 task-05 的「有消费端未填子行」warning 提示(R-05:
|
|
347
|
+
// 启发式假提示无害)——不做语义判定。路径段级匹配非裸子串('homepages.js' 文件名不命中
|
|
348
|
+
// pages 段);mp 检查先于 web(mp/pages/index 小程序页归 mp 不归 web);NEW: 前缀剥除
|
|
349
|
+
// (probe1-new-prefix-miss 同款)。
|
|
350
|
+
const CONSUMER_WEB_SEGMENTS = new Set(['routes', 'pages', 'model'])
|
|
351
|
+
const CONSUMER_MP_SEGMENT_RE = /^(?:mp|miniapp|miniprogram|weapp)$/i
|
|
352
|
+
const CONSUMER_SCRIPT_SEGMENTS = new Set(['scripts', 'script', 'test', 'tests'])
|
|
353
|
+
const CONSUMER_HINT_FILE_CAP = 20
|
|
354
|
+
/**
|
|
355
|
+
* 清单路径 → 消费端归类(task-04,导出供 task-05 经落盘/传参双通道消费)。
|
|
356
|
+
* @param {string[]} paths design 清单文件路径(NEW: 前缀/CRLF 容忍)
|
|
357
|
+
* @returns {Record<string, string[]>} 命中类别的支撑文件(键 ∈ web/mp/script,零命中 {})
|
|
358
|
+
*/
|
|
359
|
+
export function classifyConsumerHints(paths) {
|
|
360
|
+
const hints = {}
|
|
361
|
+
const push = (kind, p) => {
|
|
362
|
+
if (!hints[kind]) hints[kind] = []
|
|
363
|
+
if (hints[kind].length < CONSUMER_HINT_FILE_CAP && !hints[kind].includes(p)) hints[kind].push(p)
|
|
364
|
+
}
|
|
365
|
+
for (const raw of paths || []) {
|
|
366
|
+
const p = String(raw || '').replace(/^NEW:\s*/, '').split('\\').join('/')
|
|
367
|
+
if (!p || p.startsWith('.sillyspec/')) continue
|
|
368
|
+
const segs = p.toLowerCase().split('/')
|
|
369
|
+
if (segs.some(s => CONSUMER_MP_SEGMENT_RE.test(s))) push('mp', p)
|
|
370
|
+
else if (segs.some(s => CONSUMER_WEB_SEGMENTS.has(s))) push('web', p)
|
|
371
|
+
else if (segs.some(s => CONSUMER_SCRIPT_SEGMENTS.has(s))) push('script', p)
|
|
372
|
+
}
|
|
373
|
+
return hints
|
|
374
|
+
}
|
|
375
|
+
|
|
192
376
|
/**
|
|
193
377
|
* 探针 8 主体:design 清单三面文件(Java/SQL/前端)→ 双根(主仓 ∪ worktree)∪ 跨仓注册仓根
|
|
194
378
|
* 读取 → 归一化比对。advisory:所有输出是「候选」不是结论(UI 本地态键/服务端填充列会自然
|
|
195
379
|
* 出现在差异里,agent 逐条复核——口径注记随渲染输出)。
|
|
196
380
|
* @param {{ specBase: string, cwd: string, wtRoot?: string|null, changeName: string }} args
|
|
197
381
|
* @returns {{applicable, backendFieldCount, feKeyCount, notNullCount, javaFileCount, sqlFileCount, feFileCount,
|
|
198
|
-
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[]
|
|
382
|
+
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[],
|
|
383
|
+
* contractCount: number, contractOrphans: Array<{fe, hint?}>, missingRequired: Array<{field, contract}>}}
|
|
199
384
|
*/
|
|
200
385
|
export function runProbe8PayloadParity({ specBase, cwd, wtRoot = null, changeName }) {
|
|
201
386
|
const out = {
|
|
202
387
|
applicable: false, backendFieldCount: 0, feKeyCount: 0, notNullCount: 0,
|
|
203
388
|
javaFileCount: 0, sqlFileCount: 0, feFileCount: 0,
|
|
204
389
|
mispairs: [], feOnly: [], missingNotNull: [], notes: [],
|
|
390
|
+
contractCount: 0, contractOrphans: [], missingRequired: [],
|
|
205
391
|
}
|
|
206
392
|
if (!specBase || !changeName) return out
|
|
207
393
|
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
@@ -300,12 +486,339 @@ export function runProbe8PayloadParity({ specBase, cwd, wtRoot = null, changeNam
|
|
|
300
486
|
const nc = normFieldKey(col)
|
|
301
487
|
if (!feNorm.has(nc) && !backendNorm.has(nc)) out.missingNotNull.push({ col })
|
|
302
488
|
}
|
|
489
|
+
|
|
490
|
+
// ── 契约维度(task-01,advisory 不升硬门):design.md 契约面(接口定义/数据模型章节字段表)
|
|
491
|
+
// × 前端载荷键第四面对账——文档 vs 实现漂移:契约外载荷键(候选契约滞后/跨层私加)+ 契约必填
|
|
492
|
+
// 漏发(EHS 小程序缺发 reportOrgId 被拒同族)。无契约面仅注记,既有三维度输出不动。──
|
|
493
|
+
const contractRes = parseDesignContracts(designPath)
|
|
494
|
+
const contracts = (contractRes && contractRes.contracts) || []
|
|
495
|
+
if (contracts.length > 0) {
|
|
496
|
+
out.contractCount = contracts.length
|
|
497
|
+
const contractUnion = new Map() // norm → 契约字段原名(hint 对账用原名)
|
|
498
|
+
for (const c of contracts) {
|
|
499
|
+
for (const f of c.fields) {
|
|
500
|
+
const nf = normFieldKey(f)
|
|
501
|
+
if (!contractUnion.has(nf)) contractUnion.set(nf, f)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// 契约外载荷键:已落 feOnly/mispairs 疑似面的 fe 键(归一正名键不扰)且契约面归一化未见。
|
|
505
|
+
// hint 用 fieldTokens token-Jaccard 对契约字段原名单独跑一遍,≥0.4 取最高分者(并列取长度
|
|
506
|
+
// 差最小——同 mispairs 择优口径),仅提示性可缺省。
|
|
507
|
+
const suspected = new Set(out.feOnly)
|
|
508
|
+
for (const p of out.mispairs) suspected.add(p.fe)
|
|
509
|
+
for (const [nk, original] of feNorm) {
|
|
510
|
+
if (contractUnion.has(nk) || !suspected.has(original)) continue
|
|
511
|
+
const feToks = fieldTokens(original)
|
|
512
|
+
let hint = null
|
|
513
|
+
let bestScore = 0
|
|
514
|
+
let bestLenPenalty = Infinity
|
|
515
|
+
if (feToks.length >= 2) {
|
|
516
|
+
for (const [cn, cOrig] of contractUnion) {
|
|
517
|
+
const ct = fieldTokens(cOrig)
|
|
518
|
+
if (ct.length < 2) continue
|
|
519
|
+
const inter = feToks.filter(t => ct.includes(t)).length
|
|
520
|
+
const union = new Set([...feToks, ...ct]).size
|
|
521
|
+
const score = union > 0 ? inter / union : 0
|
|
522
|
+
if (score <= 0) continue
|
|
523
|
+
const lenPenalty = Math.abs(cn.length - nk.length)
|
|
524
|
+
if (score > bestScore || (score === bestScore && lenPenalty < bestLenPenalty)) {
|
|
525
|
+
bestScore = score
|
|
526
|
+
bestLenPenalty = lenPenalty
|
|
527
|
+
hint = cOrig
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (hint && bestScore >= 0.4) out.contractOrphans.push({ fe: original, hint })
|
|
532
|
+
else out.contractOrphans.push({ fe: original })
|
|
533
|
+
}
|
|
534
|
+
// 必填漏发:仅 design「接口定义」章节声明提交端点(POST/PUT 行)时启用——查询型契约的
|
|
535
|
+
// required 不苛求前端送全(GET 无 body)。契约 required 归一化后 ∉ feNorm 全集即列。
|
|
536
|
+
let designText = ''
|
|
537
|
+
try { designText = readFileSync(designPath, 'utf8') } catch { /* 不可读 → 不启用 */ }
|
|
538
|
+
if (designHasSubmitEndpoint(designText)) {
|
|
539
|
+
for (const c of contracts) {
|
|
540
|
+
for (const f of c.required) {
|
|
541
|
+
if (!feNorm.has(normFieldKey(f))) out.missingRequired.push({ field: f, contract: c.name })
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
out.notes.push('design 无契约面——契约维度 skipped')
|
|
547
|
+
}
|
|
303
548
|
out.backendFieldCount = backendFields.size
|
|
304
549
|
out.feKeyCount = feKeys.size
|
|
305
550
|
out.notNullCount = notNullCols.size
|
|
306
551
|
return out
|
|
307
552
|
}
|
|
308
553
|
|
|
554
|
+
// ── 探针 9(守卫一致性,2026-09-15 EHS doSubmit 越权 P1 同族驱动)常量与纯函数 ──
|
|
555
|
+
// 背景:同实体 CRUD 守卫不一致(deleteOrder/withdraw 有开立人校验、doSubmit 全链无操作人
|
|
556
|
+
// 校验→任何登录用户可提交他人开立单)是编码式权限的典型漏洞形态——探针 5 只对账 URL 存在性、
|
|
557
|
+
// 探针 8 只对账字段契约,权限守卫面零机器覆盖,全靠 verify 人工走查(EHS 恰是走查盲区)。
|
|
558
|
+
// 本探针(advisory,⚠️ 不阻断):同文件同实体变更方法组内有守卫/无守卫并存 → 定向复核提示。
|
|
559
|
+
// 宁漏勿误(R-01/R-02):≥2 方法才成组、纯标识符弱信号不计、只扫 design 清单 .java 文件。
|
|
560
|
+
const PROBE9_HEADING = '#### 探针 9:守卫一致性(advisory)'
|
|
561
|
+
// 变更动词前缀集(design §1 定稿)——方法名剥可选引导词后以前缀动词开头即变更方法
|
|
562
|
+
const PROBE9_MUTATION_VERBS = new Set(['submit', 'delete', 'remove', 'withdraw', 'update', 'handle', 'confirm', 'reject', 'audit', 'save', 'cancel', 'approve'])
|
|
563
|
+
// 引导前缀词(doSubmit/trySubmit 形态——EHS 真实方法名 doSubmit 的动词在第二词段)
|
|
564
|
+
const PROBE9_LEAD_WORDS = new Set(['do', 'try'])
|
|
565
|
+
// 方法签名行正则(design §1 定稿):访问修饰符 + 类型段(泛型/数组/逗号空格)+ 方法名 + (
|
|
566
|
+
// (构造器无「类型 空格 名」形态天然不命中;无访问修饰符的接口默认方法不收——宁漏勿误)
|
|
567
|
+
const PROBE9_METHOD_SIG_RE = /(?:public|private|protected)\s+[\w<>\[\],. ]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/
|
|
568
|
+
// 类名剥常见后缀(「实体取全类名」路径用:OrderController → Order)
|
|
569
|
+
const PROBE9_CLASS_SUFFIX_RE = /(?:Controller|RestController|Service|ServiceImpl|Mapper|Repository|Api|Manager|Handler)$/
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Java 注释等长掩码(参照 endpoint-extractor.js stripCommentsKeepLength 思路在探针内自实现,
|
|
573
|
+
* 不跨模块 import——行注释/块注释替换为空格保留换行,字符串/char 字面量内容原样不动)。
|
|
574
|
+
* FR-02:注释里的守卫文档示例(// userId.equals(...) 之类)掩码后不再误报。
|
|
575
|
+
*/
|
|
576
|
+
function probe9MaskCommentsKeepLength(text) {
|
|
577
|
+
const src = String(text || '')
|
|
578
|
+
const out = src.split('')
|
|
579
|
+
let st = 'normal' // normal | block | dq | sq
|
|
580
|
+
for (let i = 0; i < src.length; i++) {
|
|
581
|
+
const c = src[i]
|
|
582
|
+
const n = src[i + 1]
|
|
583
|
+
if (st === 'normal') {
|
|
584
|
+
if (c === '/' && n === '/') {
|
|
585
|
+
// 行注释:掩到行尾(换行符本身保留)
|
|
586
|
+
let j = i
|
|
587
|
+
while (j < src.length && src[j] !== '\n') { out[j] = ' '; j++ }
|
|
588
|
+
i = j - 1
|
|
589
|
+
} else if (c === '/' && n === '*') {
|
|
590
|
+
out[i] = ' '; out[i + 1] = ' '
|
|
591
|
+
st = 'block'; i++
|
|
592
|
+
} else if (c === '"') st = 'dq'
|
|
593
|
+
else if (c === "'") st = 'sq'
|
|
594
|
+
} else if (st === 'block') {
|
|
595
|
+
if (c === '*' && n === '/') { out[i] = ' '; out[i + 1] = ' '; st = 'normal'; i++ }
|
|
596
|
+
else if (c !== '\n') out[i] = ' '
|
|
597
|
+
} else {
|
|
598
|
+
// 字符串/char 内:转义跳过;闭合引号回 normal(内容原样保留——字符串不动)
|
|
599
|
+
if (c === '\\') i++
|
|
600
|
+
else if ((st === 'dq' && c === '"') || (st === 'sq' && c === "'")) st = 'normal'
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return out.join('')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** 驼峰切词(doSubmit → [do, Submit];submitOrder → [submit, Order];snake/连字符一并切段) */
|
|
607
|
+
const probe9SplitHumps = (s) => String(s || '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[^A-Za-z0-9]+/).filter(Boolean)
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* 类实体名词(「实体取全类名」路径):首个 class/interface 声明名剥 Controller/Service 等
|
|
611
|
+
* 常见后缀后取尾段驼峰名词(PolluteRpOrderController → order、OrderController → order)——
|
|
612
|
+
* 纯动词方法名(submit/withdraw/handle/doSubmit)剥词后无从判实体,以类实体兜底归组(EHS
|
|
613
|
+
* 真实形态:deleteOrder 与 doSubmit 同在 PolluteRpOrderController,正是要聚上的一组)。
|
|
614
|
+
* @returns {{key: string, display: string}|null} key=小写归一键;类名不可解析 → null
|
|
615
|
+
*/
|
|
616
|
+
function probe9ClassEntity(sourceText) {
|
|
617
|
+
const m = String(sourceText || '').match(/\b(?:class|interface)\s+([A-Za-z_][A-Za-z0-9_]*)/)
|
|
618
|
+
if (!m) return null
|
|
619
|
+
const bare = m[1].replace(PROBE9_CLASS_SUFFIX_RE, '') || m[1]
|
|
620
|
+
const words = probe9SplitHumps(bare)
|
|
621
|
+
return words.length > 0 ? { key: words[words.length - 1].toLowerCase(), display: words[words.length - 1] } : null
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 方法名 → 实体键:驼峰切词后从头部剥引导词(do/try)与变更动词词段。
|
|
626
|
+
* - 首个有效词段既非引导词也非动词(getOrder 的 get)→ null(非变更方法,查询面不收)
|
|
627
|
+
* - 剥词后剩余词段非空 → 尾段驼峰名词为实体(submitOrder/deleteOrder → Order)
|
|
628
|
+
* - 剥词后空/纯动词(submit/doSubmit/handle)→ 类实体兜底(类名无从判 → null 跳过)
|
|
629
|
+
* @returns {{key: string, display: string}|null}
|
|
630
|
+
*/
|
|
631
|
+
function probe9EntityOfMethodName(name, classEntity) {
|
|
632
|
+
const words = probe9SplitHumps(name)
|
|
633
|
+
let i = 0
|
|
634
|
+
let sawVerb = false
|
|
635
|
+
while (i < words.length) {
|
|
636
|
+
const w = words[i].toLowerCase()
|
|
637
|
+
if (PROBE9_LEAD_WORDS.has(w)) { i++; continue }
|
|
638
|
+
if (PROBE9_MUTATION_VERBS.has(w)) { sawVerb = true; i++; continue }
|
|
639
|
+
break
|
|
640
|
+
}
|
|
641
|
+
if (!sawVerb) return null
|
|
642
|
+
if (i >= words.length) return classEntity || null
|
|
643
|
+
const tail = words[words.length - 1]
|
|
644
|
+
return { key: tail.toLowerCase(), display: tail }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* 内部聚类(含单方法实体组与行数组——runProbe9 统计「单方法组 skipped」注记、构造签名前
|
|
649
|
+
* 3 行上下文用;导出面 clusterMutationMethods 只回 ≥2 方法组)。
|
|
650
|
+
* @returns {{lines: string[], groups: Array<{entity, methods}>, singletonCount: number}}
|
|
651
|
+
*/
|
|
652
|
+
function probe9ClusterInternal(sourceText) {
|
|
653
|
+
const normalized = String(sourceText || '').replace(/\r\n/g, '\n')
|
|
654
|
+
const lines = normalized.split('\n')
|
|
655
|
+
const sigs = []
|
|
656
|
+
for (let i = 0; i < lines.length; i++) {
|
|
657
|
+
const m = lines[i].match(PROBE9_METHOD_SIG_RE)
|
|
658
|
+
if (m) sigs.push({ name: m[1], sigLine: i })
|
|
659
|
+
}
|
|
660
|
+
const cls = probe9ClassEntity(normalized)
|
|
661
|
+
const byKey = new Map() // 实体键(小写归一) → {entity(首见原形), methods}
|
|
662
|
+
sigs.forEach((s, i) => {
|
|
663
|
+
const ent = probe9EntityOfMethodName(s.name, cls)
|
|
664
|
+
if (!ent) return
|
|
665
|
+
// 方法体 = 签名行到下一签名行前(含签名行;文件尾方法到 EOF,类尾近似)。下一签名前的
|
|
666
|
+
// 注解块(@PreAuthorize 等)归属下一方法——从体尾回收,防注解内 hasRole( 等字样误判
|
|
667
|
+
// 上一方法有守卫(冒烟实证:@PreAuthorize("hasRole('admin')") 划进上一方法体误中角色判定)。
|
|
668
|
+
let endIdx = i + 1 < sigs.length ? sigs[i + 1].sigLine : lines.length
|
|
669
|
+
while (endIdx > s.sigLine && /^\s*@/.test(lines[endIdx - 1] || '')) endIdx--
|
|
670
|
+
if (!byKey.has(ent.key)) byKey.set(ent.key, { entity: ent.display, methods: [] })
|
|
671
|
+
byKey.get(ent.key).methods.push({
|
|
672
|
+
name: s.name,
|
|
673
|
+
startLine: s.sigLine + 1,
|
|
674
|
+
endLine: endIdx,
|
|
675
|
+
body: lines.slice(s.sigLine, endIdx).join('\n'),
|
|
676
|
+
})
|
|
677
|
+
})
|
|
678
|
+
const all = [...byKey.values()]
|
|
679
|
+
return { lines, groups: all.filter(g => g.methods.length >= 2), singletonCount: all.filter(g => g.methods.length === 1).length }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* 聚类变更方法(探针 9 纯函数核心,design §1):方法签名行定位 → 变更方法实体归组 →
|
|
684
|
+
* 同文件同实体(大小写归一)≥2 方法才成组(单方法不成组,R-01 宁漏勿误)。
|
|
685
|
+
* @param {string} sourceText 单个 .java 文件全文
|
|
686
|
+
* @returns {Array<{entity: string, methods: Array<{name: string, startLine: number, endLine: number, body: string}>}>}
|
|
687
|
+
*/
|
|
688
|
+
export function clusterMutationMethods(sourceText) {
|
|
689
|
+
return probe9ClusterInternal(sourceText).groups
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── 四类守卫信号正则(design §2 / R-02:调用/比较形态优先,纯标识符弱信号不计)──
|
|
693
|
+
// ① 当前用户比对:当前用户获取调用形态(词后随 ( 或 . 链式起点——SecurityContext.getContext()
|
|
694
|
+
// 的 Holder 形态由 \w* 覆盖),或 createBy/create_by/openBy/userId 邻接 .equals( / == / !=
|
|
695
|
+
const PROBE9_CURRENT_USER_CALL_RE = /\b(?:getCurrent\w*|currentUser|getUserId|getLoginUser\w*|SecurityContext\w*|ThreadLocal\w*Util)\s*[.(]/
|
|
696
|
+
const PROBE9_OWNER_EQUALS_RE = /\b(?:createBy|create_by|openBy|userId)\s*\.\s*equals\s*\(/
|
|
697
|
+
const PROBE9_OWNER_EQ_RE = /\b(?:createBy|create_by|openBy|userId)\s*(?:==|!=)/
|
|
698
|
+
const PROBE9_OWNER_EQ_BACK_RE = /(?:==|!=)\s*[\w.\s()]{0,40}?\b(?:createBy|create_by|openBy|userId)\b/
|
|
699
|
+
// ② 角色判定:hasRole/isCompany*/checkRole 调用形态,或签名里 boolean manager/isManager/isAdmin 布尔参数
|
|
700
|
+
const PROBE9_ROLE_CALL_RE = /\b(?:hasRole\w*|isCompany\w*|checkRole\w*)\s*\(/
|
|
701
|
+
const PROBE9_ROLE_PARAM_RE = /\bboolean\s+(?:is)?(?:[Mm]anager|[Aa]dmin)\b/
|
|
702
|
+
// ③ 能力类调用:canHandle/checkPerm*/assert*Perm|Auth|Owner|User/validate*User 调用形态
|
|
703
|
+
const PROBE9_CAPABILITY_CALL_RE = /\b(?:canHandle|checkPerm\w*|assert\w*(?:Perm|Auth|Owner|User)\w*|validate\w*User\w*)\s*\(/
|
|
704
|
+
// ④ 注解式:签名上下文内权限注解(强信号,出现即中)
|
|
705
|
+
const PROBE9_ANNOTATION_RE = /@(?:PreAuthorize|RolesAllowed|SaCheckPermission|RequiresPermissions)\b/
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 守卫信号检测(探针 9 纯函数,design §2):方法体先做等长注释掩码(注释里的文档示例不
|
|
709
|
+
* 误报),再测四类信号——命中任一即该方法「有守卫」;返回空数组 = 无守卫。
|
|
710
|
+
* @param {string} methodBody 方法体文本(含签名行)
|
|
711
|
+
* @param {string} [signatureContext] 签名前 3 行 + 签名行(④注解式与②布尔参数的检测面)
|
|
712
|
+
* @returns {string[]} 命中信号类别列表('当前用户比对' | '角色判定' | '能力类调用' | '注解式')
|
|
713
|
+
*/
|
|
714
|
+
export function detectGuardSignals(methodBody, signatureContext = '') {
|
|
715
|
+
const body = probe9MaskCommentsKeepLength(methodBody)
|
|
716
|
+
const ctx = probe9MaskCommentsKeepLength(signatureContext)
|
|
717
|
+
const signals = []
|
|
718
|
+
if (PROBE9_CURRENT_USER_CALL_RE.test(body) || PROBE9_OWNER_EQUALS_RE.test(body)
|
|
719
|
+
|| PROBE9_OWNER_EQ_RE.test(body) || PROBE9_OWNER_EQ_BACK_RE.test(body)) signals.push('当前用户比对')
|
|
720
|
+
if (PROBE9_ROLE_CALL_RE.test(body) || PROBE9_ROLE_PARAM_RE.test(ctx)) signals.push('角色判定')
|
|
721
|
+
if (PROBE9_CAPABILITY_CALL_RE.test(body)) signals.push('能力类调用')
|
|
722
|
+
if (PROBE9_ANNOTATION_RE.test(ctx)) signals.push('注解式')
|
|
723
|
+
return signals
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 探针 9 主体:design 清单 .java 文件(主仓 ∪ worktree 双根 + 跨仓注册仓根回退,对齐探针 8
|
|
728
|
+
* readEntry 写法)逐文件聚类 + 信号比对——同实体组内有守卫与无守卫并存才进 inconsistentGroups
|
|
729
|
+
* (全有/全无不报,宁漏勿误;advisory:输出是定向复核提示不是结论)。无 .java → applicable=false
|
|
730
|
+
* + 不适用注记;.java 首行 `// probe9-skip` 整文件跳过;单文件解析异常 fail-soft 跳过不炸整体。
|
|
731
|
+
* @param {{ specBase: string, cwd: string, wtRoot?: string|null, changeName: string }} args
|
|
732
|
+
* @returns {{applicable: boolean, javaFileCount: number, groupCount: number,
|
|
733
|
+
* inconsistentGroups: Array<{entity: string, guarded: string[], unguarded: string[], signals: Record<string, string[]>}>,
|
|
734
|
+
* notes: string[]}}
|
|
735
|
+
*/
|
|
736
|
+
export function runProbe9GuardConsistency({ specBase, cwd, wtRoot = null, changeName }) {
|
|
737
|
+
const out = { applicable: false, javaFileCount: 0, groupCount: 0, inconsistentGroups: [], notes: [] }
|
|
738
|
+
if (!specBase || !changeName) return out
|
|
739
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
740
|
+
if (!existsSync(designPath)) return out
|
|
741
|
+
let registry = new Map()
|
|
742
|
+
try {
|
|
743
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
744
|
+
if (existsSync(yamlPath)) registry = parseRepoRegistry(readFileSync(yamlPath, 'utf8'))
|
|
745
|
+
} catch { /* 注册表不可读 → 跨仓条目落未注册注记 */ }
|
|
746
|
+
const detailed = parseFileChangeListDetailed(designPath, { repoKeys: [...registry.keys()] })
|
|
747
|
+
|
|
748
|
+
// 双根读取(对齐探针 8 readEntry:NEW: 前缀剥离、未注册跨仓前缀兜底、主仓 ∪ worktree 回退)
|
|
749
|
+
const readEntry = (e) => {
|
|
750
|
+
const probePath = String(e.path).replace(/^NEW:\s*/, '')
|
|
751
|
+
const crPrefix = probePath.match(/^cross-repo:([A-Za-z0-9_.\-]+):(.*)$/)
|
|
752
|
+
if (e.repo || crPrefix) {
|
|
753
|
+
const key = e.repo || (crPrefix && crPrefix[1])
|
|
754
|
+
const relPath = e.repo ? probePath : crPrefix[2]
|
|
755
|
+
const raw = registry.get(key)
|
|
756
|
+
if (!raw) { out.notes.push(`repo「${key}」未在 local.yaml repos 注册——该仓 Java 文件未进探针 9 比对`); return null }
|
|
757
|
+
const root = isAbsolute(raw) ? raw : resolve(cwd, raw)
|
|
758
|
+
try { return readFileSync(join(root, relPath), 'utf8') } catch { out.notes.push(`跨仓文件不可读:${key}:${relPath}`); return null }
|
|
759
|
+
}
|
|
760
|
+
for (const base of [cwd, wtRoot].filter(Boolean)) {
|
|
761
|
+
try { return readFileSync(join(base, probePath), 'utf8') } catch { /* 试下一根 */ }
|
|
762
|
+
}
|
|
763
|
+
return null
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
let nonJavaCount = 0
|
|
767
|
+
let singletonCount = 0
|
|
768
|
+
for (const e of detailed) {
|
|
769
|
+
if (!e.path || e.path.startsWith('.sillyspec/')) continue
|
|
770
|
+
if (!e.path.endsWith('.java')) { nonJavaCount++; continue }
|
|
771
|
+
const text = readEntry(e)
|
|
772
|
+
if (text == null) continue
|
|
773
|
+
// 文件级豁免:首行 // probe9-skip 整文件跳过(守卫由上游统一拦截等形态的逃生门)
|
|
774
|
+
if (/^\s*\/\/\s*probe9-skip\b/.test(text)) {
|
|
775
|
+
out.notes.push(`${e.path}:首行 // probe9-skip 豁免——整文件跳过探针 9`)
|
|
776
|
+
continue
|
|
777
|
+
}
|
|
778
|
+
let clustered
|
|
779
|
+
try {
|
|
780
|
+
clustered = probe9ClusterInternal(text)
|
|
781
|
+
} catch (err) {
|
|
782
|
+
out.notes.push(`${e.path}:解析异常 fail-soft 跳过(${err && err.message ? err.message : err})`)
|
|
783
|
+
continue
|
|
784
|
+
}
|
|
785
|
+
out.javaFileCount++
|
|
786
|
+
singletonCount += clustered.singletonCount
|
|
787
|
+
for (const g of clustered.groups) {
|
|
788
|
+
out.groupCount++
|
|
789
|
+
const signals = {}
|
|
790
|
+
const guarded = []
|
|
791
|
+
const unguarded = []
|
|
792
|
+
for (const m of g.methods) {
|
|
793
|
+
// signatureContext = 签名前 3 行 + 签名行(④注解式/②布尔参数的检测面);窗口不跨
|
|
794
|
+
// 上一方法签名行——紧凑单行方法下上一方法的 @PreAuthorize 会串扰进本方法检测面
|
|
795
|
+
// (冒烟实证:相邻单行方法时无注解方法被注解行误判有守卫,用例 4 全守卫假象)。
|
|
796
|
+
const win = clustered.lines.slice(Math.max(0, m.startLine - 4), m.startLine)
|
|
797
|
+
// 只在除本方法签名行(窗口末行)外的行里找上一方法签名行(findIndex 会命中自身签名行)
|
|
798
|
+
const cut = win.slice(0, -1).findIndex((l) => PROBE9_METHOD_SIG_RE.test(l))
|
|
799
|
+
const ctx = (cut >= 0 ? win.slice(cut + 1) : win).join('\n')
|
|
800
|
+
const sig = detectGuardSignals(m.body, ctx)
|
|
801
|
+
signals[m.name] = sig
|
|
802
|
+
if (sig.length > 0) guarded.push(m.name)
|
|
803
|
+
else unguarded.push(m.name)
|
|
804
|
+
}
|
|
805
|
+
if (guarded.length > 0 && unguarded.length > 0) {
|
|
806
|
+
out.inconsistentGroups.push({ entity: g.entity, guarded, unguarded, signals })
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (out.javaFileCount === 0) {
|
|
811
|
+
out.notes.push(nonJavaCount > 0
|
|
812
|
+
? `清单无 .java 文件(另有 ${nonJavaCount} 个非 Java 清单文件不在探针 9 扫描面)`
|
|
813
|
+
: '清单无 .java 文件')
|
|
814
|
+
return out
|
|
815
|
+
}
|
|
816
|
+
out.applicable = true
|
|
817
|
+
if (nonJavaCount > 0) out.notes.push(`${nonJavaCount} 个非 Java 清单文件不在探针 9 扫描面(v1 只扫 .java,design 非目标)`)
|
|
818
|
+
if (singletonCount > 0) out.notes.push(`单方法实体 ${singletonCount} 个不成组(≥2 方法才比对,宁漏勿误)`)
|
|
819
|
+
return out
|
|
820
|
+
}
|
|
821
|
+
|
|
309
822
|
|
|
310
823
|
const PROBE7_HEADING = '#### 探针 7:验收×测试覆盖矩阵'
|
|
311
824
|
// execute run id 格式(与 task-review.js isValidExecuteRunId 同口径锚定,防提示词注入/路径穿越)
|
|
@@ -421,13 +934,20 @@ function extractAcceptanceTerms(text) {
|
|
|
421
934
|
* 命中≠判定(R-03,不参与门禁)——只有存在命中的条目才进 hints(键 = acceptance 条目下标)。
|
|
422
935
|
* 测试文件读取双根回退(cwd → worktree 根,坑 probe1-worktree-path-blind / probe3 双根同族:
|
|
423
936
|
* apply 前新测试只在 worktree)。读不到的文件计 null 跳过。
|
|
937
|
+
* task-05 / FR-09:双根扩多根——并入 local.yaml repos 注册的跨仓仓根(调用方解析传入,
|
|
938
|
+
* probe8 :314-316 同款读法),跨仓仓根下的新测试文件可读、命中不再恒空(矩阵不因跨仓恒
|
|
939
|
+
* 预填 partial);根列表去重保序(主仓根优先命中),无跨仓注册的单仓调用行为零变化。
|
|
424
940
|
* 坑 probe7-prefill-evidence(ql-20260915-004):anchors 逐词记录首命中 file:line(grep -n
|
|
425
941
|
* 语义——首个包含该词的测试文件内的首行号),供证据列机械预填;terms/files 键维持旧形态
|
|
426
942
|
* (既有消费方/测试零回归)。
|
|
943
|
+
* @param {Array<string>} testFiles 归属测试文件(仓根相对路径)
|
|
944
|
+
* @param {string} cwd 主仓 cwd
|
|
945
|
+
* @param {string|null} wtRoot worktree 根
|
|
946
|
+
* @param {string[]} [crossRoots] 跨仓仓根(local.yaml repos 注册根,绝对路径)
|
|
427
947
|
* @returns {Record<number, {terms: string[], files: string[], anchors: Array<{term: string, file: string, line: number}>}>}
|
|
428
948
|
* 普通对象,可 JSON 序列化
|
|
429
949
|
*/
|
|
430
|
-
function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot) {
|
|
950
|
+
function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot, crossRoots = []) {
|
|
431
951
|
const hints = {}
|
|
432
952
|
if (!testFiles || testFiles.length === 0) return hints
|
|
433
953
|
const cache = new Map()
|
|
@@ -435,8 +955,7 @@ function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot) {
|
|
|
435
955
|
if (cache.has(rel)) return cache.get(rel)
|
|
436
956
|
let content = null
|
|
437
957
|
const posix = String(rel).split('\\').join('/')
|
|
438
|
-
for (const root of [cwd, wtRoot]) {
|
|
439
|
-
if (!root) continue
|
|
958
|
+
for (const root of [...new Set([cwd, wtRoot, ...crossRoots].filter(Boolean))]) {
|
|
440
959
|
try { content = readFileSync(join(root, posix), 'utf8'); break } catch { /* 换根重试 */ }
|
|
441
960
|
}
|
|
442
961
|
cache.set(rel, content)
|
|
@@ -477,7 +996,7 @@ function mdEscapeCell(text, cap = 200) {
|
|
|
477
996
|
/**
|
|
478
997
|
* 探针 7 判定/证据两列机械预填(坑 probe7-prefill-evidence,ql-20260915-004)。
|
|
479
998
|
*
|
|
480
|
-
* 背景:此前骨架判定列 `<待填:四选一>` + 证据列 `<TODO>` 全占位,24 格矩阵 agent 全量
|
|
999
|
+
* 背景:此前骨架判定列 `<待填:四选一>` + 证据列 `<TODO>` 全占位,24 格矩阵 agent 全量 probe1-noqa
|
|
481
1000
|
* 手填——CLI 已算出归属/关键词命中却弃之不用。预填规则(机械保守,agent 逐格复核改写):
|
|
482
1001
|
* - 无归属 → uncovered;acceptance 文本含 文档/部署/doc/deploy/manual/config 类词且无归属
|
|
483
1002
|
* → non-testable
|
|
@@ -517,7 +1036,7 @@ function prefillMatrixCells(item, testFiles, hint) {
|
|
|
517
1036
|
/**
|
|
518
1037
|
* 渲染探针 7 段(骨架与幂等补段共用单一实现)。
|
|
519
1038
|
* 坑 probe7-prefill-evidence(ql-20260915-004):判定/证据两列由 prefillMatrixCells 机械预填
|
|
520
|
-
* (原 `<待填:四选一>` / `<TODO>` 占位淘汰)——幂等保障沿用补段口径:段已在场(agent 已填/
|
|
1039
|
+
* (原 `<待填:四选一>` / `<TODO>` 占位淘汰)——幂等保障沿用补段口径:段已在场(agent 已填/ probe1-noqa
|
|
521
1040
|
* 未填)一律不触碰(ensureAcceptanceMatrixSection 段在场即 no-op),预填只在骨架生成与缺段补齐
|
|
522
1041
|
* 两条新写路径生效,agent 已填内容永不被覆盖。
|
|
523
1042
|
* @param {{ applicable: boolean, tasks: Array<{task: string, acceptance: string[], testFiles: string[], hints: Record<number, {terms: string[], files: string[], anchors?: Array<{term: string, file: string, line: number}>}>}> }} p7
|
|
@@ -529,8 +1048,8 @@ function renderProbe7Lines(p7) {
|
|
|
529
1048
|
L.push('- 不适用(无 TaskCard)')
|
|
530
1049
|
return L
|
|
531
1050
|
}
|
|
532
|
-
L.push('<!-- 口径注记:探针 3 = 模块目录递归存在性面(allowed_paths 目录附近有没有测试);探针 7 = allowed_paths ∪ review changedFiles 结构归属承接面(每条 acceptance
|
|
533
|
-
L.push('<!-- 预填说明(ql-20260915-004):判定列为 CLI 机械预填,agent 逐格复核改写——规则:无归属→uncovered(文档/部署/doc/deploy/manual/config 类词→non-testable);有归属且命中≥1→covered;有归属零命中→partial
|
|
1051
|
+
L.push('<!-- 口径注记:探针 3 = 模块目录递归存在性面(allowed_paths 目录附近有没有测试);探针 7 = allowed_paths ∪ review changedFiles ∪ 直接下游卡测试 结构归属承接面(每条 acceptance 由哪些测试承接;下游消费卡的测试可承接上游 provider 的 acceptance——probe7-provider-tests-in-consumer-card);两者并排冲突以 7 为准。判定枚举(四选一):covered / partial / uncovered / non-testable(文档/部署类显式逃生门)。关键词命中只是提示,命中≠判定。 -->')
|
|
1052
|
+
L.push('<!-- 预填说明(ql-20260915-004):判定列为 CLI 机械预填,agent 逐格复核改写——规则:无归属→uncovered(文档/部署/doc/deploy/manual/config 类词→non-testable);有归属且命中≥1→covered;有归属零命中→partial。covered/partial 证据须含测试锚点三形态之一(file:line / `.test.` 测试文件名 / 反引号包裹的路径或测试名),行号可省;uncovered/non-testable 证据自由形态。预填≠结论:与事实不符的格子必须改写(枚举须保持 covered/partial/uncovered/non-testable 纯值,备注写在证据列)。 -->')
|
|
534
1053
|
// 零自动化承接计数(坑 review-zero-coverage-unwalked,2026-09-16 EHS 二次复核实证:execute
|
|
535
1054
|
// 期 review 走查面事实上跟着测试覆盖走——「新增-部门支线测试充分,编辑路径与相关方支线
|
|
536
1055
|
// 零覆盖」恰是 5 个 P1 的藏身处。矩阵渲染时顺带计数零承接条目,verify 复核强制显式走查
|
|
@@ -611,6 +1130,11 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
611
1130
|
const lines = readFileSync(abs, 'utf8').split('\n')
|
|
612
1131
|
lines.forEach((line, i) => {
|
|
613
1132
|
if (probe1.matches.length >= PROBE1_MAX_MATCHES) return
|
|
1133
|
+
// probe1-noqa 行级豁免(坑 probe1-self-referential-noise,2026-09-16 EHS 变更 verify 实证:
|
|
1134
|
+
// 探针基础设施自身源码被扫描时 26 处命中全是匹配器定义/骨架模板字符串/修复注释引文——
|
|
1135
|
+
// 逐条人工裁定纯耗损。行内含 `probe1-noqa` 即跳过(注释行加 `// probe1-noqa`、
|
|
1136
|
+
// 字符串模板行在语句闭合后加 `/* probe1-noqa */` 不污染输出内容))。
|
|
1137
|
+
if (line.includes('probe1-noqa')) return
|
|
614
1138
|
if (isUnimplementedMarkerLine(line)) probe1.matches.push({ file: probePath, line: i + 1, content: line.trim().slice(0, 160) })
|
|
615
1139
|
})
|
|
616
1140
|
} catch {
|
|
@@ -740,17 +1264,59 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
740
1264
|
}
|
|
741
1265
|
} catch { /* tasks 目录不可读 → applicable 维持 false */ }
|
|
742
1266
|
probe7.applicable = cards.length > 0
|
|
1267
|
+
// 跨仓仓根解析(task-05 / FR-09):测试文件内容读取双根扩多根——跨仓卡(repo: <key>)
|
|
1268
|
+
// 的 allowed_paths/review changedFiles 相对其仓根,cwd/wtRoot 双根读不到 → 关键词命中
|
|
1269
|
+
// 恒空、矩阵恒预填 partial。读法同探针 8 先例:join(specBase, 'local.yaml') +
|
|
1270
|
+
// parseRepoRegistry,相对路径按 cwd resolve;注册表不可读 fail-open 空表(单仓双根
|
|
1271
|
+
// 行为零变化)。
|
|
1272
|
+
const probe7CrossRoots = []
|
|
1273
|
+
try {
|
|
1274
|
+
const localYamlPath = join(specBase, 'local.yaml')
|
|
1275
|
+
if (existsSync(localYamlPath)) {
|
|
1276
|
+
const registry = parseRepoRegistry(readFileSync(localYamlPath, 'utf8'))
|
|
1277
|
+
if (registry) {
|
|
1278
|
+
for (const raw of registry.values()) {
|
|
1279
|
+
if (!raw) continue
|
|
1280
|
+
const root = isAbsolute(raw) ? raw : resolve(cwd, raw)
|
|
1281
|
+
if (root && existsSync(root) && !probe7CrossRoots.includes(root)) probe7CrossRoots.push(root)
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch { /* 注册表不可读 → 无跨仓根(fail-open) */ }
|
|
1286
|
+
// 跨卡归属(坑 probe7-provider-tests-in-consumer-card,2026-09-16 E 变更 verify 实证:8 格
|
|
1287
|
+
// 被机械预填 uncovered——task-03(测试卡)的用例测的是 task-01(provider)的导出函数,但归属
|
|
1288
|
+
// 只看本卡 allowed_paths ∪ review changedFiles,provider 卡的 acceptance 永远连不上消费卡的
|
|
1289
|
+
// 测试。修法:归属集扩为「本卡 ∪ 直接依赖本卡的卡」的测试文件——M depends_on N 则 M 的测试
|
|
1290
|
+
// 可承接 N 的 acceptance(下游消费卡天然测上游产物)。只扩直接依赖(v1,传递闭包易把无关
|
|
1291
|
+
// 测试卷进来放大 partial 噪音)。
|
|
1292
|
+
const dependsOnOf = new Map()
|
|
1293
|
+
for (const c of cards) {
|
|
1294
|
+
const dm = String(c.raw).match(/^depends_on:\s*\[?([^\]\n]*)\]?/m)
|
|
1295
|
+
const deps = (dm ? dm[1] : '').split(/[,\s]+/).map(s => s.trim()).filter(s => /^task-\d+$/.test(s))
|
|
1296
|
+
dependsOnOf.set(c.task, deps)
|
|
1297
|
+
}
|
|
743
1298
|
for (const card of cards) {
|
|
744
1299
|
const fromAllowed = parseAllowedPaths(card.raw)
|
|
745
1300
|
.map(p => String(p).replace(/^NEW:\s*/, '').trim())
|
|
746
1301
|
.filter(p => p && isProbe7TestPath(p))
|
|
747
1302
|
const fromReview = runId ? readReviewChangedTestFiles(runtimeRoot, runId, card.task) : []
|
|
748
|
-
|
|
1303
|
+
// 直接下游卡的归属测试并入(provider acceptance 由消费卡测试承接)
|
|
1304
|
+
const fromDependents = []
|
|
1305
|
+
for (const other of cards) {
|
|
1306
|
+
if (other.task === card.task) continue
|
|
1307
|
+
const deps = dependsOnOf.get(other.task) || []
|
|
1308
|
+
if (!deps.includes(card.task)) continue
|
|
1309
|
+
for (const p of parseAllowedPaths(other.raw)) {
|
|
1310
|
+
const t = String(p).replace(/^NEW:\s*/, '').trim()
|
|
1311
|
+
if (t && isProbe7TestPath(t) && !fromDependents.includes(t)) fromDependents.push(t)
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const testFiles = [...new Set([...fromAllowed, ...fromReview, ...fromDependents])]
|
|
749
1315
|
probe7.tasks.push({
|
|
750
1316
|
task: card.task,
|
|
751
1317
|
acceptance: card.acceptance,
|
|
752
1318
|
testFiles,
|
|
753
|
-
hints: buildAcceptanceHints(card.acceptance, testFiles, cwd, wtRoot),
|
|
1319
|
+
hints: buildAcceptanceHints(card.acceptance, testFiles, cwd, wtRoot, probe7CrossRoots),
|
|
754
1320
|
})
|
|
755
1321
|
}
|
|
756
1322
|
}
|
|
@@ -765,7 +1331,30 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
765
1331
|
probe8.notes = [`探针 8 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
|
|
766
1332
|
}
|
|
767
1333
|
|
|
768
|
-
|
|
1334
|
+
// ── 探针 9:守卫一致性(advisory;2026-09-15 EHS doSubmit 越权 P1 驱动——同实体 CRUD
|
|
1335
|
+
// 守卫不一致是编码式权限典型漏洞形态,探针 5/8 均零覆盖)。fail-soft 同探针 8:异常降级
|
|
1336
|
+
// not-applicable 注记不炸整体。──
|
|
1337
|
+
let probe9 = { applicable: false, javaFileCount: 0, groupCount: 0, inconsistentGroups: [], notes: [] }
|
|
1338
|
+
try {
|
|
1339
|
+
probe9 = runProbe9GuardConsistency({ specBase, cwd, wtRoot, changeName })
|
|
1340
|
+
} catch (e) {
|
|
1341
|
+
probe9.notes = [`探针 9 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// ── 接口面 + 消费端归类(task-04 / FR-05 / D-005~D-007):design.md 接口段 tolerant
|
|
1345
|
+
// 解析 + 清单消费端归类——骨架「## 接口验证覆盖矩阵」段预填与 facts 落盘
|
|
1346
|
+
// (backfillFactsFromMdAndTests)共用同一解析器(单一产物源,不各读各的)。fail-soft:
|
|
1347
|
+
// design 缺失/解析异常 → 空面(骨架渲染「无接口面」注记,validator 判 N=0 走零行为注记)。──
|
|
1348
|
+
let apiFace = { endpoints: [], declared: null, sectionHint: [] }
|
|
1349
|
+
let consumerHints = {}
|
|
1350
|
+
try {
|
|
1351
|
+
if (existsSync(designPath)) {
|
|
1352
|
+
apiFace = parseDesignApiTable(readFileSync(designPath, 'utf8'))
|
|
1353
|
+
consumerHints = classifyConsumerHints(detailed.map(e => String(e.path)))
|
|
1354
|
+
}
|
|
1355
|
+
} catch { /* design 读/解析异常 → 空面(fail-soft) */ }
|
|
1356
|
+
|
|
1357
|
+
return { probe1, probe3, probe5, probe6, probe7, probe8, probe9, apiFace, consumerHints }
|
|
769
1358
|
}
|
|
770
1359
|
|
|
771
1360
|
/**
|
|
@@ -777,7 +1366,7 @@ export function renderVerifyProbesReport(result) {
|
|
|
777
1366
|
|
|
778
1367
|
L.push('#### 探针 1:未实现标记扫描(design 清单文件)')
|
|
779
1368
|
if (probe1.matches.length === 0) {
|
|
780
|
-
L.push('- ✅ 无 TODO/FIXME/尚未实现 标记命中')
|
|
1369
|
+
L.push('- ✅ 无 TODO/FIXME/尚未实现 标记命中') /* probe1-noqa */
|
|
781
1370
|
} else {
|
|
782
1371
|
for (const m of probe1.matches) L.push(`- ⚠️ \`${m.file}:${m.line}\` ${m.content}`)
|
|
783
1372
|
}
|
|
@@ -787,7 +1376,7 @@ export function renderVerifyProbesReport(result) {
|
|
|
787
1376
|
L.push('')
|
|
788
1377
|
|
|
789
1378
|
L.push('#### 探针 2:设计关键词覆盖')
|
|
790
|
-
L.push('<!--TODO: 半语义探针——从 design 提取能力关键词逐个 grep 确认实现(agent 执行)-->')
|
|
1379
|
+
L.push('<!--TODO: 半语义探针——从 design 提取能力关键词逐个 grep 确认实现(agent 执行)-->') /* probe1-noqa */
|
|
791
1380
|
L.push('')
|
|
792
1381
|
|
|
793
1382
|
L.push('#### 探针 3:验收标准测试覆盖')
|
|
@@ -818,7 +1407,7 @@ export function renderVerifyProbesReport(result) {
|
|
|
818
1407
|
L.push('')
|
|
819
1408
|
|
|
820
1409
|
L.push('#### 探针 4:决策追踪覆盖')
|
|
821
|
-
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->')
|
|
1410
|
+
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->') /* probe1-noqa */
|
|
822
1411
|
L.push('')
|
|
823
1412
|
|
|
824
1413
|
L.push('#### 探针 5:API Contract Parity')
|
|
@@ -869,6 +1458,9 @@ export function renderVerifyProbesReport(result) {
|
|
|
869
1458
|
L.push(`- ℹ️ ${probe6.note}`)
|
|
870
1459
|
L.push('')
|
|
871
1460
|
L.push(...(renderProbe8Lines(result.probe8 || { applicable: false, mispairs: [], feOnly: [], missingNotNull: [], notes: [] })))
|
|
1461
|
+
// 探针 9 紧随探针 8;旧 result 无 probe9 键(存量调用方/合成 result)→ applicable=false
|
|
1462
|
+
// 渲染「不适用」行 + notes,零回归(探针 7 兜底口径同款)。
|
|
1463
|
+
L.push(...(renderProbe9Lines(result.probe9 || { applicable: false, javaFileCount: 0, groupCount: 0, inconsistentGroups: [], notes: [] })))
|
|
872
1464
|
return L.join('\n')
|
|
873
1465
|
}
|
|
874
1466
|
|
|
@@ -876,7 +1468,8 @@ export function renderVerifyProbesReport(result) {
|
|
|
876
1468
|
* 渲染探针 8 段(advisory 口径注记随段输出——命中≠结论,agent 逐条复核)。
|
|
877
1469
|
* @param {{applicable: boolean, backendFieldCount?: number, feKeyCount?: number, notNullCount?: number,
|
|
878
1470
|
* javaFileCount?: number, sqlFileCount?: number, feFileCount?: number,
|
|
879
|
-
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[]
|
|
1471
|
+
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[],
|
|
1472
|
+
* contractCount?: number, contractOrphans?: Array<{fe, hint?}>, missingRequired?: Array<{field, contract}>}} p8
|
|
880
1473
|
* @returns {string[]} 行数组(含段标题)
|
|
881
1474
|
*/
|
|
882
1475
|
function renderProbe8Lines(p8) {
|
|
@@ -897,13 +1490,102 @@ function renderProbe8Lines(p8) {
|
|
|
897
1490
|
if ((p8.missingNotNull || []).length > 0) {
|
|
898
1491
|
L.push(`- ⚠️ NOT NULL 列前端未见(候选必填缺送/服务端填充):${p8.missingNotNull.slice(0, 8).map(c => c.col).join('、')}${p8.missingNotNull.length > 8 ? ` …共 ${p8.missingNotNull.length} 个` : ''}`)
|
|
899
1492
|
}
|
|
900
|
-
|
|
1493
|
+
// 契约维度两行(task-01,advisory):契约外载荷键(design 契约面未见)/ 必填漏发(契约
|
|
1494
|
+
// required 前端载荷未见)。无契约面时 notes 已有 skipped 注记(下方循环渲染),不空段。
|
|
1495
|
+
if ((p8.contractOrphans || []).length > 0) {
|
|
1496
|
+
L.push(`- ⚠️ 契约外载荷键 ${p8.contractOrphans.length} 条(design 契约面未见,候选契约滞后/跨层私加):${p8.contractOrphans.slice(0, 8).map(o => o.hint ? `${o.fe}(疑似对应 ${o.hint})` : o.fe).join('、')}${p8.contractOrphans.length > 8 ? ` …共 ${p8.contractOrphans.length} 条` : ''}`)
|
|
1497
|
+
}
|
|
1498
|
+
if ((p8.missingRequired || []).length > 0) {
|
|
1499
|
+
L.push(`- ⚠️ 契约必填漏发 ${p8.missingRequired.length} 条(契约 required 前端载荷未见,人工核实提交链路):${p8.missingRequired.slice(0, 8).map(r => `${r.field}←${r.contract}`).join('、')}${p8.missingRequired.length > 8 ? ` …共 ${p8.missingRequired.length} 条` : ''}`)
|
|
1500
|
+
}
|
|
1501
|
+
if ((p8.mispairs || []).length === 0 && (p8.missingNotNull || []).length === 0
|
|
1502
|
+
&& (p8.contractOrphans || []).length === 0 && (p8.missingRequired || []).length === 0) {
|
|
901
1503
|
L.push('- ✅ 载荷字段面零疑似差异(归一化覆盖 + NOT NULL 全见)')
|
|
902
1504
|
}
|
|
903
1505
|
for (const n of p8.notes || []) L.push(`- ℹ️ ${n}`)
|
|
904
1506
|
return L
|
|
905
1507
|
}
|
|
906
1508
|
|
|
1509
|
+
/**
|
|
1510
|
+
* 渲染探针 9 段(renderProbe8Lines 同构;advisory 口径注记随段输出——存在性检查非语义审计,
|
|
1511
|
+
* 聚类启发式由 agent 裁定)。汇总行「守卫不一致实体组 N 个」的 N 供 verify-postcheck 一致性
|
|
1512
|
+
* 抽查锚点(对齐探针 8 先例,本卡只出 producer 侧)。
|
|
1513
|
+
* @param {{applicable: boolean, javaFileCount?: number, groupCount?: number,
|
|
1514
|
+
* inconsistentGroups: Array<{entity: string, guarded: string[], unguarded: string[], signals: Record<string, string[]>}>,
|
|
1515
|
+
* notes: string[]}} p9
|
|
1516
|
+
* @returns {string[]} 行数组(含段标题)
|
|
1517
|
+
*/
|
|
1518
|
+
function renderProbe9Lines(p9) {
|
|
1519
|
+
const L = [PROBE9_HEADING]
|
|
1520
|
+
if (!p9 || !p9.applicable) {
|
|
1521
|
+
L.push('- 不适用(清单无 .java 改动文件,或 design.md 缺失)')
|
|
1522
|
+
for (const n of (p9 && p9.notes) || []) L.push(`- ℹ️ ${n}`)
|
|
1523
|
+
return L
|
|
1524
|
+
}
|
|
1525
|
+
L.push('<!-- 口径注记:存在性检查非语义审计——只查「守卫调用/注解模式存在」不查校验逻辑对错;聚类启发式由 agent 裁定(方法名前缀动词+尾段实体名词聚类,同文件同实体 ≥2 变更方法才比对,误组/漏组都可能——⚠️ 是定向复核提示不是结论)。 -->')
|
|
1526
|
+
L.push(`- ℹ️ 比对面:${p9.javaFileCount ?? 0} Java 文件 × 同实体变更方法组 ${p9.groupCount ?? 0}(组内 ≥2 方法才比对)`)
|
|
1527
|
+
if ((p9.inconsistentGroups || []).length > 0) {
|
|
1528
|
+
L.push(`- ⚠️ 守卫不一致实体组 ${p9.inconsistentGroups.length} 个(同实体有守卫/无守卫并存——越权风险面,2026-09-15 EHS doSubmit 越权同族,agent 逐组裁定)`)
|
|
1529
|
+
for (const g of p9.inconsistentGroups) {
|
|
1530
|
+
const guardedPart = (g.guarded || []).map(m => `${m}(${((g.signals && g.signals[m]) || []).join('、') || '?'})`).join('、')
|
|
1531
|
+
const unguardedPart = (g.unguarded || []).join('、')
|
|
1532
|
+
L.push(`- ⚠️ 实体 ${g.entity}:有守卫 [${guardedPart}] / 无守卫 [${unguardedPart}]`)
|
|
1533
|
+
}
|
|
1534
|
+
} else {
|
|
1535
|
+
L.push('- ✅ 同实体守卫信号一致(无有守卫/无守卫并存的实体组)')
|
|
1536
|
+
}
|
|
1537
|
+
for (const n of p9.notes || []) L.push(`- ℹ️ ${n}`)
|
|
1538
|
+
return L
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// ── 接口验证覆盖矩阵段渲染(task-04 / FR-05 / D-005~D-007)──
|
|
1542
|
+
// 骨架新写路径(generateVerifyResultSkeleton)与缺段补齐(ensureApiCoverageMatrixSection)
|
|
1543
|
+
// 共用单一实现——renderProbe7Lines 先例。不动 probe7 既有矩阵(独立 ## 章节并行存在,口径
|
|
1544
|
+
// 注记互指区分,D-009 非目标 / R-07)。预填口径同 probe7:CLI 机械预填、agent 逐格复核改写。
|
|
1545
|
+
const API_MATRIX_HEADING = '## 接口验证覆盖矩阵'
|
|
1546
|
+
// 写端点集合(D-007/FR-06 表间完备性 advisory 数据面):method ∈ 此集的端点须在 design
|
|
1547
|
+
// 权限矩阵段有对应行或显式豁免(「无权限约束」)——warning 计算归 task-05,此处只定集合。
|
|
1548
|
+
const API_MATRIX_WRITE_METHODS = new Set(['POST', 'PUT', 'DELETE', 'PATCH'])
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* 渲染「## 接口验证覆盖矩阵」段(task-04)。
|
|
1552
|
+
* 三态:解析有端点 → 逐端点预填行(判定列 `<待填:四选一>` 占位,同 probe7 预填口径供 agent
|
|
1553
|
+
* 复核);解析零行有声明 → 声明占位行「本变更接口面:<N> 端点(agent 声明)」(D-005 零解析
|
|
1554
|
+
* 降级,对账分母=声明数);零解析零声明 → 「无接口面」注记行(非判级 critical 变更零行为
|
|
1555
|
+
* 注记即可——判级 critical 的 error 拦截归 task-05 validator)。段尾 advisory 占位注记
|
|
1556
|
+
* (消费面/表间完备性,warning 计算归 task-05,本段只留位)。
|
|
1557
|
+
* @param {{endpoints?: Array<{method: string, path: string, rowIdx: number}>, declared?: number|null}|null} apiFace
|
|
1558
|
+
* @returns {string[]} 行数组(含段标题;调用方自理前后空行)
|
|
1559
|
+
*/
|
|
1560
|
+
function renderApiCoverageMatrixLines(apiFace) {
|
|
1561
|
+
const face = apiFace && typeof apiFace === 'object' ? apiFace : null
|
|
1562
|
+
const endpoints = face && Array.isArray(face.endpoints) ? face.endpoints : []
|
|
1563
|
+
const declared = face && typeof face.declared === 'number' ? face.declared : null
|
|
1564
|
+
const L = [`${API_MATRIX_HEADING} [层:人工判断——CLI 预填复核]`]
|
|
1565
|
+
L.push('<!-- 口径注记(与探针 7 互指,R-07):探针 7 = 验收项 × 测试承接面(每条 acceptance 由哪些测试承接);本矩阵 = 接口端点 × 验证用例面(design 接口段每个端点由哪些验证用例/冒烟步骤覆盖)——两者并排互补,双矩阵并行存在。端点集来自 design.md 接口段 tolerant 解析(parseDesignApiTable:段头宽收 + 方法/路径双条件),预填≠结论,agent 逐行复核。判定枚举(四选一):covered / partial / uncovered / non-testable。 -->')
|
|
1566
|
+
L.push('<!-- 预填说明:端点行由 CLI 机械预填,判定/用例依据 ID/结果/证据由 agent 逐格填写——用例依据 ID 锚点五形态:design接口表#METHOD /path、权限矩阵[角色×动作]、契约表@行标识、DDL@列名、载荷@构造点路径(须真实命中对应表/段,防空指)。 -->')
|
|
1567
|
+
L.push('<!-- 文法注释:子行 = 端点行下一行、两空格缩进、以「↳ <消费端>:」前缀书写(消费端细分承接面,不计矩阵行账);探索行 = 判定 uncovered 且证据列含 [探索] 标记(探索性验证不算覆盖)。 -->')
|
|
1568
|
+
if (endpoints.length === 0 && declared === null) {
|
|
1569
|
+
L.push('- 无接口面(design 接口段解析零端点且无「本变更接口面:N 端点」声明行)——本变更若实际触碰接口,先补 design 接口段表格或声明行,再重跑 `verify-probes --init` 刷新本段;判级 critical 的零面拦截归 validator')
|
|
1570
|
+
return L
|
|
1571
|
+
}
|
|
1572
|
+
if (endpoints.length > 0 && declared !== null && declared !== endpoints.length) {
|
|
1573
|
+
L.push(`<!-- 声明与解析并存(声明 ${declared} 端点 / 解析 ${endpoints.length} 端点)——以解析为准,差异需复核(design 接口段与声明行不同步的漂移信号) -->`)
|
|
1574
|
+
}
|
|
1575
|
+
L.push('| 端点 | 判定 | 用例依据 ID | 结果 | 证据 |')
|
|
1576
|
+
L.push('|---|---|---|---|---|')
|
|
1577
|
+
if (endpoints.length > 0) {
|
|
1578
|
+
for (const ep of endpoints) {
|
|
1579
|
+
L.push(`| ${mdEscapeCell(`${ep.method} ${ep.path}`, 160)} | <待填:四选一> | <待填:用例 ID> | <待填> | <待填:锚点> |`)
|
|
1580
|
+
}
|
|
1581
|
+
} else {
|
|
1582
|
+
L.push(`| 本变更接口面:${declared} 端点(agent 声明) | <待填:四选一> | <待填:用例 ID> | <待填> | <待填:锚点> |`)
|
|
1583
|
+
L.push('<!-- 解析零行降级(D-005):接口面以 agent 声明为准(对账分母=声明数);声明与实际不符时补 design 接口段表格后重跑 --init 刷新本段 -->')
|
|
1584
|
+
}
|
|
1585
|
+
L.push('<!-- advisory 尾注(warning 计算归 validator,本段只留位):有消费端未填子行的端点将列于此(advisory——消费端归类=design 清单启发式,数据面 facts.consumerHints);写端点(POST/PUT/DELETE/PATCH)未在权限矩阵段声明的将列于此(advisory——补行或显式豁免「无权限约束」,数据面 facts.apiFace.writeEndpoints;表缺行会让派生框架继承你的洞) -->')
|
|
1586
|
+
return L
|
|
1587
|
+
}
|
|
1588
|
+
|
|
907
1589
|
/**
|
|
908
1590
|
* 从 runVerifyProbes 结果构造 verify-facts.json 机器底稿对象(P3b 可复跑审计底稿)。
|
|
909
1591
|
* 命令行统一 `sillyspec verify-probes --change <name>`;指标 fail-soft——字段不可得时少列
|
|
@@ -923,6 +1605,7 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
923
1605
|
const p5 = (result && result.probe5) || {}
|
|
924
1606
|
const p6 = (result && result.probe6) || {}
|
|
925
1607
|
const p8 = (result && result.probe8) || {}
|
|
1608
|
+
const p9 = (result && result.probe9) || {}
|
|
926
1609
|
// v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
|
|
927
1610
|
// runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
|
|
928
1611
|
// 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
|
|
@@ -967,10 +1650,21 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
967
1650
|
mispairs: len(p8.mispairs),
|
|
968
1651
|
feOnly: len(p8.feOnly),
|
|
969
1652
|
missingNotNull: len(p8.missingNotNull),
|
|
1653
|
+
contractCount: num(p8.contractCount),
|
|
1654
|
+
contractOrphans: len(p8.contractOrphans),
|
|
1655
|
+
missingRequired: len(p8.missingRequired),
|
|
970
1656
|
feKeys: num(p8.feKeyCount),
|
|
971
1657
|
backendFields: num(p8.backendFieldCount),
|
|
972
1658
|
}),
|
|
973
1659
|
},
|
|
1660
|
+
probe9: {
|
|
1661
|
+
command,
|
|
1662
|
+
metrics: defined({
|
|
1663
|
+
javaFileCount: num(p9.javaFileCount),
|
|
1664
|
+
groupCount: num(p9.groupCount),
|
|
1665
|
+
inconsistentGroups: len(p9.inconsistentGroups),
|
|
1666
|
+
}),
|
|
1667
|
+
},
|
|
974
1668
|
},
|
|
975
1669
|
}
|
|
976
1670
|
}
|
|
@@ -1041,6 +1735,33 @@ export function writeVerifyFacts(changeDir, result, changeName, opts = {}) {
|
|
|
1041
1735
|
// 容错:占位行(<待填…)跳过;类型归一小写连字符(ENV-BLOCKED→env-blocked);未知类型
|
|
1042
1736
|
// 保留原值(advisory 面向 agent 复核,不静默丢弃);非表格行(prose/注释)忽略。
|
|
1043
1737
|
const HANDOVER_HEADING_RE = /^## 移交项(结构化)[^\n]*$/m
|
|
1738
|
+
// severity 第 4 列(2026-09-17-pass-cap-semantics task-01 / D-005@v2 / FR-06):三列正则扩
|
|
1739
|
+
// 四列,存量三列表格行零迁移兼容(四列/三列正则互斥命中——四列行 5 管道段、三列行 4 管道段,
|
|
1740
|
+
// [^|] 组不含管道无回溯交叠)。缺省按类型映射:db-script/env-blocked→blocking(X-07:集成
|
|
1741
|
+
// 复跑与 fix.sql 恒 blocking,分层不削弱封顶);manual-acceptance/other→advisory;未知类型
|
|
1742
|
+
// 保守取 blocking(fail-closed 侧——severity 低标会漏封顶,宁可多拦;未知类型本就该 agent
|
|
1743
|
+
// 复核,advisory 面向复核但封顶语义优先)。
|
|
1744
|
+
const HANDOVER_ROW4_RE = /^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*([^|]*?)\s*\|\s*$/
|
|
1745
|
+
const HANDOVER_ROW3_RE = /^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*$/
|
|
1746
|
+
const HANDOVER_DEFAULT_SEVERITY = (type) =>
|
|
1747
|
+
(type === 'manual-acceptance' || type === 'other') ? 'advisory' : 'blocking'
|
|
1748
|
+
// 降级理由文法(X-02):(降级:<理由>,依据 <file:line 或 D-xxx>)——全角括号/冒号/逗号,
|
|
1749
|
+
// 依据锚点 D-xxx 或 file:line。JS 正则不转义全角括号(知识库坑)。blocking→advisory 降级
|
|
1750
|
+
// 未命中本文法 → 仍按 blocking(fail-closed,防「全标 advisory」钻空——D-005@v2)。
|
|
1751
|
+
const HANDOVER_DOWNGRADE_REASON_RE = /(降级:.+?,依据\s*(?:D-\d+|[^\s,):]+:\d+))/
|
|
1752
|
+
// severity 解析:显式 blocking 优先(混写保守);显式 advisory 对 blocking 缺省类型构成降级,
|
|
1753
|
+
// 理由文法在第 4 列或条件列任一命中即认(骨架第 4 列定型前 producer 宽收两侧;抽查面归
|
|
1754
|
+
// checkProbeConsistency 消费侧后续任务);两枚举都不含 → 缺省映射。
|
|
1755
|
+
function resolveHandoverSeverity(type, severityCol, condition) {
|
|
1756
|
+
const col = String(severityCol || '')
|
|
1757
|
+
if (/blocking/i.test(col)) return 'blocking'
|
|
1758
|
+
if (/advisory/i.test(col)) {
|
|
1759
|
+
if (HANDOVER_DEFAULT_SEVERITY(type) !== 'blocking') return 'advisory'
|
|
1760
|
+
return (HANDOVER_DOWNGRADE_REASON_RE.test(col) || HANDOVER_DOWNGRADE_REASON_RE.test(String(condition || '')))
|
|
1761
|
+
? 'advisory' : 'blocking'
|
|
1762
|
+
}
|
|
1763
|
+
return HANDOVER_DEFAULT_SEVERITY(type)
|
|
1764
|
+
}
|
|
1044
1765
|
export function parseHandoverRows(md) {
|
|
1045
1766
|
const text = String(md || '').replace(/\r\n/g, '\n')
|
|
1046
1767
|
const m = text.match(HANDOVER_HEADING_RE)
|
|
@@ -1050,7 +1771,7 @@ export function parseHandoverRows(md) {
|
|
|
1050
1771
|
const section = nextSection ? after.slice(0, nextSection.index) : after
|
|
1051
1772
|
const items = []
|
|
1052
1773
|
for (const line of section.split('\n')) {
|
|
1053
|
-
const row = line.match(
|
|
1774
|
+
const row = line.match(HANDOVER_ROW4_RE) || line.match(HANDOVER_ROW3_RE)
|
|
1054
1775
|
if (!row) continue
|
|
1055
1776
|
const type = row[1].trim()
|
|
1056
1777
|
const item = row[2].trim()
|
|
@@ -1058,12 +1779,178 @@ export function parseHandoverRows(md) {
|
|
|
1058
1779
|
if (type === '类型' || /^-{2,}$/.test(type.replace(/\|/g, ''))) continue // 表头/分隔行
|
|
1059
1780
|
if (type.startsWith('<') || item.startsWith('<')) continue // 骨架占位行
|
|
1060
1781
|
if (!type || !item) continue
|
|
1061
|
-
|
|
1782
|
+
const normalizedType = type.toLowerCase().replace(/[\s_]+/g, '-')
|
|
1783
|
+
items.push({
|
|
1784
|
+
type: normalizedType,
|
|
1785
|
+
item,
|
|
1786
|
+
condition,
|
|
1787
|
+
severity: resolveHandoverSeverity(normalizedType, row[4], condition),
|
|
1788
|
+
})
|
|
1062
1789
|
}
|
|
1063
1790
|
return items
|
|
1064
1791
|
}
|
|
1065
1792
|
|
|
1793
|
+
// ── stage-contract 动态绑定(D-011 producer 侧取数;分层单向——全局硬约束 3)──
|
|
1794
|
+
// extractAcceptanceMatrixSlots 经顶层 await 动态 import 绑定(非静态 import-from 语句,
|
|
1795
|
+
// 不造 import 环:stage-contract 静态闭包不含 verify-probes,加载序任一方向均无环)。
|
|
1796
|
+
// conclusion 先例是调用侧传参形态;此处选零 gates 改动形态——首次(gates.js 收尾前置)与
|
|
1797
|
+
// 二次(testCheckResult 后)两调用点自动覆盖,主路径无条件产出(X-08)不依赖调用方传参。
|
|
1798
|
+
// 加载失败降级 null:matrixPartialRows 计 0 + fail-soft 注记(additive 字段缺省不炸)。
|
|
1799
|
+
let extractAcceptanceMatrixSlotsFn = null
|
|
1800
|
+
try {
|
|
1801
|
+
;({ extractAcceptanceMatrixSlots: extractAcceptanceMatrixSlotsFn } = await import('./stage-contract.js'))
|
|
1802
|
+
} catch { extractAcceptanceMatrixSlotsFn = null }
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* db/<file>.sql 执行声明解析(X-03 文法,2026-09-17-pass-cap-semantics task-01 / D-007 声明面):
|
|
1806
|
+
* 两类来源——①回执槽(## 集成验证回执)条目 command 含 db/<file>.sql(复用 parseEvidenceSlots
|
|
1807
|
+
* 现成解析,不自造第二套);②声明行「已对目标库执行:db/<file>.sql」(全/半角冒号均认)。
|
|
1808
|
+
* @param {string} md verify-result.md 全文
|
|
1809
|
+
* @returns {string[]} 去重排序后的 db/<file>.sql 路径(posix 形态,与 apply 文件集直接可比)
|
|
1810
|
+
*/
|
|
1811
|
+
const DB_SCRIPT_REF_RE = /(?:^|[^\w/])(db\/[\w./-]+\.sql)/g
|
|
1812
|
+
const DB_SCRIPT_DECLARE_RE = /已对目标库执行[::]\s*`?(db\/[\w./-]+\.sql)/g
|
|
1813
|
+
export function parseDbScriptDeclarations(md) {
|
|
1814
|
+
const text = String(md || '').replace(/\r\n/g, '\n')
|
|
1815
|
+
const found = new Set()
|
|
1816
|
+
const slots = parseEvidenceSlots(text)
|
|
1817
|
+
for (const receipt of slots.runtimeEvidence || []) {
|
|
1818
|
+
for (const m of String((receipt && receipt.command) || '').matchAll(DB_SCRIPT_REF_RE)) found.add(m[1])
|
|
1819
|
+
}
|
|
1820
|
+
for (const m of text.matchAll(DB_SCRIPT_DECLARE_RE)) found.add(m[1])
|
|
1821
|
+
return [...found].sort()
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// 回执 command 来源分类(X-10 / D-006:只认命令来源声明、不解析日志内容;枚举
|
|
1825
|
+
// cross-layer|build|unit,未定类默认 build——fail-closed 侧:build 不构成「集成实测已跑」,
|
|
1826
|
+
// 宁可触发封顶要求 handover)。对齐目标:change-risk-profile.js classifyReceiptSourceTag
|
|
1827
|
+
// (RECEIPT_SOURCE_CROSS_LAYER_RE/RECEIPT_SOURCE_UNIT_RE)——两侧正则族逐词保持同步演进
|
|
1828
|
+
// (打标单点归 change-risk-profile 侧,本函数是 producer 判定侧;G-3 收口:producer 侧
|
|
1829
|
+
// 曾漏 smoke 一词致 facts.integrationRan 偏 not-ran 误触封顶,已对齐,后续任一侧增词必须
|
|
1830
|
+
// 双侧同步)。cross-layer=起服务/HTTP/进程对进程;smoke 冒烟命令族计 cross-layer(D-006
|
|
1831
|
+
// 退役判据:批次 C commands.smoke 落地后由 smoke 回执一票判定「集成实测已跑」——起服务冒烟
|
|
1832
|
+
// 即跨层实测,不得默认 build 误拦金路径);unit=单测 runner 直跑(JUnitCore/node --test/
|
|
1833
|
+
// mocha/jest/vitest/pytest 等);其余(compile/lint/构建/mvn test 等混合形态)一律 build。
|
|
1834
|
+
//
|
|
1835
|
+
// 来源标记直判(2026-09-17-api-coverage-smoke task-02 / Grill B-1 修正,FR-03):条目 source
|
|
1836
|
+
// 标记 'cli-noai-smoke'(parseEvidenceSlots additive 第五字段——本文件 ensureSmokeReceiptSection
|
|
1837
|
+
// 注入的机器段行尾注回填)→ 直判 cross-layer,**优先于命令词正则分类**——修正点在标记识别
|
|
1838
|
+
// 而非命令词增补:node scripts/smoke.mjs / bash smoke.sh / python smoke.py 等脚本形态在既有
|
|
1839
|
+
// 正则下全判 build 会误拦金路径,且脚本形态词表不可枚举(RECEIPT_CROSS_LAYER_RE 正则族
|
|
1840
|
+
// 零词变动)。对齐目标:change-risk-profile.js classifyReceiptSourceTag(sourceMark 参数与
|
|
1841
|
+
// CLI_SMOKE_SOURCE_MARK 常量)——**任一侧增改标记族必须双侧同步**(G-3 铁律)。
|
|
1842
|
+
const RECEIPT_CROSS_LAYER_RE = new RegExp([
|
|
1843
|
+
'\\bcurl\\b', '\\bwget\\b', '\\bhttpie\\b', '\\bInvoke-WebRequest\\b', '\\bInvoke-RestMethod\\b',
|
|
1844
|
+
'\\biwr\\b', '\\birm\\b', 'https?://',
|
|
1845
|
+
'spring-boot:run', '\\bjava\\b[^|\\n]*\\s-jar\\b',
|
|
1846
|
+
'\\b(?:npm|pnpm|yarn|bun)\\s+(?:run\\s+)?(?:dev|start|serve|smoke)\\b',
|
|
1847
|
+
'\\bdotnet\\s+run\\b', '\\bflask\\s+run\\b', '\\buvicorn\\b', '\\bgunicorn\\b',
|
|
1848
|
+
'\\b(?:nc|netcat|telnet|socat)\\b',
|
|
1849
|
+
].join('|'), 'i')
|
|
1850
|
+
const RECEIPT_UNIT_RE = /\bJUnitCore\b|\bnode\s+--test\b|\bmocha\b|\bjest\b|\bvitest\b|\bpytest\b|\bphpunit\b|\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?test\b/i
|
|
1851
|
+
/** CLI 机器段来源标记(与 change-risk-profile.js CLI_SMOKE_SOURCE_MARK / task-01 记录 source 同值,双侧同步) */
|
|
1852
|
+
const SMOKE_RECEIPT_SOURCE_MARK = 'cli-noai-smoke'
|
|
1853
|
+
function classifyReceiptCommandSource(command, sourceMark) {
|
|
1854
|
+
if (sourceMark === SMOKE_RECEIPT_SOURCE_MARK) return 'cross-layer'
|
|
1855
|
+
const cmd = String(command || '')
|
|
1856
|
+
if (RECEIPT_CROSS_LAYER_RE.test(cmd)) return 'cross-layer'
|
|
1857
|
+
if (RECEIPT_UNIT_RE.test(cmd)) return 'unit'
|
|
1858
|
+
return 'build'
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
/**
|
|
1862
|
+
* facts.integrationRan 判定(D-006 判定表,2026-09-17-pass-cap-semantics task-01):
|
|
1863
|
+
* 已跑 = ①quality-scan 实测记录在场(路径规则同 run/verify-quality-scan.js:36
|
|
1864
|
+
* qualityScanRecordPath:specBase/.runtime/verify-quality-scan-<changeName>.json,specBase/
|
|
1865
|
+
* changeName 自 factsPath 目录上推,不依赖调用方传参)且 commands.test 实跑(status 非
|
|
1866
|
+
* skipped)且 test_strategy ∈ {full, module, evidence-auto}(evidence-auto 降级 module 已跑
|
|
1867
|
+
* 子集按 module 档算已跑;local.yaml 未配置默认 full);或 ②回执槽存在命令来源含跨层调用的
|
|
1868
|
+
* 条目。未跑 = test_strategy=skip / 无记录 / 回执仅 compile·lint·纯单测来源。记录缺失
|
|
1869
|
+
* (X-01:--done 亲测替代扫描场景时序不可得)→ not-ran + fail-open 注记(不阻断)。
|
|
1870
|
+
* @param {string} factsPath verify-facts.json 路径(<specBase>/changes/<name>/verify-facts.json)
|
|
1871
|
+
* @param {Array<{command: string}>} runtimeEvidence 回执槽解析结果(parseEvidenceSlots 产出)
|
|
1872
|
+
* @returns {{ ran: 'ran'|'not-ran', notes: string[] }}
|
|
1873
|
+
*/
|
|
1874
|
+
function judgeIntegrationRan(factsPath, runtimeEvidence) {
|
|
1875
|
+
const changeDir = dirname(factsPath)
|
|
1876
|
+
const changeName = basename(changeDir)
|
|
1877
|
+
const specBase = dirname(dirname(changeDir))
|
|
1878
|
+
let record = null
|
|
1879
|
+
try {
|
|
1880
|
+
const rec = JSON.parse(readFileSync(join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`), 'utf8'))
|
|
1881
|
+
if (rec && rec.schemaVersion === 1 && rec.source === 'cli-noai') record = rec
|
|
1882
|
+
} catch { record = null }
|
|
1883
|
+
let strategy = 'full' // 未配置 test_strategy 默认全量(verify-postcheck resolveTestStrategy 同口径)
|
|
1884
|
+
try {
|
|
1885
|
+
const yamlText = readFileSync(join(specBase, 'local.yaml'), 'utf8').replace(/\r\n?/g, '\n')
|
|
1886
|
+
const sm = yamlText.match(/^\s*test_strategy:\s*([A-Za-z_-]+)\s*(?:#.*)?$/m)
|
|
1887
|
+
if (sm) strategy = sm[1]
|
|
1888
|
+
} catch { /* local.yaml 不可读 → 按 full 缺省 */ }
|
|
1889
|
+
const scanRan = Boolean(record && record.testResult && record.testResult.status && record.testResult.status !== 'skipped')
|
|
1890
|
+
if (scanRan && ['full', 'module', 'evidence-auto'].includes(strategy)) return { ran: 'ran', notes: [] }
|
|
1891
|
+
const receipts = Array.isArray(runtimeEvidence) ? runtimeEvidence : []
|
|
1892
|
+
// 条目 source 标记透传(task-02 Grill B-1):source='cli-noai-smoke' 直判 cross-layer——
|
|
1893
|
+
// 机器段命令(node scripts/smoke.mjs 等脚本形态)不再落 build 误拦金路径。
|
|
1894
|
+
if (receipts.some(r => classifyReceiptCommandSource(r && r.command, r && r.source) === 'cross-layer')) {
|
|
1895
|
+
return { ran: 'ran', notes: [] }
|
|
1896
|
+
}
|
|
1897
|
+
const notes = []
|
|
1898
|
+
if (!record) {
|
|
1899
|
+
notes.push(`ℹ️ quality-scan 实测记录缺失(${join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`)})——facts.integrationRan 按 not-ran 判定(X-01 fail-open 注记,不阻断;--done 亲测替代扫描的场景记录时序不可得,出路=重跑质量扫描步或降级 NOTES)`)
|
|
1900
|
+
}
|
|
1901
|
+
return { ran: 'not-ran', notes }
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
/**
|
|
1905
|
+
* Runtime Evidence「不涉及」行识别(X-18 文法,2026-09-17-pass-cap-semantics task-01 / D-004):
|
|
1906
|
+
* 「## Runtime Evidence」节内匹配「行含 端点|请求-响应|服务端点 关键词且以 不涉及 收尾」的
|
|
1907
|
+
* 表格行——收尾判定剥行尾管道(含全角|)后 endswith(表格行末格「不涉及」);至少一行命中
|
|
1908
|
+
* → true;无节/无命中 → false。骨架注释渲染归 task-03,本函数只做解析。
|
|
1909
|
+
* @param {string} md verify-result.md 全文
|
|
1910
|
+
* @returns {boolean}
|
|
1911
|
+
*/
|
|
1912
|
+
function parseRuntimeEndpointExcluded(md) {
|
|
1913
|
+
const text = String(md || '').replace(/\r\n/g, '\n')
|
|
1914
|
+
const lines = text.split('\n')
|
|
1915
|
+
const start = lines.findIndex(l => l.startsWith('## Runtime Evidence'))
|
|
1916
|
+
if (start === -1) return false
|
|
1917
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1918
|
+
if (/^#{1,2}\s/.test(lines[i])) break // 同 parseEvidenceSlots sectionOf 口径:到下一 #/## 级标题止
|
|
1919
|
+
const t = lines[i].trim()
|
|
1920
|
+
if (!t.startsWith('|')) continue // 只认表格行(X-18 文法)
|
|
1921
|
+
const stripped = t.replace(/[||]\s*$/, '').trim()
|
|
1922
|
+
if (/端点|请求-响应|服务端点/.test(t) && stripped.endsWith('不涉及')) return true
|
|
1923
|
+
}
|
|
1924
|
+
return false
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/** facts.matrixPartialRows:探针 7 矩阵 verdict∈{partial, uncovered} 行数(无段 → 0) */
|
|
1928
|
+
function countMatrixPartialRows(md) {
|
|
1929
|
+
if (!extractAcceptanceMatrixSlotsFn) {
|
|
1930
|
+
console.warn('⚠️ extractAcceptanceMatrixSlots 动态绑定不可用(stage-contract 加载失败,fail-soft)——facts.matrixPartialRows 本次计 0')
|
|
1931
|
+
return 0
|
|
1932
|
+
}
|
|
1933
|
+
const matrix = extractAcceptanceMatrixSlotsFn(md)
|
|
1934
|
+
return matrix.rows.filter(r => r && (r.verdict === 'partial' || r.verdict === 'uncovered')).length
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1066
1937
|
export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
|
|
1938
|
+
// smoke 机器段 ensure(2026-09-17-api-coverage-smoke task-02,挂 backfill 合并时点——
|
|
1939
|
+
// quality-scan step 6 noAI 亲跑落记录先于本步,读得到实录):read-modify-write 盘上 md
|
|
1940
|
+
// 最新态(不基于 verifyMd 快照写回,防覆盖多 agent 并发改动);注入/态更新成功后解析源
|
|
1941
|
+
// 切盘上最新内容——机器段行立即参与 slots 解析,judgeIntegrationRan 同轮即吃到
|
|
1942
|
+
// source 标记直判 cross-layer。ensure 落盘失败/无记录 fail-soft,不阻断 facts 回填。
|
|
1943
|
+
let verifyMdText = verifyMd
|
|
1944
|
+
try {
|
|
1945
|
+
const ensureDir = dirname(factsPath)
|
|
1946
|
+
const ensured = ensureSmokeReceiptSection(join(ensureDir, 'verify-result.md'), {
|
|
1947
|
+
specBase: dirname(dirname(ensureDir)),
|
|
1948
|
+
changeName: basename(ensureDir),
|
|
1949
|
+
})
|
|
1950
|
+
if (ensured.changed) {
|
|
1951
|
+
verifyMdText = readFileSync(join(ensureDir, 'verify-result.md'), 'utf8')
|
|
1952
|
+
}
|
|
1953
|
+
} catch { /* ensure 异常 fail-soft,按传入快照继续回填 */ }
|
|
1067
1954
|
let facts = null
|
|
1068
1955
|
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
1069
1956
|
// 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
|
|
@@ -1076,7 +1963,7 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
1076
1963
|
if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
|
|
1077
1964
|
facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
|
|
1078
1965
|
}
|
|
1079
|
-
const slots = parseEvidenceSlots(
|
|
1966
|
+
const slots = parseEvidenceSlots(verifyMdText || '')
|
|
1080
1967
|
// conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
|
|
1081
1968
|
// (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
|
|
1082
1969
|
const conclusionSlot = conclusion || null
|
|
@@ -1094,13 +1981,57 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
1094
1981
|
// PASS WITH NOTES 零有效行 → advisory 警告(不阻断——存量 PASS WITH NOTES 无此章节是常态,
|
|
1095
1982
|
// 渐进采纳)。EHS 实证:被环境阻断 deferred 的集成测试里藏着 5 个 P1,清单化才有
|
|
1096
1983
|
// 「谁兜、怎么复跑」的可追溯面。
|
|
1097
|
-
const handoverItems = parseHandoverRows(
|
|
1984
|
+
const handoverItems = parseHandoverRows(verifyMdText || '')
|
|
1098
1985
|
if (handoverItems.length > 0) {
|
|
1099
1986
|
facts.handover = { count: handoverItems.length, items: handoverItems }
|
|
1100
1987
|
}
|
|
1101
1988
|
if (conclusionSlot === 'PASS WITH NOTES' && handoverItems.length === 0) {
|
|
1102
1989
|
console.warn('⚠️ 结论=PASS WITH NOTES 但「移交项(结构化)」章节零有效行——正文叙述的移交项(环境阻断复跑条件/人工验收步骤/待执行脚本)请结构化进 ## 移交项(结构化) 表格(类型枚举 env-blocked/manual-acceptance/db-script/other),避免移交项只活在 prose 里没人兜(2026-09-15 EHS 实证:被环境阻断 deferred 的集成测试里藏着 5 个 P1)。')
|
|
1103
1990
|
}
|
|
1991
|
+
// ── PASS 封顶事实面四字段(2026-09-17-pass-cap-semantics task-01 / D-011 producer 侧)──
|
|
1992
|
+
// X-08 时序纪律:全部在函数主路径无条件产出——首次 backfill = gates.js 收尾前置调用、无
|
|
1993
|
+
// testCheckResult 时点(task-02 validator 在 runValidators 时点消费,已就位);误挂二次
|
|
1994
|
+
// 回填的 testCheckResult 分支则消费时点读不到、恒误拦。判定输入自推导自读(quality-scan
|
|
1995
|
+
// 记录按 specBase 推导 / 矩阵经动态绑定),不依赖调用方传参。
|
|
1996
|
+
facts.dbScriptDeclarations = parseDbScriptDeclarations(verifyMdText || '')
|
|
1997
|
+
facts.runtimeEndpointExcluded = parseRuntimeEndpointExcluded(verifyMdText || '')
|
|
1998
|
+
const integration = judgeIntegrationRan(factsPath, slots.runtimeEvidence)
|
|
1999
|
+
facts.integrationRan = integration.ran
|
|
2000
|
+
for (const note of integration.notes) console.warn(note)
|
|
2001
|
+
facts.matrixPartialRows = countMatrixPartialRows(verifyMdText || '')
|
|
2002
|
+
// ── facts.apiFace / facts.consumerHints(2026-09-17-api-coverage-smoke task-04 / FR-05 /
|
|
2003
|
+
// D-005~D-007 producer 侧)──X-08 主路径无条件产出(同上口径,不依赖 testCheckResult
|
|
2004
|
+
// 时点):apiFace = design 接口段 tolerant 解析面(endpoints/declared + writeEndpoints
|
|
2005
|
+
// 子集=表间完备性数据面),consumerHints = 清单消费端归类启发式(web/mp/script,仅
|
|
2006
|
+
// warning 提示面)。数据通道=落盘即消费(stage-contract 零 import 纪律,分层单向)——
|
|
2007
|
+
// task-05 validator 经 facts 读,不经函数调用。design.md 缺失 → 空面落盘(判定面稳定,
|
|
2008
|
+
// validator 判 N=0 走零行为注记);与 runVerifyProbes 同一解析器(单一产物源)。
|
|
2009
|
+
{
|
|
2010
|
+
const changeDir = dirname(factsPath)
|
|
2011
|
+
const designPath = join(changeDir, 'design.md')
|
|
2012
|
+
let designText = null
|
|
2013
|
+
if (existsSync(designPath)) {
|
|
2014
|
+
try { designText = readFileSync(designPath, 'utf8') } catch { designText = null }
|
|
2015
|
+
}
|
|
2016
|
+
const face = parseDesignApiTable(designText || '')
|
|
2017
|
+
facts.apiFace = {
|
|
2018
|
+
endpoints: face.endpoints,
|
|
2019
|
+
declared: face.declared,
|
|
2020
|
+
writeEndpoints: face.endpoints.filter(e => e && API_MATRIX_WRITE_METHODS.has(e.method)),
|
|
2021
|
+
}
|
|
2022
|
+
let designPaths = []
|
|
2023
|
+
if (designText !== null) {
|
|
2024
|
+
try { designPaths = parseFileChangeListDetailed(designPath).map(e => String(e.path)) } catch { designPaths = [] }
|
|
2025
|
+
}
|
|
2026
|
+
facts.consumerHints = classifyConsumerHints(designPaths)
|
|
2027
|
+
}
|
|
2028
|
+
// ── facts.smokeRan(2026-09-17-api-coverage-smoke task-03 / FR-02 / D-002@v1 producer 侧)──
|
|
2029
|
+
// X-08 主路径无条件产出(同上四字段口径,不依赖 testCheckResult 时点):唯一事实源 =
|
|
2030
|
+
// quality-scan 记录 smokeResult 段(judgeSmokeRan 五边界态封闭),不受 verify-result.md
|
|
2031
|
+
// 篡改影响(R-06 双源兜底)。消费方 = evaluatePassEligibility 第五条件(smoke-not-run)。
|
|
2032
|
+
const smokeRanJudge = judgeSmokeRan(factsPath)
|
|
2033
|
+
facts.smokeRan = smokeRanJudge.value
|
|
2034
|
+
for (const note of smokeRanJudge.notes) console.warn(note)
|
|
1104
2035
|
let testsBackfilled = false
|
|
1105
2036
|
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
1106
2037
|
facts.tests = {
|
|
@@ -1150,7 +2081,12 @@ export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = [])
|
|
|
1150
2081
|
blocks.push([
|
|
1151
2082
|
RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
|
|
1152
2083
|
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
1153
|
-
'-
|
|
2084
|
+
'<!-- 回执双形态(2026-09-16-friction5-hardening FR-01):下方多行 YAML 形态为推荐写法(字段序无关);',
|
|
2085
|
+
' 亦认单行管道形态:- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径> -->',
|
|
2086
|
+
'- claim: <待填:一句话>',
|
|
2087
|
+
' command: <待填:命令>',
|
|
2088
|
+
' exit: <待填:0 或非 0>',
|
|
2089
|
+
' log: <待填:日志路径>',
|
|
1154
2090
|
'',
|
|
1155
2091
|
].join('\n'))
|
|
1156
2092
|
added.push(RECEIPT_SLOT_HEADING)
|
|
@@ -1201,6 +2137,235 @@ export function ensureAcceptanceMatrixSection(mdPath, probe7) {
|
|
|
1201
2137
|
return { added: true }
|
|
1202
2138
|
}
|
|
1203
2139
|
|
|
2140
|
+
/**
|
|
2141
|
+
* 接口验证覆盖矩阵段幂等补齐(task-04;ensureAcceptanceMatrixSection 先例形态):verify-
|
|
2142
|
+
* result.md 已存在但缺「## 接口验证覆盖矩阵」段时补骨架段。幂等口径(renderProbe7Lines
|
|
2143
|
+
* 补段同款):段已在场(agent 已填/未填)一律不触碰——预填只在骨架生成与缺段补齐两条新写
|
|
2144
|
+
* 路径生效,agent 已填内容永不被覆盖;二跑零改动。插入位置:「## 探针结果」章节之后(下一
|
|
2145
|
+
* ## 标题前,保持骨架章节序),无探针章则文末追加(旧格式/手写正文)。
|
|
2146
|
+
* @param {string} mdPath verify-result.md 路径
|
|
2147
|
+
* @param {{endpoints?: Array<{method: string, path: string, rowIdx: number}>, declared?: number|null}|null} apiFace
|
|
2148
|
+
* @returns {{ added: boolean, reason?: string }}
|
|
2149
|
+
*/
|
|
2150
|
+
export function ensureApiCoverageMatrixSection(mdPath, apiFace) {
|
|
2151
|
+
let text
|
|
2152
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: false, reason: 'verify-result.md 不存在或不可读' } }
|
|
2153
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
2154
|
+
if (/^## 接口验证覆盖矩阵/m.test(normalized)) return { added: false, reason: '接口验证覆盖矩阵段已在场' }
|
|
2155
|
+
const blockLines = [...renderApiCoverageMatrixLines(apiFace), '']
|
|
2156
|
+
try {
|
|
2157
|
+
const lines = normalized.split('\n')
|
|
2158
|
+
const probeIdx = lines.findIndex(l => /^## 探针结果/.test(l))
|
|
2159
|
+
if (probeIdx === -1) {
|
|
2160
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + '\n' + blockLines.join('\n'))
|
|
2161
|
+
} else {
|
|
2162
|
+
// 探针结果章终点 = 下一个 ## 级标题行(矩阵段紧随其后,与骨架章节序一致)
|
|
2163
|
+
let end = lines.length
|
|
2164
|
+
for (let i = probeIdx + 1; i < lines.length; i++) {
|
|
2165
|
+
if (/^## /.test(lines[i])) { end = i; break }
|
|
2166
|
+
}
|
|
2167
|
+
lines.splice(end, 0, ...blockLines)
|
|
2168
|
+
writeFileSync(mdPath, lines.join('\n'))
|
|
2169
|
+
}
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
console.warn(`⚠️ 接口验证覆盖矩阵段补齐落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
2172
|
+
return { added: false, reason: '落盘失败' }
|
|
2173
|
+
}
|
|
2174
|
+
return { added: true }
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// ============ smoke 回执槽机器段(2026-09-17-api-coverage-smoke task-02 / FR-03,design §3) ============
|
|
2178
|
+
//
|
|
2179
|
+
// 唯一事实源 = .runtime/verify-quality-scan-<change>.json 的 smokeResult 段(task-01 契约,
|
|
2180
|
+
// R-06 双源兜底——md 被篡改不影响 facts 推导);本组函数把该记录 ensure 式渲染成
|
|
2181
|
+
// verify-result.md「## 集成验证回执」槽内 CLI 拥有的机器段行(source: cli-noai-smoke 标注
|
|
2182
|
+
// 识别),agent 只可追加段、不可改写机器段(改写/删除由 verify-postcheck
|
|
2183
|
+
// checkProbeConsistency 回执槽一致性对比打回,Grill #6 / R-06)。
|
|
2184
|
+
/** 机器段行首锚(ensure 幂等识别——claim 钦定文案;单行管道与多行 YAML 双形态首行均命中。
|
|
2185
|
+
* 注意不可用 \b 锚中文词尾:CJK 不属 \w,「亲跑」后与「 |」之间无 word 边界恒 false(实证
|
|
2186
|
+
* 幂等失效重复注入)——改前瞻断言(后随管道分隔或行尾)。 */
|
|
2187
|
+
const SMOKE_MACHINE_CLAIM_RE = /^- claim: commands\.smoke CLI 亲跑(?=\s*[||]|$)/
|
|
2188
|
+
/** 缺态注释行(态捕获 m[1],not-configured / not-ran 两态——非回执行形态,parseEvidenceSlots 不收取) */
|
|
2189
|
+
const SMOKE_ABSENT_LINE_RE = /^<!--\s*smoke 机器段缺态:(not-configured|not-ran)\b[^>]*source: cli-noai-smoke[^>]*-->$/
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* 读 quality-scan 记录的 smokeResult 段(路径规则同 judgeIntegrationRan:specBase/
|
|
2193
|
+
* .runtime/verify-quality-scan-<changeName>.json)。共享读取点——ensure 注入与 postcheck
|
|
2194
|
+
* 一致性对比同源取数,不各读各的。记录缺失/损坏/无 smokeResult 段 → null。
|
|
2195
|
+
*/
|
|
2196
|
+
export function readSmokeResultRecord(specBase, changeName) {
|
|
2197
|
+
try {
|
|
2198
|
+
const rec = JSON.parse(readFileSync(join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`), 'utf8'))
|
|
2199
|
+
if (rec && rec.schemaVersion === 1 && rec.source === 'cli-noai'
|
|
2200
|
+
&& rec.smokeResult && typeof rec.smokeResult === 'object') {
|
|
2201
|
+
return rec.smokeResult
|
|
2202
|
+
}
|
|
2203
|
+
} catch { /* 记录缺失/损坏 → null */ }
|
|
2204
|
+
return null
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
/**
|
|
2208
|
+
* 由 smokeResult 记录推导机器段目标态(ensure 与 postcheck 共享口径,不二算):
|
|
2209
|
+
* 'passed'(configured 且亲跑绿)| 'not-configured'(未配置)| 'not-ran'(配置但
|
|
2210
|
+
* failed/超时——task-01 契约 timeout 归 status='failed')。
|
|
2211
|
+
*/
|
|
2212
|
+
export function deriveSmokeSectionState(smoke) {
|
|
2213
|
+
if (!smoke || typeof smoke !== 'object') return null
|
|
2214
|
+
if (smoke.configured === false) return 'not-configured'
|
|
2215
|
+
return smoke.status === 'passed' ? 'passed' : 'not-ran'
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
/**
|
|
2219
|
+
* facts.smokeRan 判定(2026-09-17-api-coverage-smoke task-03 / FR-02 / D-002@v1):
|
|
2220
|
+
* 唯一事实源 = quality-scan 记录 smokeResult 段(readSmokeResultRecord 共享读取点,R-06
|
|
2221
|
+
* 双源兜底——不受 verify-result.md 篡改影响;judgeIntegrationRan 同款自读先例:specBase/
|
|
2222
|
+
* changeName 自 factsPath 目录上推,不依赖调用方传参)。五边界态封闭:
|
|
2223
|
+
* configured=false(未配置键 / commands.smoke: unavailable / local.yaml 不可读)→
|
|
2224
|
+
* not-configured;status='passed'(exit 0)→ ran;status='failed'(exit 非 0/超时——
|
|
2225
|
+
* task-01 契约 timeout 归 failed,失败也是未通过)→ not-ran;记录在场但无 smokeResult
|
|
2226
|
+
* 段(升级过渡期存量记录)→ not-ran + fail-open 注记;记录缺失/损坏 → not-ran +
|
|
2227
|
+
* fail-open 注记(X-01 口径,不阻断——出路=重跑质量扫描步或降级 NOTES)。
|
|
2228
|
+
* @param {string} factsPath verify-facts.json 路径(<specBase>/changes/<name>/verify-facts.json)
|
|
2229
|
+
* @returns {{ value: 'ran'|'not-ran'|'not-configured', notes: string[] }}
|
|
2230
|
+
*/
|
|
2231
|
+
function judgeSmokeRan(factsPath) {
|
|
2232
|
+
const changeDir = dirname(factsPath)
|
|
2233
|
+
const changeName = basename(changeDir)
|
|
2234
|
+
const specBase = dirname(dirname(changeDir))
|
|
2235
|
+
const recordPath = join(specBase, '.runtime', `verify-quality-scan-${changeName}.json`)
|
|
2236
|
+
const smoke = readSmokeResultRecord(specBase, changeName)
|
|
2237
|
+
if (smoke) {
|
|
2238
|
+
if (smoke.configured === false) return { value: 'not-configured', notes: [] }
|
|
2239
|
+
return { value: smoke.status === 'passed' ? 'ran' : 'not-ran', notes: [] }
|
|
2240
|
+
}
|
|
2241
|
+
// readSmokeResultRecord null 两缺因区分注记:记录在场但无 smokeResult 段(升级过渡期
|
|
2242
|
+
// 存量记录)vs 记录缺失/损坏——同判 not-ran,注记各自点名(X-01 fail-open,不阻断)。
|
|
2243
|
+
let recordPresent = false
|
|
2244
|
+
try {
|
|
2245
|
+
const rec = JSON.parse(readFileSync(recordPath, 'utf8'))
|
|
2246
|
+
if (rec && rec.schemaVersion === 1 && rec.source === 'cli-noai') recordPresent = true
|
|
2247
|
+
} catch { recordPresent = false }
|
|
2248
|
+
return {
|
|
2249
|
+
value: 'not-ran',
|
|
2250
|
+
notes: [recordPresent
|
|
2251
|
+
? `ℹ️ quality-scan 记录在场但无 smokeResult 段(${recordPath},升级过渡期存量记录)——facts.smokeRan 按 not-ran 判定(X-01 fail-open 注记,不阻断;出路=重跑质量扫描步或降级 NOTES)`
|
|
2252
|
+
: `ℹ️ quality-scan 实测记录缺失(${recordPath})——facts.smokeRan 按 not-ran 判定(X-01 fail-open 注记,不阻断;--done 亲测替代扫描的场景记录时序不可得,出路=重跑质量扫描步或降级 NOTES)`],
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* 扫描 verify-result.md 回执槽内 CLI 拥有的 smoke 机器段标记(ensure 幂等判定与
|
|
2258
|
+
* verify-postcheck 一致性对比共享解析点):①机器段回执条目 = parseEvidenceSlots 收取且
|
|
2259
|
+
* source='cli-noai-smoke' 的条目(含 agent 冒充行——冒充也是打回面);②缺态注释行态。
|
|
2260
|
+
* @param {string} mdText verify-result.md 全文(CRLF 归一内部处理)
|
|
2261
|
+
* @returns {{ machineEntries: Array<{claim,command,exitCode,logPath,source}>, absentState: 'not-configured'|'not-ran'|null }}
|
|
2262
|
+
*/
|
|
2263
|
+
export function scanSmokeReceiptSection(mdText) {
|
|
2264
|
+
const text = String(mdText || '').replace(/\r\n/g, '\n')
|
|
2265
|
+
let machineEntries = []
|
|
2266
|
+
try {
|
|
2267
|
+
machineEntries = (parseEvidenceSlots(text).runtimeEvidence || [])
|
|
2268
|
+
.filter(e => e && e.source === SMOKE_RECEIPT_SOURCE_MARK)
|
|
2269
|
+
} catch { machineEntries = [] }
|
|
2270
|
+
let absentState = null
|
|
2271
|
+
for (const line of text.split('\n')) {
|
|
2272
|
+
const am = line.match(SMOKE_ABSENT_LINE_RE)
|
|
2273
|
+
if (am) { absentState = am[1]; break }
|
|
2274
|
+
}
|
|
2275
|
+
return { machineEntries, absentState }
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* 回执槽 smoke 机器段 ensure 式注入(ensureAcceptanceMatrixSection 先例形态,task-02):
|
|
2280
|
+
* 挂 backfill 合并时点(backfillFactsFromMdAndTests——quality-scan step 6 noAI 亲跑落记录
|
|
2281
|
+
* 先于 verify 门禁,读得到实录)。行为:记录态 passed → 段内注入机器段行(单行管道形态,
|
|
2282
|
+
* command 含 |/| 时降多行 YAML 防字段截断)+ 说明注释行;not-configured / not-ran →
|
|
2283
|
+
* 注缺态注释行;无记录 → no-op(不注入,task-03 判 not-ran)。幂等——段内已有同态 CLI 行
|
|
2284
|
+
* 零改动;态迁移(如配置后亲跑:not-configured → passed)时替换 CLI 拥有行为新态。
|
|
2285
|
+
* read-modify-write 基于盘上 md 最新态(不基于调用方快照写回,防覆盖多 agent 并发改动)。
|
|
2286
|
+
* @param {string} mdPath verify-result.md 路径
|
|
2287
|
+
* @param {{ specBase?: string, changeName?: string }} [opts] 缺省自 mdPath 目录上推(judgeIntegrationRan 同款)
|
|
2288
|
+
* @returns {{ changed: boolean, state?: string, reason?: string }}
|
|
2289
|
+
*/
|
|
2290
|
+
export function ensureSmokeReceiptSection(mdPath, opts = {}) {
|
|
2291
|
+
const changeDir = dirname(String(mdPath || ''))
|
|
2292
|
+
const changeName = opts.changeName || basename(changeDir)
|
|
2293
|
+
const specBase = opts.specBase || dirname(dirname(changeDir))
|
|
2294
|
+
let text
|
|
2295
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { changed: false, reason: 'verify-result.md 不存在或不可读' } }
|
|
2296
|
+
const smoke = readSmokeResultRecord(specBase, changeName)
|
|
2297
|
+
const state = deriveSmokeSectionState(smoke)
|
|
2298
|
+
if (!state) return { changed: false, reason: '无 quality-scan smokeResult 记录(未跑质量扫描)——不注入机器段' }
|
|
2299
|
+
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
2300
|
+
const hIdx = lines.findIndex(l => l.startsWith(RECEIPT_SLOT_HEADING))
|
|
2301
|
+
if (hIdx === -1) return { changed: false, reason: `回执槽段(${RECEIPT_SLOT_HEADING})不在场——backfillMissingEvidenceSlots 先补段,下次 ensure 注入` }
|
|
2302
|
+
// 段终点(parseEvidenceSlots sectionOf 同口径:下一 #/## 级标题或文末)
|
|
2303
|
+
let end = lines.length
|
|
2304
|
+
for (let i = hIdx + 1; i < lines.length; i++) {
|
|
2305
|
+
if (/^#{1,2}\s/.test(lines[i])) { end = i; break }
|
|
2306
|
+
}
|
|
2307
|
+
let machineIdx = -1 // 机器段行(单行形态行或多行形态首行)
|
|
2308
|
+
let absentIdx = -1 // 缺态注释行
|
|
2309
|
+
for (let i = hIdx + 1; i < end; i++) {
|
|
2310
|
+
if (machineIdx === -1 && SMOKE_MACHINE_CLAIM_RE.test(lines[i])) machineIdx = i
|
|
2311
|
+
if (absentIdx === -1 && SMOKE_ABSENT_LINE_RE.test(lines[i])) absentIdx = i
|
|
2312
|
+
}
|
|
2313
|
+
// 目标行渲染(passed 双形态:command 含 |/| 时单行管道形态会字段截断 → 降多行 YAML 形态;
|
|
2314
|
+
// exitCode/logPath 非法空值如实渲染——收取侧 fail-closed 不收,postcheck 对比兜底)
|
|
2315
|
+
let targetLines
|
|
2316
|
+
if (state === 'passed') {
|
|
2317
|
+
const command = String(smoke.command || '')
|
|
2318
|
+
const exitVal = typeof smoke.exitCode === 'number' ? smoke.exitCode : ''
|
|
2319
|
+
const noteLine = `<!-- smoke 机器段(CLI ensure 注入,source: ${SMOKE_RECEIPT_SOURCE_MARK})——唯一事实源是 .runtime/verify-quality-scan-${changeName}.json 的 smokeResult,agent 不可改写/删除(verify 一致性抽查会打回);ranAt 实录:${smoke.ranAt || '未知'},durationMs:${typeof smoke.durationMs === 'number' ? smoke.durationMs : '未知'} -->`
|
|
2320
|
+
targetLines = /[||]/.test(command)
|
|
2321
|
+
? [
|
|
2322
|
+
noteLine,
|
|
2323
|
+
'- claim: commands.smoke CLI 亲跑',
|
|
2324
|
+
` command: ${command}`,
|
|
2325
|
+
` exit: ${exitVal}`,
|
|
2326
|
+
` log: ${smoke.logPath || ''}`,
|
|
2327
|
+
` source: ${SMOKE_RECEIPT_SOURCE_MARK}`,
|
|
2328
|
+
]
|
|
2329
|
+
: [
|
|
2330
|
+
noteLine,
|
|
2331
|
+
`- claim: commands.smoke CLI 亲跑 | command: ${command} | exit: ${exitVal} | log: ${smoke.logPath || ''} | source: ${SMOKE_RECEIPT_SOURCE_MARK}`,
|
|
2332
|
+
]
|
|
2333
|
+
} else {
|
|
2334
|
+
const absentNote = state === 'not-configured'
|
|
2335
|
+
? 'commands.smoke 未配置——配置 local.yaml 后下次 verify 亲跑并自动注入机器段'
|
|
2336
|
+
: `commands.smoke 已配置但未绿跑(${smoke.reason || '失败/超时'})——修复后重跑 verify 亲测自动更新本段`
|
|
2337
|
+
targetLines = [`<!-- smoke 机器段缺态:${state}(${absentNote})source: ${SMOKE_RECEIPT_SOURCE_MARK} -->`]
|
|
2338
|
+
}
|
|
2339
|
+
// 幂等/态迁移:同态 CLI 行在场 → no-op;异态(含旧缺态行 / 机器段行)→ 原位替换;缺 → 段内末尾注入
|
|
2340
|
+
const inPlaceIdx = machineIdx !== -1 ? machineIdx : absentIdx
|
|
2341
|
+
if (state === 'passed' && machineIdx !== -1) return { changed: false, state, reason: '机器段已在场(幂等跳过)' }
|
|
2342
|
+
if (state !== 'passed' && absentIdx !== -1) {
|
|
2343
|
+
const am = lines[absentIdx].match(SMOKE_ABSENT_LINE_RE)
|
|
2344
|
+
if (am && am[1] === state) return { changed: false, state, reason: `缺态标注已在场(${state},幂等跳过)` }
|
|
2345
|
+
}
|
|
2346
|
+
try {
|
|
2347
|
+
if (inPlaceIdx !== -1) {
|
|
2348
|
+
// 原位替换(CLI 拥有行的态迁移;机器段行 1 行 ↔ 缺态行 1 行,多行 YAML 形态首行替换为整块)
|
|
2349
|
+
let replaceEnd = inPlaceIdx + 1
|
|
2350
|
+
if (SMOKE_MACHINE_CLAIM_RE.test(lines[inPlaceIdx]) && state !== 'passed') {
|
|
2351
|
+
// 旧机器段是多行 YAML 形态时连带其缩进续行一并替换
|
|
2352
|
+
while (replaceEnd < end && /^[ \t]+\w/.test(lines[replaceEnd])) replaceEnd++
|
|
2353
|
+
}
|
|
2354
|
+
lines.splice(inPlaceIdx, replaceEnd - inPlaceIdx, ...targetLines)
|
|
2355
|
+
} else {
|
|
2356
|
+
// 段内末尾注入(最后一个非空行之后)
|
|
2357
|
+
let last = end - 1
|
|
2358
|
+
while (last > hIdx && lines[last].trim() === '') last--
|
|
2359
|
+
lines.splice(last + 1, 0, ...targetLines)
|
|
2360
|
+
}
|
|
2361
|
+
writeFileSync(mdPath, lines.join('\n'))
|
|
2362
|
+
} catch (e) {
|
|
2363
|
+
console.warn(`⚠️ smoke 机器段 ensure 落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
2364
|
+
return { changed: false, state, reason: '落盘失败' }
|
|
2365
|
+
}
|
|
2366
|
+
return { changed: true, state }
|
|
2367
|
+
}
|
|
2368
|
+
|
|
1204
2369
|
/**
|
|
1205
2370
|
* 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
|
|
1206
2371
|
* 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
|
|
@@ -1227,7 +2392,7 @@ export function applyConclusionDraftToText(text, draft = 'PASS') {
|
|
|
1227
2392
|
* tasks.md 行内注解可靠)。Evidence / 状态两列是人工判断置 <待填>;D 无 task 回指 →
|
|
1228
2393
|
* ⚠️ 未闭环显式列出(这正是矩阵要暴露的风险,不静默)。
|
|
1229
2394
|
* @returns {{ rows: string[], decisionCount: number, taskCount: number }|null}
|
|
1230
|
-
* null = 无 decisions.md / 解析 0 条(矩阵段留 TODO 不注入)
|
|
2395
|
+
* null = 无 decisions.md / 解析 0 条(矩阵段留 TODO 不注入) probe1-noqa
|
|
1231
2396
|
*/
|
|
1232
2397
|
export function buildDecisionChainMatrix(changeDir) {
|
|
1233
2398
|
let decisions
|
|
@@ -1269,9 +2434,9 @@ export function buildDecisionChainMatrix(changeDir) {
|
|
|
1269
2434
|
|
|
1270
2435
|
/**
|
|
1271
2436
|
* 矩阵机械半边注入 verify-result.md(--init 接线):只在「决策追踪矩阵」段内仍是骨架
|
|
1272
|
-
* TODO 行(含 D-xxx 形态)且段内无既有表格时替换注入——幂等(agent 已写/前次注入零改动),
|
|
2437
|
+
* TODO 行(含 D-xxx 形态)且段内无既有表格时替换注入——幂等(agent 已写/前次注入零改动), probe1-noqa
|
|
1273
2438
|
* 不触碰正文其余部分。落盘失败 fail-soft 返回 null。
|
|
1274
|
-
* @returns {{ decisions: number, tasks: number }|null} null = 无 decisions/无可替换 TODO/已注入
|
|
2439
|
+
* @returns {{ decisions: number, tasks: number }|null} null = 无 decisions/无可替换 TODO/已注入 probe1-noqa
|
|
1275
2440
|
*/
|
|
1276
2441
|
export function injectDecisionChainDraft(mdPath, changeDir) {
|
|
1277
2442
|
let text
|
|
@@ -1282,8 +2447,8 @@ export function injectDecisionChainDraft(mdPath, changeDir) {
|
|
|
1282
2447
|
const sectionEnd = nextHeading === -1 ? text.length : nextHeading
|
|
1283
2448
|
const section = text.slice(headingIdx, sectionEnd)
|
|
1284
2449
|
if (/^\|/m.test(section)) return null // 已有表格 → 尊重既有内容
|
|
1285
|
-
const todoRe = /<!--TODO:[^\n]*D-xxx[^\n]*-->/
|
|
1286
|
-
if (!todoRe.test(section)) return null // 无骨架 TODO(手写正文)→ 不动
|
|
2450
|
+
const todoRe = /<!--TODO:[^\n]*D-xxx[^\n]*-->/ /* probe1-noqa */
|
|
2451
|
+
if (!todoRe.test(section)) return null // 无骨架 TODO(手写正文)→ 不动 /* probe1-noqa */
|
|
1287
2452
|
const matrix = buildDecisionChainMatrix(changeDir)
|
|
1288
2453
|
if (!matrix || matrix.rows.length === 0) return null
|
|
1289
2454
|
const block = [
|
|
@@ -1304,7 +2469,7 @@ export function injectDecisionChainDraft(mdPath, changeDir) {
|
|
|
1304
2469
|
}
|
|
1305
2470
|
|
|
1306
2471
|
/**
|
|
1307
|
-
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
2472
|
+
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。 probe1-noqa
|
|
1308
2473
|
* 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
|
|
1309
2474
|
* 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
|
|
1310
2475
|
* `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
|
|
@@ -1316,7 +2481,7 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
1316
2481
|
const L = [
|
|
1317
2482
|
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
1318
2483
|
'',
|
|
1319
|
-
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
|
|
2484
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——', /* probe1-noqa */
|
|
1320
2485
|
'> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
|
|
1321
2486
|
'>',
|
|
1322
2487
|
'> 「层」=证据可核验性分层(非执行者声明):「人工判断」指本节为语义判断,由执行 agent 填写、',
|
|
@@ -1339,34 +2504,44 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
1339
2504
|
'',
|
|
1340
2505
|
'## 集成验证回执 [层:自述声明——CLI 一致性校验]',
|
|
1341
2506
|
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
1342
|
-
'-
|
|
2507
|
+
'<!-- 回执双形态(2026-09-16-friction5-hardening FR-01):下方多行 YAML 形态为推荐写法(字段序无关);',
|
|
2508
|
+
' 亦认单行管道形态:- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径> -->',
|
|
2509
|
+
'- claim: <待填:一句话>',
|
|
2510
|
+
' command: <待填:命令>',
|
|
2511
|
+
' exit: <待填:0 或非 0>',
|
|
2512
|
+
' log: <待填:日志路径>',
|
|
1343
2513
|
'',
|
|
1344
2514
|
'## 任务完成度 [层:人工判断]',
|
|
1345
|
-
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
|
2515
|
+
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->', /* probe1-noqa */
|
|
1346
2516
|
'',
|
|
1347
2517
|
'## 设计一致性 [层:人工判断]',
|
|
1348
|
-
'<!--TODO: 实现与 design.md 的偏差(无偏差也显式写「一致」)-->',
|
|
2518
|
+
'<!--TODO: 实现与 design.md 的偏差(无偏差也显式写「一致」)-->', /* probe1-noqa */
|
|
1349
2519
|
'',
|
|
1350
2520
|
'## 探针结果(CLI 机械预填) [层:可复跑探针——gate 抽查防篡改]',
|
|
1351
2521
|
renderVerifyProbesReport(result),
|
|
1352
2522
|
'',
|
|
2523
|
+
// 接口验证覆盖矩阵段(task-04 / FR-05):紧随探针结果章(probe7 矩阵渲染面之后的独立
|
|
2524
|
+
// ## 章节);result 无 apiFace 键(存量调用方/合成 result)→ 空面渲染「无接口面」注记,零回归。
|
|
2525
|
+
...renderApiCoverageMatrixLines(result.apiFace),
|
|
2526
|
+
'',
|
|
1353
2527
|
'## 测试结果 [层:确定性检查——CLI 实测对账]',
|
|
1354
|
-
'<!--TODO: 测试命令 + 结果(通过数/失败数;known_failures 豁免逐条注明)-->',
|
|
2528
|
+
'<!--TODO: 测试命令 + 结果(通过数/失败数;known_failures 豁免逐条注明)-->', /* probe1-noqa */
|
|
1355
2529
|
'',
|
|
1356
2530
|
'## 决策追踪矩阵(如存在 decisions.md;无则删本节) [层:人工判断]',
|
|
1357
|
-
'<!--TODO: | 决策 ID | FR | Task | Evidence | 状态 |(D-xxx@vN → FR-xxx → task → 证据回指闭环)-->',
|
|
2531
|
+
'<!--TODO: | 决策 ID | FR | Task | Evidence | 状态 |(D-xxx@vN → FR-xxx → task → 证据回指闭环)-->', /* probe1-noqa */
|
|
1358
2532
|
'',
|
|
1359
2533
|
'## 技术债务 [层:人工判断]',
|
|
1360
|
-
'<!--TODO: TODO/FIXME/HACK 统计(探针 1 的命中已预填在上方探针结果)-->',
|
|
2534
|
+
'<!--TODO: TODO/FIXME/HACK 统计(探针 1 的命中已预填在上方探针结果)-->', /* probe1-noqa */
|
|
1361
2535
|
'',
|
|
1362
2536
|
'## 变更风险等级 [层:人工判断]',
|
|
1363
|
-
'<!--TODO: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,写明「显式声明 = <等级>」+ 理由;若有命中被同句否定语境抑制(如「不新增 daemon 协议」),写明被抑制关键词与理由(抑制可审计,不许用来静默降级)-->',
|
|
2537
|
+
'<!--TODO: doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical;若 design.md frontmatter 有 risk_level 显式声明,写明「显式声明 = <等级>」+ 理由;若有命中被同句否定语境抑制(如「不新增 daemon 协议」),写明被抑制关键词与理由(抑制可审计,不许用来静默降级)-->', /* probe1-noqa */
|
|
1364
2538
|
'',
|
|
1365
2539
|
'## Runtime Evidence [层:人工判断]',
|
|
1366
|
-
'<!--TODO: 关键命令输出/时间戳/commit hash 证据链;integration/deployment-critical 必填,按实际触碰的运行时组件写(启动命令/端点/请求响应/日志片段/生命周期终态断言/失败模式排除),未涉及的行写「不涉及」-->',
|
|
2540
|
+
'<!--TODO: 关键命令输出/时间戳/commit hash 证据链;integration/deployment-critical 必填,按实际触碰的运行时组件写(启动命令/端点/请求响应/日志片段/生命周期终态断言/失败模式排除),未涉及的行写「不涉及」-->', /* probe1-noqa */
|
|
2541
|
+
'<!-- 降级路径(design §3.2,D-004 收口):服务起不来时:Controller 直调冒烟(mock 下游,验绑定+校验+路由)/ 基础设施恢复后复跑固化用例——不要空填不涉及 -->',
|
|
1367
2542
|
'',
|
|
1368
2543
|
'## 代码审查 [层:人工判断]',
|
|
1369
|
-
'<!--TODO: 问题列表 + 总体评价。走查清单(零覆盖路径必查——探针 7 ⚠️ 条目即定向面):',
|
|
2544
|
+
'<!--TODO: 问题列表 + 总体评价。走查清单(零覆盖路径必查——探针 7 ⚠️ 条目即定向面):', /* probe1-noqa */
|
|
1370
2545
|
' ① 编辑/更新链路(回显、字段映射、残留态)——非新增主链路,实证盲区;',
|
|
1371
2546
|
' ② 非主分支流(相关方/旁路支线等未走查路径);',
|
|
1372
2547
|
' ③ 守卫一致性:同资源端点的操作人/权限校验模式对比(实证 doSubmit 无操作人校验而 delete/withdraw 有——越权);',
|