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.
@@ -944,6 +944,402 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
944
944
  return { ok: errors.length === 0, errors, warnings }
945
945
  }
946
946
 
947
+ // ============ 接口验证覆盖矩阵门禁(2026-09-17-api-coverage-smoke task-05 / FR-04~FR-06) ============
948
+ //
949
+ // 与探针 7(验收×测试承接面)并排互补的独立对账门(design §4,D-009 非目标 / R-07 口径注记
950
+ // 互指):探针 7 管「每条 acceptance 由哪些测试承接」,本矩阵管「design 接口段每个端点由哪些
951
+ // 验证用例/冒烟步骤覆盖」——fail-closed 记账,机械封住「接口层零派生」的 P1 缺陷面:
952
+ // - covered 记账:分子=判定 covered 的端点行;有效分母=N−non-testable 行数(N=解析端点数
953
+ // 或声明数,D-005);分子<有效分母或解析面有未覆盖端点 → error 逐条列缺覆盖端点
954
+ // - 锚点五形态(design §4 Grill #10):design接口表#<METHOD /path> 解析级(须命中
955
+ // facts.apiFace 解析产出集,防空指);权限矩阵[...]/契约表@.../DDL@.../载荷@... 形态级
956
+ // 存在即认;covered/partial 行缺锚点 → error(missingEvidence 口径同 probe7)
957
+ // - 移交联动(probe7 条件④同款形态):partial/uncovered 端点行>0 且 facts.handover 零
958
+ // 有效行 → error;blocking 是否封顶归 validatePassEligibility 条件②(④管有去向/②管去向级)
959
+ // - 探索行(uncovered+[探索] 标记)与消费端子行(两空格缩进 ↳ 前缀)不进分母分子
960
+ // - 声明降级(D-005):解析零行按声明数对账;并存以解析为准并注记 warning;解析零行零
961
+ // 声明×判级 critical → error(接口面不可静默为零);判级 critical×声明 0 端点 → warning
962
+ // - advisory(D-006/D-007 第一版,不阻断不进 errors):消费端归类在场而矩阵零子行 →
963
+ // warning 列端点;写端点(POST/PUT/DELETE/PATCH)未在权限矩阵段命中且无「无权限约束」
964
+ // 豁免 → warning
965
+ // 消费路径铁律(plan 全局硬约束 3):本模块零 import verify-probes——apiFace/consumerHints
966
+ // 数据经 facts 落盘面(readFactsForEligibility 同源读取,parseDesignApiTable 产出于
967
+ // verify-probes 侧 backfill 落盘)进入;矩阵面经 verify-result.md MD 槽解析(X-05 防篡改锚点,
968
+ // 锚定当前文档实态不读 producer 快照)。
969
+
970
+ /** 矩阵段标题定位(## 级;骨架渲染带「[层:…]」后缀——前缀匹配容忍,renderApiCoverageMatrixLines 字面同源) */
971
+ const API_COVERAGE_HEADING_RE = /^## 接口验证覆盖矩阵/m
972
+ /** 端点单元格 method+path 提取(预填行 `GET /orders/{id}` 形态;宽匹配容忍注记后缀) */
973
+ const API_ENDPOINT_CELL_RE = /(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|]+)/
974
+ /** 声明占位行(D-005 零解析降级侧,骨架「本变更接口面:N 端点(agent 声明)」字面同源) */
975
+ const API_DECLARED_ROW_RE = /本变更接口面[::]\s*(\d+)\s*端点/
976
+ /** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态) */
977
+ const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;))]+)/g
978
+ /** 探索行标记(骨架文法注释字面同源:证据列含 [探索]) */
979
+ const API_EXPLORATION_MARK_RE = /\[探索\]/
980
+
981
+ /**
982
+ * 行级提取 verify-result.md「## 接口验证覆盖矩阵」段(task-05 壳层 MD 槽解析,纯字符串零 IO)。
983
+ * 段定位:## 标题(前缀匹配容忍后缀)到下个同级(##)或更高级(#)标题。行分类:
984
+ * - 消费端子行:≥2 空白缩进 + ↳ 前缀(骨架文法字面同源)——只计数不进矩阵行账
985
+ * - 五列表行(复用 splitMatrixRowCells 切列):表头/分隔行跳过;端点列含声明行的 →
986
+ * declaredRow 单列(不进端点行账);其余 → 端点行(method/path 自端点列提取)
987
+ * - 声明占位行与探索行判定见上注释;CRLF/LF 容忍(\r 剥离)。
988
+ * @param {string} verifyMd verify-result.md 全文
989
+ * @returns {{
990
+ * present: boolean,
991
+ * rows: Array<{endpoint: string, method: string, path: string, verdict: string,
992
+ * caseId: string, result: string, evidence: string, unfilled?: boolean, exploration?: boolean}>,
993
+ * subRowCount: number,
994
+ * declaredRow: {declared: number, verdict: string, evidence: string}|null,
995
+ * }}
996
+ */
997
+ export function extractApiCoverageMatrixSlots(verifyMd) {
998
+ const out = { present: false, rows: [], subRowCount: 0, declaredRow: null }
999
+ const md = String(verifyMd || '')
1000
+ const hm = md.match(API_COVERAGE_HEADING_RE)
1001
+ if (!hm) return out
1002
+ out.present = true
1003
+ const rest = md.slice(hm.index + hm[0].length)
1004
+ const endMatch = rest.match(/^#{1,2}(?:[ \t]|$)/m)
1005
+ const sectionText = endMatch ? rest.slice(0, endMatch.index) : rest
1006
+
1007
+ for (const rawLine of sectionText.split(/\r?\n/)) {
1008
+ // 消费端子行(≥2 空白缩进 ↳ 前缀):不计矩阵行账,只计数(advisory 消费面判据)
1009
+ if (/^\s{2,}↳/.test(rawLine)) { out.subRowCount++; continue }
1010
+ const cells = splitMatrixRowCells(rawLine)
1011
+ if (!cells || cells.length !== 5) continue // 注记/防御/非五列表行不计槽
1012
+ const c = cells.map(x => x.trim())
1013
+ if (c.every(x => /^:?-{3,}:?$/.test(x))) continue // 分隔行
1014
+ if (c[0] === '端点' && c[1] === '判定') continue // 表头行
1015
+ const dm = c[0].match(API_DECLARED_ROW_RE)
1016
+ if (dm) {
1017
+ // 声明占位行(D-005 零解析降级侧):单列不进端点行账,declared 供 N 兜底
1018
+ out.declaredRow = { declared: parseInt(dm[1], 10), verdict: c[1], evidence: c[4] }
1019
+ continue
1020
+ }
1021
+ const em = c[0].match(API_ENDPOINT_CELL_RE)
1022
+ const row = {
1023
+ endpoint: c[0], method: em ? em[1].toUpperCase() : '', path: em ? em[2] : '',
1024
+ verdict: c[1], caseId: c[2], result: c[3], evidence: c[4],
1025
+ }
1026
+ if (!MATRIX_VERDICT_WHITELIST.has(row.verdict)) row.unfilled = true
1027
+ row.exploration = row.verdict === 'uncovered' && API_EXPLORATION_MARK_RE.test(`${row.result} ${row.evidence}`)
1028
+ out.rows.push(row)
1029
+ }
1030
+ return out
1031
+ }
1032
+
1033
+ /**
1034
+ * 证据锚点五形态命中(design §4 Grill #10):design接口表# 须提取出 ≥1 个 METHOD /path
1035
+ * 锚点才计命中(形态在而端点不可提取按缺锚计,fail-closed);其余四形态存在即认。
1036
+ * design接口表# 锚点是否命中解析产出集由调用侧逐锚核对(解析级校验),此处只管形态在场。
1037
+ */
1038
+ function apiEvidenceHasAnchorForm(evidence) {
1039
+ const e = String(evidence || '')
1040
+ API_ANCHOR_DESIGN_API_RE.lastIndex = 0
1041
+ if (API_ANCHOR_DESIGN_API_RE.test(e)) return true
1042
+ if (/权限矩阵\[[^\]]+\]/.test(e)) return true
1043
+ if (/契约表@\S/.test(e)) return true
1044
+ if (/DDL@\S/.test(e)) return true
1045
+ if (/载荷@\S/.test(e)) return true
1046
+ return false
1047
+ }
1048
+
1049
+ /**
1050
+ * 接口验证覆盖矩阵记账判定纯函数(X-09 双层形态的判定层:零 MD 解析、零 IO——矩阵面经
1051
+ * 壳层 extractApiCoverageMatrixSlots 解析传入,apiFace/consumerHints/handover 经 facts
1052
+ * 落盘面传入)。task-07 断言消费契约。
1053
+ * @param {{
1054
+ * matrix?: object|null, // extractApiCoverageMatrixSlots 产物
1055
+ * apiFace?: {endpoints?: Array<{method,path,rowIdx}>, declared?: number|null, writeEndpoints?: Array}|null,
1056
+ * consumerHints?: Record<string, string[]>|null,
1057
+ * facts?: object|null, // verify-facts.json(handover 联动用,缺 null)
1058
+ * factsExpected?: boolean, // false(存量未跑管线)→ 兼容 ok 不误伤
1059
+ * strict?: boolean, // isIrStrictVerifyChange(段缺失分层用)
1060
+ * riskLevel?: string|null, // detectChangeRisk().level
1061
+ * changeName?: string,
1062
+ * permSectionText?: string, // design.md 权限段(段头含 权限/角色 的段合并文本,壳内提取)
1063
+ * designText?: string, // design.md 全文(写端点豁免行级检测用)
1064
+ * }} [args]
1065
+ * @returns {{ ok: boolean, errors: string[], warnings: string[] }}
1066
+ */
1067
+ export function judgeApiCoverageMatrix(args) {
1068
+ const errors = []
1069
+ const warnings = []
1070
+ const a = args || {}
1071
+ // factsExpected=false(存量未跑管线)→ 沿用存量兼容口径 ok 不误伤(evaluatePassEligibility 同款边界)
1072
+ if (!a.factsExpected) return { ok: true, errors, warnings }
1073
+
1074
+ const changeName = a.changeName || '<变更名>'
1075
+ const INIT_HINT = `\`sillyspec verify-probes --change ${changeName} --init\``
1076
+ const matrix = a.matrix && typeof a.matrix === 'object' ? a.matrix : { present: false, rows: [], subRowCount: 0, declaredRow: null }
1077
+ const rows = Array.isArray(matrix.rows) ? matrix.rows : []
1078
+ const apiFace = a.apiFace && typeof a.apiFace === 'object' ? a.apiFace : null
1079
+ const endpoints = apiFace && Array.isArray(apiFace.endpoints)
1080
+ ? apiFace.endpoints.filter(e => e && e.method && e.path) : []
1081
+ const declared = apiFace && typeof apiFace.declared === 'number' ? apiFace.declared : null
1082
+ const writeEndpoints = apiFace && Array.isArray(apiFace.writeEndpoints)
1083
+ ? apiFace.writeEndpoints.filter(e => e && e.method && e.path) : []
1084
+ const consumerHints = a.consumerHints && typeof a.consumerHints === 'object' ? a.consumerHints : {}
1085
+ const criticalLevel = ['integration-critical', 'deployment-critical'].includes(a.riskLevel)
1086
+
1087
+ // ── 段缺失分层(对齐 :896-903 双层形态 + 判级 critical fail-closed 防删段绕过)。
1088
+ // apiFace 在场性 = 特性时序锚(X-08 无条件产出面,task-04 起 backfill 落盘):缺席说明
1089
+ // facts 最后一次回填早于本特性(特性间隙存量/旧管线 facts)→ warning 引导 --init 刷新
1090
+ // 即可(刷新后 apiFace 落盘、矩阵骨架补段,删段面转入上门硬口径);在场(新管线跑过,
1091
+ // 骨架必曾渲染矩阵段)而段缺失 = 删段 → 严格档/critical error。删 facts.json 的对抗面
1092
+ // 另由 checkProbeConsistency error 级 MD 锚点兜底(resolveFactsExpected 同口径注记)。──
1093
+ const newPipeline = apiFace != null
1094
+ if (!matrix.present) {
1095
+ if (newPipeline && a.strict) {
1096
+ errors.push(
1097
+ `接口验证覆盖矩阵段缺失(严格档,created_at ≥ ${IR_STRICT_SINCE})——verify-result.md 缺「## 接口验证覆盖矩阵」段。` +
1098
+ `修复:跑 ${INIT_HINT} 幂等补段(已有正文不覆盖),再逐行填判定与证据。`
1099
+ )
1100
+ } else if (newPipeline && criticalLevel) {
1101
+ errors.push(
1102
+ `接口验证覆盖矩阵段缺失且判级 ${a.riskLevel}——critical 变更接口面对账面不可缺失(防删段绕过对账,FR-04)。` +
1103
+ `修复:跑 ${INIT_HINT} 幂等补段后逐行填判定与证据。`
1104
+ )
1105
+ } else {
1106
+ warnings.push(
1107
+ `verify-result.md 缺「## 接口验证覆盖矩阵」段${newPipeline ? '(非严格档非判级 critical,存量不强制)' : '(facts.apiFace 缺席——特性前回填的存量底稿)'}——可跑 ${INIT_HINT} 幂等补段。`
1108
+ )
1109
+ }
1110
+ return { ok: errors.length === 0, errors, warnings }
1111
+ }
1112
+
1113
+ // ── 零接口面分层(D-005):判级 critical → error(接口面不可静默为零——「变更有接口面」以
1114
+ // 判级 critical 为机械代理);非判级 critical → 零行为零打扰(brownfield 兼容)──
1115
+ const declaredFromRow = matrix.declaredRow ? matrix.declaredRow.declared : null
1116
+ const zeroFace = endpoints.length === 0 && declared === null && declaredFromRow === null
1117
+ if (zeroFace && criticalLevel) {
1118
+ errors.push(
1119
+ `判级 ${a.riskLevel} 且接口面为零(design 接口段解析零端点且无「本变更接口面:N 端点」声明行)——critical 变更接口面不可静默为零(D-005/FR-04)。` +
1120
+ `修复:design.md 补接口段表格(每端点一行 METHOD /path)或声明行「本变更接口面:N 端点」,重跑 ${INIT_HINT} 刷新矩阵段与 facts 后逐行填判定。`
1121
+ )
1122
+ }
1123
+ // 判级 critical × 声明 0 端点 → warning 复核(D-005 故障面条款:显式零声明仍可能是漏写)
1124
+ if (criticalLevel && (declared === 0 || declaredFromRow === 0)) {
1125
+ warnings.push(
1126
+ `判级 ${a.riskLevel} 且声明接口面为 0 端点——critical 变更声明零接口面请复核(D-005):若实际触碰接口,补 design 接口段表格后重跑 ${INIT_HINT} 刷新对账面。`
1127
+ )
1128
+ }
1129
+
1130
+ const clip = (s) => { const t = String(s || ''); return t.length > 40 ? t.slice(0, 40) + '…' : t }
1131
+ const rowLabel = (r) => `${r.method && r.path ? `${r.method} ${r.path}` : clip(r.endpoint || r.verdict)}`
1132
+ const judgedRows = [...rows]
1133
+ if (matrix.declaredRow) judgedRows.push({ ...matrix.declaredRow, endpoint: `本变更接口面:${matrix.declaredRow.declared} 端点(agent 声明)`, method: '', path: '', caseId: '', result: '' })
1134
+
1135
+ // ── 行级判定槽/锚点/non-testable 理由校验(missingEvidence 口径同 probe7 :896-903)──
1136
+ const unfilledRows = judgedRows.filter(r => r.unfilled)
1137
+ if (unfilledRows.length > 0) {
1138
+ errors.push(
1139
+ `接口验证覆盖矩阵有 ${unfilledRows.length} 行判定未填(四选一 covered/partial/uncovered/non-testable):${unfilledRows.map(rowLabel).join(';')}。` +
1140
+ `修复:编辑 verify-result.md 接口验证覆盖矩阵段,把 <待填:四选一> 替换为判定值。`
1141
+ )
1142
+ }
1143
+ const parseSet = new Set(endpoints.map(e => `${e.method} ${e.path}`))
1144
+ const anchorViolations = []
1145
+ for (const r of judgedRows) {
1146
+ if (r.verdict !== 'covered' && r.verdict !== 'partial') continue
1147
+ const e = String(r.evidence || '')
1148
+ // design接口表# 锚点解析级核对(防空指):提取的 METHOD /path 须全部命中解析产出集
1149
+ API_ANCHOR_DESIGN_API_RE.lastIndex = 0
1150
+ for (const m of e.matchAll(API_ANCHOR_DESIGN_API_RE)) {
1151
+ const key = `${m[1]} ${m[2]}`
1152
+ if (!parseSet.has(key)) anchorViolations.push(`${rowLabel(r)}:design接口表#${key} 未命中 design 接口段解析面(空指)`)
1153
+ }
1154
+ if (!apiEvidenceHasAnchorForm(e)) anchorViolations.push(`${rowLabel(r)}:证据缺用例依据锚点(五形态之一)`)
1155
+ }
1156
+ if (anchorViolations.length > 0) {
1157
+ errors.push(
1158
+ `接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
1159
+ `(covered/partial 证据须含锚点五形态之一:design接口表#METHOD /path(须命中 design 接口段解析面)/ 权限矩阵[角色×动作] / 契约表@行标识 / DDL@列名 / 载荷@构造点路径):${anchorViolations.join(';')}。` +
1160
+ `修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
1161
+ )
1162
+ }
1163
+ // non-testable 理由非空即合法(:900 先例「non-testable 证据须写一句理由」,理由空按违规计)
1164
+ const ntMissing = judgedRows.filter(r => r.verdict === 'non-testable' && (String(r.evidence || '').trim() === '' || String(r.evidence).trim() === MATRIX_EVIDENCE_TODO || String(r.evidence).trim().startsWith('<待填')))
1165
+ if (ntMissing.length > 0) {
1166
+ errors.push(
1167
+ `接口验证覆盖矩阵有 ${ntMissing.length} 行 non-testable 缺理由(non-testable 证据须写一句理由):${ntMissing.map(rowLabel).join(';')}。` +
1168
+ `修复:在证据列为 non-testable 端点写一句不适用理由。`
1169
+ )
1170
+ }
1171
+
1172
+ // ── covered 记账(D-005/D-008):分子=covered 端点行;有效分母=N−non-testable 行数;
1173
+ // N=解析端点数,解析零行按声明数(declaredRow 优先——矩阵实态,facts.declared 兜底)。
1174
+ // 缺覆盖清单解析集驱动:解析面端点 − covered 行命中 − non-testable 行命中(partial/
1175
+ // uncovered/unfilled 端点行不在 covered 命中集即自然进清单;探索行不进分母分子——其
1176
+ // uncovered 判定使之天然不计分子,且不计入 non-testable 扣减)。──
1177
+ if (!zeroFace) {
1178
+ const N = endpoints.length > 0 ? endpoints.length : (declaredFromRow != null ? declaredFromRow : (declared != null ? declared : 0))
1179
+ const nonTestableCount = rows.filter(r => r.verdict === 'non-testable').length
1180
+ const validDenominator = Math.max(0, N - nonTestableCount)
1181
+ const coveredCount = rows.filter(r => r.verdict === 'covered').length
1182
+ const coveredSet = new Set(rows.filter(r => r.verdict === 'covered' && r.method && r.path).map(r => `${r.method} ${r.path}`))
1183
+ const nonTestSet = new Set(rows.filter(r => r.verdict === 'non-testable' && r.method && r.path).map(r => `${r.method} ${r.path}`))
1184
+ const missingEndpoints = endpoints.filter(e => {
1185
+ const k = `${e.method} ${e.path}`
1186
+ return !coveredSet.has(k) && !nonTestSet.has(k)
1187
+ })
1188
+ if (coveredCount < validDenominator || missingEndpoints.length > 0) {
1189
+ const list = missingEndpoints.length > 0
1190
+ ? missingEndpoints.map(e => `${e.method} ${e.path}`).join(';')
1191
+ : `声明 ${N} 端点而 covered 端点行仅 ${coveredCount}(解析零行降级,无法逐条列端点——按声明行拆出每端点行填判定)`
1192
+ errors.push(
1193
+ `接口验证覆盖矩阵覆盖不足:有效分母 ${validDenominator}(N=${N} − non-testable ${nonTestableCount}),covered 分子 ${coveredCount}——缺覆盖端点:${list}。` +
1194
+ `修复:补验证用例/冒烟步骤后把端点行改 covered 并填五形态锚点;不适用端点改 non-testable 并写一句理由;确未覆盖的走「## 移交项(结构化)」承载并保持 partial/uncovered(FR-04)。`
1195
+ )
1196
+ }
1197
+ // 声明与解析并存以解析为准并注记(骨架 :1573 漂移信号同款口径,数据面 facts.apiFace)
1198
+ if (declared !== null && endpoints.length > 0 && declared !== endpoints.length) {
1199
+ warnings.push(
1200
+ `声明与解析并存(声明 ${declared} 端点 / 解析 ${endpoints.length} 端点)——以解析为准,差异需复核(design 接口段与声明行不同步的漂移信号,D-005)。`
1201
+ )
1202
+ }
1203
+ }
1204
+
1205
+ // ── 移交联动(probe7 条件④ :1068-1073 同款形态):partial/uncovered 端点行>0 且
1206
+ // facts.handover 零有效行 → error(已覆盖不足且有未验证端点无去向不可静默);有行 →
1207
+ // 放行,blocking 是否封顶归 validatePassEligibility 条件②(④管有去向/②管去向级)。
1208
+ // 探索行属 uncovered 子集计入(探索性验证不算覆盖,同样须有去向);facts 缺失 →
1209
+ // fail-closed 按零有效行处理(factsExpected 已在入口把关,此处只防 facts 消失面)。──
1210
+ const partialRows = [
1211
+ ...rows.filter(r => r.verdict === 'partial' || r.verdict === 'uncovered'),
1212
+ ...(matrix.declaredRow && (matrix.declaredRow.verdict === 'partial' || matrix.declaredRow.verdict === 'uncovered')
1213
+ ? [{ ...matrix.declaredRow, endpoint: `本变更接口面:${matrix.declaredRow.declared} 端点(agent 声明)`, method: '', path: '' }] : []),
1214
+ ]
1215
+ if (partialRows.length > 0) {
1216
+ const factsMissing = !a.facts || typeof a.facts !== 'object'
1217
+ const handoverItems = !factsMissing && a.facts.handover && Array.isArray(a.facts.handover.items)
1218
+ ? a.facts.handover.items.filter(it => it && typeof it === 'object') : []
1219
+ if (handoverItems.length === 0) {
1220
+ errors.push(
1221
+ `接口验证覆盖矩阵含 ${partialRows.length} 行 partial/uncovered 且「## 移交项(结构化)」零有效行(facts.handover)——接口未验证端点必须有移交去向:${partialRows.map(rowLabel).join(';')}。` +
1222
+ `修复:在 verify-result.md 补「## 移交项(结构化)」有效行(任意 severity 均可,blocking 级另受 PASS 封顶约束),或修正矩阵判定(实际已覆盖的行改 covered),或降级结论为 PASS WITH NOTES 承载。` +
1223
+ (factsMissing
1224
+ ? `另:verify-facts.json 缺失/不可读而 factsExpected=true,fail-closed 按零有效行处理——重跑 verify-probes(${INIT_HINT} 幂等刷新底稿)。`
1225
+ : '')
1226
+ )
1227
+ } else {
1228
+ // 逐行关联 advisory(R-05 第一版,D-003 同款分工):行标识未命中任何 handover 条目文本
1229
+ // → console.warn 攒实证(文本命中弱关联,先 advisory 攒数据),不阻断不进 errors。
1230
+ const itemTexts = handoverItems.map(it => `${it.item || ''}|${it.condition || ''}`)
1231
+ for (const r of partialRows) {
1232
+ const keys = [r.method && r.path ? `${r.method} ${r.path}` : '', r.endpoint].map(k => String(k || '').trim()).filter(Boolean)
1233
+ const hit = keys.some(k => itemTexts.some(t => t.includes(k)))
1234
+ if (!hit) console.warn(`ℹ️ [advisory] 接口端点 ${rowLabel(r)} 的移交去向未在 handover 条目中命中——建议条目文本含该端点标识(advisory,攒实证;不阻断)`)
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ // ── advisory:消费面子行缺失(D-006 第一版)——consumerHints 有归类(design 清单启发式,
1240
+ // 变更级归类)而矩阵零子行 → warning 列端点(不阻断、不进 errors)。──
1241
+ const hintKinds = Object.keys(consumerHints).filter(k => Array.isArray(consumerHints[k]) && consumerHints[k].length > 0)
1242
+ if (hintKinds.length > 0 && matrix.subRowCount === 0 && (endpoints.length > 0 || (declaredFromRow != null ? declaredFromRow : declared) > 0)) {
1243
+ const list = endpoints.length > 0
1244
+ ? endpoints.slice(0, 8).map(e => `${e.method} ${e.path}`).join(';') + (endpoints.length > 8 ? '…' : '')
1245
+ : `声明 ${declaredFromRow != null ? declaredFromRow : declared} 端点(解析零行降级)`
1246
+ warnings.push(
1247
+ `[advisory] 消费端归类在场(${hintKinds.join('/')},facts.consumerHints——design 清单启发式)但矩阵零消费端子行——建议为接口端点补子行细分承接面(两空格缩进「↳ <消费端>:」形态,D-006,不阻断):${list}`
1248
+ )
1249
+ }
1250
+
1251
+ // ── advisory:写端点权限矩阵缺行(D-007/FR-06,文案照 design §5)——apiFace.writeEndpoints
1252
+ // 中端点未在权限矩阵段(壳内提取:design 段头含 权限/角色)命中(路径原串或模板基径)
1253
+ // 且无「无权限约束」豁免(design 行级/矩阵行级/权限段整体)→ warning。──
1254
+ const permText = String(a.permSectionText || '')
1255
+ const designLines = String(a.designText || '').replace(/\r\n/g, '\n').split('\n')
1256
+ const exemptTexts = [
1257
+ ...designLines,
1258
+ ...rows.map(r => `${r.endpoint}|${r.caseId}|${r.result}|${r.evidence}`),
1259
+ ]
1260
+ if (!permText.includes('无权限约束')) {
1261
+ for (const ep of writeEndpoints) {
1262
+ const base = String(ep.path).split(/[{:]/)[0]
1263
+ const hit = permText.includes(ep.path) || (base.length > 1 && permText.includes(base))
1264
+ if (hit) continue
1265
+ const rowExempt = exemptTexts.some(t => t.includes('无权限约束') && (t.includes(ep.path) || (base.length > 1 && t.includes(base))))
1266
+ if (!rowExempt) {
1267
+ warnings.push(
1268
+ `[advisory] 写端点 ${ep.method} ${ep.path} 未在权限矩阵声明——补行或显式豁免(表缺行会让派生框架继承你的洞)(D-007/FR-06,不阻断)`
1269
+ )
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ return { ok: errors.length === 0, errors, warnings }
1275
+ }
1276
+
1277
+ /**
1278
+ * design.md 权限矩阵段提取(task-05 壳层辅助,D-007 表间完备性数据面):段头(#/##/###)
1279
+ * 含 权限/角色 关键词的段体合并文本(含段头行——命中判据宽松侧,advisory 无害)。零段 → ''。
1280
+ */
1281
+ function extractPermissionMatrixText(designMd) {
1282
+ const text = String(designMd || '').replace(/\r\n/g, '\n')
1283
+ if (!text) return ''
1284
+ const out = []
1285
+ let collecting = false
1286
+ for (const line of text.split('\n')) {
1287
+ const h = line.match(/^#{1,3}\s+(.*)$/)
1288
+ if (h) {
1289
+ collecting = /权限|角色/.test(h[1])
1290
+ if (collecting) out.push(h[1])
1291
+ continue
1292
+ }
1293
+ if (collecting) out.push(line)
1294
+ }
1295
+ return out.join('\n')
1296
+ }
1297
+
1298
+ /**
1299
+ * 接口验证覆盖矩阵 validator 注册壳(与 validateAcceptanceMatrix 同三参签名同构,D-010:
1300
+ * 注册进 contracts.verify.validators 即覆盖 gates / machine-interface 等全部 runValidators
1301
+ * 调用方,gates.js 零改动)。壳内取数组装(IO/MD 槽解析全在壳层,判定是
1302
+ * judgeApiCoverageMatrix 纯函数——X-09 双层形态):
1303
+ * - tasks/ 不存在(quick 会话/旧变更)或 verify-result.md 未落盘(中间步骤)→ no-op
1304
+ * - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
1305
+ * - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
1306
+ * - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
1307
+ * - detectChangeRisk 判级(与 validatePassEligibility 壳同款 design/plan 双文件输入)
1308
+ * - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
1309
+ */
1310
+ export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
1311
+ const { specRoot } = context
1312
+ const errors = []
1313
+ const warnings = []
1314
+ const changeDir = resolveChangeDir(cwd, changeName, specRoot)
1315
+ // 无 TaskCard(tasks/ 不存在)→ 不校验(brownfield 零行为,validateAcceptanceMatrix 同门)
1316
+ if (!existsSync(join(changeDir, 'tasks'))) return { ok: true, errors, warnings }
1317
+ // verify-result.md 中间步骤未落盘 → 不校验(末步引擎存在性规则兜底)
1318
+ const verifyResultPath = join(changeDir, 'verify-result.md')
1319
+ if (!existsSync(verifyResultPath)) return { ok: true, errors, warnings }
1320
+ // 存量未跑管线(无 facts 且非严格档)→ 矩阵段/apiFace 俱不可得,零行为零打扰
1321
+ if (!resolveFactsExpected(changeDir)) return { ok: true, errors, warnings }
1322
+
1323
+ const facts = readFactsForEligibility(changeDir)
1324
+ const designText = readIfExists(join(changeDir, 'design.md'))
1325
+ const riskProfile = detectChangeRisk({
1326
+ designContent: designText,
1327
+ planContent: readIfExists(join(changeDir, 'plan.md')),
1328
+ })
1329
+ return judgeApiCoverageMatrix({
1330
+ matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
1331
+ apiFace: facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null,
1332
+ consumerHints: facts && facts.consumerHints && typeof facts.consumerHints === 'object' ? facts.consumerHints : null,
1333
+ facts,
1334
+ factsExpected: true,
1335
+ strict: isIrStrictVerifyChange(changeDir),
1336
+ riskLevel: riskProfile ? riskProfile.level : null,
1337
+ changeName,
1338
+ permSectionText: extractPermissionMatrixText(designText),
1339
+ designText,
1340
+ })
1341
+ }
1342
+
947
1343
  // ============ PASS 封顶事实面 validator(2026-09-17-pass-cap-semantics task-02 / D-001@v2 / D-010 / D-011) ============
948
1344
  //
949
1345
  // 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
@@ -954,6 +1350,8 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
954
1350
  // ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover)
955
1351
  // 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
956
1352
  // → 计入①事实面(D-004/FR-03,仅判级时计入)。
1353
+ // ⑤ 接口冒烟未跑(facts.smokeRan≠ran,仅判级 integration/deployment-critical 时计入,
1354
+ // FR-02/D-002@v1——2026-09-17-api-coverage-smoke task-03;不设 handover 豁免子句)。
957
1355
  //
958
1356
  // 分层纪律(D-011):事实生产走 facts 管线(verify-probes backfillFactsFromMdAndTests 首次
959
1357
  // backfill 主路径无条件产出,X-08 时序——gates 收尾前置 backfill 先于 runValidators);本壳只做
@@ -1005,7 +1403,7 @@ function countFactsHandoverItems(changeDir) {
1005
1403
  * changeName?: string,
1006
1404
  * dbScriptCandidates?: string[],
1007
1405
  * }} [args] dbScriptCandidates = 壳内取数的 verify 时点文件集(design 清单 ∪ worktree 变更)∩ db 目录任意层级 .sql
1008
- * @returns {{ ok: boolean, errors: string[], triggered: Array<{ fact: 'integration-not-run'|'blocking-handover-present'|'db-script-undeclared'|'matrix-partial-no-handover', detail: string }> }}
1406
+ * @returns {{ ok: boolean, errors: string[], triggered: Array<{ fact: 'integration-not-run'|'blocking-handover-present'|'db-script-undeclared'|'matrix-partial-no-handover'|'smoke-not-run', detail: string }> }}
1009
1407
  */
1010
1408
  export function evaluatePassEligibility(args) {
1011
1409
  const errors = []
@@ -1086,6 +1484,29 @@ export function evaluatePassEligibility(args) {
1086
1484
  )
1087
1485
  }
1088
1486
 
1487
+ // ── ⑤ smoke 冒烟未跑(2026-09-17-api-coverage-smoke task-03 / FR-02 / D-002@v1,判级
1488
+ // 限定——criticalLevel 在场才计入,runtimeEndpointExcluded 附加条件同款形态):判级
1489
+ // integration/deployment-critical 且 facts.smokeRan !== 'ran'(producer 自 quality-scan
1490
+ // 记录 smokeResult 段推导)→ triggered 加枚举 smoke-not-run。不设 handover 豁免子句——
1491
+ // advisory handover 在场不构成 smoke 缺失的 PASS 豁免(smoke 缺失的合法出路只有配
1492
+ // commands.smoke 复跑质量扫描步 / 降级 PASS WITH NOTES 移交承载两条);blocking handover
1493
+ // 在场由条件②独立拦下(出口=NOTES),两条件各自触发不互斥。factsMissing 沿双源
1494
+ // fail-closed 同族口径按触发处理(smokeRan 不可证伪);facts 在场但字段不在场(升级
1495
+ // 过渡期存量 facts,producer 重新 backfill 前)同 !== 'ran' 触发——fail-closed 侧,防
1496
+ // 「判级 critical 未跑冒烟静默 PASS」。非判级(unit-sufficient 等)任意 smokeRan 零行为。
1497
+ if (criticalLevel && (factsMissing || facts.smokeRan !== 'ran')) {
1498
+ const smokeDetail = factsMissing
1499
+ ? 'facts 缺失(fail-closed,smokeRan 不可证伪按未跑处理)'
1500
+ : `facts.smokeRan=${facts.smokeRan == null ? '不在场(按未跑处理)' : facts.smokeRan}`
1501
+ triggered.push({ fact: 'smoke-not-run', detail: `${smokeDetail},判级 ${a.changeRiskProfile.level}` })
1502
+ const configHint = facts.smokeRan === 'not-configured'
1503
+ ? '(not-configured:先在 local.yaml 配置键 commands.smoke,如 smoke: "npm run smoke")' : ''
1504
+ errors.push(
1505
+ `[fact smoke-not-run] 接口冒烟未跑(${smokeDetail})且判级 ${a.changeRiskProfile.level}——critical 变更结论=PASS 须接口冒烟绿跑(facts.smokeRan=ran,FR-02/D-002)。` +
1506
+ `出路:①配 commands.smoke 并复跑质量扫描步${configHint};②降级承载——${NOTES_FIX}(不算失败)。`
1507
+ )
1508
+ }
1509
+
1089
1510
  return { ok: errors.length === 0, errors, triggered }
1090
1511
  }
1091
1512
 
@@ -1370,7 +1791,10 @@ const contracts = {
1370
1791
  // 注册进 validator 链即覆盖 gates / machine-interface 等全部 runValidators 调用方(gates.js 零改动)。
1371
1792
  // validatePassEligibility:PASS 封顶事实面(2026-09-17-pass-cap-semantics task-02 / D-001@v2 /
1372
1793
  // D-010)——注册即覆盖同上;回退 = 移除本注册行(纯加法,未触发四条件行为零变化)。
1373
- validators: [validateVerifyOutputs, validateAcceptanceMatrix, validatePassEligibility],
1794
+ // validateApiCoverageMatrix:接口验证覆盖矩阵对账门(2026-09-17-api-coverage-smoke task-05 /
1795
+ // FR-04~FR-06,与探针 7 并排互补的独立章节)——注册即覆盖同上;回退 = 移除本注册行(纯加法,
1796
+ // factsExpected=false / 无矩阵段的存量变更零行为)。
1797
+ validators: [validateVerifyOutputs, validateAcceptanceMatrix, validatePassEligibility, validateApiCoverageMatrix],
1374
1798
  },
1375
1799
  archive: {
1376
1800
  stage: 'archive',
@@ -10,7 +10,11 @@
10
10
  * - brainstorm:前 3 条 =「交叉审查模型」三层检查(prompt 内渲染为「N. 」序号行),
11
11
  * 后 8 条 =「交叉点抽取」(渲染为「- 」列表行);
12
12
  * - plan:渲染为「- [ ] 」checkbox 行(审查清单段);
13
- * - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」)。
13
+ * - execute:渲染为 2 空格缩进的「N. 」序号行(QA 段「以下三项始终必查」);
14
+ * - verify(2026-09-17-api-coverage-smoke task-06 / FR-07 新增键):smoke 纪律条目,
15
+ * 渲染为「输出验证报告」步「### 操作」第 8 条下的 3 空格缩进「- 」列表行;
16
+ * 命中条件以条目文本内嵌说明(local.yaml 配 commands.smoke 或判级 critical 时生效——
17
+ * review-dispatch worker_prompt 单独消费条目,无 prompt 上下文,须自含)。
14
18
  */
15
19
  export const REVIEW_CHECKLISTS = {
16
20
  brainstorm: [
@@ -43,4 +47,12 @@ export const REVIEW_CHECKLISTS = {
43
47
  'design.md 整体对照(最终实现拼起来是否仍符合设计意图,而非仅各 task 局部合规)',
44
48
  '组装行为(全量测试/构建/启动通过——单 task 测试全绿 ≠ 组装正确)',
45
49
  ],
50
+ // verify 键(2026-09-17-api-coverage-smoke task-06 / FR-07):smoke 纪律四段,逐字照 design §6 ①~④;
51
+ // 首条内嵌命中条件说明(worker_prompt 单独消费条目,须自含);②③④段尾分号/句号照 design 原文保留。
52
+ verify: [
53
+ 'smoke 纪律(命中条件:local.yaml 配置 commands.smoke 或本变更判级 integration/deployment-critical 时生效)——①断言派生表:design 接口表每行 ≥1 happy-path(含出参形状断言)/ 权限矩阵每行 ≥1 反例(非授权操作应拒)/ 契约表必填每项 ≥1 空值反例 / 转移表每边 ≥1 状态断言 / 需求字面(单号格式等)→ 格式断言;',
54
+ '②负向下界:每写端点 ≥1 权限反例(E4 类)+ 全链 ≥1 注错全量回滚断言(E5 类);',
55
+ '③执行口径:脚本后台起服+轮询就绪+finally 杀(墙钟增量≈冒烟本体)、DB 会话自设严格 sql_mode、载荷从消费端构造点导出(手写正确字段的测试抓不住字段漂移);',
56
+ '④断言锚点注释(每步挂依据 ID)与矩阵行一一对应。',
57
+ ],
46
58
  }
@@ -1,3 +1,5 @@
1
+ import { REVIEW_CHECKLISTS } from '../stage-review-checklist.js'
2
+
1
3
  export const definition = {
2
4
  name: 'verify',
3
5
  title: '验证确认',
@@ -194,7 +196,7 @@ export const definition = {
194
196
  - **误判时的诚实出路(豁免级)**:在 design.md 顶部 frontmatter 加一行 \`risk_level: <真实等级>\`(doc-only / unit-sufficient / contract-required / integration-critical / deployment-critical),CLI 会以声明为准覆盖关键词判级。声明后若是 unit-sufficient 等豁免级,PASS WITH NOTES 不再被强制拦;但结论为 PASS 仍需对应证据。
195
197
  - **留痕要求(防逃逸)**:用了显式声明,必须在本报告「变更风险等级」section 写明「risk_level 由 design frontmatter 显式声明 = <等级>(覆盖关键词判级)」+ 一句话理由,让豁免可审计;若有命中被否定语境抑制,同样写明被抑制关键词与理由(抑制可审计,不许用来静默降级)。
196
198
  你只需:在 verify-result.md 的「变更风险等级」section 如实记录变更性质;若变更涉及 daemon/backend 跨进程、session/lease/lifecycle 状态机、或部署启动路径,在「Runtime Evidence」section 提供真实集成证据(启动命令、daemon↔backend 调用与日志关键片段、终态断言)。
197
- - **集成证据是自报告、CLI 不独立运行时核验**:「Runtime Evidence」由你如实填写,CLI 只校验其**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过。务必实跑后据实填写,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
199
+ - **集成回执须真实执行——commands.smoke 配置后由 CLI 亲跑并机器落盘(source: cli-noai-smoke 标记,亲跑段谎报无效)**,其余形态仍须 agent 实跑后据实填写:CLI 对其余形态只校验**字面存在**(是否含关键词),**不会替你启动 daemon、打真实请求或跑迁移**——它是否名副其实取决于你是否实跑过,不得凭堆关键词通过门控。(测试套件对账另算:commands.test 由 CLI 真实执行,那块谎报无效。)
198
200
 
199
201
  3. **生成 verify-result.md 骨架(勿从零手写)**:先跑 \`sillyspec verify-probes --change <change-name> --init\`——一条命令生成十章节骨架(已存在不覆盖),其中**探针结果章节已机械预填**(探针 1 的 TODO/FIXME 命中清单、探针 3 的测试覆盖、探针 5 的 API 契约对账表、探针 6 的删除对账三态判定),文件落 \`{SPEC_ROOT}/changes/<change-name>/verify-result.md\`(CLI 替换出的**主仓绝对路径**;若当前 cwd 在 worktree 内也绝不落 worktree 副本——CLI 校验读主仓,副本随 worktree 清理蒸发)。你只需把各 \`<!--TODO-->\` 占位替换为语义结论;半语义探针(2 关键词覆盖 / 4 决策追踪)与断言抽查、集成盲区标注由你补在对应 TODO 处
200
202
  4. **预填探针段不可篡改或删除**:verify \`--done\` gate 会重跑探针对比 verify-result.md 正文,不符即拦(ERROR);对探针结果有异议只能在预填段旁追加说明,不得改写原文
@@ -202,12 +204,14 @@ export const definition = {
202
204
  6. 给出结论:PASS / PASS WITH NOTES / FAIL(受风险门控约束)——**结论只认骨架「结论枚举:」槽行(行首锚定,把 <待填:三选一> 整体替换为枚举值);槽留待填会被 gate 判不过,正文其他位置的 PASS/FAIL 字样不参与判定**
203
205
  - **结论想写 PASS 前自查四事实条件(PASS 封顶语义,--done 门禁按 verify-facts.json 实拦)**:①集成实测未跑 ②「## 移交项(结构化)」含 blocking 级行 ③db/**/*.sql 未声明执行(verify-result.md 无「已对目标库执行」声明、回执 command 亦不含该文件)④验收×测试覆盖矩阵含 partial/uncovered 行且移交项零有效行——任一成立即不得写 PASS:改写 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)。severity 口径:db-script/env-blocked 恒 blocking;manual-acceptance/other 默认 advisory、须显式标 blocking 才计入封顶;确需把 blocking 降为 advisory 必须附「(降级:<理由>,依据 <file:line 或 D-xxx>)」
204
206
  7. **核对 module-impact.md**(若 \`{SPEC_ROOT}/changes/<change>/module-impact.md\` 存在):对照本次实际代码变更(git diff)与 module-impact.md 的模块影响矩阵,发现不一致(漏标受影响模块 / 影响类型错误 / 实际未触碰的模块被误标)则在 verify-result.md 标注。module-impact 由 plan 首版生成、execute 各 Wave 更新,verify 是最后一次核对机会(archive 仅终审不再生成)。这是 advisory 核对(不阻断 verify 完成),但 module-impact 与实际严重背离应记为风险。
207
+ 8. **smoke 纪律(local.yaml 配置 commands.smoke 或本变更判级 integration/deployment-critical 时执行以下纪律;本步 prompt 为静态导出,命中条件以文本内嵌说明注入——未命中时本段不适用)**:
208
+ ${REVIEW_CHECKLISTS.verify.map((item) => ' - ' + item).join('\n')}
205
209
 
206
210
  ### verify-result.md 章节结构(骨架已含,占位替换即可)
207
211
 
208
212
  结论(PASS / PASS WITH NOTES / FAIL)→ 证据账(cannot_verify 任务,槽段)→ 集成验证回执(槽段)→ 任务完成度 → 设计一致性 → 探针结果(已预填)→ 测试结果 → 决策追踪矩阵(\`| 决策 ID | FR | Task | Evidence | 状态 |\`,存在 decisions.md 才留)→ 技术债务 → 变更风险等级 → Runtime Evidence → 代码审查。
209
213
 
210
- **「## 集成验证回执」槽行结构**(integration/deployment-critical 变更必填;其余写「无」)双形态二选一(多行 YAML 形态推荐,字段序无关):\n- claim: <一句话>\n command: <命令>\n exit: <0 或非 0>\n log: <日志路径>\n亦认单行管道形态:\`- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径>\`——CLI 一致性校验四条件:log 存在 × mtime 在 verify 窗口内 × 日志尾无失败签名(error/exception/traceback/fatal 行首,剔除「0 errors」类良性行)× exit 0;全绿才算在场证据,字面措辞不再参与判定(v2 起 literals 仅存量回退)。CLI 不代跑集成进程——回执必须来自你真实执行过的命令。
214
+ **「## 集成验证回执」槽行结构**(integration/deployment-critical 变更必填;其余写「无」)双形态二选一(多行 YAML 形态推荐,字段序无关):\n- claim: <一句话>\n command: <命令>\n exit: <0 或非 0>\n log: <日志路径>\n亦认单行管道形态:\`- claim: <一句话> | command: <命令> | exit: <0 或非 0> | log: <日志路径>\`——CLI 一致性校验四条件:log 存在 × mtime 在 verify 窗口内 × 日志尾无失败签名(error/exception/traceback/fatal 行首,剔除「0 errors」类良性行)× exit 0;全绿才算在场证据,字面措辞不再参与判定(v2 起 literals 仅存量回退)。commands.smoke 配置后由 CLI 亲跑并机器落盘(source: cli-noai-smoke 标记的机器段即其产物);其余形态仍须 agent 实跑后据实填写(CLI 不代跑其余集成进程——回执必须来自你真实执行过的命令)。
211
215
 
212
216
  **Runtime Evidence 行结构**(integration/deployment-critical 必填;按实际触碰的运行时组件写,未涉及的行写「不涉及」勿堆关键词):长驻进程启动命令 / 触碰的服务端点 / 触发核心路径的请求(附关键响应)/ 进程日志关键片段(证明走了新路径)/ 生命周期终态断言(初始态→运行态→终态)/ 失败模式排除(逐条说明为何未触发)。
213
217
 
@@ -70,7 +70,10 @@ const RECEIPT_CONT_LINE_RE = /^[ \t]+([A-Za-z_][\w-]*):\s*(.*)$/
70
70
  * 解析 verify-result.md 的两个受控槽段(D-001@v2 slot-backfill 录入界面)。
71
71
  * 占位形态(<待填:三选一> / <待填:0 或非 0>)不含枚举词,行首锚定 + 枚举词双保险 fail-closed。
72
72
  * @param {string} mdText verify-result.md 全文(LF 归一由调用方 readFileSync 层负责或此处容错)
73
- * @returns {{ requiredEvidence: Array<{task,status,exempt:boolean,exemptionReason:string|null,verifiedFiles:string[]}>, runtimeEvidence: Array<{claim,command,exitCode,logPath}>, hasEvidenceSlot: boolean, hasReceiptSlot: boolean }}
73
+ * @returns {{ requiredEvidence: Array<{task,status,exempt:boolean,exemptionReason:string|null,verifiedFiles:string[]}>, runtimeEvidence: Array<{claim,command,exitCode,logPath,source?:string}>, hasEvidenceSlot: boolean, hasReceiptSlot: boolean }}
74
+ * runtimeEvidence[].source(2026-09-17-api-coverage-smoke task-02,additive):单行形态 |/|
75
+ * 尾注 `source: <token>` 或多行 YAML `source:` 字段回填(CLI 机器段标记 cli-noai-smoke 的
76
+ * 解析入口);无标记条目不落键,四字段 fail-closed 语义不动。
74
77
  */
75
78
  export function parseEvidenceSlots(mdText) {
76
79
  const text = String(mdText || '').replace(/\r\n/g, '\n')
@@ -128,11 +131,17 @@ export function parseEvidenceSlots(mdText) {
128
131
  for (let i = 0; i < receiptBody.length; i++) {
129
132
  const m = receiptBody[i].match(RECEIPT_LINE_RE)
130
133
  if (m) {
134
+ // source 尾注提取(2026-09-17-api-coverage-smoke task-02,additive 第五字段):log 是
135
+ // rest-of-line,行内 |/| 尾注段中认 `source: <token>`(CLI 机器段行尾注形态
136
+ // 「| source: cli-noai-smoke」);首段 log 剥尾注口径零变化,四字段收取不动,无尾注
137
+ // 条目不落 source 键(存量回执零回归)。
138
+ const srcM = m[4].match(/[||]\s*source:\s*([^\s||]+)/)
131
139
  runtimeEvidence.push({
132
140
  claim: m[1].trim(),
133
141
  command: m[2].trim(),
134
142
  exitCode: Number(m[3]),
135
143
  logPath: m[4].split(/[||]/)[0].trim().replace(/^`|`$/g, ''),
144
+ ...(srcM ? { source: srcM[1] } : {}),
136
145
  })
137
146
  continue
138
147
  }
@@ -155,7 +164,10 @@ export function parseEvidenceSlots(mdText) {
155
164
  ? fields.log.split(/[||]/)[0].trim().replace(/^`|`$/g, '')
156
165
  : ''
157
166
  if (!claim || !command || !/^\d+$/.test(exit) || !log) continue
158
- runtimeEvidence.push({ claim, command, exitCode: Number(exit), logPath: log })
167
+ // source 字段回填(多行 YAML 形态,task-02 additive):fields.source 聚合时已过
168
+ // stripPairedBackticks(同款口径),空值不落键——四字段 fail-closed 收取条件不动。
169
+ const src = typeof fields.source === 'string' ? fields.source.trim() : ''
170
+ runtimeEvidence.push({ claim, command, exitCode: Number(exit), logPath: log, ...(src ? { source: src } : {}) })
159
171
  }
160
172
  }
161
173
 
@@ -190,12 +202,16 @@ export function validateFactsV2(facts) {
190
202
  errors.push(`requiredEvidence[${item.task}] satisfied 但 verifiedFiles 为空`)
191
203
  }
192
204
  }
193
- // PASS 封顶事实面五字段 additive 登记(2026-09-17-pass-cap-semantics task-01 / D-011 故障面):
194
- // 字段不在场一律不报错(存量 facts 零迁移通过,producer=backfillFactsFromMdAndTests 首次
195
- // backfill 产出);在场才校验类型/枚举。schemaVersion 保持 2。
205
+ // PASS 封顶事实面 additive 登记(2026-09-17-pass-cap-semantics task-01 / D-011 故障面;
206
+ // smokeRan 增 2026-09-17-api-coverage-smoke task-03 / FR-02):字段不在场一律不报错
207
+ // (存量 facts 零迁移通过,producer=backfillFactsFromMdAndTests 首次 backfill 产出);
208
+ // 在场才校验类型/枚举。schemaVersion 保持 2。
196
209
  if (facts.integrationRan != null && !['ran', 'not-ran'].includes(facts.integrationRan)) {
197
210
  errors.push(`integrationRan 非法枚举值:${facts.integrationRan}`)
198
211
  }
212
+ if (facts.smokeRan != null && !['ran', 'not-ran', 'not-configured'].includes(facts.smokeRan)) {
213
+ errors.push(`smokeRan 非法枚举值:${facts.smokeRan}`)
214
+ }
199
215
  if (facts.dbScriptDeclarations != null
200
216
  && (!Array.isArray(facts.dbScriptDeclarations) || !facts.dbScriptDeclarations.every(x => typeof x === 'string'))) {
201
217
  errors.push('dbScriptDeclarations 应为 string[]')