sillyspec 3.28.11 → 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/.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 +27 -5
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +2 -0
- package/src/diagnostic-codes.js +95 -0
- package/src/doctor-diagnostics.js +141 -1
- package/src/machine-interface.js +37 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract.js +426 -2
- package/src/stage-review-checklist.js +13 -1
- package/src/stages/verify.js +6 -2
- package/src/verify-facts-schema.js +21 -5
- package/src/verify-postcheck.js +86 -2
- package/src/verify-probes.js +447 -8
package/src/verify-probes.js
CHANGED
|
@@ -290,6 +290,89 @@ function designHasSubmitEndpoint(text) {
|
|
|
290
290
|
return false
|
|
291
291
|
}
|
|
292
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
|
+
|
|
293
376
|
/**
|
|
294
377
|
* 探针 8 主体:design 清单三面文件(Java/SQL/前端)→ 双根(主仓 ∪ worktree)∪ 跨仓注册仓根
|
|
295
378
|
* 读取 → 归一化比对。advisory:所有输出是「候选」不是结论(UI 本地态键/服务端填充列会自然
|
|
@@ -1258,7 +1341,20 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
1258
1341
|
probe9.notes = [`探针 9 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
|
|
1259
1342
|
}
|
|
1260
1343
|
|
|
1261
|
-
|
|
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 }
|
|
1262
1358
|
}
|
|
1263
1359
|
|
|
1264
1360
|
/**
|
|
@@ -1442,6 +1538,54 @@ function renderProbe9Lines(p9) {
|
|
|
1442
1538
|
return L
|
|
1443
1539
|
}
|
|
1444
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
|
+
|
|
1445
1589
|
/**
|
|
1446
1590
|
* 从 runVerifyProbes 结果构造 verify-facts.json 机器底稿对象(P3b 可复跑审计底稿)。
|
|
1447
1591
|
* 命令行统一 `sillyspec verify-probes --change <name>`;指标 fail-soft——字段不可得时少列
|
|
@@ -1687,6 +1831,14 @@ export function parseDbScriptDeclarations(md) {
|
|
|
1687
1831
|
// 退役判据:批次 C commands.smoke 落地后由 smoke 回执一票判定「集成实测已跑」——起服务冒烟
|
|
1688
1832
|
// 即跨层实测,不得默认 build 误拦金路径);unit=单测 runner 直跑(JUnitCore/node --test/
|
|
1689
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 铁律)。
|
|
1690
1842
|
const RECEIPT_CROSS_LAYER_RE = new RegExp([
|
|
1691
1843
|
'\\bcurl\\b', '\\bwget\\b', '\\bhttpie\\b', '\\bInvoke-WebRequest\\b', '\\bInvoke-RestMethod\\b',
|
|
1692
1844
|
'\\biwr\\b', '\\birm\\b', 'https?://',
|
|
@@ -1696,7 +1848,10 @@ const RECEIPT_CROSS_LAYER_RE = new RegExp([
|
|
|
1696
1848
|
'\\b(?:nc|netcat|telnet|socat)\\b',
|
|
1697
1849
|
].join('|'), 'i')
|
|
1698
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
|
|
1699
|
-
|
|
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'
|
|
1700
1855
|
const cmd = String(command || '')
|
|
1701
1856
|
if (RECEIPT_CROSS_LAYER_RE.test(cmd)) return 'cross-layer'
|
|
1702
1857
|
if (RECEIPT_UNIT_RE.test(cmd)) return 'unit'
|
|
@@ -1734,7 +1889,9 @@ function judgeIntegrationRan(factsPath, runtimeEvidence) {
|
|
|
1734
1889
|
const scanRan = Boolean(record && record.testResult && record.testResult.status && record.testResult.status !== 'skipped')
|
|
1735
1890
|
if (scanRan && ['full', 'module', 'evidence-auto'].includes(strategy)) return { ran: 'ran', notes: [] }
|
|
1736
1891
|
const receipts = Array.isArray(runtimeEvidence) ? runtimeEvidence : []
|
|
1737
|
-
|
|
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')) {
|
|
1738
1895
|
return { ran: 'ran', notes: [] }
|
|
1739
1896
|
}
|
|
1740
1897
|
const notes = []
|
|
@@ -1778,6 +1935,22 @@ function countMatrixPartialRows(md) {
|
|
|
1778
1935
|
}
|
|
1779
1936
|
|
|
1780
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,按传入快照继续回填 */ }
|
|
1781
1954
|
let facts = null
|
|
1782
1955
|
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
1783
1956
|
// 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
|
|
@@ -1790,7 +1963,7 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
1790
1963
|
if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
|
|
1791
1964
|
facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
|
|
1792
1965
|
}
|
|
1793
|
-
const slots = parseEvidenceSlots(
|
|
1966
|
+
const slots = parseEvidenceSlots(verifyMdText || '')
|
|
1794
1967
|
// conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
|
|
1795
1968
|
// (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
|
|
1796
1969
|
const conclusionSlot = conclusion || null
|
|
@@ -1808,7 +1981,7 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
1808
1981
|
// PASS WITH NOTES 零有效行 → advisory 警告(不阻断——存量 PASS WITH NOTES 无此章节是常态,
|
|
1809
1982
|
// 渐进采纳)。EHS 实证:被环境阻断 deferred 的集成测试里藏着 5 个 P1,清单化才有
|
|
1810
1983
|
// 「谁兜、怎么复跑」的可追溯面。
|
|
1811
|
-
const handoverItems = parseHandoverRows(
|
|
1984
|
+
const handoverItems = parseHandoverRows(verifyMdText || '')
|
|
1812
1985
|
if (handoverItems.length > 0) {
|
|
1813
1986
|
facts.handover = { count: handoverItems.length, items: handoverItems }
|
|
1814
1987
|
}
|
|
@@ -1820,12 +1993,45 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
1820
1993
|
// testCheckResult 时点(task-02 validator 在 runValidators 时点消费,已就位);误挂二次
|
|
1821
1994
|
// 回填的 testCheckResult 分支则消费时点读不到、恒误拦。判定输入自推导自读(quality-scan
|
|
1822
1995
|
// 记录按 specBase 推导 / 矩阵经动态绑定),不依赖调用方传参。
|
|
1823
|
-
facts.dbScriptDeclarations = parseDbScriptDeclarations(
|
|
1824
|
-
facts.runtimeEndpointExcluded = parseRuntimeEndpointExcluded(
|
|
1996
|
+
facts.dbScriptDeclarations = parseDbScriptDeclarations(verifyMdText || '')
|
|
1997
|
+
facts.runtimeEndpointExcluded = parseRuntimeEndpointExcluded(verifyMdText || '')
|
|
1825
1998
|
const integration = judgeIntegrationRan(factsPath, slots.runtimeEvidence)
|
|
1826
1999
|
facts.integrationRan = integration.ran
|
|
1827
2000
|
for (const note of integration.notes) console.warn(note)
|
|
1828
|
-
facts.matrixPartialRows = countMatrixPartialRows(
|
|
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)
|
|
1829
2035
|
let testsBackfilled = false
|
|
1830
2036
|
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
1831
2037
|
facts.tests = {
|
|
@@ -1931,6 +2137,235 @@ export function ensureAcceptanceMatrixSection(mdPath, probe7) {
|
|
|
1931
2137
|
return { added: true }
|
|
1932
2138
|
}
|
|
1933
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
|
+
|
|
1934
2369
|
/**
|
|
1935
2370
|
* 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
|
|
1936
2371
|
* 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
|
|
@@ -2085,6 +2520,10 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
2085
2520
|
'## 探针结果(CLI 机械预填) [层:可复跑探针——gate 抽查防篡改]',
|
|
2086
2521
|
renderVerifyProbesReport(result),
|
|
2087
2522
|
'',
|
|
2523
|
+
// 接口验证覆盖矩阵段(task-04 / FR-05):紧随探针结果章(probe7 矩阵渲染面之后的独立
|
|
2524
|
+
// ## 章节);result 无 apiFace 键(存量调用方/合成 result)→ 空面渲染「无接口面」注记,零回归。
|
|
2525
|
+
...renderApiCoverageMatrixLines(result.apiFace),
|
|
2526
|
+
'',
|
|
2088
2527
|
'## 测试结果 [层:确定性检查——CLI 实测对账]',
|
|
2089
2528
|
'<!--TODO: 测试命令 + 结果(通过数/失败数;known_failures 豁免逐条注明)-->', /* probe1-noqa */
|
|
2090
2529
|
'',
|