sillyspec 3.28.2 → 3.28.4
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-auto/SKILL.md +16 -55
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
- package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
- package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
- package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
- package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
- package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
- package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
- package/.claude/skills/sillyspec-state/SKILL.md +7 -46
- package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
- package/AGENTS.md +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +127 -2
- package/src/change-delete.js +2 -1
- package/src/change-list.js +7 -2
- package/src/change-risk-profile.js +77 -6
- package/src/config-cat.js +12 -1
- package/src/constants.js +9 -1
- package/src/design-facts.js +64 -0
- package/src/dispatch/backends/sillyhub-mcp.js +17 -5
- package/src/docs-check.js +153 -27
- package/src/docs-migrate.js +124 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +353 -28
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +35 -0
- package/src/progress.js +64 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +159 -13
- package/src/run/complete-handlers.js +162 -9
- package/src/run/complete.js +104 -5
- package/src/run/gates.js +116 -8
- package/src/run/progress-confirm.js +66 -0
- package/src/run/prompt.js +235 -36
- package/src/run/quick-audit.js +12 -5
- package/src/run/scan-profile.js +20 -2
- package/src/run/shared.js +84 -11
- package/src/run/stage.js +42 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scan-postcheck.js +5 -1
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review.js +3 -0
- package/src/stages/archive.js +4 -4
- package/src/stages/brainstorm-auto.js +2 -1
- package/src/stages/brainstorm.js +11 -25
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +10 -14
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -27
- package/src/stages/quick.js +15 -13
- package/src/stages/verify.js +23 -21
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +225 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +421 -49
- package/src/verify-probes.js +142 -13
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +51 -13
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -1
- package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/verify-postcheck.js
CHANGED
|
@@ -23,8 +23,9 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execSync, spawnSync } from 'child_process'
|
|
26
|
+
import { IR_STRICT_SINCE } from './constants.js'
|
|
26
27
|
import { gitQuiet } from './git-helper.js'
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
28
29
|
import { join } from 'path'
|
|
29
30
|
import { verifyApiParity } from './contract-matrix.js'
|
|
30
31
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
@@ -36,10 +37,30 @@ import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
|
|
|
36
37
|
// fs/path/git-helper/change-list/plan-postcheck/contract-matrix/foreign-declared/run-shared,
|
|
37
38
|
// 不反向依赖本模块,此处直连不引入循环
|
|
38
39
|
import { runVerifyProbes } from './verify-probes.js'
|
|
40
|
+
import { parseEvidenceSlots, classifyVerifiedFile } from './verify-facts-schema.js'
|
|
39
41
|
|
|
40
42
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
41
43
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
42
44
|
const OUTPUT_TAIL_CHARS = 4000
|
|
45
|
+
// 判账失败行台账上限(坑 verify-test-reconcile-tail-blindspot):对账按完整输出判、tail 只存
|
|
46
|
+
// 末 4000 字符 → 失败行落在盲区时只剩 reason 里 5 行×120 字符采样,归因必须全量复跑。修法是
|
|
47
|
+
// 判账行集(remaining/exempted 全量原文)随结果透传落盘/打印;台账只防病态体量——单行截
|
|
48
|
+
// 300 字符、行数截 200,超出注明计数。judge 判账仍用全量行集,不受此截断影响。
|
|
49
|
+
const FAILURE_LEDGER_MAX_LINES = 200
|
|
50
|
+
const FAILURE_LEDGER_MAX_CHARS = 300
|
|
51
|
+
|
|
52
|
+
/** 失败行台账截断(落盘/展示用;判账本身不受影响)。export 供 test 验证截断语义 */
|
|
53
|
+
export function capFailureLedger(lines) {
|
|
54
|
+
if (!Array.isArray(lines) || lines.length === 0) return []
|
|
55
|
+
const out = lines.slice(0, FAILURE_LEDGER_MAX_LINES).map(l => {
|
|
56
|
+
const s = String(l)
|
|
57
|
+
return s.length > FAILURE_LEDGER_MAX_CHARS ? s.slice(0, FAILURE_LEDGER_MAX_CHARS) + '…' : s
|
|
58
|
+
})
|
|
59
|
+
if (lines.length > FAILURE_LEDGER_MAX_LINES) {
|
|
60
|
+
out.push(`…(共 ${lines.length} 行,仅列前 ${FAILURE_LEDGER_MAX_LINES} 行;判账按全量行集不受此截断影响)`)
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
43
64
|
|
|
44
65
|
/**
|
|
45
66
|
* 行尾归一(坑 verify-modules-crlf-blanket-fallback,2026-08-20 实证):Windows 仓的
|
|
@@ -95,6 +116,36 @@ function extractLintCommand(yamlText) {
|
|
|
95
116
|
return null
|
|
96
117
|
}
|
|
97
118
|
|
|
119
|
+
/**
|
|
120
|
+
* lint advisory 观察期计数(刀③,2026-09-08):advisory 失败不阻断,但「观察期后升级硬门」
|
|
121
|
+
* 需要失败率数据支撑(更硬 vs 更吵的决策依据——硬门误伤纯文档/跨平台路径会制造新噪音)。
|
|
122
|
+
* 每次非 skipped 实测追加计数进 <specBase>/.runtime/verify-lint-tally.json(passed/failed
|
|
123
|
+
* 都记,分母同要);history 截尾 20 条。写失败静默降级——计数器不许反向阻断 verify。
|
|
124
|
+
*/
|
|
125
|
+
function recordVerifyLintTally({ specBase, result }) {
|
|
126
|
+
try {
|
|
127
|
+
const runtimeDir = join(specBase, '.runtime')
|
|
128
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
129
|
+
const tallyPath = join(runtimeDir, 'verify-lint-tally.json')
|
|
130
|
+
let tally = { totalRuns: 0, failedRuns: 0, history: [] }
|
|
131
|
+
try {
|
|
132
|
+
tally = { ...tally, ...JSON.parse(readFileSync(tallyPath, 'utf8')) }
|
|
133
|
+
} catch { /* 首跑/文件损坏 → 重置重新累计 */ }
|
|
134
|
+
tally.totalRuns += 1
|
|
135
|
+
if (result.status === 'failed') tally.failedRuns += 1
|
|
136
|
+
tally.lastRunAt = new Date().toISOString()
|
|
137
|
+
tally.lastStatus = result.status
|
|
138
|
+
tally.history = [
|
|
139
|
+
...(Array.isArray(tally.history) ? tally.history : []),
|
|
140
|
+
{ at: tally.lastRunAt, status: result.status, reason: result.reason || null },
|
|
141
|
+
].slice(-20)
|
|
142
|
+
writeFileSync(tallyPath, JSON.stringify(tally, null, 2))
|
|
143
|
+
return { failedRuns: tally.failedRuns, totalRuns: tally.totalRuns }
|
|
144
|
+
} catch {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
/**
|
|
99
150
|
* verify --done 实测跑 local.yaml commands.lint(2026-08-21 审查 CLI-1)。
|
|
100
151
|
* 此前 lint 全靠 agent 自跑自报("我跑过 lint 了"纯口头),与 test 侧的对账不对称——
|
|
@@ -142,18 +193,36 @@ export function runVerifyLintCheck({ cwd, specBase }) {
|
|
|
142
193
|
}
|
|
143
194
|
const durationMs = Date.now() - startedAt
|
|
144
195
|
const outputTail = output.length > OUTPUT_TAIL_CHARS ? '…' + output.slice(-OUTPUT_TAIL_CHARS) : output
|
|
196
|
+
const status = exitCode === 0 ? 'passed' : 'failed'
|
|
197
|
+
const finalReason = exitCode === 0 ? null : reason
|
|
198
|
+
const tally = recordVerifyLintTally({ specBase, result: { status, reason: finalReason } })
|
|
145
199
|
|
|
146
200
|
return {
|
|
147
|
-
status
|
|
201
|
+
status,
|
|
148
202
|
command,
|
|
149
203
|
exitCode,
|
|
150
204
|
durationMs,
|
|
151
205
|
outputTail,
|
|
152
|
-
reason:
|
|
206
|
+
reason: finalReason,
|
|
207
|
+
tally, // { failedRuns, totalRuns } | null——观察期失败率,升级硬门的决策依据
|
|
153
208
|
}
|
|
154
209
|
}
|
|
155
210
|
|
|
156
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* lint 硬门决策(2026-09-09 升硬,外部数据驱动):观察期 tally 14 次 5 败全真阳性零误伤,
|
|
213
|
+
* 且 pre-push 钩子本就硬拦——verify 侧硬门只是同一标准 fail-fast 前移。
|
|
214
|
+
* 逃生档:SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕,对齐 quick 门 skip env 先例)。
|
|
215
|
+
* @returns {boolean} failed 且非逃生档 → true(调用方 rollback)
|
|
216
|
+
*/
|
|
217
|
+
export function shouldBlockVerifyLint(lintCheck, env = process.env) {
|
|
218
|
+
if (!lintCheck || lintCheck.status !== 'failed') return false
|
|
219
|
+
return env.SILLYSPEC_VERIFY_LINT_GATE !== 'advisory'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 打印 lint 实测结果(2026-09-09 起按 shouldBlockVerifyLint 分支):硬门档「已阻断」+
|
|
224
|
+
* 逃生 env 指引(rollback 由 gates 接线侧执行);advisory 档保留旧观察期措辞。
|
|
225
|
+
*/
|
|
157
226
|
export function printVerifyLintCheck(result) {
|
|
158
227
|
if (result.status === 'skipped') {
|
|
159
228
|
console.warn(`\n⚠️ Verify lint 实测跳过:${result.reason}`)
|
|
@@ -163,8 +232,22 @@ export function printVerifyLintCheck(result) {
|
|
|
163
232
|
console.log(`\n✅ Verify lint 实测通过:\`${result.command}\` 退出码 0(${(result.durationMs / 1000).toFixed(1)}s)`)
|
|
164
233
|
return
|
|
165
234
|
}
|
|
166
|
-
|
|
167
|
-
|
|
235
|
+
// 文案随门禁档位分支(2026-09-09 复核修正 P2:硬门档下旧「不阻断/观察期」措辞与
|
|
236
|
+
// gates 的 rollback 行为打架,agent 会误判 CLI 抽风)——rollback 本体在 gates 接线侧。
|
|
237
|
+
if (shouldBlockVerifyLint(result)) {
|
|
238
|
+
console.error(`\n❌ Verify lint 实测失败——verify 完成已阻断(2026-09-09 起硬门):\`${result.command}\` — ${result.reason}`)
|
|
239
|
+
console.error(' agent 的 lint 自报告与实测不符时以实测为准;修复后重跑 --done(进度不丢)。')
|
|
240
|
+
if (result.tally) {
|
|
241
|
+
console.error(` 📊 累计 ${result.tally.failedRuns}/${result.tally.totalRuns} 次(.runtime/verify-lint-tally.json)。`)
|
|
242
|
+
}
|
|
243
|
+
console.error(' 确认要跳过实测:SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
244
|
+
} else {
|
|
245
|
+
console.error(`\n⚠️ Verify lint 实测失败(advisory 档,不阻断本次完成):\`${result.command}\` — ${result.reason}`)
|
|
246
|
+
console.error(' agent 的 lint 自报告与实测不符时以实测为准;请修复后重跑,避免格式债推迟到 commit 被 pre-commit hook 拦截。')
|
|
247
|
+
if (result.tally) {
|
|
248
|
+
console.error(` 📊 lint advisory 失败累计 ${result.tally.failedRuns}/${result.tally.totalRuns} 次(数据落 .runtime/verify-lint-tally.json)。`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
168
251
|
if (result.outputTail) {
|
|
169
252
|
const tail = result.outputTail.split('\n').slice(-15).join('\n')
|
|
170
253
|
console.error(' 输出(末尾):')
|
|
@@ -586,24 +669,37 @@ export function extractKnownFailures(yamlText) {
|
|
|
586
669
|
if (inline) {
|
|
587
670
|
return inline[1].split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
|
588
671
|
}
|
|
589
|
-
// 坑 verify-known-failures-comment-line-truncation(2026-08-28
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
672
|
+
// 坑 verify-known-failures-comment-line-truncation(2026-08-28 连续踩两次)→ 坑
|
|
673
|
+
// verify-known-failures-block-fragile-chain(2026-09-07 工具复盘升级修复):块式捕获曾要求
|
|
674
|
+
// 「- 项行 / 注释行 / 空行」**连续成链**((...)+),任何一行不合形态——手改残留的缩进杂行、
|
|
675
|
+
// 空项 ` -`、误删半截的段残迹——链条即断,其后所有条目**静默丢失**(实证形态:G/H 注释段
|
|
676
|
+
// 踩着 F 段残迹重写,F 段遗留行断链 → G/H 条目全丢 → 清单残缺 → verify 假红,且丢得多静默
|
|
677
|
+
// 就多难归因)。改为逐行扫描:块 = known_failures: 头之后连续的「缩进行 / 注释行(任意缩进,
|
|
678
|
+
// 含列首)/ 空行」,首个列首正文行收块;块内杂行一律容忍,`- ` 项行无论隔着什么都提取——
|
|
679
|
+
// 只有 YAML 本就不合法的形态(列首裸 `- 项`)才不取,不再因断链静默丢项。
|
|
680
|
+
const lines = yaml.split('\n')
|
|
681
|
+
const headIdx = lines.findIndex(l => /^known_failures:\s*(?:#.*)?$/.test(l))
|
|
682
|
+
if (headIdx === -1) return []
|
|
683
|
+
const itemRe = /^[ \t]+-[ \t]+(.+)$/
|
|
684
|
+
const items = []
|
|
685
|
+
for (let i = headIdx + 1; i < lines.length; i++) {
|
|
686
|
+
const l = lines[i]
|
|
687
|
+
if (l.trim() === '') continue // 空行:块内分隔,放行
|
|
688
|
+
if (/^[ \t]*#/.test(l)) continue // 注释行(任意缩进含列首):段头/归因注记,放行
|
|
689
|
+
if (!/^[ \t]/.test(l)) break // 列首正文行:下一个顶层键,收块
|
|
690
|
+
const m = l.match(itemRe) // 其余缩进行一律留在块内;项行才提取(杂行不提取但不断链)
|
|
691
|
+
if (m) items.push(m[1])
|
|
692
|
+
}
|
|
693
|
+
return items
|
|
694
|
+
.map(s => {
|
|
695
|
+
const item = s.trim()
|
|
696
|
+
const full = item.match(/^(['"])(.*)\1$/)
|
|
697
|
+
if (full) return full[2] // 整体引号值原样保留(内部 # 不是注释)
|
|
698
|
+
const stripped = item.replace(/[ \t]+#.*$/, '').trim() // 行尾注释须 # 前有空白(YAML 语义),裸 # 不截
|
|
699
|
+
const q2 = stripped.match(/^(['"])(.*)\1$/)
|
|
700
|
+
return q2 ? q2[2] : stripped
|
|
701
|
+
})
|
|
702
|
+
.filter(Boolean)
|
|
607
703
|
}
|
|
608
704
|
|
|
609
705
|
// 单条失败行标记(跨 pytest/jest/vitest/go/generic)。大小写不敏感。
|
|
@@ -698,11 +794,15 @@ export function partitionFailures(output, knownFailures) {
|
|
|
698
794
|
if (ENV_NOISE_RE.test(bare)) continue
|
|
699
795
|
if (PER_TEST_FAIL_RE.test(bare) && !SUMMARY_LINE_RE.test(bare)) failureLines.push(l)
|
|
700
796
|
}
|
|
701
|
-
|
|
797
|
+
// 豁免匹配同样在剥 ANSI 后的行上做(坑 verify-known-failures-ansi-exemption-split,2026-09-07
|
|
798
|
+
// 工具复盘):TTY 捕获的失败行里色码把可见词拦腰拆开(`× \x1b[31mtests/foo.test.ts\x1b[0m > case`
|
|
799
|
+
// 连不成 `tests/foo.test.ts > case`),同一行被迫拆成两段各配一条模式才能豁免。模式侧同步剥
|
|
800
|
+
// ANSI(从原始输出誊抄来的模式可能自带码);返回仍保留原文(remaining/exempted 展示不变形)。
|
|
801
|
+
const pats = (knownFailures || []).map(p => String(p).replace(ANSI_RE, '').toLowerCase()).filter(Boolean)
|
|
702
802
|
const exempted = []
|
|
703
803
|
const remaining = []
|
|
704
804
|
for (const l of failureLines) {
|
|
705
|
-
const ll = l.toLowerCase()
|
|
805
|
+
const ll = l.replace(ANSI_RE, '').toLowerCase()
|
|
706
806
|
if (pats.some(p => ll.includes(p))) exempted.push(l)
|
|
707
807
|
else remaining.push(l)
|
|
708
808
|
}
|
|
@@ -718,19 +818,28 @@ export function partitionFailures(output, knownFailures) {
|
|
|
718
818
|
* 有未豁免失败行 / 检测不到失败行(保守)→ failed
|
|
719
819
|
* fail-safe:检测不到失败行绝不自动 pass(避免解析盲区导致假 PASS)。
|
|
720
820
|
*
|
|
721
|
-
*
|
|
821
|
+
* 判账行集全量随结果返回(坑 verify-test-reconcile-tail-blindspot):对账按完整输出判,但
|
|
822
|
+
* outputTail 只留末 OUTPUT_TAIL_CHARS 字符——失败行落在盲区时 reason 采样不足以归因,只能
|
|
823
|
+
* 全量复跑。remainingLines/exemptedLines(全量原文行)由调用方透传到 test-result.json 与
|
|
824
|
+
* 控制台明细,盲区归因不再依赖复跑;passed 分支同样带 exemptedLines(豁免披露复核需要)。
|
|
825
|
+
*
|
|
826
|
+
* @returns {{ status: 'passed'|'failed', reason: string|null, exemptedCount: number,
|
|
827
|
+
* remainingLines: string[], exemptedLines: string[] }}
|
|
722
828
|
*/
|
|
723
829
|
export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailures) {
|
|
724
|
-
if (exitCode === 0) return { status: 'passed', reason: baseReason, exemptedCount: 0 }
|
|
830
|
+
if (exitCode === 0) return { status: 'passed', reason: baseReason, exemptedCount: 0, remainingLines: [], exemptedLines: [] }
|
|
831
|
+
const { failureLines, exempted, remaining } = partitionFailures(output, knownFailures || [])
|
|
725
832
|
if (!knownFailures || knownFailures.length === 0) {
|
|
726
|
-
|
|
833
|
+
// 无清单分支同样带行集:豁免与否都不该丢判账依据(tail 盲区归因同权)
|
|
834
|
+
return { status: 'failed', reason: baseReason, exemptedCount: 0, remainingLines: remaining, exemptedLines: [] }
|
|
727
835
|
}
|
|
728
|
-
const { failureLines, exempted, remaining } = partitionFailures(output, knownFailures)
|
|
729
836
|
if (failureLines.length > 0 && remaining.length === 0) {
|
|
730
837
|
return {
|
|
731
838
|
status: 'passed',
|
|
732
839
|
reason: `全部 ${failureLines.length} 个失败行命中 known_failures 已豁免(${exempted.length} 条)— 请人工复核豁免清单是否过宽`,
|
|
733
840
|
exemptedCount: exempted.length,
|
|
841
|
+
remainingLines: [],
|
|
842
|
+
exemptedLines: exempted,
|
|
734
843
|
}
|
|
735
844
|
}
|
|
736
845
|
// 剩余未豁免失败行:列出具体行 + 预存债指引(坑 verify-known-failures-stale-list)。
|
|
@@ -742,7 +851,9 @@ export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailur
|
|
|
742
851
|
const t = String(l).trim()
|
|
743
852
|
return t.length > 120 ? t.slice(0, 120) + '…' : t
|
|
744
853
|
})
|
|
745
|
-
|
|
854
|
+
// 指针改指台账(坑 verify-test-reconcile-tail-blindspot):原「见上方测试输出」只对 tail
|
|
855
|
+
// 窗口内的行成立,盲区行必须指向完整台账(控制台明细段 + test-result.json 的 failure_remaining)
|
|
856
|
+
const more = remaining.length > 5 ? `\n …(其余 ${remaining.length - 5} 行完整清单见下方「未豁免失败行」明细,已全量落盘 test-result.json 的 failure_remaining——不受输出 tail 截断影响)` : ''
|
|
746
857
|
detail = `${remaining.length} 个失败行未命中 known_failures 清单:\n - ${sample.join('\n - ')}${more}\n → 若是预存债(变更前就失败),加入 local.yaml 的 known_failures 清单;若是本次变更引入的真实失败,请修复`
|
|
747
858
|
} else {
|
|
748
859
|
detail = '失败输出未检测到可豁免的失败行(保守判 fail)'
|
|
@@ -751,6 +862,8 @@ export function judgeWithKnownFailures(exitCode, output, baseReason, knownFailur
|
|
|
751
862
|
status: 'failed',
|
|
752
863
|
reason: baseReason ? `${baseReason}(${detail})` : detail,
|
|
753
864
|
exemptedCount: exempted.length,
|
|
865
|
+
remainingLines: remaining,
|
|
866
|
+
exemptedLines: exempted,
|
|
754
867
|
}
|
|
755
868
|
}
|
|
756
869
|
|
|
@@ -814,6 +927,8 @@ function runOneModule(name, testCommand, cwd, knownFailures = []) {
|
|
|
814
927
|
outputTail,
|
|
815
928
|
reason: judged.reason,
|
|
816
929
|
exemptedCount: judged.exemptedCount,
|
|
930
|
+
failureRemaining: judged.remainingLines,
|
|
931
|
+
failureExempted: judged.exemptedLines,
|
|
817
932
|
}
|
|
818
933
|
}
|
|
819
934
|
|
|
@@ -1062,6 +1177,13 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
1062
1177
|
|
|
1063
1178
|
const rawStrategy = extractTestStrategy(yamlText)
|
|
1064
1179
|
const knownFailures = extractKnownFailures(yamlText)
|
|
1180
|
+
if (knownFailures.length > 0) {
|
|
1181
|
+
// 豁免清单加载可见性(坑 verify-known-failures-block-fragile-chain 的观测面):条数即时报出
|
|
1182
|
+
// ——清单被块内残迹截断/误删段时「N 条」与预期不符一眼可见,不必等假红再反推。
|
|
1183
|
+
// 走 stderr(console.warn):本模块运行期叙述统一不碰 stdout(machine-interface 调用时
|
|
1184
|
+
// stdout 留给机器可读输出)
|
|
1185
|
+
console.warn(`📋 known_failures 豁免清单已加载:${knownFailures.length} 条模式(local.yaml)`)
|
|
1186
|
+
}
|
|
1065
1187
|
|
|
1066
1188
|
// —— evidence-auto 生效策略解析(D-005@v2 / task-11)——
|
|
1067
1189
|
// 按变更目录 module-impact.md 影响面取生效策略(行为→module、纯文档/门禁→skip、
|
|
@@ -1304,6 +1426,8 @@ function runCrossRepoFullTest(entry) {
|
|
|
1304
1426
|
mode: 'cross-repo-full',
|
|
1305
1427
|
repoKey: entry.repoKey,
|
|
1306
1428
|
exemptedCount: judged.exemptedCount,
|
|
1429
|
+
failureRemaining: judged.remainingLines,
|
|
1430
|
+
failureExempted: judged.exemptedLines,
|
|
1307
1431
|
}
|
|
1308
1432
|
}
|
|
1309
1433
|
|
|
@@ -1327,6 +1451,9 @@ function mergeResultStatus({ status, mainResult, crossResults, reason }) {
|
|
|
1327
1451
|
resultPath: mainResult.resultPath,
|
|
1328
1452
|
mode: 'cross-repo-merged',
|
|
1329
1453
|
fallbackReason: mainResult.fallbackReason || null,
|
|
1454
|
+
// 判账行集跨仓合并(坑 verify-test-reconcile-tail-blindspot):mergedTail 截断不影响台账
|
|
1455
|
+
failureRemaining: [...(mainResult.failureRemaining || []), ...crossResults.flatMap(r => r.result.failureRemaining || [])],
|
|
1456
|
+
failureExempted: [...(mainResult.failureExempted || []), ...crossResults.flatMap(r => r.result.failureExempted || [])],
|
|
1330
1457
|
}
|
|
1331
1458
|
}
|
|
1332
1459
|
|
|
@@ -1457,6 +1584,8 @@ function runFullCommand({ yamlText, localYamlPath, cwd, specBase, changeName, fa
|
|
|
1457
1584
|
mode: 'full',
|
|
1458
1585
|
fallbackReason,
|
|
1459
1586
|
exemptedCount: judged.exemptedCount,
|
|
1587
|
+
failureRemaining: judged.remainingLines,
|
|
1588
|
+
failureExempted: judged.exemptedLines,
|
|
1460
1589
|
}
|
|
1461
1590
|
|
|
1462
1591
|
writeRunResult({ specBase, changeName, result, extra: fallbackReason ? { fallback_reason: fallbackReason } : {} })
|
|
@@ -1500,6 +1629,10 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1500
1629
|
mode: 'module-subset',
|
|
1501
1630
|
fallbackReason: null,
|
|
1502
1631
|
exemptedCount: perModule.reduce((n, r) => n + (r.exemptedCount || 0), 0),
|
|
1632
|
+
// 判账行集聚合(坑 verify-test-reconcile-tail-blindspot):合并 tail 会二次截断,行集不截——
|
|
1633
|
+
// 每模块台账另见 extra.modules[].failure_remaining(带模块归属)
|
|
1634
|
+
failureRemaining: perModule.flatMap(r => r.failureRemaining || []),
|
|
1635
|
+
failureExempted: perModule.flatMap(r => r.failureExempted || []),
|
|
1503
1636
|
}
|
|
1504
1637
|
|
|
1505
1638
|
writeRunResult({
|
|
@@ -1515,6 +1648,8 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1515
1648
|
duration_ms: r.durationMs,
|
|
1516
1649
|
output_tail: r.outputTail,
|
|
1517
1650
|
reason: r.reason,
|
|
1651
|
+
...(r.failureRemaining && r.failureRemaining.length ? { failure_remaining: capFailureLedger(r.failureRemaining) } : {}),
|
|
1652
|
+
...(r.failureExempted && r.failureExempted.length ? { failure_exempted: capFailureLedger(r.failureExempted) } : {}),
|
|
1518
1653
|
})),
|
|
1519
1654
|
},
|
|
1520
1655
|
})
|
|
@@ -1523,9 +1658,9 @@ function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [] }
|
|
|
1523
1658
|
|
|
1524
1659
|
/**
|
|
1525
1660
|
* 结果落盘到 .runtime/verify-runs/<ts>/test-result.json(供追溯与 SillyHub 消费)。
|
|
1526
|
-
* 多模块时 extra.modules 描述各模块明细。
|
|
1661
|
+
* 多模块时 extra.modules 描述各模块明细。export 供 test 验证台账落盘形状。
|
|
1527
1662
|
*/
|
|
1528
|
-
function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
1663
|
+
export function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
1529
1664
|
try {
|
|
1530
1665
|
const ts = new Date().toISOString().slice(0, 19).replace(/[-T:]/g, '')
|
|
1531
1666
|
const runDir = join(specBase, '.runtime', 'verify-runs', ts)
|
|
@@ -1538,11 +1673,19 @@ function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
|
1538
1673
|
status: result.status,
|
|
1539
1674
|
duration_ms: result.durationMs,
|
|
1540
1675
|
output_tail: result.outputTail,
|
|
1676
|
+
// 判账失败行台账(坑 verify-test-reconcile-tail-blindspot):按完整输出判账的行集全量
|
|
1677
|
+
// 落盘(受 capFailureLedger 防病态体量截断)——失败行落在 output_tail 盲区时归因读这里,
|
|
1678
|
+
// 不再需要全量复跑。空集不落键(成功/跳过运行不添噪声)。
|
|
1679
|
+
...(result.failureRemaining && result.failureRemaining.length ? { failure_remaining: capFailureLedger(result.failureRemaining) } : {}),
|
|
1680
|
+
...(result.failureExempted && result.failureExempted.length ? { failure_exempted: capFailureLedger(result.failureExempted) } : {}),
|
|
1541
1681
|
reason: result.reason,
|
|
1542
1682
|
ran_at: new Date().toISOString(),
|
|
1543
1683
|
...extra,
|
|
1544
1684
|
}, null, 2) + '\n')
|
|
1545
1685
|
result.resultPath = resultPath
|
|
1686
|
+
// verify-runs 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
|
|
1687
|
+
// 回收(pruneArchivedChangeRuntime 按目录内 JSON change 字段归属);写入侧滚动是
|
|
1688
|
+
// 启发式,不进证据类目录。
|
|
1546
1689
|
} catch (e) {
|
|
1547
1690
|
console.warn(`⚠️ verify 实测结果落盘失败: ${e.message}`)
|
|
1548
1691
|
}
|
|
@@ -1565,11 +1708,30 @@ export function printVerifyTestCheck(result) {
|
|
|
1565
1708
|
if (result.exemptedCount > 0) {
|
|
1566
1709
|
console.log(`\n✅ Verify 实测通过(含 ${result.exemptedCount} 个 known_failures 豁免):\`${result.command}\` — ${result.reason}`)
|
|
1567
1710
|
console.warn(' ⚠️ 本次 PASS 依赖 known_failures 豁免清单——请人工复核清单是否过宽(避免误豁免本变更引入的真实失败)。')
|
|
1711
|
+
// 豁免披露明细(坑 verify-test-reconcile-tail-blindspot):被豁免的失败行同样可能落在
|
|
1712
|
+
// tail 盲区——复核"清单是否过宽"不能只看 tail。前 10 条样例进控制台,全量台账在
|
|
1713
|
+
// test-result.json 的 failure_exempted。
|
|
1714
|
+
if (result.failureExempted && result.failureExempted.length) {
|
|
1715
|
+
console.warn(` 已豁免失败行(共 ${result.failureExempted.length} 行,前 10 条;全量见 test-result.json 的 failure_exempted):`)
|
|
1716
|
+
for (const line of capFailureLedger(result.failureExempted).slice(0, 10)) console.warn(` ~ ${line}`)
|
|
1717
|
+
}
|
|
1568
1718
|
} else {
|
|
1569
1719
|
console.log(`\n✅ Verify 实测通过:\`${result.command}\` 退出码 0(${(result.durationMs / 1000).toFixed(1)}s)`)
|
|
1570
1720
|
}
|
|
1571
1721
|
} else {
|
|
1572
1722
|
console.error(`\n❌ Verify 实测失败:\`${result.command}\` — ${result.reason}`)
|
|
1723
|
+
// 未豁免失败行台账(坑 verify-test-reconcile-tail-blindspot):判账按完整输出、下方 tail
|
|
1724
|
+
// 只留末段——失败行落在盲区时 tail 里看不到,归因靠本段全量行集(≤30 条样例进控制台,
|
|
1725
|
+
// 全量落盘 failure_remaining),不再需要全量复跑定位。
|
|
1726
|
+
if (result.failureRemaining && result.failureRemaining.length) {
|
|
1727
|
+
const ledger = capFailureLedger(result.failureRemaining)
|
|
1728
|
+
const shown = ledger.slice(0, 30)
|
|
1729
|
+
console.error(` 未豁免失败行(按完整输出判账,不受 tail 截断影响;共 ${result.failureRemaining.length} 行):`)
|
|
1730
|
+
for (const line of shown) console.error(` ! ${line}`)
|
|
1731
|
+
if (ledger.length > shown.length) {
|
|
1732
|
+
console.error(` ! …(其余 ${ledger.length - shown.length} 行全量见 test-result.json 的 failure_remaining)`)
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1573
1735
|
if (result.outputTail) {
|
|
1574
1736
|
const tail = result.outputTail.split('\n').slice(-20).join('\n')
|
|
1575
1737
|
console.error(' 输出(末尾):')
|
|
@@ -1781,7 +1943,7 @@ export function printVerifyDeletionCheck(result) {
|
|
|
1781
1943
|
* @param {{cwd:string, specBase:string, changeName?:string}} args
|
|
1782
1944
|
* @returns {{status:'skipped'|'passed'|'warning', items:Array, unacknowledged:Array, summary:string, reason:string|null}}
|
|
1783
1945
|
*/
|
|
1784
|
-
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null }) {
|
|
1946
|
+
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null, verifyStartAt = null }) {
|
|
1785
1947
|
if (!changeName) {
|
|
1786
1948
|
return { status: 'skipped', items: [], unacknowledged: [], summary: '', reason: '无 changeName(quick 等无关联变更场景),evidence 对账跳过' }
|
|
1787
1949
|
}
|
|
@@ -1802,6 +1964,13 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1802
1964
|
const verifyResultPath = join(specBase, 'changes', changeName, 'verify-result.md')
|
|
1803
1965
|
const report = existsSync(verifyResultPath) ? readFileSync(verifyResultPath, 'utf8') : ''
|
|
1804
1966
|
|
|
1967
|
+
// ── v2 槽优先(2026-09-08-ir-verify-facts FR-02):「## 证据账」槽段在场走分类核验;
|
|
1968
|
+
// 无槽(存量)降级 legacy 子串提及对账——行为等同旧版(D-001@v2,warning 不阻断)。──
|
|
1969
|
+
const slots = parseEvidenceSlots(report)
|
|
1970
|
+
if (slots.hasEvidenceSlot) {
|
|
1971
|
+
return runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, verifyStartAt })
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1805
1974
|
const unacknowledged = []
|
|
1806
1975
|
for (const item of items) {
|
|
1807
1976
|
const task = item && item.task
|
|
@@ -1816,16 +1985,115 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1816
1985
|
}
|
|
1817
1986
|
|
|
1818
1987
|
if (unacknowledged.length > 0) {
|
|
1819
|
-
return { status: 'warning', items, unacknowledged,
|
|
1988
|
+
return { status: 'warning', items, unacknowledged, legacy: true,
|
|
1820
1989
|
summary: `${items.length} 个 cannot_verify evidence 任务中 ${unacknowledged.length} 个未在 verify-result.md 体现`, reason: null }
|
|
1821
1990
|
}
|
|
1822
|
-
return { status: 'passed', items, unacknowledged: [],
|
|
1991
|
+
return { status: 'passed', items, unacknowledged: [], legacy: true,
|
|
1823
1992
|
summary: `${items.length} 个 cannot_verify evidence 任务均在 verify-result.md 体现(满足度由 agent 自报告)`, reason: null }
|
|
1824
1993
|
}
|
|
1825
1994
|
|
|
1826
|
-
/**
|
|
1995
|
+
/**
|
|
1996
|
+
* v2 分类核验(design Phase 2,FR-02):证据账槽行状态 + verifiedFiles 逐文件核验。
|
|
1997
|
+
* code 类(classifyVerifiedFile):存在 × mtime ≥ verifyStartAt × git diff 交集三核验;
|
|
1998
|
+
* artifact 类(.runtime/日志/文档):存在 × mtime(diff 交集豁免——resolveVerifyChangedFiles
|
|
1999
|
+
* 天然不覆盖该类路径,硬要求交集必假红)。verifyStartAt 缺省走 R-05 fallback(design.md
|
|
2000
|
+
* created_at 之后宽容 + warning)。status 语义扩 'blocked'(missing 无豁免 / satisfied 核验
|
|
2001
|
+
* 不过)——gates 接线(task-03)据此阻断 verify 完成。
|
|
2002
|
+
*/
|
|
2003
|
+
function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, verifyStartAt }) {
|
|
2004
|
+
const warnings = []
|
|
2005
|
+
let startAt = verifyStartAt ? new Date(verifyStartAt).getTime() : null
|
|
2006
|
+
if (!startAt) {
|
|
2007
|
+
// R-05 fallback:DB 不可得/接线间隙(W2→W3)——design.md created_at 之后即宽容
|
|
2008
|
+
try {
|
|
2009
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
2010
|
+
if (existsSync(designPath)) {
|
|
2011
|
+
const fm = readFileSync(designPath, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
2012
|
+
const cm = fm && fm[1].match(/^created_at:\s*(.+)$/m)
|
|
2013
|
+
if (cm) {
|
|
2014
|
+
const t = new Date(cm[1].trim()).getTime()
|
|
2015
|
+
if (!Number.isNaN(t)) { startAt = t; warnings.push('verifyStartAt 未提供,走 design.md created_at 宽容基准(R-05 fallback)') }
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
} catch { /* fallback 的 fallback:不设基准(只核存在性) */ }
|
|
2019
|
+
if (!startAt) warnings.push('verifyStartAt 与 design created_at 均不可得——mtime 核验降级为仅存在性')
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// git diff 集合(code 类交集核验用;fail-soft 拿不到 → diffHit 记 null 不判红)
|
|
2023
|
+
let changedSet = null
|
|
2024
|
+
try {
|
|
2025
|
+
const changed = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase })
|
|
2026
|
+
changedSet = new Set((changed || []).map(p => String(p).replace(/\\/g, '/')))
|
|
2027
|
+
} catch { changedSet = null }
|
|
2028
|
+
|
|
2029
|
+
const detailed = []
|
|
2030
|
+
let blockedCount = 0
|
|
2031
|
+
for (const item of items) {
|
|
2032
|
+
const task = item && item.task
|
|
2033
|
+
if (!task) continue
|
|
2034
|
+
const slot = slots.requiredEvidence.find(e => e.task === task) || null
|
|
2035
|
+
if (!slot) {
|
|
2036
|
+
detailed.push({ task, verdict: item.verdict || 'cannot_verify', status: 'missing', exempt: false,
|
|
2037
|
+
verification: null, reason: `cannot_verify 任务 ${task} 未在证据账槽段登记(逐 task 一行:状态三选一 + verifiedFiles)` })
|
|
2038
|
+
blockedCount++
|
|
2039
|
+
continue
|
|
2040
|
+
}
|
|
2041
|
+
const perFile = []
|
|
2042
|
+
let allOk = true
|
|
2043
|
+
if (slot.status === 'satisfied' || slot.status === 'partial') {
|
|
2044
|
+
if (slot.verifiedFiles.length === 0) {
|
|
2045
|
+
allOk = false
|
|
2046
|
+
perFile.push({ path: null, pathClass: null, filesExist: false, mtimeOk: null, diffHit: null,
|
|
2047
|
+
reason: `${slot.status} 但 verifiedFiles 为空(精确路径必填)` })
|
|
2048
|
+
}
|
|
2049
|
+
for (const vf of slot.verifiedFiles) {
|
|
2050
|
+
const pathClass = classifyVerifiedFile(vf)
|
|
2051
|
+
const abs = join(cwd, vf)
|
|
2052
|
+
const filesExist = existsSync(abs)
|
|
2053
|
+
let mtimeOk = null
|
|
2054
|
+
if (filesExist && startAt) {
|
|
2055
|
+
try { mtimeOk = statSync(abs).mtimeMs >= startAt - 60_000 } catch { mtimeOk = null }
|
|
2056
|
+
}
|
|
2057
|
+
let diffHit = null
|
|
2058
|
+
if (pathClass === 'code') {
|
|
2059
|
+
diffHit = changedSet === null ? null : changedSet.has(vf)
|
|
2060
|
+
} else {
|
|
2061
|
+
diffHit = true // artifact 类豁免 diff(design Phase 2:日志/文档天然不在 diff)
|
|
2062
|
+
}
|
|
2063
|
+
const ok = filesExist && mtimeOk !== false && diffHit !== false
|
|
2064
|
+
if (!ok) allOk = false
|
|
2065
|
+
perFile.push({ path: vf, pathClass, filesExist, mtimeOk, diffHit,
|
|
2066
|
+
reason: !filesExist ? '文件不存在'
|
|
2067
|
+
: (mtimeOk === false ? `mtime 早于 verifyStartAt(${new Date(startAt).toISOString()})`
|
|
2068
|
+
: (diffHit === false ? '不在本变更 git diff 内' : null)) })
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
const failed = (slot.status === 'satisfied' && !allOk) || (slot.status === 'missing' && !slot.exempt)
|
|
2072
|
+
if (failed) blockedCount++
|
|
2073
|
+
detailed.push({
|
|
2074
|
+
task, verdict: item.verdict || 'cannot_verify', status: slot.status, exempt: slot.exempt,
|
|
2075
|
+
verification: perFile.length > 0 ? perFile : null,
|
|
2076
|
+
reason: failed
|
|
2077
|
+
? (slot.status === 'missing' ? 'missing 且无(豁免:<理由>)后缀——cannot_verify 未闭环'
|
|
2078
|
+
: (slot.status === 'satisfied' ? 'satisfied 核验不过(见 verification 明细)' : 'partial 但 verifiedFiles 全部核验不过'))
|
|
2079
|
+
: (slot.exempt ? `missing 已豁免:${slot.exemptionReason}`
|
|
2080
|
+
: (slot.status === 'partial' ? 'partial(部分核验通过,理由见槽行括注)' : '核验通过')),
|
|
2081
|
+
})
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (blockedCount > 0) {
|
|
2085
|
+
return { status: 'blocked', items, detailed, warnings,
|
|
2086
|
+
unacknowledged: detailed.filter(d => d.status === 'missing' && !d.exempt && !d.verification).map(d => ({ task: d.task, reason: d.reason })),
|
|
2087
|
+
summary: `${items.length} 个 cannot_verify 任务中 ${blockedCount} 个未闭环(missing 无豁免或核验不过)`, reason: null }
|
|
2088
|
+
}
|
|
2089
|
+
return { status: 'passed', items, detailed, unacknowledged: [], warnings,
|
|
2090
|
+
summary: `${items.length} 个 cannot_verify 任务证据账全部闭环(状态 + 分类核验通过)`, reason: null }
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/** 打印 required-evidence 对账结果(v2:blocked 由 gates 阻断;legacy warning 不阻断) */
|
|
1827
2094
|
export function printVerifyRequiredEvidenceCheck(result) {
|
|
1828
2095
|
if (result.status === 'skipped') return // 静默:无 evidence 文件 / 无 changeName / 解析失败
|
|
2096
|
+
if (result.status === 'blocked') return // 阻断明细由 gates 接线侧统一输出(本函数不重复打印)
|
|
1829
2097
|
if (result.status === 'passed') {
|
|
1830
2098
|
console.log(`\n✅ required-evidence 对账通过:${result.summary}`)
|
|
1831
2099
|
return
|
|
@@ -2101,7 +2369,7 @@ function attributeSuspectTasks(runtimeRoot, changeName, paths) {
|
|
|
2101
2369
|
* sources: string[], // actual 实际用到的源(诊断可见性;additive)
|
|
2102
2370
|
* }}
|
|
2103
2371
|
*/
|
|
2104
|
-
export function reconcileTargetFiles({ cwd, specBase = null, changeName = null, runtimeRoot = null }) {
|
|
2372
|
+
export function reconcileTargetFiles({ cwd, specBase = null, changeName = null, runtimeRoot = null, strictMode = false }) {
|
|
2105
2373
|
const matched = []
|
|
2106
2374
|
const missing = []
|
|
2107
2375
|
const undeclared = []
|
|
@@ -2124,6 +2392,19 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2124
2392
|
skipReason: `变更 ${changeName} 无 task 卡(tasks/ 缺失或为空),target_files 对账跳过`, notes, form: null, sources: [] }
|
|
2125
2393
|
}
|
|
2126
2394
|
if (decl.declarations.length === 0) {
|
|
2395
|
+
// IR 严格档(2026-09-07-ir-hardening D-003@v1,FR-01):主仓卡全部零声明 → strictViolation。
|
|
2396
|
+
// 判据(Grill P2-④修订):noDeclarationCount > 0 且 == cardCount - 跨仓卡数——跨仓卡不参与
|
|
2397
|
+
// 计数也不充当豁免(防「塞一张跨仓卡让全部主仓卡免检」绕过)。部分主仓卡已声明 → 不触发(灰度 WARNING)。
|
|
2398
|
+
if (strictMode && decl.noDeclarationCount > 0
|
|
2399
|
+
&& decl.noDeclarationCount === decl.cardCount - decl.crossRepoCards.length) {
|
|
2400
|
+
return { status: 'skipped', matched, missing, undeclared,
|
|
2401
|
+
skipReason: `严格模式变更(created_at ≥ IR_STRICT_SINCE)主仓 task 卡全部未声明 target_files(${decl.noDeclarationCount}/${decl.cardCount - decl.crossRepoCards.length} 张主仓卡${decl.crossRepoCards.length > 0 ? `,另 ${decl.crossRepoCards.length} 张跨仓卡已剔除` : ''})`,
|
|
2402
|
+
notes, form: null, sources: [],
|
|
2403
|
+
strictViolation: {
|
|
2404
|
+
code: 'target_files_all_missing_strict',
|
|
2405
|
+
message: `严格模式要求每个主仓 task 卡声明 target_files(计划改动的文件清单,taskcard 骨架已预置字段)——逐卡 Edit 填入:已存在文件写仓根相对精确路径,新建文件加 NEW: 前缀;填毕重跑 --done(进度不丢)`,
|
|
2406
|
+
} }
|
|
2407
|
+
}
|
|
2127
2408
|
// 存量零红门禁:全部卡无声明 / 全部跨仓 → WARNING 语义跳过,不产生 missing/undeclared
|
|
2128
2409
|
const reason = decl.noDeclarationCount === 0
|
|
2129
2410
|
? `本变更 task 卡全部为跨仓卡(${decl.crossRepoCards.length} 张),主仓 target_files 对账跳过`
|
|
@@ -2146,24 +2427,35 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2146
2427
|
}
|
|
2147
2428
|
|
|
2148
2429
|
// —— 三类差集 ——
|
|
2149
|
-
|
|
2430
|
+
// 对账键(2026-09-08 用户反馈⑥:计划文件名 vs 实测文件名的字面差脆):声明侧 task 卡是
|
|
2431
|
+
// agent 手写(大小写随手、尾部注记、./ 前缀残留),actual 侧是 git 机器产出——字面相等
|
|
2432
|
+
// 判定下这些形态差全部落②类假红(missing_declared,ERROR 态阻断 verify)。统一 canonical
|
|
2433
|
+
// key 再比对:normalizeReviewChangedFile 归一(./ 前缀/尾部注记/行内注释/反斜杠)+
|
|
2434
|
+
// win32/darwin 大小写折叠(默认文件系统大小写不敏感,两形指同一文件;Linux 保持敏感)。
|
|
2435
|
+
// 报告侧仍用原始字面串(用户按自己写的名字找到声明处)。
|
|
2436
|
+
const foldCase = process.platform === 'win32' || process.platform === 'darwin';
|
|
2437
|
+
const pathKey = (p) => {
|
|
2438
|
+
const n = normalizeReviewChangedFile(p);
|
|
2439
|
+
return foldCase ? n.toLowerCase() : n;
|
|
2440
|
+
};
|
|
2441
|
+
const actualKeySet = new Set(actual.files.map(pathKey));
|
|
2150
2442
|
const declaredPaths = [...new Set(decl.declarations.map(d => d.path))].sort()
|
|
2151
|
-
const
|
|
2443
|
+
const declaredKeySet = new Set(declaredPaths.map(pathKey))
|
|
2152
2444
|
// ①交集(计数进 evidence):唯一路径口径(多卡声明同文件只计一次)
|
|
2153
2445
|
for (const path of declaredPaths) {
|
|
2154
|
-
if (
|
|
2446
|
+
if (actualKeySet.has(pathKey(path))) matched.push(path)
|
|
2155
2447
|
}
|
|
2156
2448
|
// ②声明没做(计划落空,ERROR):逐卡逐条报 task-NN + path;NEW: 声明的取剥前缀 path、带
|
|
2157
|
-
// isNew
|
|
2449
|
+
// isNew 标("新建文件没建"与"存量文件没动"的修复指引不同)
|
|
2158
2450
|
for (const d of decl.declarations) {
|
|
2159
|
-
if (!
|
|
2451
|
+
if (!actualKeySet.has(pathKey(d.path))) {
|
|
2160
2452
|
missing.push(d.isNew ? { task: d.task, path: d.path, isNew: true } : { task: d.task, path: d.path })
|
|
2161
2453
|
}
|
|
2162
2454
|
}
|
|
2163
2455
|
// ③做了没声明(scope creep,WARNING):actual − 声明集;suspectTask 尽力归因仅报告(D-002)
|
|
2164
|
-
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !
|
|
2456
|
+
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !declaredKeySet.has(pathKey(p))))
|
|
2165
2457
|
for (const path of actual.files) {
|
|
2166
|
-
if (
|
|
2458
|
+
if (declaredKeySet.has(pathKey(path))) continue
|
|
2167
2459
|
const s = suspect.get(normalizeReviewChangedFile(path))
|
|
2168
2460
|
undeclared.push(s ? { path, suspectTask: s } : { path })
|
|
2169
2461
|
}
|
|
@@ -2337,7 +2629,20 @@ function detectHeadAdvanceSinceFacts(cwd, facts) {
|
|
|
2337
2629
|
* // 诊断(additive):探针子节在场性;报告未读到的早期 skip 为 null
|
|
2338
2630
|
* }}
|
|
2339
2631
|
*/
|
|
2340
|
-
|
|
2632
|
+
/**
|
|
2633
|
+
* IR 严格模式判别(change: 2026-09-07-ir-hardening,D-001@v1):
|
|
2634
|
+
* changes.created_at ≥ IR_STRICT_SINCE → true(严格档:P3b 探针段缺失 / P3a 主仓卡全零声明 ERROR)。
|
|
2635
|
+
* fail-open:读不到 created_at(无行/db 异常/pm 缺失)→ false,落存量豁免不误伤。
|
|
2636
|
+
* @param {{ getChangeCreatedAt?: Function }} opts.pm - ProgressManager(或含同名方法的对象)
|
|
2637
|
+
*/
|
|
2638
|
+
export function isStrictChange({ pm, cwd, changeName } = {}) {
|
|
2639
|
+
if (!pm || typeof pm.getChangeCreatedAt !== 'function' || !changeName) return false
|
|
2640
|
+
const createdAt = pm.getChangeCreatedAt(cwd, changeName)
|
|
2641
|
+
if (!createdAt || typeof createdAt !== 'string') return false
|
|
2642
|
+
return createdAt >= IR_STRICT_SINCE
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
export function checkProbeConsistency({ cwd, specBase = null, changeName = null, runtimeRoot = null, strictMode = false }) {
|
|
2341
2646
|
if (!changeName) {
|
|
2342
2647
|
return { status: 'skipped', severity: null, mismatches: [],
|
|
2343
2648
|
skipReason: '无 changeName(quick 等无关联变更场景),探针一致性抽查跳过', subsections: null }
|
|
@@ -2370,8 +2675,20 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
|
|
|
2370
2675
|
note: 'verify-facts.json 在场而正文探针子节全缺——疑似 agent 删除机械预填段(对比基准是正文,删 facts.json 绕不过防护)',
|
|
2371
2676
|
}], null)
|
|
2372
2677
|
}
|
|
2678
|
+
if (strictMode) {
|
|
2679
|
+
// IR 严格档(2026-09-07-ir-hardening D-002@v1,FR-01):闸门后变更不跑 verify-probes --init
|
|
2680
|
+
// 即零子节——不再是「存量兼容 skip」而是 ERROR(可绕过口收紧)。文案必须带可执行指引(R-02)。
|
|
2681
|
+
return finish('mismatch', 'error', [{
|
|
2682
|
+
probe: 'prefill',
|
|
2683
|
+
code: 'probe_prefill_missing_strict',
|
|
2684
|
+
expected: '#### 探针 预填子节在场',
|
|
2685
|
+
actual: '全部缺失',
|
|
2686
|
+
severity: 'error',
|
|
2687
|
+
note: '严格模式变更(created_at ≥ IR_STRICT_SINCE)的 verify-result.md 缺探针预填子节——先跑 sillyspec verify-probes --change <变更名> --init 生成九章节骨架并如实填写,再重跑 --done',
|
|
2688
|
+
}], null)
|
|
2689
|
+
}
|
|
2373
2690
|
return finish('skipped', null, [],
|
|
2374
|
-
'verify-result.md 无 #### 探针 子节且无 verify-facts.json
|
|
2691
|
+
'verify-result.md 无 #### 探针 子节且无 verify-facts.json(存量旧格式报告,闸门前变更),探针一致性抽查跳过')
|
|
2375
2692
|
}
|
|
2376
2693
|
|
|
2377
2694
|
// —— 重跑探针取当前指标(specDir 显式传 sb:根口径由调用方给定,不做祖先推断/漂移锚定) ——
|
|
@@ -2424,7 +2741,62 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
|
|
|
2424
2741
|
note: 'contract gap(missing backend)行数与重跑不符(探针5 扫描根/缓存口径环境敏感,WARNING 不阻断)' })
|
|
2425
2742
|
}
|
|
2426
2743
|
|
|
2427
|
-
|
|
2744
|
+
// —— facts 基线对比(2026-09-08-ir-verify-facts FR-05 / task-05):重跑指标 vs
|
|
2745
|
+
// verify-facts.json probes 快照(init 时点)。md 锚点对账查「正文没被改」,本维度查
|
|
2746
|
+
// 「md 被手改对齐新代码后 facts 底稿过期」(P3d 数据源保鲜)。分级沿用现实现口径:
|
|
2747
|
+
// probe1/6=ERROR、probe3/5=WARNING;probe6 继承 HEAD-advance 降级(R-06)。——
|
|
2748
|
+
const factsSnapshot = readVerifyFacts(changeDir)
|
|
2749
|
+
let factsConsistency = null
|
|
2750
|
+
if (factsSnapshot && factsSnapshot.probes) {
|
|
2751
|
+
const fm = []
|
|
2752
|
+
const adv = detectHeadAdvanceSinceFacts(cwd, factsSnapshot)
|
|
2753
|
+
const snap1 = (((factsSnapshot.probes.probe1 || {}).metrics) || {}).matches
|
|
2754
|
+
if (typeof snap1 === 'number' && snap1 !== curProbe1Hits) {
|
|
2755
|
+
fm.push({ probe: 'probe1', snapshot: snap1, rerun: curProbe1Hits, severity: 'error',
|
|
2756
|
+
note: 'facts 快照命中数与重跑不符——md 疑已手改对齐新代码而 facts 底稿过期(修复:verify-probes --init 刷新 facts + 按新结果同步 md 探针段)' })
|
|
2757
|
+
}
|
|
2758
|
+
const snap6 = (((factsSnapshot.probes.probe6 || {}).metrics) || {}).deletions
|
|
2759
|
+
if (!curProbe6.unavailable && typeof snap6 === 'number' && snap6 !== curProbe6Deletions && !adv) {
|
|
2760
|
+
// adv(HEAD 前移)时豁免:同一漂移已由 md 锚点维度的 probe6 WARNING 报出(R-06 继承,
|
|
2761
|
+
// 双维度重复报同一信号是噪音);非 adv 的不符 = facts 底稿被手改对齐——ERROR
|
|
2762
|
+
fm.push({ probe: 'probe6', snapshot: snap6, rerun: curProbe6Deletions, severity: 'error',
|
|
2763
|
+
note: 'facts 快照删除数与重跑不符——底稿疑似过期(修复:verify-probes --init 刷新 facts + 同步 md 探针段)' })
|
|
2764
|
+
}
|
|
2765
|
+
const snap3 = (((factsSnapshot.probes.probe3 || {}).metrics) || {}).hasTest
|
|
2766
|
+
if (typeof snap3 === 'number' && snap3 !== curProbe3HasTest) {
|
|
2767
|
+
fm.push({ probe: 'probe3', snapshot: snap3, rerun: curProbe3HasTest, severity: 'warning',
|
|
2768
|
+
note: 'facts 快照 hasTest 数与重跑不符(测试布局环境敏感,WARNING)' })
|
|
2769
|
+
}
|
|
2770
|
+
const snap5 = (((factsSnapshot.probes.probe5 || {}).metrics) || {}).backendEndpoints
|
|
2771
|
+
const cur5 = (current.probe5 || {}).backendCount
|
|
2772
|
+
if (typeof snap5 === 'number' && typeof cur5 === 'number' && snap5 !== cur5) {
|
|
2773
|
+
fm.push({ probe: 'probe5', snapshot: snap5, rerun: cur5, severity: 'warning',
|
|
2774
|
+
note: 'facts 快照 backend 端点数与重跑不符(扫描根口径环境敏感,WARNING)' })
|
|
2775
|
+
}
|
|
2776
|
+
factsConsistency = {
|
|
2777
|
+
checked: fm.length === 0 ? ['probe1', 'probe3', 'probe5', 'probe6'] : [...new Set(fm.map(x => x.probe))],
|
|
2778
|
+
verdict: fm.length === 0 ? 'match' : 'mismatch',
|
|
2779
|
+
detail: fm.length === 0 ? null : fm,
|
|
2780
|
+
}
|
|
2781
|
+
for (const x of fm) {
|
|
2782
|
+
mismatches.push({ probe: `facts:${x.probe}`, expected: x.rerun, actual: x.snapshot, severity: x.severity, note: x.note })
|
|
2783
|
+
}
|
|
2784
|
+
// 固化 factsConsistency(fail-soft;不动 probes 快照语义——probes 归 --init 所有)
|
|
2785
|
+
try {
|
|
2786
|
+
const factsPath2 = join(changeDir, 'verify-facts.json')
|
|
2787
|
+
const f2 = JSON.parse(readFileSync(factsPath2, 'utf8'))
|
|
2788
|
+
f2.factsConsistency = factsConsistency
|
|
2789
|
+
writeFileSync(factsPath2, JSON.stringify(f2, null, 2) + '\n')
|
|
2790
|
+
} catch { /* 固化失败不阻断对账 */ }
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
if (mismatches.length === 0) {
|
|
2794
|
+
const okResult = finish('ok', null, [], null)
|
|
2795
|
+
okResult.factsConsistency = factsConsistency
|
|
2796
|
+
return okResult
|
|
2797
|
+
}
|
|
2428
2798
|
const severity = mismatches.some(m => m.severity === 'error') ? 'error' : 'warning'
|
|
2429
|
-
|
|
2799
|
+
const mismatchResult = finish('mismatch', severity, mismatches, null)
|
|
2800
|
+
mismatchResult.factsConsistency = factsConsistency
|
|
2801
|
+
return mismatchResult
|
|
2430
2802
|
}
|