sillyspec 3.28.7 → 3.28.9
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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- 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/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- 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 +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
package/src/verify-probes.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* 探针3 测试覆盖:逐 task 按 allowed_paths 定位模块目录,递归找测试文件(co-located tests/ 陷阱)
|
|
9
9
|
* 探针5 API 契约对账:复用 contract-matrix.verifyApiParity(endpoints.json × 前端调用)+ 表格渲染
|
|
10
10
|
* 探针6 删除对账:git diff --name-status HEAD 的 D/R × design 声明操作三态判定
|
|
11
|
+
* 探针7 验收×测试覆盖矩阵:task 卡 acceptance 自解析(jsYaml,string/array 双形态)+ 双源结构
|
|
12
|
+
* 归属(allowed_paths 测试模式 ∪ execute-runs review.json changedFiles test/ 前缀)+
|
|
13
|
+
* 关键词命中提示(命中≠判定,不参与门禁;2026-09-14-acceptance-test-matrix)
|
|
11
14
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
15
|
*
|
|
13
16
|
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
@@ -17,8 +20,9 @@
|
|
|
17
20
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
18
21
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
19
22
|
*/
|
|
20
|
-
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs'
|
|
21
24
|
import { join, dirname, basename } from 'path'
|
|
25
|
+
import jsYaml from 'js-yaml'
|
|
22
26
|
import { gitQuiet } from './git-helper.js'
|
|
23
27
|
import {
|
|
24
28
|
FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
|
|
@@ -95,10 +99,259 @@ function findTestFiles(rootDir, cwd, cap = 10) {
|
|
|
95
99
|
return found
|
|
96
100
|
}
|
|
97
101
|
|
|
102
|
+
// ── 探针 7(验收×测试覆盖矩阵,2026-09-14-acceptance-test-matrix FR-01)常量与纯函数 ──
|
|
103
|
+
const PROBE7_HEADING = '#### 探针 7:验收×测试覆盖矩阵'
|
|
104
|
+
// execute run id 格式(与 task-review.js isValidExecuteRunId 同口径锚定,防提示词注入/路径穿越)
|
|
105
|
+
const PROBE7_EXEC_RUN_ID_RE = /^exec-\d{4}-\d{2}-\d{2}-\d{6}(?:-[a-z0-9]{1,8}){0,2}$/
|
|
106
|
+
// 关键词提取:≥3 字符标识符([A-Za-z_][A-Za-z0-9_]{2,})∪ ≥2 字连续 CJK 片段,按出现顺序交错
|
|
107
|
+
const PROBE7_TERM_RE = /[A-Za-z_][A-Za-z0-9_]{2,}|[\u4e00-\u9fff]{2,}/g
|
|
108
|
+
const PROBE7_TERM_CAP = 5
|
|
109
|
+
// 坑 probe7-prefill-evidence(ql-20260915-004):无归属时的 non-testable 判别词(机械保守:
|
|
110
|
+
// acceptance 文本命中即预填 non-testable,agent 复核可改写)
|
|
111
|
+
const PROBE7_NONTESTABLE_RE = /文档|部署|doc|deploy|manual|config/i
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 解析 task-NN.md frontmatter 的 acceptance(string/array 双形态归一为数组)。
|
|
115
|
+
* 口径锚 src/stages/plan-postcheck.js acceptance best-effort 段(string → 原文单条、array →
|
|
116
|
+
* 逐条)——不 import 它:parseTaskContracts 只返回 provides/expects_from 不含 acceptance。
|
|
117
|
+
* @param {string} content task 卡全文
|
|
118
|
+
* @returns {string[]|null} acceptance 条目数组;null = 无 frontmatter(调用方跳过该卡);
|
|
119
|
+
* frontmatter 在场但无 acceptance / 非法 YAML → [](防御行,plan-postcheck 已拦缺失)
|
|
120
|
+
*/
|
|
121
|
+
export function parseTaskAcceptance(content) {
|
|
122
|
+
const fmMatch = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
123
|
+
if (!fmMatch) return null
|
|
124
|
+
try {
|
|
125
|
+
const fmObj = jsYaml.load(fmMatch[1]) || {}
|
|
126
|
+
if (typeof fmObj.acceptance === 'string') {
|
|
127
|
+
const t = fmObj.acceptance.trim()
|
|
128
|
+
return t ? [t] : []
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(fmObj.acceptance)) {
|
|
131
|
+
return fmObj.acceptance.filter(x => typeof x === 'string' && x.trim() !== '').map(x => x.trim())
|
|
132
|
+
}
|
|
133
|
+
} catch { /* frontmatter 非法 YAML → 防御性空(plan-postcheck 侧已拦) */ }
|
|
134
|
+
return []
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* allowed_paths 条目是否测试形态(结构归属面):test/ 前缀,或文件名含 .test. / _test. / spec
|
|
139
|
+
* 惯例((?:^|[._-])test(?:[._-]|$) 覆盖 foo.test.mjs / foo_test.js / test-foo.js 三形)。
|
|
140
|
+
* 比探针 3 的存在性面(TEST_FILE_RE 子串宽松匹配)严格——这是 3/7 口径差异的落点。
|
|
141
|
+
*/
|
|
142
|
+
function isProbe7TestPath(p) {
|
|
143
|
+
const posix = String(p).split('\\').join('/')
|
|
144
|
+
if (/^test\//.test(posix)) return true
|
|
145
|
+
const name = basename(posix)
|
|
146
|
+
return /(?:^|[._-])test(?:[._-]|$)/i.test(name) || /spec/i.test(name)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 内联解析当前 execute runId(探针 7 专用——禁静态 import task-review:
|
|
151
|
+
* task-review→verify-postcheck→verify-probes 三步环,先例见 backfillFactsFromMdAndTests
|
|
152
|
+
* 对 stage-contract 的分层注释)。优先读 marker current-execute-run-id-<change>;
|
|
153
|
+
* 读不到/非法则扫描 execute-runs/ 下 mtime 最新的 exec-* 目录兜底;两路皆空 → null
|
|
154
|
+
* (review 源归空集,不报错)。
|
|
155
|
+
* @param {string} runtimeRoot
|
|
156
|
+
* @param {string} changeName
|
|
157
|
+
* @returns {string|null}
|
|
158
|
+
*/
|
|
159
|
+
function resolveExecuteRunIdInline(runtimeRoot, changeName) {
|
|
160
|
+
try {
|
|
161
|
+
const marker = join(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
162
|
+
if (existsSync(marker)) {
|
|
163
|
+
const c = readFileSync(marker, 'utf8').trim()
|
|
164
|
+
if (c && PROBE7_EXEC_RUN_ID_RE.test(c)) return c
|
|
165
|
+
}
|
|
166
|
+
} catch { /* marker 读取失败 → 目录扫描兜底 */ }
|
|
167
|
+
try {
|
|
168
|
+
const runsDir = join(runtimeRoot, 'execute-runs')
|
|
169
|
+
if (!existsSync(runsDir)) return null
|
|
170
|
+
const cands = readdirSync(runsDir)
|
|
171
|
+
.filter(n => PROBE7_EXEC_RUN_ID_RE.test(n))
|
|
172
|
+
.map(n => ({ n, p: join(runsDir, n) }))
|
|
173
|
+
.filter(x => { try { return statSync(x.p).isDirectory() } catch { return false } })
|
|
174
|
+
.map(x => { try { return { n: x.n, m: statSync(x.p).mtimeMs } } catch { return { n: x.n, m: 0 } } })
|
|
175
|
+
.sort((a, b) => b.m - a.m)
|
|
176
|
+
return cands.length > 0 ? cands[0].n : null
|
|
177
|
+
} catch {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
98
182
|
/**
|
|
99
|
-
*
|
|
183
|
+
* 读当前 runId 下某 task 的 review.json changedFiles 中 test/ 前缀路径(归属第二源)。
|
|
184
|
+
* review.json 读不到 / JSON 非法 / changedFiles 非数组 → 空集不报错(提示面,宁缺毋噪)。
|
|
185
|
+
*/
|
|
186
|
+
function readReviewChangedTestFiles(runtimeRoot, runId, taskId) {
|
|
187
|
+
try {
|
|
188
|
+
const review = JSON.parse(readFileSync(join(runtimeRoot, 'execute-runs', runId, 'tasks', taskId, 'review.json'), 'utf8'))
|
|
189
|
+
if (!review || !Array.isArray(review.changedFiles)) return []
|
|
190
|
+
return review.changedFiles
|
|
191
|
+
.filter(f => typeof f === 'string')
|
|
192
|
+
.map(f => f.split('\\').join('/'))
|
|
193
|
+
.filter(f => f.startsWith('test/'))
|
|
194
|
+
} catch {
|
|
195
|
+
return []
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** acceptance 行关键词提取(标识符 ∪ CJK 片段,去重、按出现顺序,上限 5 词防膨胀) */
|
|
200
|
+
function extractAcceptanceTerms(text) {
|
|
201
|
+
const terms = []
|
|
202
|
+
const seen = new Set()
|
|
203
|
+
const re = new RegExp(PROBE7_TERM_RE.source, 'g') // 共享 g 正则有 lastIndex 状态坑,每次新建
|
|
204
|
+
let m
|
|
205
|
+
while ((m = re.exec(String(text || ''))) !== null) {
|
|
206
|
+
if (!seen.has(m[0])) { seen.add(m[0]); terms.push(m[0]) }
|
|
207
|
+
if (terms.length >= PROBE7_TERM_CAP) break
|
|
208
|
+
}
|
|
209
|
+
return terms
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 关键词命中提示:逐 acceptance 条目提取词,在归属测试文件内容里 grep(大小写敏感子串)。
|
|
214
|
+
* 命中≠判定(R-03,不参与门禁)——只有存在命中的条目才进 hints(键 = acceptance 条目下标)。
|
|
215
|
+
* 测试文件读取双根回退(cwd → worktree 根,坑 probe1-worktree-path-blind / probe3 双根同族:
|
|
216
|
+
* apply 前新测试只在 worktree)。读不到的文件计 null 跳过。
|
|
217
|
+
* 坑 probe7-prefill-evidence(ql-20260915-004):anchors 逐词记录首命中 file:line(grep -n
|
|
218
|
+
* 语义——首个包含该词的测试文件内的首行号),供证据列机械预填;terms/files 键维持旧形态
|
|
219
|
+
* (既有消费方/测试零回归)。
|
|
220
|
+
* @returns {Record<number, {terms: string[], files: string[], anchors: Array<{term: string, file: string, line: number}>}>}
|
|
221
|
+
* 普通对象,可 JSON 序列化
|
|
222
|
+
*/
|
|
223
|
+
function buildAcceptanceHints(acceptanceItems, testFiles, cwd, wtRoot) {
|
|
224
|
+
const hints = {}
|
|
225
|
+
if (!testFiles || testFiles.length === 0) return hints
|
|
226
|
+
const cache = new Map()
|
|
227
|
+
const readTest = (rel) => {
|
|
228
|
+
if (cache.has(rel)) return cache.get(rel)
|
|
229
|
+
let content = null
|
|
230
|
+
const posix = String(rel).split('\\').join('/')
|
|
231
|
+
for (const root of [cwd, wtRoot]) {
|
|
232
|
+
if (!root) continue
|
|
233
|
+
try { content = readFileSync(join(root, posix), 'utf8'); break } catch { /* 换根重试 */ }
|
|
234
|
+
}
|
|
235
|
+
cache.set(rel, content)
|
|
236
|
+
return content
|
|
237
|
+
}
|
|
238
|
+
// 首命中行号(1-based):indexOf 前缀行计数,CRLF 不影响(按 \n 切)
|
|
239
|
+
const firstHitLine = (content, term) => {
|
|
240
|
+
const idx = content.indexOf(term)
|
|
241
|
+
return idx === -1 ? null : content.slice(0, idx).split('\n').length
|
|
242
|
+
}
|
|
243
|
+
acceptanceItems.forEach((item, idx) => {
|
|
244
|
+
const hitTerms = []
|
|
245
|
+
const hitFiles = new Set()
|
|
246
|
+
const anchors = []
|
|
247
|
+
for (const term of extractAcceptanceTerms(item)) {
|
|
248
|
+
const hits = testFiles.filter(f => { const c = readTest(f); return !!c && c.includes(term) })
|
|
249
|
+
if (hits.length > 0) {
|
|
250
|
+
hitTerms.push(term)
|
|
251
|
+
for (const f of hits) hitFiles.add(f)
|
|
252
|
+
// 首命中文件内的首命中行(单文件 grep -n 首命中即可,机械保守)
|
|
253
|
+
const firstFile = hits[0]
|
|
254
|
+
const line = firstHitLine(readTest(firstFile) || '', term)
|
|
255
|
+
if (line !== null) anchors.push({ term, file: firstFile, line })
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (hitTerms.length > 0) hints[idx] = { terms: hitTerms, files: [...hitFiles], anchors }
|
|
259
|
+
})
|
|
260
|
+
return hints
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** 表格单元格转义:管道转 \|(GFM 行内字面量)、连续空白折叠、超长截断(纯展示层) */
|
|
264
|
+
function mdEscapeCell(text, cap = 200) {
|
|
265
|
+
let s = String(text).replace(/\s+/g, ' ').trim().replace(/\|/g, '\\|')
|
|
266
|
+
if (s.length > cap) s = s.slice(0, cap) + '…'
|
|
267
|
+
return s
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 探针 7 判定/证据两列机械预填(坑 probe7-prefill-evidence,ql-20260915-004)。
|
|
272
|
+
*
|
|
273
|
+
* 背景:此前骨架判定列 `<待填:四选一>` + 证据列 `<TODO>` 全占位,24 格矩阵 agent 全量
|
|
274
|
+
* 手填——CLI 已算出归属/关键词命中却弃之不用。预填规则(机械保守,agent 逐格复核改写):
|
|
275
|
+
* - 无归属 → uncovered;acceptance 文本含 文档/部署/doc/deploy/manual/config 类词且无归属
|
|
276
|
+
* → non-testable
|
|
277
|
+
* - 有归属且命中 ≥1 → covered,证据 = 首命中 `file:line`(term)锚点(≤3 条防膨胀)
|
|
278
|
+
* - 有归属零命中 → partial,证据 = 无机械命中提示 + 反引号归属文件(满足门禁测试锚点口径)
|
|
279
|
+
*
|
|
280
|
+
* 门禁兼容(stage-contract.js extractAcceptanceMatrixSlots / MATRIX_VERDICT_WHITELIST):判定列
|
|
281
|
+
* 须为纯枚举(精确 trim 匹配白名单)——行内尾注「(预填,复核后可改)」会破坏枚举解析,故
|
|
282
|
+
* 尾注改置矩阵段头注记;covered/partial 证据须含测试锚点(file:line / 反引号 / .test.),
|
|
283
|
+
* non-testable 证据须非空——预填值均按此口径构造(预填即可过门,复核责任在段头注记明示)。
|
|
284
|
+
* @param {string} item acceptance 条目文本
|
|
285
|
+
* @param {string[]|undefined} testFiles 归属测试文件
|
|
286
|
+
* @param {{terms: string[], files: string[], anchors?: Array<{term: string, file: string, line: number}>}|null} hint
|
|
287
|
+
* @returns {{verdict: string, evidence: string}}
|
|
288
|
+
*/
|
|
289
|
+
function prefillMatrixCells(item, testFiles, hint) {
|
|
290
|
+
const hasAttribution = Array.isArray(testFiles) && testFiles.length > 0
|
|
291
|
+
if (!hasAttribution) {
|
|
292
|
+
return {
|
|
293
|
+
verdict: PROBE7_NONTESTABLE_RE.test(String(item || '')) ? 'non-testable' : 'uncovered',
|
|
294
|
+
evidence: '(无归属测试)',
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const anchors = (hint && Array.isArray(hint.anchors)) ? hint.anchors : []
|
|
298
|
+
if (anchors.length > 0) {
|
|
299
|
+
const evidence = anchors.slice(0, 3)
|
|
300
|
+
.map(a => `\`${mdEscapeCell(a.file, 120)}:${a.line}\`(${mdEscapeCell(a.term, 40)})`)
|
|
301
|
+
.join('、')
|
|
302
|
+
return { verdict: 'covered', evidence }
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
verdict: 'partial',
|
|
306
|
+
evidence: `(无机械命中——人工核验 \`${mdEscapeCell(testFiles[0], 120)}\`)`,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 渲染探针 7 段(骨架与幂等补段共用单一实现)。
|
|
312
|
+
* 坑 probe7-prefill-evidence(ql-20260915-004):判定/证据两列由 prefillMatrixCells 机械预填
|
|
313
|
+
* (原 `<待填:四选一>` / `<TODO>` 占位淘汰)——幂等保障沿用补段口径:段已在场(agent 已填/
|
|
314
|
+
* 未填)一律不触碰(ensureAcceptanceMatrixSection 段在场即 no-op),预填只在骨架生成与缺段补齐
|
|
315
|
+
* 两条新写路径生效,agent 已填内容永不被覆盖。
|
|
316
|
+
* @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
|
|
317
|
+
* @returns {string[]} 行数组(含段标题;调用方自理前后空行)
|
|
318
|
+
*/
|
|
319
|
+
function renderProbe7Lines(p7) {
|
|
320
|
+
const L = [PROBE7_HEADING]
|
|
321
|
+
if (!p7 || !p7.applicable) {
|
|
322
|
+
L.push('- 不适用(无 TaskCard)')
|
|
323
|
+
return L
|
|
324
|
+
}
|
|
325
|
+
L.push('<!-- 口径注记:探针 3 = 模块目录递归存在性面(allowed_paths 目录附近有没有测试);探针 7 = allowed_paths ∪ review changedFiles 结构归属承接面(每条 acceptance 由哪些测试承接);两者并排冲突以 7 为准。判定枚举(四选一):covered / partial / uncovered / non-testable(文档/部署类显式逃生门)。关键词命中只是提示,命中≠判定。 -->')
|
|
326
|
+
L.push('<!-- 预填说明(ql-20260915-004):判定列为 CLI 机械预填,agent 逐格复核改写——规则:无归属→uncovered(文档/部署/doc/deploy/manual/config 类词→non-testable);有归属且命中≥1→covered;有归属零命中→partial。证据列给首命中 file:line 锚点或人工核验提示。预填≠结论:与事实不符的格子必须改写(枚举须保持 covered/partial/uncovered/non-testable 纯值,备注写在证据列)。 -->')
|
|
327
|
+
for (const t of (p7.tasks || [])) {
|
|
328
|
+
L.push('')
|
|
329
|
+
L.push(`**${t.task}**`)
|
|
330
|
+
if (!Array.isArray(t.acceptance) || t.acceptance.length === 0) {
|
|
331
|
+
L.push('- (卡无 acceptance——防御,plan-postcheck 已拦)')
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
L.push('| acceptance 条目 | 归属测试文件 | 关键词命中(提示,命中≠判定) | 判定 | 证据 |')
|
|
335
|
+
L.push('|---|---|---|---|---|')
|
|
336
|
+
t.acceptance.forEach((item, i) => {
|
|
337
|
+
const attribCell = (t.testFiles && t.testFiles.length > 0)
|
|
338
|
+
? t.testFiles.map(f => `\`${f}\``).join('<br>')
|
|
339
|
+
: '无归属测试——判定大概率 uncovered'
|
|
340
|
+
const h = (t.hints && t.hints[i]) || null
|
|
341
|
+
const hintCell = h
|
|
342
|
+
? `${h.terms.map(x => mdEscapeCell(x)).join('、')}(${(h.files || []).map(f => `\`${f}\``).join('、')})`
|
|
343
|
+
: '—'
|
|
344
|
+
const pre = prefillMatrixCells(item, t.testFiles, h)
|
|
345
|
+
L.push(`| ${mdEscapeCell(item)} | ${attribCell} | ${hintCell} | ${pre.verdict} | ${pre.evidence} |`)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
return L
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 跑四个机械探针 + 探针 7 验收×测试覆盖矩阵。
|
|
100
353
|
* @param {{ cwd: string, changeName: string, specDir?: string|null }} opts
|
|
101
|
-
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object }}
|
|
354
|
+
* @returns {{ probe1: object, probe3: object, probe5: object, probe6: object, probe7: object }}
|
|
102
355
|
*/
|
|
103
356
|
export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
104
357
|
const specBase = resolveVerifyProbesSpecBase(cwd, specDir)
|
|
@@ -121,9 +374,14 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
121
374
|
probe1.globEntries.push(e.path)
|
|
122
375
|
continue
|
|
123
376
|
}
|
|
124
|
-
|
|
377
|
+
// NEW: 待建前缀剥离(坑 probe1-new-prefix-miss,2026-09-12 驾驭第十七批③,用户实证
|
|
378
|
+
// 「NEW: 前缀文件已合入主仓」时 existsSync(join(cwd,'NEW:src/x')) 恒 miss 落 skippedFiles
|
|
379
|
+
// 留 ⚠️ 噪声):design 清单的 NEW: 是「新建意图」标记,文件 apply 后已是无前缀实体——
|
|
380
|
+
// 探针按目标文件扫(与 pathMatches 比对侧剥除同源)。
|
|
381
|
+
const probePath = String(e.path).replace(/^NEW:\s*/, '')
|
|
382
|
+
let abs = join(cwd, probePath)
|
|
125
383
|
if (!existsSync(abs)) {
|
|
126
|
-
const wtAbs = wtRoot ? join(wtRoot,
|
|
384
|
+
const wtAbs = wtRoot ? join(wtRoot, probePath) : null
|
|
127
385
|
if (wtAbs && existsSync(wtAbs)) {
|
|
128
386
|
abs = wtAbs
|
|
129
387
|
probe1.worktreeHits++
|
|
@@ -136,7 +394,7 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
136
394
|
const lines = readFileSync(abs, 'utf8').split('\n')
|
|
137
395
|
lines.forEach((line, i) => {
|
|
138
396
|
if (probe1.matches.length >= PROBE1_MAX_MATCHES) return
|
|
139
|
-
if (TODO_MARKER_RE.test(line)) probe1.matches.push({ file:
|
|
397
|
+
if (TODO_MARKER_RE.test(line)) probe1.matches.push({ file: probePath, line: i + 1, content: line.trim().slice(0, 160) })
|
|
140
398
|
})
|
|
141
399
|
} catch {
|
|
142
400
|
probe1.skippedFiles.push(e.path)
|
|
@@ -215,7 +473,44 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
|
|
|
215
473
|
probe6.deletions.push({ path: posix, status: st, designOp: op || null, verdict })
|
|
216
474
|
}
|
|
217
475
|
|
|
218
|
-
|
|
476
|
+
// ── 探针 7:验收×测试覆盖矩阵(acceptance 自解析 + 双源结构归属 + 关键词提示)──
|
|
477
|
+
// applicable 顶层键:tasks/ 目录不存在或全无 frontmatter = false(quick 会话/旧变更零行为
|
|
478
|
+
// 变化——不渲染矩阵、不产生待填槽)。归属双源 = 卡 allowed_paths 测试模式 ∪ 当前 runId
|
|
479
|
+
// review.json changedFiles 的 test/ 前缀(runId 由 resolveExecuteRunIdInline 内联解析——
|
|
480
|
+
// 禁静态 import task-review,三步环见该函数注释)。
|
|
481
|
+
const probe7 = { applicable: false, tasks: [] }
|
|
482
|
+
{
|
|
483
|
+
const tasksDir = join(changeDir, 'tasks')
|
|
484
|
+
if (existsSync(tasksDir)) {
|
|
485
|
+
const runId = resolveExecuteRunIdInline(runtimeRoot, changeName)
|
|
486
|
+
const cards = []
|
|
487
|
+
try {
|
|
488
|
+
for (const f of readdirSync(tasksDir).filter(n => /^task-\d+\.md$/.test(n)).sort()) {
|
|
489
|
+
const raw = readFileSync(join(tasksDir, f), 'utf8')
|
|
490
|
+
const acceptance = parseTaskAcceptance(raw)
|
|
491
|
+
if (acceptance === null) continue // 无 frontmatter → 跳过(plan-postcheck 已拦)
|
|
492
|
+
const fmId = (raw.match(/^id:\s*(\S+)/m) || [])[1]
|
|
493
|
+
cards.push({ task: fmId || f.replace(/\.md$/, ''), acceptance, raw })
|
|
494
|
+
}
|
|
495
|
+
} catch { /* tasks 目录不可读 → applicable 维持 false */ }
|
|
496
|
+
probe7.applicable = cards.length > 0
|
|
497
|
+
for (const card of cards) {
|
|
498
|
+
const fromAllowed = parseAllowedPaths(card.raw)
|
|
499
|
+
.map(p => String(p).replace(/^NEW:\s*/, '').trim())
|
|
500
|
+
.filter(p => p && isProbe7TestPath(p))
|
|
501
|
+
const fromReview = runId ? readReviewChangedTestFiles(runtimeRoot, runId, card.task) : []
|
|
502
|
+
const testFiles = [...new Set([...fromAllowed, ...fromReview])]
|
|
503
|
+
probe7.tasks.push({
|
|
504
|
+
task: card.task,
|
|
505
|
+
acceptance: card.acceptance,
|
|
506
|
+
testFiles,
|
|
507
|
+
hints: buildAcceptanceHints(card.acceptance, testFiles, cwd, wtRoot),
|
|
508
|
+
})
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return { probe1, probe3, probe5, probe6, probe7 }
|
|
219
514
|
}
|
|
220
515
|
|
|
221
516
|
/**
|
|
@@ -257,6 +552,12 @@ export function renderVerifyProbesReport(result) {
|
|
|
257
552
|
L.push(`- ℹ️ ${probe3.note}`)
|
|
258
553
|
L.push('')
|
|
259
554
|
|
|
555
|
+
// 探针 7 紧随探针 3(3.5 已被断言有效性抽查占用,编号顺延取 7 兼容锚定正则 /#### 探针 (\d+)/);
|
|
556
|
+
// 骨架序 3 → 7 → 4,ensureAcceptanceMatrixSection 补段同序。result 无 probe7 键(存量调用方/
|
|
557
|
+
// 合成 result)→ applicable=false 渲染「不适用」行,零回归。
|
|
558
|
+
L.push(...renderProbe7Lines(result.probe7 || { applicable: false, tasks: [] }))
|
|
559
|
+
L.push('')
|
|
560
|
+
|
|
260
561
|
L.push('#### 探针 4:决策追踪覆盖')
|
|
261
562
|
L.push('<!--TODO: 语义探针——D-xxx@vN → FR-xxx → plan/task 引用 → 证据回指闭环(agent 执行)-->')
|
|
262
563
|
L.push('')
|
|
@@ -503,6 +804,46 @@ export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = [])
|
|
|
503
804
|
return { added }
|
|
504
805
|
}
|
|
505
806
|
|
|
807
|
+
/**
|
|
808
|
+
* 探针 7 矩阵段幂等补齐(--init 段落级,2026-09-14-acceptance-test-matrix FR-01;学
|
|
809
|
+
* backfillMissingEvidenceSlots 形态):verify-result.md 已存在但缺「#### 探针 7」段且
|
|
810
|
+
* applicable=true(有 TaskCard)时补矩阵骨架段——不触碰既有正文;二跑零改动(幂等)。
|
|
811
|
+
* 插入位置:文件内有「#### 探针 3」小节时插在该小节之后(保持骨架序 3 → 7 → 4),否则
|
|
812
|
+
* 文末追加(旧格式/手写正文)。段已在场(冒号全/半角均认,防走样标题绕过检测)或
|
|
813
|
+
* applicable=false → no-op。本变更自举通道:verify-result.md 先于特性存在的存量走此补段。
|
|
814
|
+
* @param {string} mdPath verify-result.md 路径
|
|
815
|
+
* @param {{ applicable: boolean, tasks: Array }} probe7 runVerifyProbes 返回的 probe7
|
|
816
|
+
* @returns {{ added: boolean, reason?: string }}
|
|
817
|
+
*/
|
|
818
|
+
export function ensureAcceptanceMatrixSection(mdPath, probe7) {
|
|
819
|
+
let text
|
|
820
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: false, reason: 'verify-result.md 不存在或不可读' } }
|
|
821
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
822
|
+
if (/^#### 探针 7[::]/m.test(normalized)) return { added: false, reason: '探针 7 段已在场' }
|
|
823
|
+
const p7 = probe7 && typeof probe7 === 'object' ? probe7 : { applicable: false, tasks: [] }
|
|
824
|
+
if (!p7.applicable) return { added: false, reason: '不适用(无 TaskCard)' }
|
|
825
|
+
const blockLines = [...renderProbe7Lines(p7), '']
|
|
826
|
+
try {
|
|
827
|
+
const lines = normalized.split('\n')
|
|
828
|
+
const p3Idx = lines.findIndex(l => /^#### 探针 3[::]/.test(l))
|
|
829
|
+
if (p3Idx === -1) {
|
|
830
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + '\n' + blockLines.join('\n'))
|
|
831
|
+
} else {
|
|
832
|
+
// 探针 3 小节终点 = 下一个任意级别标题行(与 verify-postcheck extractProbeSubsections 同口径)
|
|
833
|
+
let end = lines.length
|
|
834
|
+
for (let i = p3Idx + 1; i < lines.length; i++) {
|
|
835
|
+
if (/^#{1,6}\s/.test(lines[i])) { end = i; break }
|
|
836
|
+
}
|
|
837
|
+
lines.splice(end, 0, ...blockLines)
|
|
838
|
+
writeFileSync(mdPath, lines.join('\n'))
|
|
839
|
+
}
|
|
840
|
+
} catch (e) {
|
|
841
|
+
console.warn(`⚠️ 探针 7 矩阵段补齐落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
842
|
+
return { added: false, reason: '落盘失败' }
|
|
843
|
+
}
|
|
844
|
+
return { added: true }
|
|
845
|
+
}
|
|
846
|
+
|
|
506
847
|
/**
|
|
507
848
|
* 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
|
|
508
849
|
* 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
|