sillyspec 3.28.3 → 3.28.5
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 +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
package/src/verify-postcheck.js
CHANGED
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execSync, spawnSync } from 'child_process'
|
|
26
|
+
import { createHash } from 'crypto'
|
|
26
27
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
27
28
|
import { gitQuiet } from './git-helper.js'
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
29
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
29
30
|
import { join } from 'path'
|
|
30
31
|
import { verifyApiParity } from './contract-matrix.js'
|
|
31
32
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
@@ -37,6 +38,7 @@ import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
|
|
|
37
38
|
// fs/path/git-helper/change-list/plan-postcheck/contract-matrix/foreign-declared/run-shared,
|
|
38
39
|
// 不反向依赖本模块,此处直连不引入循环
|
|
39
40
|
import { runVerifyProbes } from './verify-probes.js'
|
|
41
|
+
import { parseEvidenceSlots, classifyVerifiedFile } from './verify-facts-schema.js'
|
|
40
42
|
|
|
41
43
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
42
44
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
@@ -115,6 +117,36 @@ function extractLintCommand(yamlText) {
|
|
|
115
117
|
return null
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
/**
|
|
121
|
+
* lint advisory 观察期计数(刀③,2026-09-08):advisory 失败不阻断,但「观察期后升级硬门」
|
|
122
|
+
* 需要失败率数据支撑(更硬 vs 更吵的决策依据——硬门误伤纯文档/跨平台路径会制造新噪音)。
|
|
123
|
+
* 每次非 skipped 实测追加计数进 <specBase>/.runtime/verify-lint-tally.json(passed/failed
|
|
124
|
+
* 都记,分母同要);history 截尾 20 条。写失败静默降级——计数器不许反向阻断 verify。
|
|
125
|
+
*/
|
|
126
|
+
function recordVerifyLintTally({ specBase, result }) {
|
|
127
|
+
try {
|
|
128
|
+
const runtimeDir = join(specBase, '.runtime')
|
|
129
|
+
mkdirSync(runtimeDir, { recursive: true })
|
|
130
|
+
const tallyPath = join(runtimeDir, 'verify-lint-tally.json')
|
|
131
|
+
let tally = { totalRuns: 0, failedRuns: 0, history: [] }
|
|
132
|
+
try {
|
|
133
|
+
tally = { ...tally, ...JSON.parse(readFileSync(tallyPath, 'utf8')) }
|
|
134
|
+
} catch { /* 首跑/文件损坏 → 重置重新累计 */ }
|
|
135
|
+
tally.totalRuns += 1
|
|
136
|
+
if (result.status === 'failed') tally.failedRuns += 1
|
|
137
|
+
tally.lastRunAt = new Date().toISOString()
|
|
138
|
+
tally.lastStatus = result.status
|
|
139
|
+
tally.history = [
|
|
140
|
+
...(Array.isArray(tally.history) ? tally.history : []),
|
|
141
|
+
{ at: tally.lastRunAt, status: result.status, reason: result.reason || null },
|
|
142
|
+
].slice(-20)
|
|
143
|
+
writeFileSync(tallyPath, JSON.stringify(tally, null, 2))
|
|
144
|
+
return { failedRuns: tally.failedRuns, totalRuns: tally.totalRuns }
|
|
145
|
+
} catch {
|
|
146
|
+
return null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
118
150
|
/**
|
|
119
151
|
* verify --done 实测跑 local.yaml commands.lint(2026-08-21 审查 CLI-1)。
|
|
120
152
|
* 此前 lint 全靠 agent 自跑自报("我跑过 lint 了"纯口头),与 test 侧的对账不对称——
|
|
@@ -162,18 +194,36 @@ export function runVerifyLintCheck({ cwd, specBase }) {
|
|
|
162
194
|
}
|
|
163
195
|
const durationMs = Date.now() - startedAt
|
|
164
196
|
const outputTail = output.length > OUTPUT_TAIL_CHARS ? '…' + output.slice(-OUTPUT_TAIL_CHARS) : output
|
|
197
|
+
const status = exitCode === 0 ? 'passed' : 'failed'
|
|
198
|
+
const finalReason = exitCode === 0 ? null : reason
|
|
199
|
+
const tally = recordVerifyLintTally({ specBase, result: { status, reason: finalReason } })
|
|
165
200
|
|
|
166
201
|
return {
|
|
167
|
-
status
|
|
202
|
+
status,
|
|
168
203
|
command,
|
|
169
204
|
exitCode,
|
|
170
205
|
durationMs,
|
|
171
206
|
outputTail,
|
|
172
|
-
reason:
|
|
207
|
+
reason: finalReason,
|
|
208
|
+
tally, // { failedRuns, totalRuns } | null——观察期失败率,升级硬门的决策依据
|
|
173
209
|
}
|
|
174
210
|
}
|
|
175
211
|
|
|
176
|
-
/**
|
|
212
|
+
/**
|
|
213
|
+
* lint 硬门决策(2026-09-09 升硬,外部数据驱动):观察期 tally 14 次 5 败全真阳性零误伤,
|
|
214
|
+
* 且 pre-push 钩子本就硬拦——verify 侧硬门只是同一标准 fail-fast 前移。
|
|
215
|
+
* 逃生档:SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕,对齐 quick 门 skip env 先例)。
|
|
216
|
+
* @returns {boolean} failed 且非逃生档 → true(调用方 rollback)
|
|
217
|
+
*/
|
|
218
|
+
export function shouldBlockVerifyLint(lintCheck, env = process.env) {
|
|
219
|
+
if (!lintCheck || lintCheck.status !== 'failed') return false
|
|
220
|
+
return env.SILLYSPEC_VERIFY_LINT_GATE !== 'advisory'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 打印 lint 实测结果(2026-09-09 起按 shouldBlockVerifyLint 分支):硬门档「已阻断」+
|
|
225
|
+
* 逃生 env 指引(rollback 由 gates 接线侧执行);advisory 档保留旧观察期措辞。
|
|
226
|
+
*/
|
|
177
227
|
export function printVerifyLintCheck(result) {
|
|
178
228
|
if (result.status === 'skipped') {
|
|
179
229
|
console.warn(`\n⚠️ Verify lint 实测跳过:${result.reason}`)
|
|
@@ -183,8 +233,22 @@ export function printVerifyLintCheck(result) {
|
|
|
183
233
|
console.log(`\n✅ Verify lint 实测通过:\`${result.command}\` 退出码 0(${(result.durationMs / 1000).toFixed(1)}s)`)
|
|
184
234
|
return
|
|
185
235
|
}
|
|
186
|
-
|
|
187
|
-
|
|
236
|
+
// 文案随门禁档位分支(2026-09-09 复核修正 P2:硬门档下旧「不阻断/观察期」措辞与
|
|
237
|
+
// gates 的 rollback 行为打架,agent 会误判 CLI 抽风)——rollback 本体在 gates 接线侧。
|
|
238
|
+
if (shouldBlockVerifyLint(result)) {
|
|
239
|
+
console.error(`\n❌ Verify lint 实测失败——verify 完成已阻断(2026-09-09 起硬门):\`${result.command}\` — ${result.reason}`)
|
|
240
|
+
console.error(' agent 的 lint 自报告与实测不符时以实测为准;修复后重跑 --done(进度不丢)。')
|
|
241
|
+
if (result.tally) {
|
|
242
|
+
console.error(` 📊 累计 ${result.tally.failedRuns}/${result.tally.totalRuns} 次(.runtime/verify-lint-tally.json)。`)
|
|
243
|
+
}
|
|
244
|
+
console.error(' 确认要跳过实测:SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
245
|
+
} else {
|
|
246
|
+
console.error(`\n⚠️ Verify lint 实测失败(advisory 档,不阻断本次完成):\`${result.command}\` — ${result.reason}`)
|
|
247
|
+
console.error(' agent 的 lint 自报告与实测不符时以实测为准;请修复后重跑,避免格式债推迟到 commit 被 pre-commit hook 拦截。')
|
|
248
|
+
if (result.tally) {
|
|
249
|
+
console.error(` 📊 lint advisory 失败累计 ${result.tally.failedRuns}/${result.tally.totalRuns} 次(数据落 .runtime/verify-lint-tally.json)。`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
188
252
|
if (result.outputTail) {
|
|
189
253
|
const tail = result.outputTail.split('\n').slice(-15).join('\n')
|
|
190
254
|
console.error(' 输出(末尾):')
|
|
@@ -995,8 +1059,33 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
|
|
|
995
1059
|
!p.startsWith('.sillyspec/changes/') &&
|
|
996
1060
|
!p.startsWith('.sillyspec/.runtime/') &&
|
|
997
1061
|
!p.startsWith('.sillyspec/quicklog/'))
|
|
998
|
-
|
|
999
|
-
|
|
1062
|
+
// 已提交口径补齐(2026-09-10 用户反馈②「先提交则 diff 空」时序两难的 diff 半边):
|
|
1063
|
+
// worktree 内已 commit 的改动对主仓 base..HEAD 与 status --porcelain 双双不可见——
|
|
1064
|
+
// 用 merge-base(主仓 HEAD, worktree HEAD)(= worktree 创建锚点)到 worktree HEAD 的
|
|
1065
|
+
// commit diff 补入,未提交 ∪ 已提交两形态全覆盖。wtGitDir===cwd(in-place 退化)时
|
|
1066
|
+
// 已提交改动已在 mainFiles,跳过防双并。fail-open:git 失败退回 status-only 现状。
|
|
1067
|
+
let committedFiles = []
|
|
1068
|
+
if (wtGitDir !== cwd) {
|
|
1069
|
+
try {
|
|
1070
|
+
const mainHead = gitQuiet(cwd, ['rev-parse', 'HEAD'], { timeout: 15000 })
|
|
1071
|
+
const wtHead = gitQuiet(wtGitDir, ['rev-parse', 'HEAD'], { timeout: 15000 })
|
|
1072
|
+
const mb = (mainHead && wtHead)
|
|
1073
|
+
? gitQuiet(wtGitDir, ['merge-base', wtHead, mainHead], { timeout: 15000 })
|
|
1074
|
+
: null
|
|
1075
|
+
if (mb) {
|
|
1076
|
+
const diffOut = gitQuiet(wtGitDir, ['diff', '--name-only', mb, wtHead], { timeout: 30000 })
|
|
1077
|
+
committedFiles = String(diffOut || '').split('\n')
|
|
1078
|
+
.map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
|
|
1079
|
+
.filter(p => p && p !== '.sillyspec' &&
|
|
1080
|
+
!p.startsWith('.sillyspec/changes/') &&
|
|
1081
|
+
!p.startsWith('.sillyspec/.runtime/') &&
|
|
1082
|
+
!p.startsWith('.sillyspec/quicklog/'))
|
|
1083
|
+
}
|
|
1084
|
+
} catch { /* 已提交补齐失败退回 status-only(fail-open,不拖垮未提交并入) */ }
|
|
1085
|
+
}
|
|
1086
|
+
const merged = [...new Set([...(mainFiles || []), ...wtFiles, ...committedFiles])]
|
|
1087
|
+
if (wtFiles.length > 0 || committedFiles.length > 0) {
|
|
1088
|
+
mainFiles = merged
|
|
1000
1089
|
}
|
|
1001
1090
|
}
|
|
1002
1091
|
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open) */ }
|
|
@@ -1620,6 +1709,9 @@ export function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
|
1620
1709
|
...extra,
|
|
1621
1710
|
}, null, 2) + '\n')
|
|
1622
1711
|
result.resultPath = resultPath
|
|
1712
|
+
// verify-runs 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
|
|
1713
|
+
// 回收(pruneArchivedChangeRuntime 按目录内 JSON change 字段归属);写入侧滚动是
|
|
1714
|
+
// 启发式,不进证据类目录。
|
|
1623
1715
|
} catch (e) {
|
|
1624
1716
|
console.warn(`⚠️ verify 实测结果落盘失败: ${e.message}`)
|
|
1625
1717
|
}
|
|
@@ -1863,6 +1955,40 @@ export function printVerifyDeletionCheck(result) {
|
|
|
1863
1955
|
console.warn(' 提示:确认删除是否预期。预期删除请在 design.md 清单用「删除」操作显式声明;误删请恢复。')
|
|
1864
1956
|
}
|
|
1865
1957
|
|
|
1958
|
+
/**
|
|
1959
|
+
* verify-result.md 内容回退检测(2026-09-10 用户反馈③:报告在 verify 中途被平台同步覆盖
|
|
1960
|
+
* 回旧版一次,只能靠人眼发现后重写)。覆盖者在本仓之外(daemon/平台侧按服务端版本回写),
|
|
1961
|
+
* CLI 侧能做的是**检测可见**:每次 CLI 读到该文件时记高水位指纹(content hash + mtime),
|
|
1962
|
+
* 下次读到「内容变了且 mtime 回退」= 旧版复活的确定性指纹(服务端回写会顺带恢复服务端
|
|
1963
|
+
* mtime,时间倒流唯此一路)→ ⚠️ 告警。纯 advisory 不阻断;高水位只前进不后退(回退态
|
|
1964
|
+
* 不覆盖指纹,agent 重写后新指纹正常接管)。fail-open:任何异常视为无回退。
|
|
1965
|
+
* @param {string} specBase spec 根(hwm 文件落 .runtime/)
|
|
1966
|
+
* @param {string} changeName 变更名
|
|
1967
|
+
* @param {string} absPath verify-result.md 绝对路径
|
|
1968
|
+
* @returns {{ regressed: boolean, prevMtime: number|null }}
|
|
1969
|
+
*/
|
|
1970
|
+
export function trackVerifyResultRegression(specBase, changeName, absPath) {
|
|
1971
|
+
try {
|
|
1972
|
+
if (!absPath || !existsSync(absPath)) return { regressed: false, prevMtime: null }
|
|
1973
|
+
const st = statSync(absPath)
|
|
1974
|
+
const hash = createHash('sha256').update(readFileSync(absPath)).digest('hex')
|
|
1975
|
+
const hwmPath = join(specBase, '.runtime', `verify-result-hwm-${changeName}.json`)
|
|
1976
|
+
let prev = null
|
|
1977
|
+
try { prev = JSON.parse(readFileSync(hwmPath, 'utf8')) } catch { prev = null }
|
|
1978
|
+
const regressed = !!(prev && typeof prev.hash === 'string' &&
|
|
1979
|
+
prev.hash !== hash && typeof prev.mtimeMs === 'number' &&
|
|
1980
|
+
st.mtimeMs < prev.mtimeMs - 1000)
|
|
1981
|
+
// 高水位只前进:当前 mtime 更新(正常编辑/重写)才覆盖指纹;回退态保留旧指纹供重写后再比对
|
|
1982
|
+
if (!prev || st.mtimeMs >= (prev.mtimeMs || 0)) {
|
|
1983
|
+
try {
|
|
1984
|
+
mkdirSync(join(specBase, '.runtime'), { recursive: true })
|
|
1985
|
+
writeFileSync(hwmPath, JSON.stringify({ hash, mtimeMs: st.mtimeMs, ts: Date.now() }) + '\n')
|
|
1986
|
+
} catch { /* hwm 落盘失败 → 下次当首次(fail-open) */ }
|
|
1987
|
+
}
|
|
1988
|
+
return { regressed, prevMtime: prev && prev.mtimeMs ? prev.mtimeMs : null }
|
|
1989
|
+
} catch { return { regressed: false, prevMtime: null } }
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1866
1992
|
/**
|
|
1867
1993
|
* required-evidence 对账(advisory,不阻断 verify 完成)。
|
|
1868
1994
|
*
|
|
@@ -1877,7 +2003,7 @@ export function printVerifyDeletionCheck(result) {
|
|
|
1877
2003
|
* @param {{cwd:string, specBase:string, changeName?:string}} args
|
|
1878
2004
|
* @returns {{status:'skipped'|'passed'|'warning', items:Array, unacknowledged:Array, summary:string, reason:string|null}}
|
|
1879
2005
|
*/
|
|
1880
|
-
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null }) {
|
|
2006
|
+
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null, verifyStartAt = null }) {
|
|
1881
2007
|
if (!changeName) {
|
|
1882
2008
|
return { status: 'skipped', items: [], unacknowledged: [], summary: '', reason: '无 changeName(quick 等无关联变更场景),evidence 对账跳过' }
|
|
1883
2009
|
}
|
|
@@ -1898,6 +2024,13 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1898
2024
|
const verifyResultPath = join(specBase, 'changes', changeName, 'verify-result.md')
|
|
1899
2025
|
const report = existsSync(verifyResultPath) ? readFileSync(verifyResultPath, 'utf8') : ''
|
|
1900
2026
|
|
|
2027
|
+
// ── v2 槽优先(2026-09-08-ir-verify-facts FR-02):「## 证据账」槽段在场走分类核验;
|
|
2028
|
+
// 无槽(存量)降级 legacy 子串提及对账——行为等同旧版(D-001@v2,warning 不阻断)。──
|
|
2029
|
+
const slots = parseEvidenceSlots(report)
|
|
2030
|
+
if (slots.hasEvidenceSlot) {
|
|
2031
|
+
return runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, verifyStartAt })
|
|
2032
|
+
}
|
|
2033
|
+
|
|
1901
2034
|
const unacknowledged = []
|
|
1902
2035
|
for (const item of items) {
|
|
1903
2036
|
const task = item && item.task
|
|
@@ -1912,16 +2045,115 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1912
2045
|
}
|
|
1913
2046
|
|
|
1914
2047
|
if (unacknowledged.length > 0) {
|
|
1915
|
-
return { status: 'warning', items, unacknowledged,
|
|
2048
|
+
return { status: 'warning', items, unacknowledged, legacy: true,
|
|
1916
2049
|
summary: `${items.length} 个 cannot_verify evidence 任务中 ${unacknowledged.length} 个未在 verify-result.md 体现`, reason: null }
|
|
1917
2050
|
}
|
|
1918
|
-
return { status: 'passed', items, unacknowledged: [],
|
|
2051
|
+
return { status: 'passed', items, unacknowledged: [], legacy: true,
|
|
1919
2052
|
summary: `${items.length} 个 cannot_verify evidence 任务均在 verify-result.md 体现(满足度由 agent 自报告)`, reason: null }
|
|
1920
2053
|
}
|
|
1921
2054
|
|
|
1922
|
-
/**
|
|
2055
|
+
/**
|
|
2056
|
+
* v2 分类核验(design Phase 2,FR-02):证据账槽行状态 + verifiedFiles 逐文件核验。
|
|
2057
|
+
* code 类(classifyVerifiedFile):存在 × mtime ≥ verifyStartAt × git diff 交集三核验;
|
|
2058
|
+
* artifact 类(.runtime/日志/文档):存在 × mtime(diff 交集豁免——resolveVerifyChangedFiles
|
|
2059
|
+
* 天然不覆盖该类路径,硬要求交集必假红)。verifyStartAt 缺省走 R-05 fallback(design.md
|
|
2060
|
+
* created_at 之后宽容 + warning)。status 语义扩 'blocked'(missing 无豁免 / satisfied 核验
|
|
2061
|
+
* 不过)——gates 接线(task-03)据此阻断 verify 完成。
|
|
2062
|
+
*/
|
|
2063
|
+
function runRequiredEvidenceCheckV2({ items, slots, cwd, specBase, changeName, verifyStartAt }) {
|
|
2064
|
+
const warnings = []
|
|
2065
|
+
let startAt = verifyStartAt ? new Date(verifyStartAt).getTime() : null
|
|
2066
|
+
if (!startAt) {
|
|
2067
|
+
// R-05 fallback:DB 不可得/接线间隙(W2→W3)——design.md created_at 之后即宽容
|
|
2068
|
+
try {
|
|
2069
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
2070
|
+
if (existsSync(designPath)) {
|
|
2071
|
+
const fm = readFileSync(designPath, 'utf8').match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
2072
|
+
const cm = fm && fm[1].match(/^created_at:\s*(.+)$/m)
|
|
2073
|
+
if (cm) {
|
|
2074
|
+
const t = new Date(cm[1].trim()).getTime()
|
|
2075
|
+
if (!Number.isNaN(t)) { startAt = t; warnings.push('verifyStartAt 未提供,走 design.md created_at 宽容基准(R-05 fallback)') }
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
} catch { /* fallback 的 fallback:不设基准(只核存在性) */ }
|
|
2079
|
+
if (!startAt) warnings.push('verifyStartAt 与 design created_at 均不可得——mtime 核验降级为仅存在性')
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// git diff 集合(code 类交集核验用;fail-soft 拿不到 → diffHit 记 null 不判红)
|
|
2083
|
+
let changedSet = null
|
|
2084
|
+
try {
|
|
2085
|
+
const changed = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase })
|
|
2086
|
+
changedSet = new Set((changed || []).map(p => String(p).replace(/\\/g, '/')))
|
|
2087
|
+
} catch { changedSet = null }
|
|
2088
|
+
|
|
2089
|
+
const detailed = []
|
|
2090
|
+
let blockedCount = 0
|
|
2091
|
+
for (const item of items) {
|
|
2092
|
+
const task = item && item.task
|
|
2093
|
+
if (!task) continue
|
|
2094
|
+
const slot = slots.requiredEvidence.find(e => e.task === task) || null
|
|
2095
|
+
if (!slot) {
|
|
2096
|
+
detailed.push({ task, verdict: item.verdict || 'cannot_verify', status: 'missing', exempt: false,
|
|
2097
|
+
verification: null, reason: `cannot_verify 任务 ${task} 未在证据账槽段登记(逐 task 一行:状态三选一 + verifiedFiles)` })
|
|
2098
|
+
blockedCount++
|
|
2099
|
+
continue
|
|
2100
|
+
}
|
|
2101
|
+
const perFile = []
|
|
2102
|
+
let allOk = true
|
|
2103
|
+
if (slot.status === 'satisfied' || slot.status === 'partial') {
|
|
2104
|
+
if (slot.verifiedFiles.length === 0) {
|
|
2105
|
+
allOk = false
|
|
2106
|
+
perFile.push({ path: null, pathClass: null, filesExist: false, mtimeOk: null, diffHit: null,
|
|
2107
|
+
reason: `${slot.status} 但 verifiedFiles 为空(精确路径必填)` })
|
|
2108
|
+
}
|
|
2109
|
+
for (const vf of slot.verifiedFiles) {
|
|
2110
|
+
const pathClass = classifyVerifiedFile(vf)
|
|
2111
|
+
const abs = join(cwd, vf)
|
|
2112
|
+
const filesExist = existsSync(abs)
|
|
2113
|
+
let mtimeOk = null
|
|
2114
|
+
if (filesExist && startAt) {
|
|
2115
|
+
try { mtimeOk = statSync(abs).mtimeMs >= startAt - 60_000 } catch { mtimeOk = null }
|
|
2116
|
+
}
|
|
2117
|
+
let diffHit = null
|
|
2118
|
+
if (pathClass === 'code') {
|
|
2119
|
+
diffHit = changedSet === null ? null : changedSet.has(vf)
|
|
2120
|
+
} else {
|
|
2121
|
+
diffHit = true // artifact 类豁免 diff(design Phase 2:日志/文档天然不在 diff)
|
|
2122
|
+
}
|
|
2123
|
+
const ok = filesExist && mtimeOk !== false && diffHit !== false
|
|
2124
|
+
if (!ok) allOk = false
|
|
2125
|
+
perFile.push({ path: vf, pathClass, filesExist, mtimeOk, diffHit,
|
|
2126
|
+
reason: !filesExist ? '文件不存在'
|
|
2127
|
+
: (mtimeOk === false ? `mtime 早于 verifyStartAt(${new Date(startAt).toISOString()})`
|
|
2128
|
+
: (diffHit === false ? '不在本变更 git diff 内' : null)) })
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
const failed = (slot.status === 'satisfied' && !allOk) || (slot.status === 'missing' && !slot.exempt)
|
|
2132
|
+
if (failed) blockedCount++
|
|
2133
|
+
detailed.push({
|
|
2134
|
+
task, verdict: item.verdict || 'cannot_verify', status: slot.status, exempt: slot.exempt,
|
|
2135
|
+
verification: perFile.length > 0 ? perFile : null,
|
|
2136
|
+
reason: failed
|
|
2137
|
+
? (slot.status === 'missing' ? 'missing 且无(豁免:<理由>)后缀——cannot_verify 未闭环'
|
|
2138
|
+
: (slot.status === 'satisfied' ? 'satisfied 核验不过(见 verification 明细)' : 'partial 但 verifiedFiles 全部核验不过'))
|
|
2139
|
+
: (slot.exempt ? `missing 已豁免:${slot.exemptionReason}`
|
|
2140
|
+
: (slot.status === 'partial' ? 'partial(部分核验通过,理由见槽行括注)' : '核验通过')),
|
|
2141
|
+
})
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (blockedCount > 0) {
|
|
2145
|
+
return { status: 'blocked', items, detailed, warnings,
|
|
2146
|
+
unacknowledged: detailed.filter(d => d.status === 'missing' && !d.exempt && !d.verification).map(d => ({ task: d.task, reason: d.reason })),
|
|
2147
|
+
summary: `${items.length} 个 cannot_verify 任务中 ${blockedCount} 个未闭环(missing 无豁免或核验不过)`, reason: null }
|
|
2148
|
+
}
|
|
2149
|
+
return { status: 'passed', items, detailed, unacknowledged: [], warnings,
|
|
2150
|
+
summary: `${items.length} 个 cannot_verify 任务证据账全部闭环(状态 + 分类核验通过)`, reason: null }
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
/** 打印 required-evidence 对账结果(v2:blocked 由 gates 阻断;legacy warning 不阻断) */
|
|
1923
2154
|
export function printVerifyRequiredEvidenceCheck(result) {
|
|
1924
2155
|
if (result.status === 'skipped') return // 静默:无 evidence 文件 / 无 changeName / 解析失败
|
|
2156
|
+
if (result.status === 'blocked') return // 阻断明细由 gates 接线侧统一输出(本函数不重复打印)
|
|
1925
2157
|
if (result.status === 'passed') {
|
|
1926
2158
|
console.log(`\n✅ required-evidence 对账通过:${result.summary}`)
|
|
1927
2159
|
return
|
|
@@ -2052,14 +2284,20 @@ function parsePorcelainFilePaths(raw) {
|
|
|
2052
2284
|
* 统一收尾:filterDeliverableFiles 过滤基建产物(.sillyspec/changes|.runtime|quicklog、
|
|
2053
2285
|
* meta.json——流程产物不算 scope creep)+ 去重排序(输出确定性)。
|
|
2054
2286
|
*
|
|
2055
|
-
*
|
|
2287
|
+
* baseAnchor:与 files 同源的 diff 基点锚(scope-audit 行数采集复用,本模块自身不消费)——
|
|
2288
|
+
* 形态 A 读同一份 worktree meta.json(优先级 baselineCommit>actualBaseHash>baseHash,与
|
|
2289
|
+
* resolveMainChangedFiles 同口径,三锚全缺 null);形态 B 取 merge-base hash(分支不存在/
|
|
2290
|
+
* merge-base 不可得 null);降级返回恒 null。
|
|
2291
|
+
*
|
|
2292
|
+
* @returns={{ ok: boolean, form: 'worktree'|'post-apply', files: string[], sources: string[], foreignExcluded: number, degradedReason: string|null, baseAnchor: string|null }}
|
|
2056
2293
|
*/
|
|
2057
|
-
function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, changeName }) {
|
|
2294
|
+
export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, changeName }) {
|
|
2058
2295
|
const metaPath = join(specBase, '.runtime', 'worktrees', changeName, 'meta.json')
|
|
2059
2296
|
const form = existsSync(metaPath) ? 'worktree' : 'post-apply'
|
|
2060
2297
|
const sources = []
|
|
2061
2298
|
const union = new Set()
|
|
2062
2299
|
let foreignExcluded = 0
|
|
2300
|
+
let baseAnchor = null
|
|
2063
2301
|
|
|
2064
2302
|
if (form === 'worktree') {
|
|
2065
2303
|
// —— 形态 A:worktree 存活,整链复用(锚点优先级 baselineCommit>actualBaseHash>baseHash、
|
|
@@ -2067,22 +2305,36 @@ function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, changeName })
|
|
|
2067
2305
|
const files = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase })
|
|
2068
2306
|
if (files === null) {
|
|
2069
2307
|
return { ok: false, form, files: [], sources, foreignExcluded,
|
|
2070
|
-
degradedReason: 'worktree 锚点 diff 与主仓 fallback 均失败(git 不可用 / 非仓库)' }
|
|
2308
|
+
degradedReason: 'worktree 锚点 diff 与主仓 fallback 均失败(git 不可用 / 非仓库)', baseAnchor: null }
|
|
2071
2309
|
}
|
|
2310
|
+
// baseAnchor:读同一份 worktree meta.json 锚点(baselineCommit>actualBaseHash>baseHash,
|
|
2311
|
+
// resolveMainChangedFiles 同口径;解析失败/三锚全缺 → null)
|
|
2312
|
+
try {
|
|
2313
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
2314
|
+
baseAnchor = meta?.baselineCommit || meta?.actualBaseHash || meta?.baseHash || null
|
|
2315
|
+
} catch {}
|
|
2072
2316
|
sources.push('worktree:diff-base..HEAD', 'worktree:status-porcelain(uncommitted)')
|
|
2073
2317
|
for (const f of files) union.add(normalizeReconcilePath(f))
|
|
2074
2318
|
} else {
|
|
2075
2319
|
// —— 形态 B:post-apply 自取三源 ——
|
|
2076
2320
|
let diffOk = false
|
|
2077
2321
|
let statusOk = false
|
|
2078
|
-
// B1 merge-base 锚定 diff(分支/merge-base 不可得时静默省略该源,不算 git
|
|
2322
|
+
// B1 merge-base 锚定 diff(分支/merge-base 不可得时静默省略该源,不算 git 失败)。
|
|
2323
|
+
// 分支删除后回退审计 tag(quick-df1fed77):apply+cleanup 删分支时 worktree.js:1065 打
|
|
2324
|
+
// sillyspec-audit/<branch> tag 锚定 tip(gc 安全)——tag ref 可继续算 merge-base,baseAnchor
|
|
2325
|
+
// 与 B1 diff 源在归档/已提交形态下恢复完整(否则恒 null,行数只剩 HEAD 未提交窗口口径)。
|
|
2079
2326
|
const branch = 'sillyspec/' + changeName
|
|
2080
2327
|
const branchHash = gitQuiet(cwd, ['rev-parse', '--verify', '--quiet', branch + '^{commit}'], { timeout: 30 * 1000 })
|
|
2081
|
-
|
|
2328
|
+
const auditTag = 'sillyspec-audit/' + branch
|
|
2329
|
+
const diffRef = (typeof branchHash === 'string' && branchHash.trim())
|
|
2330
|
+
? branch
|
|
2331
|
+
: (gitQuiet(cwd, ['rev-parse', '--verify', '--quiet', auditTag + '^{commit}'], { timeout: 30 * 1000 }) ? auditTag : null)
|
|
2332
|
+
if (diffRef) {
|
|
2082
2333
|
// 形态 B 定义下 meta 已删,baseBranch 无从读 → 缺省 'main'(run/prompt.js:764 同缺省);
|
|
2083
2334
|
// 主分支叫 master 等仓库 merge-base 失败 → 与分支不存在同处置(省略该源)
|
|
2084
|
-
const mergeBase = gitQuiet(cwd, ['merge-base', 'main',
|
|
2335
|
+
const mergeBase = gitQuiet(cwd, ['merge-base', 'main', diffRef], { timeout: 30 * 1000 })
|
|
2085
2336
|
if (typeof mergeBase === 'string' && mergeBase.trim()) {
|
|
2337
|
+
baseAnchor = mergeBase.trim()
|
|
2086
2338
|
const files = runGitDiffNameOnly(cwd, mergeBase.trim())
|
|
2087
2339
|
if (files !== null) {
|
|
2088
2340
|
diffOk = true
|
|
@@ -2117,12 +2369,12 @@ function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, changeName })
|
|
|
2117
2369
|
}
|
|
2118
2370
|
if (!diffOk && !statusOk) {
|
|
2119
2371
|
return { ok: false, form, files: [], sources, foreignExcluded,
|
|
2120
|
-
degradedReason: 'merge-base 锚定 diff 未得且主仓 status 失败(git 不可用 / 非仓库 / 无锚点分支)' }
|
|
2372
|
+
degradedReason: 'merge-base 锚定 diff 未得且主仓 status 失败(git 不可用 / 非仓库 / 无锚点分支)', baseAnchor: null }
|
|
2121
2373
|
}
|
|
2122
2374
|
}
|
|
2123
2375
|
|
|
2124
2376
|
const files = [...new Set(filterDeliverableFiles([...union]).filter(Boolean))].sort()
|
|
2125
|
-
return { ok: true, form, files, sources, foreignExcluded, degradedReason: null }
|
|
2377
|
+
return { ok: true, form, files, sources, foreignExcluded, degradedReason: null, baseAnchor }
|
|
2126
2378
|
}
|
|
2127
2379
|
|
|
2128
2380
|
/**
|
|
@@ -2255,24 +2507,35 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2255
2507
|
}
|
|
2256
2508
|
|
|
2257
2509
|
// —— 三类差集 ——
|
|
2258
|
-
|
|
2510
|
+
// 对账键(2026-09-08 用户反馈⑥:计划文件名 vs 实测文件名的字面差脆):声明侧 task 卡是
|
|
2511
|
+
// agent 手写(大小写随手、尾部注记、./ 前缀残留),actual 侧是 git 机器产出——字面相等
|
|
2512
|
+
// 判定下这些形态差全部落②类假红(missing_declared,ERROR 态阻断 verify)。统一 canonical
|
|
2513
|
+
// key 再比对:normalizeReviewChangedFile 归一(./ 前缀/尾部注记/行内注释/反斜杠)+
|
|
2514
|
+
// win32/darwin 大小写折叠(默认文件系统大小写不敏感,两形指同一文件;Linux 保持敏感)。
|
|
2515
|
+
// 报告侧仍用原始字面串(用户按自己写的名字找到声明处)。
|
|
2516
|
+
const foldCase = process.platform === 'win32' || process.platform === 'darwin';
|
|
2517
|
+
const pathKey = (p) => {
|
|
2518
|
+
const n = normalizeReviewChangedFile(p);
|
|
2519
|
+
return foldCase ? n.toLowerCase() : n;
|
|
2520
|
+
};
|
|
2521
|
+
const actualKeySet = new Set(actual.files.map(pathKey));
|
|
2259
2522
|
const declaredPaths = [...new Set(decl.declarations.map(d => d.path))].sort()
|
|
2260
|
-
const
|
|
2523
|
+
const declaredKeySet = new Set(declaredPaths.map(pathKey))
|
|
2261
2524
|
// ①交集(计数进 evidence):唯一路径口径(多卡声明同文件只计一次)
|
|
2262
2525
|
for (const path of declaredPaths) {
|
|
2263
|
-
if (
|
|
2526
|
+
if (actualKeySet.has(pathKey(path))) matched.push(path)
|
|
2264
2527
|
}
|
|
2265
2528
|
// ②声明没做(计划落空,ERROR):逐卡逐条报 task-NN + path;NEW: 声明的取剥前缀 path、带
|
|
2266
|
-
// isNew
|
|
2529
|
+
// isNew 标("新建文件没建"与"存量文件没动"的修复指引不同)
|
|
2267
2530
|
for (const d of decl.declarations) {
|
|
2268
|
-
if (!
|
|
2531
|
+
if (!actualKeySet.has(pathKey(d.path))) {
|
|
2269
2532
|
missing.push(d.isNew ? { task: d.task, path: d.path, isNew: true } : { task: d.task, path: d.path })
|
|
2270
2533
|
}
|
|
2271
2534
|
}
|
|
2272
2535
|
// ③做了没声明(scope creep,WARNING):actual − 声明集;suspectTask 尽力归因仅报告(D-002)
|
|
2273
|
-
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !
|
|
2536
|
+
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !declaredKeySet.has(pathKey(p))))
|
|
2274
2537
|
for (const path of actual.files) {
|
|
2275
|
-
if (
|
|
2538
|
+
if (declaredKeySet.has(pathKey(path))) continue
|
|
2276
2539
|
const s = suspect.get(normalizeReviewChangedFile(path))
|
|
2277
2540
|
undeclared.push(s ? { path, suspectTask: s } : { path })
|
|
2278
2541
|
}
|
|
@@ -2558,7 +2821,62 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
|
|
|
2558
2821
|
note: 'contract gap(missing backend)行数与重跑不符(探针5 扫描根/缓存口径环境敏感,WARNING 不阻断)' })
|
|
2559
2822
|
}
|
|
2560
2823
|
|
|
2561
|
-
|
|
2824
|
+
// —— facts 基线对比(2026-09-08-ir-verify-facts FR-05 / task-05):重跑指标 vs
|
|
2825
|
+
// verify-facts.json probes 快照(init 时点)。md 锚点对账查「正文没被改」,本维度查
|
|
2826
|
+
// 「md 被手改对齐新代码后 facts 底稿过期」(P3d 数据源保鲜)。分级沿用现实现口径:
|
|
2827
|
+
// probe1/6=ERROR、probe3/5=WARNING;probe6 继承 HEAD-advance 降级(R-06)。——
|
|
2828
|
+
const factsSnapshot = readVerifyFacts(changeDir)
|
|
2829
|
+
let factsConsistency = null
|
|
2830
|
+
if (factsSnapshot && factsSnapshot.probes) {
|
|
2831
|
+
const fm = []
|
|
2832
|
+
const adv = detectHeadAdvanceSinceFacts(cwd, factsSnapshot)
|
|
2833
|
+
const snap1 = (((factsSnapshot.probes.probe1 || {}).metrics) || {}).matches
|
|
2834
|
+
if (typeof snap1 === 'number' && snap1 !== curProbe1Hits) {
|
|
2835
|
+
fm.push({ probe: 'probe1', snapshot: snap1, rerun: curProbe1Hits, severity: 'error',
|
|
2836
|
+
note: 'facts 快照命中数与重跑不符——md 疑已手改对齐新代码而 facts 底稿过期(修复:verify-probes --init 刷新 facts + 按新结果同步 md 探针段)' })
|
|
2837
|
+
}
|
|
2838
|
+
const snap6 = (((factsSnapshot.probes.probe6 || {}).metrics) || {}).deletions
|
|
2839
|
+
if (!curProbe6.unavailable && typeof snap6 === 'number' && snap6 !== curProbe6Deletions && !adv) {
|
|
2840
|
+
// adv(HEAD 前移)时豁免:同一漂移已由 md 锚点维度的 probe6 WARNING 报出(R-06 继承,
|
|
2841
|
+
// 双维度重复报同一信号是噪音);非 adv 的不符 = facts 底稿被手改对齐——ERROR
|
|
2842
|
+
fm.push({ probe: 'probe6', snapshot: snap6, rerun: curProbe6Deletions, severity: 'error',
|
|
2843
|
+
note: 'facts 快照删除数与重跑不符——底稿疑似过期(修复:verify-probes --init 刷新 facts + 同步 md 探针段)' })
|
|
2844
|
+
}
|
|
2845
|
+
const snap3 = (((factsSnapshot.probes.probe3 || {}).metrics) || {}).hasTest
|
|
2846
|
+
if (typeof snap3 === 'number' && snap3 !== curProbe3HasTest) {
|
|
2847
|
+
fm.push({ probe: 'probe3', snapshot: snap3, rerun: curProbe3HasTest, severity: 'warning',
|
|
2848
|
+
note: 'facts 快照 hasTest 数与重跑不符(测试布局环境敏感,WARNING)' })
|
|
2849
|
+
}
|
|
2850
|
+
const snap5 = (((factsSnapshot.probes.probe5 || {}).metrics) || {}).backendEndpoints
|
|
2851
|
+
const cur5 = (current.probe5 || {}).backendCount
|
|
2852
|
+
if (typeof snap5 === 'number' && typeof cur5 === 'number' && snap5 !== cur5) {
|
|
2853
|
+
fm.push({ probe: 'probe5', snapshot: snap5, rerun: cur5, severity: 'warning',
|
|
2854
|
+
note: 'facts 快照 backend 端点数与重跑不符(扫描根口径环境敏感,WARNING)' })
|
|
2855
|
+
}
|
|
2856
|
+
factsConsistency = {
|
|
2857
|
+
checked: fm.length === 0 ? ['probe1', 'probe3', 'probe5', 'probe6'] : [...new Set(fm.map(x => x.probe))],
|
|
2858
|
+
verdict: fm.length === 0 ? 'match' : 'mismatch',
|
|
2859
|
+
detail: fm.length === 0 ? null : fm,
|
|
2860
|
+
}
|
|
2861
|
+
for (const x of fm) {
|
|
2862
|
+
mismatches.push({ probe: `facts:${x.probe}`, expected: x.rerun, actual: x.snapshot, severity: x.severity, note: x.note })
|
|
2863
|
+
}
|
|
2864
|
+
// 固化 factsConsistency(fail-soft;不动 probes 快照语义——probes 归 --init 所有)
|
|
2865
|
+
try {
|
|
2866
|
+
const factsPath2 = join(changeDir, 'verify-facts.json')
|
|
2867
|
+
const f2 = JSON.parse(readFileSync(factsPath2, 'utf8'))
|
|
2868
|
+
f2.factsConsistency = factsConsistency
|
|
2869
|
+
writeFileSync(factsPath2, JSON.stringify(f2, null, 2) + '\n')
|
|
2870
|
+
} catch { /* 固化失败不阻断对账 */ }
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
if (mismatches.length === 0) {
|
|
2874
|
+
const okResult = finish('ok', null, [], null)
|
|
2875
|
+
okResult.factsConsistency = factsConsistency
|
|
2876
|
+
return okResult
|
|
2877
|
+
}
|
|
2562
2878
|
const severity = mismatches.some(m => m.severity === 'error') ? 'error' : 'warning'
|
|
2563
|
-
|
|
2879
|
+
const mismatchResult = finish('mismatch', severity, mismatches, null)
|
|
2880
|
+
mismatchResult.factsConsistency = factsConsistency
|
|
2881
|
+
return mismatchResult
|
|
2564
2882
|
}
|