sillyspec 3.28.11 → 3.28.13
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 +222 -223
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -8
- package/.claude/skills/sillyspec-verify/SKILL.md +87 -91
- 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 +29 -7
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +2 -0
- package/src/decision-distill.js +32 -25
- package/src/diagnostic-codes.js +95 -0
- package/src/doctor-diagnostics.js +170 -1
- package/src/fr-index.js +359 -0
- package/src/knowledge-stats.js +99 -2
- package/src/machine-interface.js +37 -0
- package/src/run/archive-distill.js +29 -0
- package/src/run/prompt.js +32 -1
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract.js +496 -19
- package/src/stage-review-checklist.js +13 -1
- package/src/stages/brainstorm.js +6 -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 +1293 -27
package/src/stage-contract.js
CHANGED
|
@@ -17,6 +17,11 @@ import { evaluateRules } from './stage-contract-engine.js'
|
|
|
17
17
|
import { getRule } from './stage-contract-spec.js'
|
|
18
18
|
import { backfillFrontmatter } from './scan-postcheck.js'
|
|
19
19
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
20
|
+
// FR 索引消费(2026-09-18-fr-index-l1 L1):重复嫌疑软门(advisory)——fr-index 是纯 fs+decision-distill
|
|
21
|
+
// 叶子链,静态引入无环;knowledge-hits 同为叶子。
|
|
22
|
+
import { parseChangeRequirements, resolveTouchedDomains, readActiveFrDigest, frTitleOverlap } from './fr-index.js'
|
|
23
|
+
import { discoverModuleIndex } from './decision-distill.js'
|
|
24
|
+
import { appendKnowledgeHit } from './knowledge-hits.js'
|
|
20
25
|
// design.md 文件清单解析(2026-09-17-pass-cap-semantics task-02 事实③):change-list 是纯 fs
|
|
21
26
|
// 叶子模块(design-facts / scope-audit / verify-probes 等同款直连惯例),静态引入无环。
|
|
22
27
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
@@ -321,6 +326,37 @@ function validateBrainstormOutputs(cwd, changeName, context = {}) {
|
|
|
321
326
|
const errors = [...engineResult.errors]
|
|
322
327
|
const warnings = [...engineResult.warnings]
|
|
323
328
|
|
|
329
|
+
// ── FR 重复嫌疑软门(2026-09-18-fr-index-l1 L1,D-005:advisory 永不阻断)──
|
|
330
|
+
// 新 requirements 的无承接 FR × 同域 active 索引条目标题 bigram 重叠 ≥0.6 → warning
|
|
331
|
+
// (双出路:加承接行或改标题区分)+ fr-duplicate-warning 遥测(L3 证据发生器指标③)。
|
|
332
|
+
// fail-soft 全包:索引不存在/解析失败零打扰(同步实现——全部叶子静态导入)。
|
|
333
|
+
try {
|
|
334
|
+
const frKnowledgeRoot = specRoot ? join(specRoot, 'knowledge') : join(cwd, '.sillyspec', 'knowledge')
|
|
335
|
+
const req = parseChangeRequirements(changeDir)
|
|
336
|
+
if (!req.missing && req.frs.length > 0) {
|
|
337
|
+
const domains = resolveTouchedDomains(changeDir, discoverModuleIndex(frKnowledgeRoot))
|
|
338
|
+
const active = readActiveFrDigest(frKnowledgeRoot, domains)
|
|
339
|
+
if (active.length > 0) {
|
|
340
|
+
for (const fr of req.frs) {
|
|
341
|
+
if (fr.supersedes.length > 0 || !fr.title) continue
|
|
342
|
+
let hit = null
|
|
343
|
+
for (const a of active) {
|
|
344
|
+
const o = frTitleOverlap(fr.title, a.title)
|
|
345
|
+
if (!hit || o > hit.o) hit = { a, o }
|
|
346
|
+
}
|
|
347
|
+
if (hit && hit.o >= 0.6) {
|
|
348
|
+
warnings.push(`疑似重复 FR:新「${fr.local} ${fr.title}」与现行 ${hit.a.id}「${hit.a.title}」标题重叠度 ${(hit.o * 100).toFixed(0)}%——若为改写/取代请在该 FR 块加承接行 \`承接: ${hit.a.id}\`,若为不同需求请改标题区分(advisory,L1 观察指标)`)
|
|
349
|
+
try {
|
|
350
|
+
appendKnowledgeHit(join(specRoot || join(cwd, '.sillyspec'), '.runtime'), {
|
|
351
|
+
type: 'fr-duplicate-warning', change: changeName, title: fr.title, candidate: hit.a.id, overlap: Number(hit.o.toFixed(2)),
|
|
352
|
+
})
|
|
353
|
+
} catch { /* 遥测 fail-soft */ }
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch { /* FR 软门 fail-soft:索引/解析异常零打扰 */ }
|
|
359
|
+
|
|
324
360
|
if (designContent) {
|
|
325
361
|
const content = designContent
|
|
326
362
|
|
|
@@ -944,16 +980,416 @@ function validateAcceptanceMatrix(cwd, changeName, context = {}) {
|
|
|
944
980
|
return { ok: errors.length === 0, errors, warnings }
|
|
945
981
|
}
|
|
946
982
|
|
|
983
|
+
// ============ 接口验证覆盖矩阵门禁(2026-09-17-api-coverage-smoke task-05 / FR-04~FR-06) ============
|
|
984
|
+
//
|
|
985
|
+
// 与探针 7(验收×测试承接面)并排互补的独立对账门(design §4,D-009 非目标 / R-07 口径注记
|
|
986
|
+
// 互指):探针 7 管「每条 acceptance 由哪些测试承接」,本矩阵管「design 接口段每个端点由哪些
|
|
987
|
+
// 验证用例/冒烟步骤覆盖」——fail-closed 记账,机械封住「接口层零派生」的 P1 缺陷面:
|
|
988
|
+
// - covered 记账:分子=判定 covered 的端点行;有效分母=N−non-testable 行数(N=解析端点数
|
|
989
|
+
// 或声明数,D-005);分子<有效分母或解析面有未覆盖端点 → error 逐条列缺覆盖端点
|
|
990
|
+
// - 锚点五形态(design §4 Grill #10):design接口表#<METHOD /path> 解析级(须命中
|
|
991
|
+
// facts.apiFace 解析产出集,防空指);权限矩阵[...]/契约表@.../DDL@.../载荷@... 形态级
|
|
992
|
+
// 存在即认;covered/partial 行缺锚点 → error(missingEvidence 口径同 probe7)
|
|
993
|
+
// - 移交联动(probe7 条件④同款形态):partial/uncovered 端点行>0 且 facts.handover 零
|
|
994
|
+
// 有效行 → error;blocking 是否封顶归 validatePassEligibility 条件②(④管有去向/②管去向级)
|
|
995
|
+
// - 探索行(uncovered+[探索] 标记)与消费端子行(两空格缩进 ↳ 前缀)不进分母分子
|
|
996
|
+
// - 声明降级(D-005):解析零行按声明数对账;并存以解析为准并注记 warning;解析零行零
|
|
997
|
+
// 声明×判级 critical → error(接口面不可静默为零);判级 critical×声明 0 端点 → warning
|
|
998
|
+
// - advisory(D-006/D-007 第一版,不阻断不进 errors):消费端归类在场而矩阵零子行 →
|
|
999
|
+
// warning 列端点;写端点(POST/PUT/DELETE/PATCH)未在权限矩阵段命中且无「无权限约束」
|
|
1000
|
+
// 豁免 → warning
|
|
1001
|
+
// 消费路径铁律(plan 全局硬约束 3):本模块零 import verify-probes——apiFace/consumerHints
|
|
1002
|
+
// 数据经 facts 落盘面(readFactsForEligibility 同源读取,parseDesignApiTable 产出于
|
|
1003
|
+
// verify-probes 侧 backfill 落盘)进入;矩阵面经 verify-result.md MD 槽解析(X-05 防篡改锚点,
|
|
1004
|
+
// 锚定当前文档实态不读 producer 快照)。
|
|
1005
|
+
|
|
1006
|
+
/** 矩阵段标题定位(## 级;骨架渲染带「[层:…]」后缀——前缀匹配容忍,renderApiCoverageMatrixLines 字面同源) */
|
|
1007
|
+
const API_COVERAGE_HEADING_RE = /^## 接口验证覆盖矩阵/m
|
|
1008
|
+
/** 端点单元格 method+path 提取(预填行 `GET /orders/{id}` 形态;宽匹配容忍注记后缀) */
|
|
1009
|
+
const API_ENDPOINT_CELL_RE = /(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|]+)/
|
|
1010
|
+
/** 声明占位行(D-005 零解析降级侧,骨架「本变更接口面:N 端点(agent 声明)」字面同源) */
|
|
1011
|
+
const API_DECLARED_ROW_RE = /本变更接口面[::]\s*(\d+)\s*端点/
|
|
1012
|
+
/** design接口表# 锚点提取(解析级校验用——防空指,Grill #10:非物理行号,METHOD /path 形态) */
|
|
1013
|
+
const API_ANCHOR_DESIGN_API_RE = /design接口表#\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/[^\s|,。;;))]+)/g
|
|
1014
|
+
/** 探索行标记(骨架文法注释字面同源:证据列含 [探索]) */
|
|
1015
|
+
const API_EXPLORATION_MARK_RE = /\[探索\]/
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* 行级提取 verify-result.md「## 接口验证覆盖矩阵」段(task-05 壳层 MD 槽解析,纯字符串零 IO)。
|
|
1019
|
+
* 段定位:## 标题(前缀匹配容忍后缀)到下个同级(##)或更高级(#)标题。行分类:
|
|
1020
|
+
* - 消费端子行:≥2 空白缩进 + ↳ 前缀(骨架文法字面同源)——只计数不进矩阵行账
|
|
1021
|
+
* - 五列表行(复用 splitMatrixRowCells 切列):表头/分隔行跳过;端点列含声明行的 →
|
|
1022
|
+
* declaredRow 单列(不进端点行账);其余 → 端点行(method/path 自端点列提取)
|
|
1023
|
+
* - 声明占位行与探索行判定见上注释;CRLF/LF 容忍(\r 剥离)。
|
|
1024
|
+
* @param {string} verifyMd verify-result.md 全文
|
|
1025
|
+
* @returns {{
|
|
1026
|
+
* present: boolean,
|
|
1027
|
+
* rows: Array<{endpoint: string, method: string, path: string, verdict: string,
|
|
1028
|
+
* caseId: string, result: string, evidence: string, unfilled?: boolean, exploration?: boolean}>,
|
|
1029
|
+
* subRowCount: number,
|
|
1030
|
+
* declaredRow: {declared: number, verdict: string, evidence: string}|null,
|
|
1031
|
+
* }}
|
|
1032
|
+
*/
|
|
1033
|
+
export function extractApiCoverageMatrixSlots(verifyMd) {
|
|
1034
|
+
const out = { present: false, rows: [], subRowCount: 0, declaredRow: null }
|
|
1035
|
+
const md = String(verifyMd || '')
|
|
1036
|
+
const hm = md.match(API_COVERAGE_HEADING_RE)
|
|
1037
|
+
if (!hm) return out
|
|
1038
|
+
out.present = true
|
|
1039
|
+
const rest = md.slice(hm.index + hm[0].length)
|
|
1040
|
+
const endMatch = rest.match(/^#{1,2}(?:[ \t]|$)/m)
|
|
1041
|
+
const sectionText = endMatch ? rest.slice(0, endMatch.index) : rest
|
|
1042
|
+
|
|
1043
|
+
for (const rawLine of sectionText.split(/\r?\n/)) {
|
|
1044
|
+
// 消费端子行(≥2 空白缩进 ↳ 前缀):不计矩阵行账,只计数(advisory 消费面判据)
|
|
1045
|
+
if (/^\s{2,}↳/.test(rawLine)) { out.subRowCount++; continue }
|
|
1046
|
+
const cells = splitMatrixRowCells(rawLine)
|
|
1047
|
+
if (!cells || cells.length !== 5) continue // 注记/防御/非五列表行不计槽
|
|
1048
|
+
const c = cells.map(x => x.trim())
|
|
1049
|
+
if (c.every(x => /^:?-{3,}:?$/.test(x))) continue // 分隔行
|
|
1050
|
+
if (c[0] === '端点' && c[1] === '判定') continue // 表头行
|
|
1051
|
+
const dm = c[0].match(API_DECLARED_ROW_RE)
|
|
1052
|
+
if (dm) {
|
|
1053
|
+
// 声明占位行(D-005 零解析降级侧):单列不进端点行账,declared 供 N 兜底
|
|
1054
|
+
out.declaredRow = { declared: parseInt(dm[1], 10), verdict: c[1], evidence: c[4] }
|
|
1055
|
+
continue
|
|
1056
|
+
}
|
|
1057
|
+
const em = c[0].match(API_ENDPOINT_CELL_RE)
|
|
1058
|
+
const row = {
|
|
1059
|
+
endpoint: c[0], method: em ? em[1].toUpperCase() : '', path: em ? em[2] : '',
|
|
1060
|
+
verdict: c[1], caseId: c[2], result: c[3], evidence: c[4],
|
|
1061
|
+
}
|
|
1062
|
+
if (!MATRIX_VERDICT_WHITELIST.has(row.verdict)) row.unfilled = true
|
|
1063
|
+
row.exploration = row.verdict === 'uncovered' && API_EXPLORATION_MARK_RE.test(`${row.result} ${row.evidence}`)
|
|
1064
|
+
out.rows.push(row)
|
|
1065
|
+
}
|
|
1066
|
+
return out
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/**
|
|
1070
|
+
* 证据锚点五形态命中(design §4 Grill #10):design接口表# 须提取出 ≥1 个 METHOD /path
|
|
1071
|
+
* 锚点才计命中(形态在而端点不可提取按缺锚计,fail-closed);其余四形态存在即认。
|
|
1072
|
+
* design接口表# 锚点是否命中解析产出集由调用侧逐锚核对(解析级校验),此处只管形态在场。
|
|
1073
|
+
*/
|
|
1074
|
+
function apiEvidenceHasAnchorForm(evidence) {
|
|
1075
|
+
const e = String(evidence || '')
|
|
1076
|
+
API_ANCHOR_DESIGN_API_RE.lastIndex = 0
|
|
1077
|
+
if (API_ANCHOR_DESIGN_API_RE.test(e)) return true
|
|
1078
|
+
if (/权限矩阵\[[^\]]+\]/.test(e)) return true
|
|
1079
|
+
if (/契约表@\S/.test(e)) return true
|
|
1080
|
+
if (/DDL@\S/.test(e)) return true
|
|
1081
|
+
if (/载荷@\S/.test(e)) return true
|
|
1082
|
+
return false
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* 接口验证覆盖矩阵记账判定纯函数(X-09 双层形态的判定层:零 MD 解析、零 IO——矩阵面经
|
|
1087
|
+
* 壳层 extractApiCoverageMatrixSlots 解析传入,apiFace/consumerHints/handover 经 facts
|
|
1088
|
+
* 落盘面传入)。task-07 断言消费契约。
|
|
1089
|
+
* @param {{
|
|
1090
|
+
* matrix?: object|null, // extractApiCoverageMatrixSlots 产物
|
|
1091
|
+
* apiFace?: {endpoints?: Array<{method,path,rowIdx}>, declared?: number|null, writeEndpoints?: Array}|null,
|
|
1092
|
+
* consumerHints?: Record<string, string[]>|null,
|
|
1093
|
+
* facts?: object|null, // verify-facts.json(handover 联动用,缺 null)
|
|
1094
|
+
* factsExpected?: boolean, // false(存量未跑管线)→ 兼容 ok 不误伤
|
|
1095
|
+
* strict?: boolean, // isIrStrictVerifyChange(段缺失分层用)
|
|
1096
|
+
* riskLevel?: string|null, // detectChangeRisk().level
|
|
1097
|
+
* changeName?: string,
|
|
1098
|
+
* permSectionText?: string, // design.md 权限段(段头含 权限/角色 的段合并文本,壳内提取)
|
|
1099
|
+
* designText?: string, // design.md 全文(写端点豁免行级检测用)
|
|
1100
|
+
* }} [args]
|
|
1101
|
+
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
1102
|
+
*/
|
|
1103
|
+
export function judgeApiCoverageMatrix(args) {
|
|
1104
|
+
const errors = []
|
|
1105
|
+
const warnings = []
|
|
1106
|
+
const a = args || {}
|
|
1107
|
+
// factsExpected=false(存量未跑管线)→ 沿用存量兼容口径 ok 不误伤(evaluatePassEligibility 同款边界)
|
|
1108
|
+
if (!a.factsExpected) return { ok: true, errors, warnings }
|
|
1109
|
+
|
|
1110
|
+
const changeName = a.changeName || '<变更名>'
|
|
1111
|
+
const INIT_HINT = `\`sillyspec verify-probes --change ${changeName} --init\``
|
|
1112
|
+
const matrix = a.matrix && typeof a.matrix === 'object' ? a.matrix : { present: false, rows: [], subRowCount: 0, declaredRow: null }
|
|
1113
|
+
const rows = Array.isArray(matrix.rows) ? matrix.rows : []
|
|
1114
|
+
const apiFace = a.apiFace && typeof a.apiFace === 'object' ? a.apiFace : null
|
|
1115
|
+
const endpoints = apiFace && Array.isArray(apiFace.endpoints)
|
|
1116
|
+
? apiFace.endpoints.filter(e => e && e.method && e.path) : []
|
|
1117
|
+
const declared = apiFace && typeof apiFace.declared === 'number' ? apiFace.declared : null
|
|
1118
|
+
const writeEndpoints = apiFace && Array.isArray(apiFace.writeEndpoints)
|
|
1119
|
+
? apiFace.writeEndpoints.filter(e => e && e.method && e.path) : []
|
|
1120
|
+
const consumerHints = a.consumerHints && typeof a.consumerHints === 'object' ? a.consumerHints : {}
|
|
1121
|
+
const criticalLevel = ['integration-critical', 'deployment-critical'].includes(a.riskLevel)
|
|
1122
|
+
|
|
1123
|
+
// ── 段缺失分层(对齐 :896-903 双层形态 + 判级 critical fail-closed 防删段绕过)。
|
|
1124
|
+
// apiFace 在场性 = 特性时序锚(X-08 无条件产出面,task-04 起 backfill 落盘):缺席说明
|
|
1125
|
+
// facts 最后一次回填早于本特性(特性间隙存量/旧管线 facts)→ warning 引导 --init 刷新
|
|
1126
|
+
// 即可(刷新后 apiFace 落盘、矩阵骨架补段,删段面转入上门硬口径);在场(新管线跑过,
|
|
1127
|
+
// 骨架必曾渲染矩阵段)而段缺失 = 删段 → 严格档/critical error。删 facts.json 的对抗面
|
|
1128
|
+
// 另由 checkProbeConsistency error 级 MD 锚点兜底(resolveFactsExpected 同口径注记)。──
|
|
1129
|
+
const newPipeline = apiFace != null
|
|
1130
|
+
if (!matrix.present) {
|
|
1131
|
+
if (newPipeline && a.strict) {
|
|
1132
|
+
errors.push(
|
|
1133
|
+
`接口验证覆盖矩阵段缺失(严格档,created_at ≥ ${IR_STRICT_SINCE})——verify-result.md 缺「## 接口验证覆盖矩阵」段。` +
|
|
1134
|
+
`修复:跑 ${INIT_HINT} 幂等补段(已有正文不覆盖),再逐行填判定与证据。`
|
|
1135
|
+
)
|
|
1136
|
+
} else if (newPipeline && criticalLevel) {
|
|
1137
|
+
errors.push(
|
|
1138
|
+
`接口验证覆盖矩阵段缺失且判级 ${a.riskLevel}——critical 变更接口面对账面不可缺失(防删段绕过对账,FR-04)。` +
|
|
1139
|
+
`修复:跑 ${INIT_HINT} 幂等补段后逐行填判定与证据。`
|
|
1140
|
+
)
|
|
1141
|
+
} else {
|
|
1142
|
+
warnings.push(
|
|
1143
|
+
`verify-result.md 缺「## 接口验证覆盖矩阵」段${newPipeline ? '(非严格档非判级 critical,存量不强制)' : '(facts.apiFace 缺席——特性前回填的存量底稿)'}——可跑 ${INIT_HINT} 幂等补段。`
|
|
1144
|
+
)
|
|
1145
|
+
}
|
|
1146
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// ── 零接口面分层(D-005):判级 critical → error(接口面不可静默为零——「变更有接口面」以
|
|
1150
|
+
// 判级 critical 为机械代理);非判级 critical → 零行为零打扰(brownfield 兼容)──
|
|
1151
|
+
const declaredFromRow = matrix.declaredRow ? matrix.declaredRow.declared : null
|
|
1152
|
+
const zeroFace = endpoints.length === 0 && declared === null && declaredFromRow === null
|
|
1153
|
+
if (zeroFace && criticalLevel) {
|
|
1154
|
+
errors.push(
|
|
1155
|
+
`判级 ${a.riskLevel} 且接口面为零(design 接口段解析零端点且无「本变更接口面:N 端点」声明行)——critical 变更接口面不可静默为零(D-005/FR-04)。` +
|
|
1156
|
+
`修复:design.md 补接口段表格(每端点一行 METHOD /path)或声明行「本变更接口面:N 端点」,重跑 ${INIT_HINT} 刷新矩阵段与 facts 后逐行填判定。`
|
|
1157
|
+
)
|
|
1158
|
+
}
|
|
1159
|
+
// 判级 critical × 声明 0 端点 → warning 复核(D-005 故障面条款:显式零声明仍可能是漏写)
|
|
1160
|
+
if (criticalLevel && (declared === 0 || declaredFromRow === 0)) {
|
|
1161
|
+
warnings.push(
|
|
1162
|
+
`判级 ${a.riskLevel} 且声明接口面为 0 端点——critical 变更声明零接口面请复核(D-005):若实际触碰接口,补 design 接口段表格后重跑 ${INIT_HINT} 刷新对账面。`
|
|
1163
|
+
)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const clip = (s) => { const t = String(s || ''); return t.length > 40 ? t.slice(0, 40) + '…' : t }
|
|
1167
|
+
const rowLabel = (r) => `${r.method && r.path ? `${r.method} ${r.path}` : clip(r.endpoint || r.verdict)}`
|
|
1168
|
+
const judgedRows = [...rows]
|
|
1169
|
+
if (matrix.declaredRow) judgedRows.push({ ...matrix.declaredRow, endpoint: `本变更接口面:${matrix.declaredRow.declared} 端点(agent 声明)`, method: '', path: '', caseId: '', result: '' })
|
|
1170
|
+
|
|
1171
|
+
// ── 行级判定槽/锚点/non-testable 理由校验(missingEvidence 口径同 probe7 :896-903)──
|
|
1172
|
+
const unfilledRows = judgedRows.filter(r => r.unfilled)
|
|
1173
|
+
if (unfilledRows.length > 0) {
|
|
1174
|
+
errors.push(
|
|
1175
|
+
`接口验证覆盖矩阵有 ${unfilledRows.length} 行判定未填(四选一 covered/partial/uncovered/non-testable):${unfilledRows.map(rowLabel).join(';')}。` +
|
|
1176
|
+
`修复:编辑 verify-result.md 接口验证覆盖矩阵段,把 <待填:四选一> 替换为判定值。`
|
|
1177
|
+
)
|
|
1178
|
+
}
|
|
1179
|
+
const parseSet = new Set(endpoints.map(e => `${e.method} ${e.path}`))
|
|
1180
|
+
const anchorViolations = []
|
|
1181
|
+
for (const r of judgedRows) {
|
|
1182
|
+
if (r.verdict !== 'covered' && r.verdict !== 'partial') continue
|
|
1183
|
+
const e = String(r.evidence || '')
|
|
1184
|
+
// design接口表# 锚点解析级核对(防空指):提取的 METHOD /path 须全部命中解析产出集
|
|
1185
|
+
API_ANCHOR_DESIGN_API_RE.lastIndex = 0
|
|
1186
|
+
for (const m of e.matchAll(API_ANCHOR_DESIGN_API_RE)) {
|
|
1187
|
+
const key = `${m[1]} ${m[2]}`
|
|
1188
|
+
if (!parseSet.has(key)) anchorViolations.push(`${rowLabel(r)}:design接口表#${key} 未命中 design 接口段解析面(空指)`)
|
|
1189
|
+
}
|
|
1190
|
+
if (!apiEvidenceHasAnchorForm(e)) anchorViolations.push(`${rowLabel(r)}:证据缺用例依据锚点(五形态之一)`)
|
|
1191
|
+
}
|
|
1192
|
+
if (anchorViolations.length > 0) {
|
|
1193
|
+
errors.push(
|
|
1194
|
+
`接口验证覆盖矩阵有 ${anchorViolations.length} 项证据锚点缺失/空指` +
|
|
1195
|
+
`(covered/partial 证据须含锚点五形态之一:design接口表#METHOD /path(须命中 design 接口段解析面)/ 权限矩阵[角色×动作] / 契约表@行标识 / DDL@列名 / 载荷@构造点路径):${anchorViolations.join(';')}。` +
|
|
1196
|
+
`修复:在证据列补真实锚点(design接口表# 锚点须与 design.md 接口段端点一致,防编造端点)。`
|
|
1197
|
+
)
|
|
1198
|
+
}
|
|
1199
|
+
// non-testable 理由非空即合法(:900 先例「non-testable 证据须写一句理由」,理由空按违规计)
|
|
1200
|
+
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('<待填')))
|
|
1201
|
+
if (ntMissing.length > 0) {
|
|
1202
|
+
errors.push(
|
|
1203
|
+
`接口验证覆盖矩阵有 ${ntMissing.length} 行 non-testable 缺理由(non-testable 证据须写一句理由):${ntMissing.map(rowLabel).join(';')}。` +
|
|
1204
|
+
`修复:在证据列为 non-testable 端点写一句不适用理由。`
|
|
1205
|
+
)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// ── covered 记账(D-005/D-008):分子=covered 端点行;有效分母=N−non-testable 行数;
|
|
1209
|
+
// N=解析端点数,解析零行按声明数(declaredRow 优先——矩阵实态,facts.declared 兜底)。
|
|
1210
|
+
// 缺覆盖清单解析集驱动:解析面端点 − covered 行命中 − non-testable 行命中(partial/
|
|
1211
|
+
// uncovered/unfilled 端点行不在 covered 命中集即自然进清单;探索行不进分母分子——其
|
|
1212
|
+
// uncovered 判定使之天然不计分子,且不计入 non-testable 扣减)。──
|
|
1213
|
+
if (!zeroFace) {
|
|
1214
|
+
const N = endpoints.length > 0 ? endpoints.length : (declaredFromRow != null ? declaredFromRow : (declared != null ? declared : 0))
|
|
1215
|
+
const nonTestableCount = rows.filter(r => r.verdict === 'non-testable').length
|
|
1216
|
+
const validDenominator = Math.max(0, N - nonTestableCount)
|
|
1217
|
+
const coveredCount = rows.filter(r => r.verdict === 'covered').length
|
|
1218
|
+
const coveredSet = new Set(rows.filter(r => r.verdict === 'covered' && r.method && r.path).map(r => `${r.method} ${r.path}`))
|
|
1219
|
+
const nonTestSet = new Set(rows.filter(r => r.verdict === 'non-testable' && r.method && r.path).map(r => `${r.method} ${r.path}`))
|
|
1220
|
+
const missingEndpoints = endpoints.filter(e => {
|
|
1221
|
+
const k = `${e.method} ${e.path}`
|
|
1222
|
+
return !coveredSet.has(k) && !nonTestSet.has(k)
|
|
1223
|
+
})
|
|
1224
|
+
if (coveredCount < validDenominator || missingEndpoints.length > 0) {
|
|
1225
|
+
const list = missingEndpoints.length > 0
|
|
1226
|
+
? missingEndpoints.map(e => `${e.method} ${e.path}`).join(';')
|
|
1227
|
+
: `声明 ${N} 端点而 covered 端点行仅 ${coveredCount}(解析零行降级,无法逐条列端点——按声明行拆出每端点行填判定)`
|
|
1228
|
+
errors.push(
|
|
1229
|
+
`接口验证覆盖矩阵覆盖不足:有效分母 ${validDenominator}(N=${N} − non-testable ${nonTestableCount}),covered 分子 ${coveredCount}——缺覆盖端点:${list}。` +
|
|
1230
|
+
`修复:补验证用例/冒烟步骤后把端点行改 covered 并填五形态锚点;不适用端点改 non-testable 并写一句理由;确未覆盖的走「## 移交项(结构化)」承载并保持 partial/uncovered(FR-04)。`
|
|
1231
|
+
)
|
|
1232
|
+
}
|
|
1233
|
+
// 声明与解析并存以解析为准并注记(骨架 :1573 漂移信号同款口径,数据面 facts.apiFace)
|
|
1234
|
+
if (declared !== null && endpoints.length > 0 && declared !== endpoints.length) {
|
|
1235
|
+
warnings.push(
|
|
1236
|
+
`声明与解析并存(声明 ${declared} 端点 / 解析 ${endpoints.length} 端点)——以解析为准,差异需复核(design 接口段与声明行不同步的漂移信号,D-005)。`
|
|
1237
|
+
)
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ── 移交联动(probe7 条件④ :1068-1073 同款形态):partial/uncovered 端点行>0 且
|
|
1242
|
+
// facts.handover 零有效行 → error(已覆盖不足且有未验证端点无去向不可静默);有行 →
|
|
1243
|
+
// 放行,blocking 是否封顶归 validatePassEligibility 条件②(④管有去向/②管去向级)。
|
|
1244
|
+
// 探索行属 uncovered 子集计入(探索性验证不算覆盖,同样须有去向);facts 缺失 →
|
|
1245
|
+
// fail-closed 按零有效行处理(factsExpected 已在入口把关,此处只防 facts 消失面)。──
|
|
1246
|
+
const partialRows = [
|
|
1247
|
+
...rows.filter(r => r.verdict === 'partial' || r.verdict === 'uncovered'),
|
|
1248
|
+
...(matrix.declaredRow && (matrix.declaredRow.verdict === 'partial' || matrix.declaredRow.verdict === 'uncovered')
|
|
1249
|
+
? [{ ...matrix.declaredRow, endpoint: `本变更接口面:${matrix.declaredRow.declared} 端点(agent 声明)`, method: '', path: '' }] : []),
|
|
1250
|
+
]
|
|
1251
|
+
if (partialRows.length > 0) {
|
|
1252
|
+
const factsMissing = !a.facts || typeof a.facts !== 'object'
|
|
1253
|
+
const handoverItems = !factsMissing && a.facts.handover && Array.isArray(a.facts.handover.items)
|
|
1254
|
+
? a.facts.handover.items.filter(it => it && typeof it === 'object') : []
|
|
1255
|
+
if (handoverItems.length === 0) {
|
|
1256
|
+
errors.push(
|
|
1257
|
+
`接口验证覆盖矩阵含 ${partialRows.length} 行 partial/uncovered 且「## 移交项(结构化)」零有效行(facts.handover)——接口未验证端点必须有移交去向:${partialRows.map(rowLabel).join(';')}。` +
|
|
1258
|
+
`修复:在 verify-result.md 补「## 移交项(结构化)」有效行(任意 severity 均可,blocking 级另受 PASS 封顶约束),或修正矩阵判定(实际已覆盖的行改 covered),或降级结论为 PASS WITH NOTES 承载。` +
|
|
1259
|
+
(factsMissing
|
|
1260
|
+
? `另:verify-facts.json 缺失/不可读而 factsExpected=true,fail-closed 按零有效行处理——重跑 verify-probes(${INIT_HINT} 幂等刷新底稿)。`
|
|
1261
|
+
: '')
|
|
1262
|
+
)
|
|
1263
|
+
} else {
|
|
1264
|
+
// 逐行关联 advisory(R-05 第一版,D-003 同款分工):行标识未命中任何 handover 条目文本
|
|
1265
|
+
// → console.warn 攒实证(文本命中弱关联,先 advisory 攒数据),不阻断不进 errors。
|
|
1266
|
+
const itemTexts = handoverItems.map(it => `${it.item || ''}|${it.condition || ''}`)
|
|
1267
|
+
for (const r of partialRows) {
|
|
1268
|
+
const keys = [r.method && r.path ? `${r.method} ${r.path}` : '', r.endpoint].map(k => String(k || '').trim()).filter(Boolean)
|
|
1269
|
+
const hit = keys.some(k => itemTexts.some(t => t.includes(k)))
|
|
1270
|
+
if (!hit) console.warn(`ℹ️ [advisory] 接口端点 ${rowLabel(r)} 的移交去向未在 handover 条目中命中——建议条目文本含该端点标识(advisory,攒实证;不阻断)`)
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// ── advisory:消费面子行缺失(D-006 第一版)——consumerHints 有归类(design 清单启发式,
|
|
1276
|
+
// 变更级归类)而矩阵零子行 → warning 列端点(不阻断、不进 errors)。──
|
|
1277
|
+
const hintKinds = Object.keys(consumerHints).filter(k => Array.isArray(consumerHints[k]) && consumerHints[k].length > 0)
|
|
1278
|
+
if (hintKinds.length > 0 && matrix.subRowCount === 0 && (endpoints.length > 0 || (declaredFromRow != null ? declaredFromRow : declared) > 0)) {
|
|
1279
|
+
const list = endpoints.length > 0
|
|
1280
|
+
? endpoints.slice(0, 8).map(e => `${e.method} ${e.path}`).join(';') + (endpoints.length > 8 ? '…' : '')
|
|
1281
|
+
: `声明 ${declaredFromRow != null ? declaredFromRow : declared} 端点(解析零行降级)`
|
|
1282
|
+
warnings.push(
|
|
1283
|
+
`[advisory] 消费端归类在场(${hintKinds.join('/')},facts.consumerHints——design 清单启发式)但矩阵零消费端子行——建议为接口端点补子行细分承接面(两空格缩进「↳ <消费端>:」形态,D-006,不阻断):${list}`
|
|
1284
|
+
)
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ── advisory:写端点权限矩阵缺行(D-007/FR-06,文案照 design §5)——apiFace.writeEndpoints
|
|
1288
|
+
// 中端点未在权限矩阵段(壳内提取:design 段头含 权限/角色)命中(路径原串或模板基径)
|
|
1289
|
+
// 且无「无权限约束」豁免(design 行级/矩阵行级/权限段整体)→ warning。──
|
|
1290
|
+
const permText = String(a.permSectionText || '')
|
|
1291
|
+
const designLines = String(a.designText || '').replace(/\r\n/g, '\n').split('\n')
|
|
1292
|
+
const exemptTexts = [
|
|
1293
|
+
...designLines,
|
|
1294
|
+
...rows.map(r => `${r.endpoint}|${r.caseId}|${r.result}|${r.evidence}`),
|
|
1295
|
+
]
|
|
1296
|
+
if (!permText.includes('无权限约束')) {
|
|
1297
|
+
for (const ep of writeEndpoints) {
|
|
1298
|
+
const base = String(ep.path).split(/[{:]/)[0]
|
|
1299
|
+
const hit = permText.includes(ep.path) || (base.length > 1 && permText.includes(base))
|
|
1300
|
+
if (hit) continue
|
|
1301
|
+
const rowExempt = exemptTexts.some(t => t.includes('无权限约束') && (t.includes(ep.path) || (base.length > 1 && t.includes(base))))
|
|
1302
|
+
if (!rowExempt) {
|
|
1303
|
+
warnings.push(
|
|
1304
|
+
`[advisory] 写端点 ${ep.method} ${ep.path} 未在权限矩阵声明——补行或显式豁免(表缺行会让派生框架继承你的洞)(D-007/FR-06,不阻断)`
|
|
1305
|
+
)
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return { ok: errors.length === 0, errors, warnings }
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* design.md 权限矩阵段提取(task-05 壳层辅助,D-007 表间完备性数据面):段头(#/##/###)
|
|
1315
|
+
* 含 权限/角色 关键词的段体合并文本(含段头行——命中判据宽松侧,advisory 无害)。零段 → ''。
|
|
1316
|
+
*/
|
|
1317
|
+
function extractPermissionMatrixText(designMd) {
|
|
1318
|
+
const text = String(designMd || '').replace(/\r\n/g, '\n')
|
|
1319
|
+
if (!text) return ''
|
|
1320
|
+
const out = []
|
|
1321
|
+
let collecting = false
|
|
1322
|
+
for (const line of text.split('\n')) {
|
|
1323
|
+
const h = line.match(/^#{1,3}\s+(.*)$/)
|
|
1324
|
+
if (h) {
|
|
1325
|
+
collecting = /权限|角色/.test(h[1])
|
|
1326
|
+
if (collecting) out.push(h[1])
|
|
1327
|
+
continue
|
|
1328
|
+
}
|
|
1329
|
+
if (collecting) out.push(line)
|
|
1330
|
+
}
|
|
1331
|
+
return out.join('\n')
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* 接口验证覆盖矩阵 validator 注册壳(与 validateAcceptanceMatrix 同三参签名同构,D-010:
|
|
1336
|
+
* 注册进 contracts.verify.validators 即覆盖 gates / machine-interface 等全部 runValidators
|
|
1337
|
+
* 调用方,gates.js 零改动)。壳内取数组装(IO/MD 槽解析全在壳层,判定是
|
|
1338
|
+
* judgeApiCoverageMatrix 纯函数——X-09 双层形态):
|
|
1339
|
+
* - tasks/ 不存在(quick 会话/旧变更)或 verify-result.md 未落盘(中间步骤)→ no-op
|
|
1340
|
+
* - factsExpected=false(存量未跑管线,resolveFactsExpected 同源口径)→ no-op 零行为
|
|
1341
|
+
* - verify-facts.json(apiFace/consumerHints/handover,缺失容 null——fail-closed 归纯函数)
|
|
1342
|
+
* - 矩阵段 MD 槽解析(X-05 防篡改锚点:锚定当前文档实态)
|
|
1343
|
+
* - detectChangeRisk 判级(与 validatePassEligibility 壳同款 design/plan 双文件输入)
|
|
1344
|
+
* - design.md 权限矩阵段提取(fs 只读,壳非纯函数)
|
|
1345
|
+
*/
|
|
1346
|
+
export function validateApiCoverageMatrix(cwd, changeName, context = {}) {
|
|
1347
|
+
const { specRoot } = context
|
|
1348
|
+
const errors = []
|
|
1349
|
+
const warnings = []
|
|
1350
|
+
const changeDir = resolveChangeDir(cwd, changeName, specRoot)
|
|
1351
|
+
// 无 TaskCard(tasks/ 不存在)→ 不校验(brownfield 零行为,validateAcceptanceMatrix 同门)
|
|
1352
|
+
if (!existsSync(join(changeDir, 'tasks'))) return { ok: true, errors, warnings }
|
|
1353
|
+
// verify-result.md 中间步骤未落盘 → 不校验(末步引擎存在性规则兜底)
|
|
1354
|
+
const verifyResultPath = join(changeDir, 'verify-result.md')
|
|
1355
|
+
if (!existsSync(verifyResultPath)) return { ok: true, errors, warnings }
|
|
1356
|
+
// 存量未跑管线(无 facts 且非严格档)→ 矩阵段/apiFace 俱不可得,零行为零打扰
|
|
1357
|
+
if (!resolveFactsExpected(changeDir)) return { ok: true, errors, warnings }
|
|
1358
|
+
|
|
1359
|
+
const facts = readFactsForEligibility(changeDir)
|
|
1360
|
+
const designText = readIfExists(join(changeDir, 'design.md'))
|
|
1361
|
+
const riskProfile = detectChangeRisk({
|
|
1362
|
+
designContent: designText,
|
|
1363
|
+
planContent: readIfExists(join(changeDir, 'plan.md')),
|
|
1364
|
+
})
|
|
1365
|
+
return judgeApiCoverageMatrix({
|
|
1366
|
+
matrix: extractApiCoverageMatrixSlots(readFileSync(verifyResultPath, 'utf8')),
|
|
1367
|
+
apiFace: facts && facts.apiFace && typeof facts.apiFace === 'object' ? facts.apiFace : null,
|
|
1368
|
+
consumerHints: facts && facts.consumerHints && typeof facts.consumerHints === 'object' ? facts.consumerHints : null,
|
|
1369
|
+
facts,
|
|
1370
|
+
factsExpected: true,
|
|
1371
|
+
strict: isIrStrictVerifyChange(changeDir),
|
|
1372
|
+
riskLevel: riskProfile ? riskProfile.level : null,
|
|
1373
|
+
changeName,
|
|
1374
|
+
permSectionText: extractPermissionMatrixText(designText),
|
|
1375
|
+
designText,
|
|
1376
|
+
})
|
|
1377
|
+
}
|
|
1378
|
+
|
|
947
1379
|
// ============ PASS 封顶事实面 validator(2026-09-17-pass-cap-semantics task-02 / D-001@v2 / D-010 / D-011) ============
|
|
948
1380
|
//
|
|
949
1381
|
// 「要不要集成证据」(validateVerifyOutputs 的 requiresEvidence)与「能不能写 PASS」(本
|
|
950
1382
|
// validator)解耦:结论=PASS 时四个已知未验证区任一在场即 error——
|
|
951
|
-
// ① 集成实测未跑(facts.integrationRan
|
|
952
|
-
// ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking
|
|
1383
|
+
// ① 集成实测未跑(facts.integrationRan≠ran,D-006 判定表——字段不在场按未跑处理,与⑤同族口径)
|
|
1384
|
+
// ② blocking 级移交项在场(facts.handover[].severity,D-005@v2——④管「有去向」、②管「去向是否 blocking」;
|
|
1385
|
+
// facts.handover 不在场=无法核对 blocking 行,fail-closed 拦下)
|
|
953
1386
|
// ③ db 脚本未声明执行(verify 时点文件集 ∩ db/**/*.sql 对账 facts.dbScriptDeclarations,D-012)
|
|
954
|
-
// ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover
|
|
1387
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(facts.matrixPartialRows × facts.handover——
|
|
1388
|
+
// matrixPartialRows 不在场=无法核对矩阵,按含 partial 处理,仍以「零有效移交行」为触发前提)
|
|
955
1389
|
// 附加:runtimeEndpointExcluded=true 且判级 integration/deployment-critical 且 handover 零行
|
|
956
1390
|
// → 计入①事实面(D-004/FR-03,仅判级时计入)。
|
|
1391
|
+
// ⑤ 接口冒烟未跑(facts.smokeRan≠ran,仅判级 integration/deployment-critical 时计入,
|
|
1392
|
+
// FR-02/D-002@v1——2026-09-17-api-coverage-smoke task-03;不设 handover 豁免子句)。
|
|
957
1393
|
//
|
|
958
1394
|
// 分层纪律(D-011):事实生产走 facts 管线(verify-probes backfillFactsFromMdAndTests 首次
|
|
959
1395
|
// backfill 主路径无条件产出,X-08 时序——gates 收尾前置 backfill 先于 runValidators);本壳只做
|
|
@@ -1005,7 +1441,7 @@ function countFactsHandoverItems(changeDir) {
|
|
|
1005
1441
|
* changeName?: string,
|
|
1006
1442
|
* dbScriptCandidates?: string[],
|
|
1007
1443
|
* }} [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 }> }}
|
|
1444
|
+
* @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
1445
|
*/
|
|
1010
1446
|
export function evaluatePassEligibility(args) {
|
|
1011
1447
|
const errors = []
|
|
@@ -1019,6 +1455,8 @@ export function evaluatePassEligibility(args) {
|
|
|
1019
1455
|
const factsMissing = !a.facts || typeof a.facts !== 'object'
|
|
1020
1456
|
const facts = factsMissing ? {} : a.facts
|
|
1021
1457
|
const handoverItems = (facts.handover && Array.isArray(facts.handover.items)) ? facts.handover.items : []
|
|
1458
|
+
const handoverFieldMissing = !factsMissing && !(facts.handover && Array.isArray(facts.handover.items))
|
|
1459
|
+
const matrixPartialMissing = !factsMissing && facts.matrixPartialRows == null
|
|
1022
1460
|
const blockingRows = handoverItems.filter(it => it && it.severity === 'blocking')
|
|
1023
1461
|
const criticalLevel = a.changeRiskProfile && ['integration-critical', 'deployment-critical'].includes(a.changeRiskProfile.level)
|
|
1024
1462
|
const NOTES_FIX = '改写结论为 PASS WITH NOTES 并在「## 移交项(结构化)」表格如实分行(类型枚举 env-blocked/manual-acceptance/db-script/other)'
|
|
@@ -1031,24 +1469,30 @@ export function evaluatePassEligibility(args) {
|
|
|
1031
1469
|
)
|
|
1032
1470
|
}
|
|
1033
1471
|
|
|
1034
|
-
// ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES
|
|
1035
|
-
|
|
1036
|
-
|
|
1472
|
+
// ① 集成实测未跑(时序口径 X-01:validator 时点只认已落盘 quality-scan 记录,被拦出路=重跑质量扫描步或降级 NOTES,不算失败;
|
|
1473
|
+
// 字段不在场 fail-closed 按未跑处理——⑤同族口径,防存量底稿缺字段静默 PASS)
|
|
1474
|
+
if (factsMissing || facts.integrationRan !== 'ran') {
|
|
1475
|
+
const integrationDetail = factsMissing
|
|
1476
|
+
? 'facts 缺失(fail-closed 按未跑处理)'
|
|
1477
|
+
: `facts.integrationRan=${facts.integrationRan == null ? '不在场(按未跑处理)' : facts.integrationRan}`
|
|
1478
|
+
triggered.push({ fact: 'integration-not-run', detail: integrationDetail })
|
|
1037
1479
|
errors.push(
|
|
1038
|
-
`[fact integration-not-run] 集成实测未跑(${
|
|
1039
|
-
`出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts
|
|
1480
|
+
`[fact integration-not-run] 集成实测未跑(${integrationDetail})——结论=PASS 须集成实测已跑(D-006 判定表:quality-scan 实测记录(skip 除外)或跨层回执条目)。` +
|
|
1481
|
+
`出路:重跑质量扫描步落实测记录,或提供跨层实测回执后重跑 verify-probes 刷新 facts(字段不在场多为存量底稿未含新字段,重跑即刷新);仍未跑则${NOTES_FIX}承载(不算失败)。`
|
|
1040
1482
|
)
|
|
1041
1483
|
}
|
|
1042
1484
|
|
|
1043
|
-
// ② blocking 级移交项在场(blocking 移交项与结论 PASS
|
|
1044
|
-
if (factsMissing || blockingRows.length > 0) {
|
|
1485
|
+
// ② blocking 级移交项在场(blocking 移交项与结论 PASS 互斥;facts.handover 不在场=无法核对,fail-closed)
|
|
1486
|
+
if (factsMissing || handoverFieldMissing || blockingRows.length > 0) {
|
|
1045
1487
|
const list = blockingRows.map(it => `${it.type || '未知类型'}|${String(it.item || '').slice(0, 60)}`).join(';')
|
|
1046
|
-
triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : `${blockingRows.length} 条 blocking 行:${list}` })
|
|
1488
|
+
triggered.push({ fact: 'blocking-handover-present', detail: factsMissing ? 'facts 缺失(fail-closed)' : handoverFieldMissing ? 'facts.handover 不在场(fail-closed 无法核对)' : `${blockingRows.length} 条 blocking 行:${list}` })
|
|
1047
1489
|
errors.push(
|
|
1048
1490
|
`[fact blocking-handover-present] ${factsMissing
|
|
1049
1491
|
? '移交项 blocking 级行无法核对(verify-facts.json 缺失,fail-closed 按在场触发)'
|
|
1050
|
-
:
|
|
1051
|
-
|
|
1492
|
+
: handoverFieldMissing
|
|
1493
|
+
? '移交项 blocking 级行无法核对(facts.handover 不在场——存量底稿未含该字段或管线故障,fail-closed 拦下;重跑 verify-probes 幂等刷新 facts 即可消解)'
|
|
1494
|
+
: `移交项含 ${blockingRows.length} 条 blocking 级行——承认有必须兜底的未竟事项即不是 PASS(④管「有去向」、②管「去向是否 blocking」)`}。` +
|
|
1495
|
+
`触发行:${factsMissing ? '(facts 缺失,无法核对——fail-closed)' : (list || '(handover 不可核对,无行可列)')}。修复:${NOTES_FIX};确属 advisory 的按降级文法「(降级:<理由>,依据 <file:line 或 D-xxx>)」显式降级。`
|
|
1052
1496
|
)
|
|
1053
1497
|
}
|
|
1054
1498
|
|
|
@@ -1064,11 +1508,18 @@ export function evaluatePassEligibility(args) {
|
|
|
1064
1508
|
)
|
|
1065
1509
|
}
|
|
1066
1510
|
|
|
1067
|
-
// ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity
|
|
1068
|
-
|
|
1069
|
-
|
|
1511
|
+
// ④ 矩阵含 partial/uncovered 且移交项零有效行(任意 severity——部分实现必须有移交去向;
|
|
1512
|
+
// matrixPartialRows 不在场=无法核对矩阵,fail-closed 按含 partial 处理,触发前提仍是零有效移交行——
|
|
1513
|
+
// 有真移交行在场即「去向已承载」,不因字段缺失误拦)
|
|
1514
|
+
if (factsMissing || ((matrixPartialMissing || facts.matrixPartialRows > 0) && handoverItems.length === 0)) {
|
|
1515
|
+
const matrixDesc = factsMissing
|
|
1516
|
+
? '(facts 缺失,无法核对——fail-closed)'
|
|
1517
|
+
: matrixPartialMissing
|
|
1518
|
+
? '(facts.matrixPartialRows 不在场,无法核对——fail-closed 按含 partial 处理;重跑 verify-probes 刷新 facts 即可消解)'
|
|
1519
|
+
: `${facts.matrixPartialRows} 行 partial/uncovered`
|
|
1520
|
+
triggered.push({ fact: 'matrix-partial-no-handover', detail: factsMissing ? 'facts 缺失(fail-closed)' : matrixPartialMissing ? 'facts.matrixPartialRows 不在场(fail-closed 按含 partial 处理)且 handover 零有效行' : `matrixPartialRows=${facts.matrixPartialRows} 且 handover 零有效行` })
|
|
1070
1521
|
errors.push(
|
|
1071
|
-
`[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${
|
|
1522
|
+
`[fact matrix-partial-no-handover] 验收×测试覆盖矩阵含 ${matrixDesc} 且「## 移交项(结构化)」零有效行——部分实现必须有移交去向。修复:${NOTES_FIX}承载(任意 severity 均可,blocking 级另受条件②约束)。`
|
|
1072
1523
|
)
|
|
1073
1524
|
}
|
|
1074
1525
|
|
|
@@ -1086,6 +1537,29 @@ export function evaluatePassEligibility(args) {
|
|
|
1086
1537
|
)
|
|
1087
1538
|
}
|
|
1088
1539
|
|
|
1540
|
+
// ── ⑤ smoke 冒烟未跑(2026-09-17-api-coverage-smoke task-03 / FR-02 / D-002@v1,判级
|
|
1541
|
+
// 限定——criticalLevel 在场才计入,runtimeEndpointExcluded 附加条件同款形态):判级
|
|
1542
|
+
// integration/deployment-critical 且 facts.smokeRan !== 'ran'(producer 自 quality-scan
|
|
1543
|
+
// 记录 smokeResult 段推导)→ triggered 加枚举 smoke-not-run。不设 handover 豁免子句——
|
|
1544
|
+
// advisory handover 在场不构成 smoke 缺失的 PASS 豁免(smoke 缺失的合法出路只有配
|
|
1545
|
+
// commands.smoke 复跑质量扫描步 / 降级 PASS WITH NOTES 移交承载两条);blocking handover
|
|
1546
|
+
// 在场由条件②独立拦下(出口=NOTES),两条件各自触发不互斥。factsMissing 沿双源
|
|
1547
|
+
// fail-closed 同族口径按触发处理(smokeRan 不可证伪);facts 在场但字段不在场(升级
|
|
1548
|
+
// 过渡期存量 facts,producer 重新 backfill 前)同 !== 'ran' 触发——fail-closed 侧,防
|
|
1549
|
+
// 「判级 critical 未跑冒烟静默 PASS」。非判级(unit-sufficient 等)任意 smokeRan 零行为。
|
|
1550
|
+
if (criticalLevel && (factsMissing || facts.smokeRan !== 'ran')) {
|
|
1551
|
+
const smokeDetail = factsMissing
|
|
1552
|
+
? 'facts 缺失(fail-closed,smokeRan 不可证伪按未跑处理)'
|
|
1553
|
+
: `facts.smokeRan=${facts.smokeRan == null ? '不在场(按未跑处理)' : facts.smokeRan}`
|
|
1554
|
+
triggered.push({ fact: 'smoke-not-run', detail: `${smokeDetail},判级 ${a.changeRiskProfile.level}` })
|
|
1555
|
+
const configHint = facts.smokeRan === 'not-configured'
|
|
1556
|
+
? '(not-configured:先在 local.yaml 配置键 commands.smoke,如 smoke: "npm run smoke")' : ''
|
|
1557
|
+
errors.push(
|
|
1558
|
+
`[fact smoke-not-run] 接口冒烟未跑(${smokeDetail})且判级 ${a.changeRiskProfile.level}——critical 变更结论=PASS 须接口冒烟绿跑(facts.smokeRan=ran,FR-02/D-002)。` +
|
|
1559
|
+
`出路:①配 commands.smoke 并复跑质量扫描步${configHint};②降级承载——${NOTES_FIX}(不算失败)。`
|
|
1560
|
+
)
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1089
1563
|
return { ok: errors.length === 0, errors, triggered }
|
|
1090
1564
|
}
|
|
1091
1565
|
|
|
@@ -1370,7 +1844,10 @@ const contracts = {
|
|
|
1370
1844
|
// 注册进 validator 链即覆盖 gates / machine-interface 等全部 runValidators 调用方(gates.js 零改动)。
|
|
1371
1845
|
// validatePassEligibility:PASS 封顶事实面(2026-09-17-pass-cap-semantics task-02 / D-001@v2 /
|
|
1372
1846
|
// D-010)——注册即覆盖同上;回退 = 移除本注册行(纯加法,未触发四条件行为零变化)。
|
|
1373
|
-
|
|
1847
|
+
// validateApiCoverageMatrix:接口验证覆盖矩阵对账门(2026-09-17-api-coverage-smoke task-05 /
|
|
1848
|
+
// FR-04~FR-06,与探针 7 并排互补的独立章节)——注册即覆盖同上;回退 = 移除本注册行(纯加法,
|
|
1849
|
+
// factsExpected=false / 无矩阵段的存量变更零行为)。
|
|
1850
|
+
validators: [validateVerifyOutputs, validateAcceptanceMatrix, validatePassEligibility, validateApiCoverageMatrix],
|
|
1374
1851
|
},
|
|
1375
1852
|
archive: {
|
|
1376
1853
|
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
|
}
|