sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/verify-probes.js
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
* verify-probes.md 模板定义六个探针,agent 此前逐条手跑 grep/递归查找/git 对账再手工拼表格。
|
|
6
6
|
* 本模块把纯机械的四个探针命令化(语义判断的留 agent,输出里显式标注):
|
|
7
7
|
* 探针1 未实现标记扫描:design §6 清单的具体文件逐行 grep TODO/FIXME/尚未实现 等
|
|
8
|
-
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/
|
|
8
|
+
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/ 陷阱
|
|
9
|
+
* + JVM src/test/<lang> 同包镜像根——Maven/Gradle 布局测试与 main 侧永不 co-located)
|
|
9
10
|
* 探针5 API 契约对账:复用 contract-matrix.verifyApiParity(endpoints.json × 前端调用)+ 表格渲染
|
|
10
11
|
* 探针6 删除对账:git diff --name-status HEAD 的 D/R × design 声明操作三态判定
|
|
12
|
+
* 探针7 验收×测试覆盖矩阵:task 卡 acceptance 自解析(jsYaml,string/array 双形态)+ 双源结构
|
|
13
|
+
* 归属(allowed_paths 测试模式 ∪ execute-runs review.json changedFiles test/ 前缀)+
|
|
14
|
+
* 关键词命中提示(命中≠判定,不参与门禁;2026-09-14-acceptance-test-matrix)
|
|
11
15
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
16
|
*
|
|
13
17
|
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
@@ -17,21 +21,56 @@
|
|
|
17
21
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
18
22
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
19
23
|
*/
|
|
20
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
21
|
-
import { join, dirname, basename } from 'path'
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs'
|
|
25
|
+
import { join, dirname, basename, resolve, isAbsolute } from 'path'
|
|
26
|
+
import jsYaml from 'js-yaml'
|
|
22
27
|
import { gitQuiet } from './git-helper.js'
|
|
23
28
|
import {
|
|
24
29
|
FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
|
|
25
30
|
} from './verify-facts-schema.js'
|
|
26
31
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
27
32
|
import { parseDecisions } from './decision-distill.js'
|
|
28
|
-
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
33
|
+
import { parseAllowedPaths, parseRepo, parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
29
34
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
30
35
|
import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
|
|
31
36
|
import { resolveSpecDir, resolveRuntimeRoot, detectWorktreeSpecDrift } from './run/shared.js'
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
// 探针1 未实现标记匹配(坑 probe1-literal-false-positive,2026-09-15 复盘实证:16 命中全
|
|
39
|
+
// 字面误报——TODO_FLAG_TODO 业务常量、「XXX完成处置」中文占位模板)。口径分三级:
|
|
40
|
+
// - 尚未实现:中文短语高置信,保持子串匹配;
|
|
41
|
+
// - TODO/FIXME/HACK:ASCII 标识符边界匹配——TODO_FLAG_TODO / parseHackArgs 等标识符内部不再
|
|
42
|
+
// 命中(两侧任一边贴 [A-Za-z0-9_] 即视为标识符成分);
|
|
43
|
+
// - XXX:边界匹配且前或后紧邻 CJK 表意字符即排除——「XXX完成处置」「订单XXX号」类中文占位
|
|
44
|
+
// 模板不再命中(独立代码注释 `XXX:` / `// XXX fix` 紧邻标点空白,仍命中)。
|
|
45
|
+
const CJK_IDEOGRAPH_CLASS = '\\u4e00-\\u9fff\\u3400-\\u4dbf'
|
|
46
|
+
const TODO_ASCII_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_])(?:TODO|FIXME|HACK)(?![A-Za-z0-9_])`)
|
|
47
|
+
const XXX_MARKER_RE = new RegExp(`(^|[^A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])XXX(?![A-Za-z0-9_${CJK_IDEOGRAPH_CLASS}])`)
|
|
48
|
+
export function isUnimplementedMarkerLine(line) {
|
|
49
|
+
if (line.includes('尚未实现')) return true
|
|
50
|
+
if (TODO_ASCII_MARKER_RE.test(line)) return true
|
|
51
|
+
if (XXX_MARKER_RE.test(line)) return true
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
// 测试文件名判定(坑 probe3-testname-false-positive,2026-09-15 EHS 生产实证:model 目录下
|
|
55
|
+
// TestData.java 数据夹具被旧 /test|spec/i 子串命中 → task-01 假绿「找到 1 个测试文件」;
|
|
56
|
+
// specification.md / contest.css 同族反向噪音)。三路命中,任一即测试文件:
|
|
57
|
+
// - 分隔符分词(按非字母数字切段)含 test/tests/spec/specs —— foo.test.js、test_utils.py、foo-spec.ts
|
|
58
|
+
// - 裸词文件名(去扩展名即上述词,大小写归一)—— test.js、Tests.java
|
|
59
|
+
// - 驼峰末段后缀 Test/Tests/Spec/Specs/IT(Java/Kotlin 惯例 FooTest/FooIT)——只认末位驼峰段:
|
|
60
|
+
// RpFlowEngineTest 命中;TestData/TestUtil/TestMain(Test 是首段=夹具命名惯势)不命中
|
|
61
|
+
// 取向:漏检罕见命名(FooTestCase/TestFoo 前缀式)→ ⚠️ 落 agent 手查(fail-visible 可接受);
|
|
62
|
+
// 假绿掩盖真缺测是 fail-hidden,更糟——宁紧勿松。
|
|
63
|
+
const TEST_NAME_TOKENS = new Set(['test', 'tests', 'spec', 'specs'])
|
|
64
|
+
const TEST_NAME_SUFFIXES = new Set(['Test', 'Tests', 'Spec', 'Specs', 'IT'])
|
|
65
|
+
export function isTestFileName(name) {
|
|
66
|
+
const stem = String(name || '').replace(/\.[^.]+$/, '')
|
|
67
|
+
if (!stem) return false
|
|
68
|
+
if (TEST_NAME_TOKENS.has(stem.toLowerCase())) return true
|
|
69
|
+
const tokens = stem.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
|
|
70
|
+
if (tokens.some(t => TEST_NAME_TOKENS.has(t))) return true
|
|
71
|
+
const lastHump = stem.match(/(?:[A-Z][a-z0-9]*|[A-Z]+)$/)
|
|
72
|
+
return !!lastHump && TEST_NAME_SUFFIXES.has(lastHump[0])
|
|
73
|
+
}
|
|
35
74
|
const PROBE1_MAX_MATCHES = 200
|
|
36
75
|
|
|
37
76
|
/**
|
|
@@ -85,7 +124,7 @@ function findTestFiles(rootDir, cwd, cap = 10) {
|
|
|
85
124
|
const full = join(d, e.name)
|
|
86
125
|
if (e.isDirectory()) {
|
|
87
126
|
if (!skip.has(e.name)) walk(full)
|
|
88
|
-
} else if (e.isFile() &&
|
|
127
|
+
} else if (e.isFile() && isTestFileName(e.name)) {
|
|
89
128
|
found.push(full.split('\\').join('/').replace(cwd.split('\\').join('/').replace(/\/$/, '') + '/', ''))
|
|
90
129
|
if (found.length >= cap) return
|
|
91
130
|
}
|
|
@@ -95,10 +134,441 @@ function findTestFiles(rootDir, cwd, cap = 10) {
|
|
|
95
134
|
return found
|
|
96
135
|
}
|
|
97
136
|
|
|
137
|
+
// ── 探针 8(载荷字段契约对账,2026-09-16 EHS 二次独立复核驱动)常量与纯函数 ──
|
|
138
|
+
// 5 个 P1 里 4 处是字段/载荷错位(leaderUserId↔rpLeaderUserId 三处 Jackson 静默丢弃、小程序
|
|
139
|
+
// 缺发 reportOrgId、sourceShdId↔safelyHiddenId、report_org_name NOT NULL 边界)——探针 5 只对
|
|
140
|
+
// 账 method+path(URL 级),载荷级错位零覆盖。本探针三面启发式比对(advisory,命中≠结论):
|
|
141
|
+
// 前端请求载荷键 × 后端 Java 字段(归一化覆盖)∪ 子串疑似错位配对 ∪ SQL NOT NULL 列缺送核对。
|
|
142
|
+
const PROBE8_HEADING = '#### 探针 8:载荷字段契约对账(advisory)'
|
|
143
|
+
// 服务端填充/标准列豁免(NOT NULL 但不该由前端送)
|
|
144
|
+
const PROBE8_SERVER_FILLED_RE = /^(id|create_by|create_date|update_by|update_date|remarks|dept_id|tenant_id|del_flag)$/i
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 归一化键:小写 + 剥全部分隔线(snake/连字符)——两侧同构(rp_category / rpCategory /
|
|
148
|
+
* rp-category → rpcategory)。注意不能「小写后再 camel 化」(会单侧引入大写不对称)。
|
|
149
|
+
*/
|
|
150
|
+
const normFieldKey = (s) => String(s || '').trim().toLowerCase().replace(/[_-]/g, '')
|
|
151
|
+
|
|
152
|
+
/** Java private 字段名提取(全大写常量 TODO_FLAG_TODO 排除——EHS 探针1 同款教训) */
|
|
153
|
+
export function extractJavaFields(text) {
|
|
154
|
+
const names = new Set()
|
|
155
|
+
const re = /private\s+(?:static\s+|final\s+)*[\w.$<>[\]]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*[;=]/g
|
|
156
|
+
let m
|
|
157
|
+
while ((m = re.exec(String(text || ''))) !== null) {
|
|
158
|
+
if (!/^[A-Z0-9_]+$/.test(m[1])) names.add(m[1])
|
|
159
|
+
}
|
|
160
|
+
return names
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** SQL CREATE TABLE 行的 NOT NULL 业务列(审计列豁免) */
|
|
164
|
+
export function extractSqlNotNullColumns(text) {
|
|
165
|
+
const cols = new Set()
|
|
166
|
+
for (const line of String(text || '').split('\n')) {
|
|
167
|
+
const m = line.match(/^\s*`?([a-zA-Z_][a-zA-Z0-9_]*)`?\s+(?:varchar|char|int|integer|bigint|decimal|datetime|date|timestamp|text|double|float)\b[^,]*NOT NULL\b/i)
|
|
168
|
+
if (m && !PROBE8_SERVER_FILLED_RE.test(m[1])) cols.add(m[1])
|
|
169
|
+
}
|
|
170
|
+
return cols
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 请求载荷键提取:apiFetch/request/axios/$http/fetch( 调用行后 8 行窗口内的对象键
|
|
175
|
+
* (key: 后随值起始为字面量/对象/数组/null/undefined/负号——排除 case/三元等形态)。
|
|
176
|
+
* UI 本地态对象(请求区外)天然不收。
|
|
177
|
+
*/
|
|
178
|
+
export function extractPayloadKeys(text) {
|
|
179
|
+
const keys = new Set()
|
|
180
|
+
const lines = String(text || '').split('\n')
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
if (!/\b(apiFetch|request|axios|\$http|fetch)\s*\(/.test(lines[i])) continue
|
|
183
|
+
for (let j = i; j < Math.min(i + 8, lines.length); j++) {
|
|
184
|
+
for (const m of lines[j].matchAll(/(?:^|[{,(\s])([A-Za-z_][A-Za-z0-9]*)\s*:\s(?:["'`\d{[tfn-]|null\b|undefined\b)/g)) {
|
|
185
|
+
keys.add(m[1])
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return keys
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 探针 8 主体:design 清单三面文件(Java/SQL/前端)→ 双根(主仓 ∪ worktree)∪ 跨仓注册仓根
|
|
194
|
+
* 读取 → 归一化比对。advisory:所有输出是「候选」不是结论(UI 本地态键/服务端填充列会自然
|
|
195
|
+
* 出现在差异里,agent 逐条复核——口径注记随渲染输出)。
|
|
196
|
+
* @param {{ specBase: string, cwd: string, wtRoot?: string|null, changeName: string }} args
|
|
197
|
+
* @returns {{applicable, backendFieldCount, feKeyCount, notNullCount, javaFileCount, sqlFileCount, feFileCount,
|
|
198
|
+
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[]}}
|
|
199
|
+
*/
|
|
200
|
+
export function runProbe8PayloadParity({ specBase, cwd, wtRoot = null, changeName }) {
|
|
201
|
+
const out = {
|
|
202
|
+
applicable: false, backendFieldCount: 0, feKeyCount: 0, notNullCount: 0,
|
|
203
|
+
javaFileCount: 0, sqlFileCount: 0, feFileCount: 0,
|
|
204
|
+
mispairs: [], feOnly: [], missingNotNull: [], notes: [],
|
|
205
|
+
}
|
|
206
|
+
if (!specBase || !changeName) return out
|
|
207
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
208
|
+
if (!existsSync(designPath)) return out
|
|
209
|
+
let registry = new Map()
|
|
210
|
+
try {
|
|
211
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
212
|
+
if (existsSync(yamlPath)) registry = parseRepoRegistry(readFileSync(yamlPath, 'utf8'))
|
|
213
|
+
} catch { /* 注册表不可读 → 跨仓条目落未注册注记 */ }
|
|
214
|
+
const detailed = parseFileChangeListDetailed(designPath, { repoKeys: [...registry.keys()] })
|
|
215
|
+
|
|
216
|
+
const readEntry = (e) => {
|
|
217
|
+
// NEW: 待建前缀剥离(坑 probe1-new-prefix-miss 同款):design 清单的 NEW: 是新建意图标记,
|
|
218
|
+
// 文件系统实体无前缀——按目标路径读。
|
|
219
|
+
const probePath = String(e.path).replace(/^NEW:\s*/, '')
|
|
220
|
+
// 未注册跨仓前缀兜底识别(change-list 只对注册 key 落 e.repo;未注册 key 整条 path 保留
|
|
221
|
+
// cross-repo: 前缀原样)——现身「未注册」注记而非静默不可读。
|
|
222
|
+
const crPrefix = probePath.match(/^cross-repo:([A-Za-z0-9_.\-]+):(.*)$/)
|
|
223
|
+
if (e.repo || crPrefix) {
|
|
224
|
+
const key = e.repo || (crPrefix && crPrefix[1])
|
|
225
|
+
const relPath = e.repo ? probePath : crPrefix[2]
|
|
226
|
+
const raw = registry.get(key)
|
|
227
|
+
if (!raw) { out.notes.push(`repo「${key}」未在 local.yaml repos 注册——该仓前端文件未进探针 8 比对`); return null }
|
|
228
|
+
const root = isAbsolute(raw) ? raw : resolve(cwd, raw)
|
|
229
|
+
try { return readFileSync(join(root, relPath), 'utf8') } catch { out.notes.push(`跨仓文件不可读:${key}:${relPath}`); return null }
|
|
230
|
+
}
|
|
231
|
+
// 主仓 ∪ worktree 双根(与探针 1 同款回退)
|
|
232
|
+
for (const base of [cwd, wtRoot].filter(Boolean)) {
|
|
233
|
+
try { return readFileSync(join(base, probePath), 'utf8') } catch { /* 试下一根 */ }
|
|
234
|
+
}
|
|
235
|
+
return null
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const backendFields = new Set()
|
|
239
|
+
const notNullCols = new Set()
|
|
240
|
+
const feKeys = new Set()
|
|
241
|
+
for (const e of detailed) {
|
|
242
|
+
if (!e.path || e.path.startsWith('.sillyspec/')) continue
|
|
243
|
+
const text = readEntry(e)
|
|
244
|
+
if (text == null) continue
|
|
245
|
+
if (e.path.endsWith('.java')) {
|
|
246
|
+
out.javaFileCount++
|
|
247
|
+
for (const f of extractJavaFields(text)) backendFields.add(f)
|
|
248
|
+
} else if (e.path.endsWith('.sql')) {
|
|
249
|
+
out.sqlFileCount++
|
|
250
|
+
for (const c of extractSqlNotNullColumns(text)) notNullCols.add(c)
|
|
251
|
+
} else if (/\.(js|jsx|ts|tsx)$/.test(e.path)) {
|
|
252
|
+
out.feFileCount++
|
|
253
|
+
for (const k of extractPayloadKeys(text)) feKeys.add(k)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// 后端零面(无 Java 无 SQL)→ 不适用(纯前端/文档变更不打扰)
|
|
257
|
+
if (out.javaFileCount === 0 && out.sqlFileCount === 0) return out
|
|
258
|
+
out.applicable = true
|
|
259
|
+
|
|
260
|
+
const backendNorm = new Map() // norm → 原名
|
|
261
|
+
for (const f of backendFields) backendNorm.set(normFieldKey(f), f)
|
|
262
|
+
const feNorm = new Map()
|
|
263
|
+
for (const k of feKeys) feNorm.set(normFieldKey(k), k)
|
|
264
|
+
// 配对 token(camelCase/snake 切词;单字符段只保留 id)——子串关系抓 leaderUserId⊂rpLeaderUserId,
|
|
265
|
+
// token 重叠(Jaccard≥0.6)抓语义近形 punishedDutyUserId↔punishedLeaderUserId(EHS 二次复核
|
|
266
|
+
// 两类实证形态)。并列取长度差最小者(leaderUserId 优先配 rpLeaderUserId 而非更长的
|
|
267
|
+
// punishedLeaderUserId)。
|
|
268
|
+
const fieldTokens = (s) => String(s).replace(/([a-z0-9])([A-Z])/g, '$1 $2').split(/[^A-Za-z0-9]+/)
|
|
269
|
+
.map(t => t.toLowerCase()).filter(t => t.length > 1 || t === 'id')
|
|
270
|
+
|
|
271
|
+
for (const [nk, original] of feNorm) {
|
|
272
|
+
if (backendNorm.has(nk)) continue // 归一化覆盖(rpCategory ↔ rp_category)
|
|
273
|
+
const feToks = fieldTokens(original)
|
|
274
|
+
let best = null
|
|
275
|
+
let bestScore = 0
|
|
276
|
+
let bestLenPenalty = Infinity
|
|
277
|
+
for (const [bn, beOrig] of backendNorm) {
|
|
278
|
+
let score = 0
|
|
279
|
+
if ((bn.includes(nk) || nk.includes(bn)) && Math.min(bn.length, nk.length) >= 4) score = 1
|
|
280
|
+
else if (feToks.length >= 2) {
|
|
281
|
+
const bt = fieldTokens(beOrig)
|
|
282
|
+
if (bt.length >= 2) {
|
|
283
|
+
const inter = feToks.filter(t => bt.includes(t)).length
|
|
284
|
+
const union = new Set([...feToks, ...bt]).size
|
|
285
|
+
if (union > 0) score = inter / union
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (score <= 0) continue
|
|
289
|
+
const lenPenalty = Math.abs(bn.length - nk.length)
|
|
290
|
+
if (score > bestScore || (score === bestScore && lenPenalty < bestLenPenalty)) {
|
|
291
|
+
bestScore = score
|
|
292
|
+
bestLenPenalty = lenPenalty
|
|
293
|
+
best = beOrig
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (best && bestScore >= 0.6) out.mispairs.push({ fe: original, be: best })
|
|
297
|
+
else out.feOnly.push(original)
|
|
298
|
+
}
|
|
299
|
+
for (const col of notNullCols) {
|
|
300
|
+
const nc = normFieldKey(col)
|
|
301
|
+
if (!feNorm.has(nc) && !backendNorm.has(nc)) out.missingNotNull.push({ col })
|
|
302
|
+
}
|
|
303
|
+
out.backendFieldCount = backendFields.size
|
|
304
|
+
out.feKeyCount = feKeys.size
|
|
305
|
+
out.notNullCount = notNullCols.size
|
|
306
|
+
return out
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
const PROBE7_HEADING = '#### 探针 7:验收×测试覆盖矩阵'
|
|
311
|
+
// execute run id 格式(与 task-review.js isValidExecuteRunId 同口径锚定,防提示词注入/路径穿越)
|
|
312
|
+
const PROBE7_EXEC_RUN_ID_RE = /^exec-\d{4}-\d{2}-\d{2}-\d{6}(?:-[a-z0-9]{1,8}){0,2}$/
|
|
313
|
+
// 关键词提取:≥3 字符标识符([A-Za-z_][A-Za-z0-9_]{2,})∪ ≥2 字连续 CJK 片段,按出现顺序交错
|
|
314
|
+
const PROBE7_TERM_RE = /[A-Za-z_][A-Za-z0-9_]{2,}|[\u4e00-\u9fff]{2,}/g
|
|
315
|
+
const PROBE7_TERM_CAP = 5
|
|
316
|
+
// 坑 probe7-prefill-evidence(ql-20260915-004):无归属时的 non-testable 判别词(机械保守:
|
|
317
|
+
// acceptance 文本命中即预填 non-testable,agent 复核可改写)
|
|
318
|
+
const PROBE7_NONTESTABLE_RE = /文档|部署|doc|deploy|manual|config/i
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 解析 task-NN.md frontmatter 的 acceptance(string/array 双形态归一为数组)。
|
|
322
|
+
* 口径锚 src/stages/plan-postcheck.js acceptance best-effort 段(string → 原文单条、array →
|
|
323
|
+
* 逐条)——不 import 它:parseTaskContracts 只返回 provides/expects_from 不含 acceptance。
|
|
324
|
+
* @param {string} content task 卡全文
|
|
325
|
+
* @returns {string[]|null} acceptance 条目数组;null = 无 frontmatter(调用方跳过该卡);
|
|
326
|
+
* frontmatter 在场但无 acceptance / 非法 YAML → [](防御行,plan-postcheck 已拦缺失)
|
|
327
|
+
*/
|
|
328
|
+
export function parseTaskAcceptance(content) {
|
|
329
|
+
const fmMatch = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
330
|
+
if (!fmMatch) return null
|
|
331
|
+
try {
|
|
332
|
+
const fmObj = jsYaml.load(fmMatch[1]) || {}
|
|
333
|
+
if (typeof fmObj.acceptance === 'string') {
|
|
334
|
+
const t = fmObj.acceptance.trim()
|
|
335
|
+
return t ? [t] : []
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(fmObj.acceptance)) {
|
|
338
|
+
return fmObj.acceptance.filter(x => typeof x === 'string' && x.trim() !== '').map(x => x.trim())
|
|
339
|
+
}
|
|
340
|
+
} catch { /* frontmatter 非法 YAML → 防御性空(plan-postcheck 侧已拦) */ }
|
|
341
|
+
return []
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* allowed_paths 条目是否测试形态(结构归属面):test/ 前缀,或文件名含 .test. / _test. / spec
|
|
346
|
+
* 惯例((?:^|[._-])test(?:[._-]|$) 覆盖 foo.test.mjs / foo_test.js / test-foo.js 三形)。
|
|
347
|
+
* 比探针 3 的存在性面(isTestFileName 文件名形态判定,宽松存在性提示)严格——这是 3/7 口径差异的落点。
|
|
348
|
+
*/
|
|
349
|
+
function isProbe7TestPath(p) {
|
|
350
|
+
const posix = String(p).split('\\').join('/')
|
|
351
|
+
if (/^test\//.test(posix)) return true
|
|
352
|
+
const name = basename(posix)
|
|
353
|
+
return /(?:^|[._-])test(?:[._-]|$)/i.test(name) || /spec/i.test(name)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 内联解析当前 execute runId(探针 7 专用——禁静态 import task-review:
|
|
358
|
+
* task-review→verify-postcheck→verify-probes 三步环,先例见 backfillFactsFromMdAndTests
|
|
359
|
+
* 对 stage-contract 的分层注释)。优先读 marker current-execute-run-id-<change>;
|
|
360
|
+
* 读不到/非法则扫描 execute-runs/ 下 mtime 最新的 exec-* 目录兜底;两路皆空 → null
|
|
361
|
+
* (review 源归空集,不报错)。
|
|
362
|
+
* @param {string} runtimeRoot
|
|
363
|
+
* @param {string} changeName
|
|
364
|
+
* @returns {string|null}
|
|
365
|
+
*/
|
|
366
|
+
function resolveExecuteRunIdInline(runtimeRoot, changeName) {
|
|
367
|
+
try {
|
|
368
|
+
const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
369
|
+
if (existsSync(marker)) {
|
|
370
|
+
const c = readFileSync(marker, 'utf8').trim()
|
|
371
|
+
if (c && PROBE7_EXEC_RUN_ID_RE.test(c)) return c
|
|
372
|
+
}
|
|
373
|
+
} catch { /* marker 读取失败 → 目录扫描兜底 */ }
|
|
374
|
+
try {
|
|
375
|
+
const runsDir = join(runtimeRoot, 'execute-runs')
|
|
376
|
+
if (!existsSync(runsDir)) return null
|
|
377
|
+
const cands = readdirSync(runsDir)
|
|
378
|
+
.filter(n => PROBE7_EXEC_RUN_ID_RE.test(n))
|
|
379
|
+
.map(n => ({ n, p: join(runsDir, n) }))
|
|
380
|
+
.filter(x => { try { return statSync(x.p).isDirectory() } catch { return false } })
|
|
381
|
+
.map(x => { try { return { n: x.n, m: statSync(x.p).mtimeMs } } catch { return { n: x.n, m: 0 } } })
|
|
382
|
+
.sort((a, b) => b.m - a.m)
|
|
383
|
+
return cands.length > 0 ? cands[0].n : null
|
|
384
|
+
} catch {
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* 读当前 runId 下某 task 的 review.json changedFiles 中 test/ 前缀路径(归属第二源)。
|
|
391
|
+
* review.json 读不到 / JSON 非法 / changedFiles 非数组 → 空集不报错(提示面,宁缺毋噪)。
|
|
392
|
+
*/
|
|
393
|
+
function readReviewChangedTestFiles(runtimeRoot, runId, taskId) {
|
|
394
|
+
try {
|
|
395
|
+
const review = JSON.parse(readFileSync(join(runtimeRoot, 'execute-runs', runId, 'tasks', taskId, 'review.json'), 'utf8'))
|
|
396
|
+
if (!review || !Array.isArray(review.changedFiles)) return []
|
|
397
|
+
return review.changedFiles
|
|
398
|
+
.filter(f => typeof f === 'string')
|
|
399
|
+
.map(f => f.split('\\').join('/'))
|
|
400
|
+
.filter(f => f.startsWith('test/'))
|
|
401
|
+
} catch {
|
|
402
|
+
return []
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** acceptance 行关键词提取(标识符 ∪ CJK 片段,去重、按出现顺序,上限 5 词防膨胀) */
|
|
407
|
+
function extractAcceptanceTerms(text) {
|
|
408
|
+
const terms = []
|
|
409
|
+
const seen = new Set()
|
|
410
|
+
const re = new RegExp(PROBE7_TERM_RE.source, 'g') // 共享 g 正则有 lastIndex 状态坑,每次新建
|
|
411
|
+
let m
|
|
412
|
+
while ((m = re.exec(String(text || ''))) !== null) {
|
|
413
|
+
if (!seen.has(m[0])) { seen.add(m[0]); terms.push(m[0]) }
|
|
414
|
+
if (terms.length >= PROBE7_TERM_CAP) break
|
|
415
|
+
}
|
|
416
|
+
return terms
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* 关键词命中提示:逐 acceptance 条目提取词,在归属测试文件内容里 grep(大小写敏感子串)。
|
|
421
|
+
* 命中≠判定(R-03,不参与门禁)——只有存在命中的条目才进 hints(键 = acceptance 条目下标)。
|
|
422
|
+
* 测试文件读取双根回退(cwd → worktree 根,坑 probe1-worktree-path-blind / probe3 双根同族:
|
|
423
|
+
* apply 前新测试只在 worktree)。读不到的文件计 null 跳过。
|
|
424
|
+
* 坑 probe7-prefill-evidence(ql-20260915-004):anchors 逐词记录首命中 file:line(grep -n
|
|
425
|
+
* 语义——首个包含该词的测试文件内的首行号),供证据列机械预填;terms/files 键维持旧形态
|
|
426
|
+
* (既有消费方/测试零回归)。
|
|
427
|
+
* @returns {Record<number, {terms: string[], files: string[], anchors: Array<{term: string, file: string, line: number}>}>}
|
|
428
|
+
* 普通对象,可 JSON 序列化
|
|
429
|
+
*/
|
|
430
|
+
function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot) {
|
|
431
|
+
const hints = {}
|
|
432
|
+
if (!testFiles || testFiles.length === 0) return hints
|
|
433
|
+
const cache = new Map()
|
|
434
|
+
const readTest = (rel) => {
|
|
435
|
+
if (cache.has(rel)) return cache.get(rel)
|
|
436
|
+
let content = null
|
|
437
|
+
const posix = String(rel).split('\\').join('/')
|
|
438
|
+
for (const root of [cwd, wtRoot]) {
|
|
439
|
+
if (!root) continue
|
|
440
|
+
try { content = readFileSync(join(root, posix), 'utf8'); break } catch { /* 换根重试 */ }
|
|
441
|
+
}
|
|
442
|
+
cache.set(rel, content)
|
|
443
|
+
return content
|
|
444
|
+
}
|
|
445
|
+
// 首命中行号(1-based):indexOf 前缀行计数,CRLF 不影响(按 \n 切)
|
|
446
|
+
const firstHitLine = (content, term) => {
|
|
447
|
+
const idx = content.indexOf(term)
|
|
448
|
+
return idx === -1 ? null : content.slice(0, idx).split('\n').length
|
|
449
|
+
}
|
|
450
|
+
acceptanceItems.forEach((item, idx) => {
|
|
451
|
+
const hitTerms = []
|
|
452
|
+
const hitFiles = new Set()
|
|
453
|
+
const anchors = []
|
|
454
|
+
for (const term of extractAcceptanceTerms(item)) {
|
|
455
|
+
const hits = testFiles.filter(f => { const c = readTest(f); return !!c && c.includes(term) })
|
|
456
|
+
if (hits.length > 0) {
|
|
457
|
+
hitTerms.push(term)
|
|
458
|
+
for (const f of hits) hitFiles.add(f)
|
|
459
|
+
// 首命中文件内的首命中行(单文件 grep -n 首命中即可,机械保守)
|
|
460
|
+
const firstFile = hits[0]
|
|
461
|
+
const line = firstHitLine(readTest(firstFile) || '', term)
|
|
462
|
+
if (line !== null) anchors.push({ term, file: firstFile, line })
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (hitTerms.length > 0) hints[idx] = { terms: hitTerms, files: [...hitFiles], anchors }
|
|
466
|
+
})
|
|
467
|
+
return hints
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** 表格单元格转义:管道转 \|(GFM 行内字面量)、连续空白折叠、超长截断(纯展示层) */
|
|
471
|
+
function mdEscapeCell(text, cap = 200) {
|
|
472
|
+
let s = String(text).replace(/\s+/g, ' ').trim().replace(/\|/g, '\\|')
|
|
473
|
+
if (s.length > cap) s = s.slice(0, cap) + '…'
|
|
474
|
+
return s
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* 探针 7 判定/证据两列机械预填(坑 probe7-prefill-evidence,ql-20260915-004)。
|
|
479
|
+
*
|
|
480
|
+
* 背景:此前骨架判定列 `<待填:四选一>` + 证据列 `<TODO>` 全占位,24 格矩阵 agent 全量
|
|
481
|
+
* 手填——CLI 已算出归属/关键词命中却弃之不用。预填规则(机械保守,agent 逐格复核改写):
|
|
482
|
+
* - 无归属 → uncovered;acceptance 文本含 文档/部署/doc/deploy/manual/config 类词且无归属
|
|
483
|
+
* → non-testable
|
|
484
|
+
* - 有归属且命中 ≥1 → covered,证据 = 首命中 `file:line`(term)锚点(≤3 条防膨胀)
|
|
485
|
+
* - 有归属零命中 → partial,证据 = 无机械命中提示 + 反引号归属文件(满足门禁测试锚点口径)
|
|
486
|
+
*
|
|
487
|
+
* 门禁兼容(stage-contract.js extractAcceptanceMatrixSlots / MATRIX_VERDICT_WHITELIST):判定列
|
|
488
|
+
* 须为纯枚举(精确 trim 匹配白名单)——行内尾注「(预填,复核后可改)」会破坏枚举解析,故
|
|
489
|
+
* 尾注改置矩阵段头注记;covered/partial 证据须含测试锚点(file:line / 反引号 / .test.),
|
|
490
|
+
* non-testable 证据须非空——预填值均按此口径构造(预填即可过门,复核责任在段头注记明示)。
|
|
491
|
+
* @param {string} item acceptance 条目文本
|
|
492
|
+
* @param {string[]|undefined} testFiles 归属测试文件
|
|
493
|
+
* @param {{terms: string[], files: string[], anchors?: Array<{term: string, file: string, line: number}>}|null} hint
|
|
494
|
+
* @returns {{verdict: string, evidence: string}}
|
|
495
|
+
*/
|
|
496
|
+
function prefillMatrixCells(item, testFiles, hint) {
|
|
497
|
+
const hasAttribution = Array.isArray(testFiles) && testFiles.length > 0
|
|
498
|
+
if (!hasAttribution) {
|
|
499
|
+
return {
|
|
500
|
+
verdict: PROBE7_NONTESTABLE_RE.test(String(item || '')) ? 'non-testable' : 'uncovered',
|
|
501
|
+
evidence: '(无归属测试)',
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const anchors = (hint && Array.isArray(hint.anchors)) ? hint.anchors : []
|
|
505
|
+
if (anchors.length > 0) {
|
|
506
|
+
const evidence = anchors.slice(0, 3)
|
|
507
|
+
.map(a => `\`${mdEscapeCell(a.file, 120)}:${a.line}\`(${mdEscapeCell(a.term, 40)})`)
|
|
508
|
+
.join('、')
|
|
509
|
+
return { verdict: 'covered', evidence }
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
verdict: 'partial',
|
|
513
|
+
evidence: `(无机械命中——人工核验 \`${mdEscapeCell(testFiles[0], 120)}\`)`,
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 渲染探针 7 段(骨架与幂等补段共用单一实现)。
|
|
519
|
+
* 坑 probe7-prefill-evidence(ql-20260915-004):判定/证据两列由 prefillMatrixCells 机械预填
|
|
520
|
+
* (原 `<待填:四选一>` / `<TODO>` 占位淘汰)——幂等保障沿用补段口径:段已在场(agent 已填/
|
|
521
|
+
* 未填)一律不触碰(ensureAcceptanceMatrixSection 段在场即 no-op),预填只在骨架生成与缺段补齐
|
|
522
|
+
* 两条新写路径生效,agent 已填内容永不被覆盖。
|
|
523
|
+
* @param {{ applicable: boolean, tasks: Array<{task: string, acceptance: string[], testFiles: string[], hints: Record<number, {terms: string[], files: string[], anchors?: Array<{term: string, file: string, line: number}>}>}> }} p7
|
|
524
|
+
* @returns {string[]} 行数组(含段标题;调用方自理前后空行)
|
|
525
|
+
*/
|
|
526
|
+
function renderProbe7Lines(p7) {
|
|
527
|
+
const L = [PROBE7_HEADING]
|
|
528
|
+
if (!p7 || !p7.applicable) {
|
|
529
|
+
L.push('- 不适用(无 TaskCard)')
|
|
530
|
+
return L
|
|
531
|
+
}
|
|
532
|
+
L.push('<!-- 口径注记:探针 3 = 模块目录递归存在性面(allowed_paths 目录附近有没有测试);探针 7 = allowed_paths ∪ review changedFiles 结构归属承接面(每条 acceptance 由哪些测试承接);两者并排冲突以 7 为准。判定枚举(四选一):covered / partial / uncovered / non-testable(文档/部署类显式逃生门)。关键词命中只是提示,命中≠判定。 -->')
|
|
533
|
+
L.push('<!-- 预填说明(ql-20260915-004):判定列为 CLI 机械预填,agent 逐格复核改写——规则:无归属→uncovered(文档/部署/doc/deploy/manual/config 类词→non-testable);有归属且命中≥1→covered;有归属零命中→partial。证据列给首命中 file:line 锚点或人工核验提示。预填≠结论:与事实不符的格子必须改写(枚举须保持 covered/partial/uncovered/non-testable 纯值,备注写在证据列)。 -->')
|
|
534
|
+
// 零自动化承接计数(坑 review-zero-coverage-unwalked,2026-09-16 EHS 二次复核实证:execute
|
|
535
|
+
// 期 review 走查面事实上跟着测试覆盖走——「新增-部门支线测试充分,编辑路径与相关方支线
|
|
536
|
+
// 零覆盖」恰是 5 个 P1 的藏身处。矩阵渲染时顺带计数零承接条目,verify 复核强制显式走查
|
|
537
|
+
// 这些路径并在「代码审查」节登记走查结论——把二次复核「提前兑现移交项」的打法固化)。
|
|
538
|
+
let zeroCoverage = 0
|
|
539
|
+
for (const t of (p7.tasks || [])) {
|
|
540
|
+
L.push('')
|
|
541
|
+
L.push(`**${t.task}**`)
|
|
542
|
+
if (!Array.isArray(t.acceptance) || t.acceptance.length === 0) {
|
|
543
|
+
L.push('- (卡无 acceptance——防御,plan-postcheck 已拦)')
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
L.push('| acceptance 条目 | 归属测试文件 | 关键词命中(提示,命中≠判定) | 判定 | 证据 |')
|
|
547
|
+
L.push('|---|---|---|---|---|')
|
|
548
|
+
t.acceptance.forEach((item, i) => {
|
|
549
|
+
const attribCell = (t.testFiles && t.testFiles.length > 0)
|
|
550
|
+
? t.testFiles.map(f => `\`${f}\``).join('<br>')
|
|
551
|
+
: '无归属测试——判定大概率 uncovered'
|
|
552
|
+
const h = (t.hints && t.hints[i]) || null
|
|
553
|
+
const hintCell = h
|
|
554
|
+
? `${h.terms.map(x => mdEscapeCell(x)).join('、')}(${(h.files || []).map(f => `\`${f}\``).join('、')})`
|
|
555
|
+
: '—'
|
|
556
|
+
const pre = prefillMatrixCells(item, t.testFiles, h)
|
|
557
|
+
if (pre.verdict === 'uncovered' || pre.verdict === 'partial') zeroCoverage++
|
|
558
|
+
L.push(`| ${mdEscapeCell(item)} | ${attribCell} | ${hintCell} | ${pre.verdict} | ${pre.evidence} |`)
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
if (zeroCoverage > 0) {
|
|
562
|
+
L.push('')
|
|
563
|
+
L.push(`- ⚠️ 零/半自动化承接条目 ${zeroCoverage} 条——这些路径无测试兜底,verify 复核必须**显式走查**(尤其编辑/更新链路与非主分支流:二次复核实证它们正是 P1 藏身处),走查结论登记进「代码审查」节`)
|
|
564
|
+
}
|
|
565
|
+
return L
|
|
566
|
+
}
|
|
567
|
+
|
|
98
568
|
/**
|
|
99
|
-
*
|
|
569
|
+
* 跑四个机械探针 + 探针 7 验收×测试覆盖矩阵。
|
|
100
570
|
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
101
|
-
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object }}
|
|
571
|
+
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object, probe7: object }}
|
|
102
572
|
*/
|
|
103
573
|
export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
104
574
|
const specBase = resolveVerifyProbesSpecBase(cwd, specDir)
|
|
@@ -141,7 +611,7 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
141
611
|
const lines = readFileSync(abs, 'utf8').split('\n')
|
|
142
612
|
lines.forEach((line, i) => {
|
|
143
613
|
if (probe1.matches.length >= PROBE1_MAX_MATCHES) return
|
|
144
|
-
if (
|
|
614
|
+
if (isUnimplementedMarkerLine(line)) probe1.matches.push({ file: probePath, line: i + 1, content: line.trim().slice(0, 160) })
|
|
145
615
|
})
|
|
146
616
|
} catch {
|
|
147
617
|
probe1.skippedFiles.push(e.path)
|
|
@@ -156,8 +626,23 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
156
626
|
let allowed = []
|
|
157
627
|
if (existsSync(cardPath)) allowed = parseAllowedPaths(readFileSync(cardPath, 'utf8'))
|
|
158
628
|
const moduleDirs = [...new Set(allowed.map(p => dirname(p.split('\\').join('/'))).filter(d => d && d !== '.'))]
|
|
159
|
-
|
|
629
|
+
// JVM 镜像测试根(坑 probe3-java-mirror-blind,2026-09-15 EHS 生产实证 task-02~06 五连
|
|
630
|
+
// 假⚠️):Maven/Gradle 布局测试在 src/test/<lang> 同包镜像树,与 src/main 侧永不
|
|
631
|
+
// co-located——只递归 allowed_paths 目录(全在 main 侧)必然落空。对命中 src/main/<lang>
|
|
632
|
+
// 的目录补推镜像目录进扫描集(主仓或 worktree 任一侧存在才列/才扫——不存在的镜像根=
|
|
633
|
+
// 该包真无测试,保持 ⚠️ 真信号)。
|
|
634
|
+
const mirrorDirs = []
|
|
160
635
|
for (const d of moduleDirs) {
|
|
636
|
+
const m = d.match(/^(.*\/src\/)main(\/(?:java|kotlin|scala|groovy)(?:\/.*)?)$/)
|
|
637
|
+
if (!m) continue
|
|
638
|
+
const mirror = `${m[1]}test${m[2]}`
|
|
639
|
+
if (mirrorDirs.includes(mirror) || moduleDirs.includes(mirror)) continue
|
|
640
|
+
if (existsSync(join(cwd, mirror)) || (wtRoot && wtRoot !== cwd && existsSync(join(wtRoot, mirror)))) {
|
|
641
|
+
mirrorDirs.push(mirror)
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const testFiles = []
|
|
645
|
+
for (const d of [...moduleDirs, ...mirrorDirs]) {
|
|
161
646
|
// 双根并集扫描(坑 probe3-worktree-test-false-negative,2026-09-10 驾驭小结第五批②,
|
|
162
647
|
// 用户实证 5 条假 warning):apply 前新测试文件只在 worktree(untracked),而模块目录在
|
|
163
648
|
// 主仓**已存在**——旧「主仓目录缺失才回退 worktree」条件不触发,探针 3 报「未找到测试
|
|
@@ -175,6 +660,7 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
175
660
|
probe3.tasks.push({
|
|
176
661
|
task: taskId,
|
|
177
662
|
moduleDirs,
|
|
663
|
+
mirrorDirs,
|
|
178
664
|
testFiles: testFiles.slice(0, 10),
|
|
179
665
|
testFileCount: testFiles.length,
|
|
180
666
|
hasTest: testFiles.length > 0,
|
|
@@ -185,6 +671,19 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
185
671
|
// ── 探针 5:API 契约对账(复用 verifyApiParity:endpoints.json × 前端调用)──
|
|
186
672
|
const runtimeRoot = resolveRuntimeRoot({ specRoot: specDir }, specBase)
|
|
187
673
|
const probe5 = verifyApiParity(specBase, cwd, runtimeRoot, changeName)
|
|
674
|
+
// 跨仓 task 卡计数(坑 probe5-cross-repo-scope-blindness,2026-09-15 复盘:跨仓仓的前端
|
|
675
|
+
// 调用不在 parity 扫描根内,「frontend 0 调用」会被误读为漏配——渲染侧显式注记扫描面
|
|
676
|
+
// 边界。计数失败按 0(不渲染注记,零新增失败面)。
|
|
677
|
+
try {
|
|
678
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
679
|
+
if (changeName && existsSync(tasksDir)) {
|
|
680
|
+
let crossRepoCardCount = 0
|
|
681
|
+
for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n))) {
|
|
682
|
+
if (parseRepo(readFileSync(join(tasksDir, f), 'utf8'))) crossRepoCardCount++
|
|
683
|
+
}
|
|
684
|
+
if (crossRepoCardCount > 0) probe5.crossRepoCardCount = crossRepoCardCount
|
|
685
|
+
}
|
|
686
|
+
} catch { /* tasks 目录不可读 → 不注记 */ }
|
|
188
687
|
|
|
189
688
|
// ── 探针 6:代码删除对账(git diff --name-status HEAD 的 D/R × design 声明三态)──
|
|
190
689
|
const probe6 = { deletions: [], unavailable: false, note: '以 git 事实为准(真实 > 声明);是否 FAIL blocker 由 agent 诚实判定' }
|
|
@@ -220,7 +719,53 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
220
719
|
probe6.deletions.push({ path: posix, status: st, designOp: op || null, verdict })
|
|
221
720
|
}
|
|
222
721
|
|
|
223
|
-
|
|
722
|
+
// ── 探针 7:验收×测试覆盖矩阵(acceptance 自解析 + 双源结构归属 + 关键词提示)──
|
|
723
|
+
// applicable 顶层键:tasks/ 目录不存在或全无 frontmatter = false(quick 会话/旧变更零行为
|
|
724
|
+
// 变化——不渲染矩阵、不产生待填槽)。归属双源 = 卡 allowed_paths 测试模式 ∪ 当前 runId
|
|
725
|
+
// review.json changedFiles 的 test/ 前缀(runId 由 resolveExecuteRunIdInline 内联解析——
|
|
726
|
+
// 禁静态 import task-review,三步环见该函数注释)。
|
|
727
|
+
const probe7 = { applicable: false, tasks: [] }
|
|
728
|
+
{
|
|
729
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
730
|
+
if (existsSync(tasksDir)) {
|
|
731
|
+
const runId = resolveExecuteRunIdInline(runtimeRoot, changeName)
|
|
732
|
+
const cards = []
|
|
733
|
+
try {
|
|
734
|
+
for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
|
|
735
|
+
const raw = readFileSync(join(tasksDir, f), 'utf8')
|
|
736
|
+
const acceptance = parseTaskAcceptance(raw)
|
|
737
|
+
if (acceptance === null) continue // 无 frontmatter → 跳过(plan-postcheck 已拦)
|
|
738
|
+
const fmId = (raw.match(/^id:\s*(\S+)/m) || [])[1]
|
|
739
|
+
cards.push({ task: fmId || f.replace(/\.md$/, ''), acceptance, raw })
|
|
740
|
+
}
|
|
741
|
+
} catch { /* tasks 目录不可读 → applicable 维持 false */ }
|
|
742
|
+
probe7.applicable = cards.length > 0
|
|
743
|
+
for (const card of cards) {
|
|
744
|
+
const fromAllowed = parseAllowedPaths(card.raw)
|
|
745
|
+
.map(p => String(p).replace(/^NEW:\s*/, '').trim())
|
|
746
|
+
.filter(p => p && isProbe7TestPath(p))
|
|
747
|
+
const fromReview = runId ? readReviewChangedTestFiles(runtimeRoot, runId, card.task) : []
|
|
748
|
+
const testFiles = [...new Set([...fromAllowed, ...fromReview])]
|
|
749
|
+
probe7.tasks.push({
|
|
750
|
+
task: card.task,
|
|
751
|
+
acceptance: card.acceptance,
|
|
752
|
+
testFiles,
|
|
753
|
+
hints: buildAcceptanceHints(card.acceptance, testFiles, cwd, wtRoot),
|
|
754
|
+
})
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ── 探针 8:载荷字段契约对账(advisory;2026-09-16 EHS 二次复核驱动——字段错位族 4 处
|
|
760
|
+
// P1 是 URL 级 parity 的真空带)。fail-soft:内部全兜,异常降级 not-applicable 不炸整体。──
|
|
761
|
+
let probe8 = { applicable: false, mispairs: [], feOnly: [], missingNotNull: [], notes: [] }
|
|
762
|
+
try {
|
|
763
|
+
probe8 = runProbe8PayloadParity({ specBase, cwd, wtRoot, changeName })
|
|
764
|
+
} catch (e) {
|
|
765
|
+
probe8.notes = [`探针 8 执行失败(fail-soft 跳过):${e && e.message ? e.message : e}`]
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return { probe1, probe3, probe5, probe6, probe7, probe8 }
|
|
224
769
|
}
|
|
225
770
|
|
|
226
771
|
/**
|
|
@@ -253,21 +798,34 @@ export function renderVerifyProbesReport(result) {
|
|
|
253
798
|
if (!t.located) {
|
|
254
799
|
L.push(`- ⚠️ ${t.task}: 无 task 卡/allowed_paths,无法定位模块目录(agent 手查)`)
|
|
255
800
|
} else if (t.hasTest) {
|
|
256
|
-
|
|
801
|
+
const mirrorNote = (t.mirrorDirs && t.mirrorDirs.length > 0)
|
|
802
|
+
? `;JVM 镜像测试根(${t.mirrorDirs.join('、')})` : ''
|
|
803
|
+
L.push(`- ✅ ${t.task}: 模块目录(${t.moduleDirs.join('、')})${mirrorNote}找到 ${t.testFileCount} 个测试文件(${t.testFiles.slice(0, 5).join('、')}${t.testFileCount > 5 ? ' …' : ''})`)
|
|
257
804
|
} else {
|
|
258
|
-
|
|
805
|
+
const mirrorNote = (t.mirrorDirs && t.mirrorDirs.length > 0)
|
|
806
|
+
? '(含 co-located tests/ 与 JVM src/test 镜像根)' : '(含 co-located tests/)'
|
|
807
|
+
L.push(`- ⚠️ ${t.task}: 模块目录(${t.moduleDirs.join('、')})递归未找到测试文件${mirrorNote}`)
|
|
259
808
|
}
|
|
260
809
|
}
|
|
261
810
|
}
|
|
262
811
|
L.push(`- ℹ️ ${probe3.note}`)
|
|
263
812
|
L.push('')
|
|
264
813
|
|
|
814
|
+
// 探针 7 紧随探针 3(3.5 已被断言有效性抽查占用,编号顺延取 7 兼容锚定正则 /#### 探针 (\d+)/);
|
|
815
|
+
// 骨架序 3 → 7 → 4,ensureAcceptanceMatrixSection 补段同序。result 无 probe7 键(存量调用方/
|
|
816
|
+
// 合成 result)→ applicable=false 渲染「不适用」行,零回归。
|
|
817
|
+
L.push(...renderProbe7Lines(result.probe7 || { applicable: false, tasks: [] }))
|
|
818
|
+
L.push('')
|
|
819
|
+
|
|
265
820
|
L.push('#### 探针 4:决策追踪覆盖')
|
|
266
821
|
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->')
|
|
267
822
|
L.push('')
|
|
268
823
|
|
|
269
824
|
L.push('#### 探针 5:API Contract Parity')
|
|
270
825
|
L.push(`- ${probe5.summary || `backend ${probe5.backendCount ?? 0} 端点 / frontend ${probe5.frontendCount ?? 0} 调用`}`)
|
|
826
|
+
if (probe5.crossRepoCardCount > 0) {
|
|
827
|
+
L.push(`- ℹ️ parity 扫描面只含主仓——另有 ${probe5.crossRepoCardCount} 张跨仓 task 卡的仓不在扫描根内,跨仓前端调用/端点请到对应仓核对(D-004 跨仓对账不在本变更范围)`)
|
|
828
|
+
}
|
|
271
829
|
if ((probe5.scanRoots || []).length > 1) {
|
|
272
830
|
L.push(`- ℹ️ 后端端点比对集为多根并集(主仓既有 ∪ worktree 新增 ∪ 存量 artifact),共扫 ${probe5.scanRoots.length} 个根`)
|
|
273
831
|
}
|
|
@@ -285,7 +843,16 @@ export function renderVerifyProbesReport(result) {
|
|
|
285
843
|
L.push('- ❌ contract gap 是真实集成缺陷——诚实判 FAIL 并回 execute 补端点(CLI 仅 advisory 不硬阻断)')
|
|
286
844
|
}
|
|
287
845
|
if ((probe5.unusedBackend || []).length > 0) {
|
|
288
|
-
|
|
846
|
+
// unused 分层展示(probe5-unused-stock-noise):本变更相关(artifact 端点集内)逐条列出,
|
|
847
|
+
// 存量其余折叠计数——EHS 实证 490 个存量端点刷屏淹没了真信号。旧字段形态(无分层)零回归全列。
|
|
848
|
+
const relevant = Array.isArray(probe5.unusedChangeRelevant) && probe5.unusedChangeRelevant.length >= 0
|
|
849
|
+
? probe5.unusedChangeRelevant : probe5.unusedBackend
|
|
850
|
+
const stock = typeof probe5.unusedStockCount === 'number' ? probe5.unusedStockCount : 0
|
|
851
|
+
L.push(`- ⚠️ ${relevant.length} 个本变更端点前端未调用(warning 不阻断):${relevant.slice(0, 5).map(u => `${u.method} ${u.path}`).join('、')}${relevant.length > 5 ? ' …' : ''}`)
|
|
852
|
+
if (stock > 0) L.push(`- ℹ️ 另有 ${stock} 个存量端点未调用(他模块存量噪音,已折叠不逐条列出)`)
|
|
853
|
+
}
|
|
854
|
+
if ((probe5.crossRepoNotes || []).length > 0) {
|
|
855
|
+
for (const n of probe5.crossRepoNotes.slice(0, 5)) L.push(`- ℹ️ ${n}`)
|
|
289
856
|
}
|
|
290
857
|
L.push('')
|
|
291
858
|
|
|
@@ -300,9 +867,43 @@ export function renderVerifyProbesReport(result) {
|
|
|
300
867
|
}
|
|
301
868
|
}
|
|
302
869
|
L.push(`- ℹ️ ${probe6.note}`)
|
|
870
|
+
L.push('')
|
|
871
|
+
L.push(...(renderProbe8Lines(result.probe8 || { applicable: false, mispairs: [], feOnly: [], missingNotNull: [], notes: [] })))
|
|
303
872
|
return L.join('\n')
|
|
304
873
|
}
|
|
305
874
|
|
|
875
|
+
/**
|
|
876
|
+
* 渲染探针 8 段(advisory 口径注记随段输出——命中≠结论,agent 逐条复核)。
|
|
877
|
+
* @param {{applicable: boolean, backendFieldCount?: number, feKeyCount?: number, notNullCount?: number,
|
|
878
|
+
* javaFileCount?: number, sqlFileCount?: number, feFileCount?: number,
|
|
879
|
+
* mispairs: Array<{fe,be}>, feOnly: string[], missingNotNull: Array<{col}>, notes: string[]}} p8
|
|
880
|
+
* @returns {string[]} 行数组(含段标题)
|
|
881
|
+
*/
|
|
882
|
+
function renderProbe8Lines(p8) {
|
|
883
|
+
const L = [PROBE8_HEADING]
|
|
884
|
+
if (!p8 || !p8.applicable) {
|
|
885
|
+
L.push('- 不适用(清单无 Java/SQL 后端面,或 design.md 缺失)')
|
|
886
|
+
for (const n of (p8 && p8.notes) || []) L.push(`- ℹ️ ${n}`)
|
|
887
|
+
return L
|
|
888
|
+
}
|
|
889
|
+
L.push('<!-- 口径注记:静态启发式对账——前端请求载荷键(apiFetch/request/axios/fetch 调用邻近对象键)× 后端 Java private 字段(归一化覆盖)∪ 疑似错位配对 ∪ SQL NOT NULL 列缺送。命中≠结论:UI 本地态键/服务端填充列会自然出现在差异里,agent 逐条复核(2026-09-16 EHS 二次复核实证:leaderUserId↔rpLeaderUserId 字段错位致相关方支线三端不可用、缺发 reportOrgId 致小程序开立被拒——URL 级 parity 抓不住载荷级错位)。 -->')
|
|
890
|
+
L.push(`- ℹ️ 比对面:前端载荷键 ${p8.feKeyCount ?? 0}(${p8.feFileCount ?? 0} 前端文件)× 后端字段 ${p8.backendFieldCount ?? 0}(${p8.javaFileCount ?? 0} Java 文件)+ NOT NULL 列 ${p8.notNullCount ?? 0}(${p8.sqlFileCount ?? 0} SQL 文件)`)
|
|
891
|
+
if ((p8.mispairs || []).length > 0) {
|
|
892
|
+
L.push(`- ⚠️ 疑似字段错位配对(前后端名近形,人工核实一对一映射):${p8.mispairs.slice(0, 8).map(p => `${p.fe} ↔ ${p.be}`).join('、')}${p8.mispairs.length > 8 ? ` …共 ${p8.mispairs.length} 对` : ''}`)
|
|
893
|
+
}
|
|
894
|
+
if ((p8.feOnly || []).length > 0) {
|
|
895
|
+
L.push(`- ℹ️ 前端独有键(候选 UI 本地态/字段错位/跨层无关):${p8.feOnly.slice(0, 10).join('、')}${p8.feOnly.length > 10 ? ` …共 ${p8.feOnly.length} 个` : ''}`)
|
|
896
|
+
}
|
|
897
|
+
if ((p8.missingNotNull || []).length > 0) {
|
|
898
|
+
L.push(`- ⚠️ NOT NULL 列前端未见(候选必填缺送/服务端填充):${p8.missingNotNull.slice(0, 8).map(c => c.col).join('、')}${p8.missingNotNull.length > 8 ? ` …共 ${p8.missingNotNull.length} 个` : ''}`)
|
|
899
|
+
}
|
|
900
|
+
if ((p8.mispairs || []).length === 0 && (p8.missingNotNull || []).length === 0) {
|
|
901
|
+
L.push('- ✅ 载荷字段面零疑似差异(归一化覆盖 + NOT NULL 全见)')
|
|
902
|
+
}
|
|
903
|
+
for (const n of p8.notes || []) L.push(`- ℹ️ ${n}`)
|
|
904
|
+
return L
|
|
905
|
+
}
|
|
906
|
+
|
|
306
907
|
/**
|
|
307
908
|
* 从 runVerifyProbes 结果构造 verify-facts.json 机器底稿对象(P3b 可复跑审计底稿)。
|
|
308
909
|
* 命令行统一 `sillyspec verify-probes --change <name>`;指标 fail-soft——字段不可得时少列
|
|
@@ -321,6 +922,7 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
321
922
|
const p3 = (result && result.probe3) || {}
|
|
322
923
|
const p5 = (result && result.probe5) || {}
|
|
323
924
|
const p6 = (result && result.probe6) || {}
|
|
925
|
+
const p8 = (result && result.probe8) || {}
|
|
324
926
|
// v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
|
|
325
927
|
// runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
|
|
326
928
|
// 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
|
|
@@ -359,6 +961,16 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
359
961
|
unavailable: typeof p6.unavailable === 'boolean' ? p6.unavailable : undefined,
|
|
360
962
|
}),
|
|
361
963
|
},
|
|
964
|
+
probe8: {
|
|
965
|
+
command,
|
|
966
|
+
metrics: defined({
|
|
967
|
+
mispairs: len(p8.mispairs),
|
|
968
|
+
feOnly: len(p8.feOnly),
|
|
969
|
+
missingNotNull: len(p8.missingNotNull),
|
|
970
|
+
feKeys: num(p8.feKeyCount),
|
|
971
|
+
backendFields: num(p8.backendFieldCount),
|
|
972
|
+
}),
|
|
973
|
+
},
|
|
362
974
|
},
|
|
363
975
|
}
|
|
364
976
|
}
|
|
@@ -421,6 +1033,36 @@ export function writeVerifyFacts(changeDir, result, changeName, opts = {}) {
|
|
|
421
1033
|
* @param {{ verifyMd: string, testCheckResult?: object|null }} opts
|
|
422
1034
|
* @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
|
|
423
1035
|
*/
|
|
1036
|
+
// 移交项结构化(坑 handover-only-in-prose,2026-09-15/16 EHS 生产实证):verify 结论
|
|
1037
|
+
// PASS WITH NOTES 的移交条目(环境阻断复跑/人工验收/待执行脚本)此前只活在结论槽正文
|
|
1038
|
+
// 叙述——后续独立复核发现被环境阻断 deferred 的集成测试里正藏着 5 个 P1;「移交项没有
|
|
1039
|
+
// 结构化清单 = 没人兜」。骨架新增「## 移交项(结构化)」章节(三列表格 + 类型枚举注释),
|
|
1040
|
+
// 本函数解析其表行为机器可读 items(facts.handover 回填 + advisory 判定用)。
|
|
1041
|
+
// 容错:占位行(<待填…)跳过;类型归一小写连字符(ENV-BLOCKED→env-blocked);未知类型
|
|
1042
|
+
// 保留原值(advisory 面向 agent 复核,不静默丢弃);非表格行(prose/注释)忽略。
|
|
1043
|
+
const HANDOVER_HEADING_RE = /^## 移交项(结构化)[^\n]*$/m
|
|
1044
|
+
export function parseHandoverRows(md) {
|
|
1045
|
+
const text = String(md || '').replace(/\r\n/g, '\n')
|
|
1046
|
+
const m = text.match(HANDOVER_HEADING_RE)
|
|
1047
|
+
if (!m) return []
|
|
1048
|
+
const after = text.slice(m.index + m[0].length)
|
|
1049
|
+
const nextSection = after.match(/\n## /)
|
|
1050
|
+
const section = nextSection ? after.slice(0, nextSection.index) : after
|
|
1051
|
+
const items = []
|
|
1052
|
+
for (const line of section.split('\n')) {
|
|
1053
|
+
const row = line.match(/^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]*?)\s*\|\s*$/)
|
|
1054
|
+
if (!row) continue
|
|
1055
|
+
const type = row[1].trim()
|
|
1056
|
+
const item = row[2].trim()
|
|
1057
|
+
const condition = row[3].trim()
|
|
1058
|
+
if (type === '类型' || /^-{2,}$/.test(type.replace(/\|/g, ''))) continue // 表头/分隔行
|
|
1059
|
+
if (type.startsWith('<') || item.startsWith('<')) continue // 骨架占位行
|
|
1060
|
+
if (!type || !item) continue
|
|
1061
|
+
items.push({ type: type.toLowerCase().replace(/[\s_]+/g, '-'), item, condition })
|
|
1062
|
+
}
|
|
1063
|
+
return items
|
|
1064
|
+
}
|
|
1065
|
+
|
|
424
1066
|
export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
|
|
425
1067
|
let facts = null
|
|
426
1068
|
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
@@ -448,6 +1090,17 @@ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResu
|
|
|
448
1090
|
if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
|
|
449
1091
|
facts.runtimeEvidence = slots.runtimeEvidence
|
|
450
1092
|
}
|
|
1093
|
+
// 移交项结构化回填(handover-only-in-prose):有有效行 → facts.handover 落盘(机器可读);
|
|
1094
|
+
// PASS WITH NOTES 零有效行 → advisory 警告(不阻断——存量 PASS WITH NOTES 无此章节是常态,
|
|
1095
|
+
// 渐进采纳)。EHS 实证:被环境阻断 deferred 的集成测试里藏着 5 个 P1,清单化才有
|
|
1096
|
+
// 「谁兜、怎么复跑」的可追溯面。
|
|
1097
|
+
const handoverItems = parseHandoverRows(verifyMd || '')
|
|
1098
|
+
if (handoverItems.length > 0) {
|
|
1099
|
+
facts.handover = { count: handoverItems.length, items: handoverItems }
|
|
1100
|
+
}
|
|
1101
|
+
if (conclusionSlot === 'PASS WITH NOTES' && handoverItems.length === 0) {
|
|
1102
|
+
console.warn('⚠️ 结论=PASS WITH NOTES 但「移交项(结构化)」章节零有效行——正文叙述的移交项(环境阻断复跑条件/人工验收步骤/待执行脚本)请结构化进 ## 移交项(结构化) 表格(类型枚举 env-blocked/manual-acceptance/db-script/other),避免移交项只活在 prose 里没人兜(2026-09-15 EHS 实证:被环境阻断 deferred 的集成测试里藏着 5 个 P1)。')
|
|
1103
|
+
}
|
|
451
1104
|
let testsBackfilled = false
|
|
452
1105
|
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
453
1106
|
facts.tests = {
|
|
@@ -508,6 +1161,46 @@ export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = [])
|
|
|
508
1161
|
return { added }
|
|
509
1162
|
}
|
|
510
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* 探针 7 矩阵段幂等补齐(--init 段落级,2026-09-14-acceptance-test-matrix FR-01;学
|
|
1166
|
+
* backfillMissingEvidenceSlots 形态):verify-result.md 已存在但缺「#### 探针 7」段且
|
|
1167
|
+
* applicable=true(有 TaskCard)时补矩阵骨架段——不触碰既有正文;二跑零改动(幂等)。
|
|
1168
|
+
* 插入位置:文件内有「#### 探针 3」小节时插在该小节之后(保持骨架序 3 → 7 → 4),否则
|
|
1169
|
+
* 文末追加(旧格式/手写正文)。段已在场(冒号全/半角均认,防走样标题绕过检测)或
|
|
1170
|
+
* applicable=false → no-op。本变更自举通道:verify-result.md 先于特性存在的存量走此补段。
|
|
1171
|
+
* @param {string} mdPath verify-result.md 路径
|
|
1172
|
+
* @param {{ applicable: boolean, tasks: Array }} probe7 runVerifyProbes 返回的 probe7
|
|
1173
|
+
* @returns {{ added: boolean, reason?: string }}
|
|
1174
|
+
*/
|
|
1175
|
+
export function ensureAcceptanceMatrixSection(mdPath, probe7) {
|
|
1176
|
+
let text
|
|
1177
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: false, reason: 'verify-result.md 不存在或不可读' } }
|
|
1178
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
1179
|
+
if (/^#### 探针 7[::]/m.test(normalized)) return { added: false, reason: '探针 7 段已在场' }
|
|
1180
|
+
const p7 = probe7 && typeof probe7 === 'object' ? probe7 : { applicable: false, tasks: [] }
|
|
1181
|
+
if (!p7.applicable) return { added: false, reason: '不适用(无 TaskCard)' }
|
|
1182
|
+
const blockLines = [...renderProbe7Lines(p7), '']
|
|
1183
|
+
try {
|
|
1184
|
+
const lines = normalized.split('\n')
|
|
1185
|
+
const p3Idx = lines.findIndex(l => /^#### 探针 3[::]/.test(l))
|
|
1186
|
+
if (p3Idx === -1) {
|
|
1187
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + '\n' + blockLines.join('\n'))
|
|
1188
|
+
} else {
|
|
1189
|
+
// 探针 3 小节终点 = 下一个任意级别标题行(与 verify-postcheck extractProbeSubsections 同口径)
|
|
1190
|
+
let end = lines.length
|
|
1191
|
+
for (let i = p3Idx + 1; i < lines.length; i++) {
|
|
1192
|
+
if (/^#{1,6}\s/.test(lines[i])) { end = i; break }
|
|
1193
|
+
}
|
|
1194
|
+
lines.splice(end, 0, ...blockLines)
|
|
1195
|
+
writeFileSync(mdPath, lines.join('\n'))
|
|
1196
|
+
}
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
console.warn(`⚠️ 探针 7 矩阵段补齐落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
1199
|
+
return { added: false, reason: '落盘失败' }
|
|
1200
|
+
}
|
|
1201
|
+
return { added: true }
|
|
1202
|
+
}
|
|
1203
|
+
|
|
511
1204
|
/**
|
|
512
1205
|
* 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
|
|
513
1206
|
* 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
|
|
@@ -625,11 +1318,21 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
625
1318
|
'',
|
|
626
1319
|
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
|
|
627
1320
|
'> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
|
|
1321
|
+
'>',
|
|
1322
|
+
'> 「层」=证据可核验性分层(非执行者声明):「人工判断」指本节为语义判断,由执行 agent 填写、',
|
|
1323
|
+
'> CLI 不机械复跑(gate 抽查 + 人类审批点复核兜底);「可复跑探针/确定性检查/CLI 一致性校验」= gate 可机械复核段。',
|
|
628
1324
|
'',
|
|
629
1325
|
'## 结论 [层:人工判断]',
|
|
630
1326
|
'',
|
|
631
1327
|
'结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
|
|
632
1328
|
'',
|
|
1329
|
+
'## 移交项(结构化) [层:人工判断——CLI 清单核验]',
|
|
1330
|
+
'<!-- 结论=PASS WITH NOTES 时本节必填(prose 移交叙述转结构化,复跑/验收有据可查、agent 可恢复复跑);结论=PASS/FAIL 写「无」 -->',
|
|
1331
|
+
'<!-- 类型枚举:env-blocked(环境阻断,条件列必填复跑口径)/ manual-acceptance(人工验收,条件列必填验收步骤)/ db-script(待执行脚本,条件列必填执行环境与顺序)/ other -->',
|
|
1332
|
+
'| 类型 | 条目 | 复跑/验收条件 |',
|
|
1333
|
+
'|---|---|---|',
|
|
1334
|
+
'| <待填:env-blocked / manual-acceptance / db-script / other> | <待填:移交条目> | <待填:复跑/验收条件> |',
|
|
1335
|
+
'',
|
|
633
1336
|
'## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
|
|
634
1337
|
'<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
|
|
635
1338
|
'- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
|
|
@@ -663,7 +1366,15 @@ export function generateVerifyResultSkeleton(result) {
|
|
|
663
1366
|
'<!--TODO: 关键命令输出/时间戳/commit hash 证据链;integration/deployment-critical 必填,按实际触碰的运行时组件写(启动命令/端点/请求响应/日志片段/生命周期终态断言/失败模式排除),未涉及的行写「不涉及」-->',
|
|
664
1367
|
'',
|
|
665
1368
|
'## 代码审查 [层:人工判断]',
|
|
666
|
-
'<!--TODO: 问题列表 +
|
|
1369
|
+
'<!--TODO: 问题列表 + 总体评价。走查清单(零覆盖路径必查——探针 7 ⚠️ 条目即定向面):',
|
|
1370
|
+
' ① 编辑/更新链路(回显、字段映射、残留态)——非新增主链路,实证盲区;',
|
|
1371
|
+
' ② 非主分支流(相关方/旁路支线等未走查路径);',
|
|
1372
|
+
' ③ 守卫一致性:同资源端点的操作人/权限校验模式对比(实证 doSubmit 无操作人校验而 delete/withdraw 有——越权);',
|
|
1373
|
+
' ④ 载荷字段契约(探针 8 ⚠️ 配对逐条核实);',
|
|
1374
|
+
' ⑤ 分页/并发/事务原子性(无测试基建端的纯逻辑面)-->',
|
|
1375
|
+
'',
|
|
1376
|
+
'## 独立复核(可选回流槽) [层:人工判断——复核后追加]',
|
|
1377
|
+
'<!-- verify 完成后的深度复核(独立子代理/二次审查)结论回流至此:缺陷分级(P1 功能不可用 / P2 需求子项 / P3 建议修)+ 修复证据链 + 对「结论枚举」的影响改写。无复核时本节写「无」或删除。复核结论不再只活在聊天记录(2026-09-16 EHS 二次复核实证:5 个 P1 只有聊天可查,变更档案仍写 PASS WITH NOTES)。 -->',
|
|
667
1378
|
'',
|
|
668
1379
|
]
|
|
669
1380
|
return L.join('\n')
|