sillyspec 3.28.3 → 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 +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 +24 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/src/archive-delta.js +82 -0
- 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 +3 -1
- package/src/docs-check.js +34 -0
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +181 -6
- package/src/module-impact.js +77 -12
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +16 -0
- package/src/progress.js +62 -1
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +10 -2
- package/src/run/command.js +30 -9
- package/src/run/complete-handlers.js +152 -6
- package/src/run/complete.js +45 -0
- package/src/run/gates.js +94 -6
- package/src/run/prompt.js +96 -5
- package/src/run/shared.js +74 -6
- package/src/run/stage.js +35 -0
- package/src/runtime-hygiene.js +84 -0
- 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.js +2 -8
- package/src/stages/doctor.js +41 -515
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +5 -26
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +10 -6
- package/src/sync-noise.js +158 -0
- package/src/sync.js +42 -17
- package/src/task-review.js +25 -1
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +257 -19
- 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/src/verify-postcheck.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { execSync, spawnSync } from 'child_process'
|
|
26
26
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
27
27
|
import { gitQuiet } from './git-helper.js'
|
|
28
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs'
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
29
29
|
import { join } from 'path'
|
|
30
30
|
import { verifyApiParity } from './contract-matrix.js'
|
|
31
31
|
import { parseFileChangeListDetailed, pathMatches } from './change-list.js'
|
|
@@ -37,6 +37,7 @@ import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
|
|
|
37
37
|
// fs/path/git-helper/change-list/plan-postcheck/contract-matrix/foreign-declared/run-shared,
|
|
38
38
|
// 不反向依赖本模块,此处直连不引入循环
|
|
39
39
|
import { runVerifyProbes } from './verify-probes.js'
|
|
40
|
+
import { parseEvidenceSlots, classifyVerifiedFile } from './verify-facts-schema.js'
|
|
40
41
|
|
|
41
42
|
// 测试命令最长执行时间;超时视为失败(防止 CLI 被挂起的测试卡死)
|
|
42
43
|
const TEST_TIMEOUT_MS = Number(process.env.SILLYSPEC_TEST_TIMEOUT_MS) || 10 * 60 * 1000
|
|
@@ -115,6 +116,36 @@ function extractLintCommand(yamlText) {
|
|
|
115
116
|
return null
|
|
116
117
|
}
|
|
117
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
|
+
|
|
118
149
|
/**
|
|
119
150
|
* verify --done 实测跑 local.yaml commands.lint(2026-08-21 审查 CLI-1)。
|
|
120
151
|
* 此前 lint 全靠 agent 自跑自报("我跑过 lint 了"纯口头),与 test 侧的对账不对称——
|
|
@@ -162,18 +193,36 @@ export function runVerifyLintCheck({ cwd, specBase }) {
|
|
|
162
193
|
}
|
|
163
194
|
const durationMs = Date.now() - startedAt
|
|
164
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 } })
|
|
165
199
|
|
|
166
200
|
return {
|
|
167
|
-
status
|
|
201
|
+
status,
|
|
168
202
|
command,
|
|
169
203
|
exitCode,
|
|
170
204
|
durationMs,
|
|
171
205
|
outputTail,
|
|
172
|
-
reason:
|
|
206
|
+
reason: finalReason,
|
|
207
|
+
tally, // { failedRuns, totalRuns } | null——观察期失败率,升级硬门的决策依据
|
|
173
208
|
}
|
|
174
209
|
}
|
|
175
210
|
|
|
176
|
-
/**
|
|
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
|
+
*/
|
|
177
226
|
export function printVerifyLintCheck(result) {
|
|
178
227
|
if (result.status === 'skipped') {
|
|
179
228
|
console.warn(`\n⚠️ Verify lint 实测跳过:${result.reason}`)
|
|
@@ -183,8 +232,22 @@ export function printVerifyLintCheck(result) {
|
|
|
183
232
|
console.log(`\n✅ Verify lint 实测通过:\`${result.command}\` 退出码 0(${(result.durationMs / 1000).toFixed(1)}s)`)
|
|
184
233
|
return
|
|
185
234
|
}
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
251
|
if (result.outputTail) {
|
|
189
252
|
const tail = result.outputTail.split('\n').slice(-15).join('\n')
|
|
190
253
|
console.error(' 输出(末尾):')
|
|
@@ -1620,6 +1683,9 @@ export function writeRunResult({ specBase, changeName, result, extra = {} }) {
|
|
|
1620
1683
|
...extra,
|
|
1621
1684
|
}, null, 2) + '\n')
|
|
1622
1685
|
result.resultPath = resultPath
|
|
1686
|
+
// verify-runs 回收不在此处(ql-20260908-005 已定分野):变更归属类证据走归档时精确
|
|
1687
|
+
// 回收(pruneArchivedChangeRuntime 按目录内 JSON change 字段归属);写入侧滚动是
|
|
1688
|
+
// 启发式,不进证据类目录。
|
|
1623
1689
|
} catch (e) {
|
|
1624
1690
|
console.warn(`⚠️ verify 实测结果落盘失败: ${e.message}`)
|
|
1625
1691
|
}
|
|
@@ -1877,7 +1943,7 @@ export function printVerifyDeletionCheck(result) {
|
|
|
1877
1943
|
* @param {{cwd:string, specBase:string, changeName?:string}} args
|
|
1878
1944
|
* @returns {{status:'skipped'|'passed'|'warning', items:Array, unacknowledged:Array, summary:string, reason:string|null}}
|
|
1879
1945
|
*/
|
|
1880
|
-
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null }) {
|
|
1946
|
+
export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = null, verifyStartAt = null }) {
|
|
1881
1947
|
if (!changeName) {
|
|
1882
1948
|
return { status: 'skipped', items: [], unacknowledged: [], summary: '', reason: '无 changeName(quick 等无关联变更场景),evidence 对账跳过' }
|
|
1883
1949
|
}
|
|
@@ -1898,6 +1964,13 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1898
1964
|
const verifyResultPath = join(specBase, 'changes', changeName, 'verify-result.md')
|
|
1899
1965
|
const report = existsSync(verifyResultPath) ? readFileSync(verifyResultPath, 'utf8') : ''
|
|
1900
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
|
+
|
|
1901
1974
|
const unacknowledged = []
|
|
1902
1975
|
for (const item of items) {
|
|
1903
1976
|
const task = item && item.task
|
|
@@ -1912,16 +1985,115 @@ export function runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName = nul
|
|
|
1912
1985
|
}
|
|
1913
1986
|
|
|
1914
1987
|
if (unacknowledged.length > 0) {
|
|
1915
|
-
return { status: 'warning', items, unacknowledged,
|
|
1988
|
+
return { status: 'warning', items, unacknowledged, legacy: true,
|
|
1916
1989
|
summary: `${items.length} 个 cannot_verify evidence 任务中 ${unacknowledged.length} 个未在 verify-result.md 体现`, reason: null }
|
|
1917
1990
|
}
|
|
1918
|
-
return { status: 'passed', items, unacknowledged: [],
|
|
1991
|
+
return { status: 'passed', items, unacknowledged: [], legacy: true,
|
|
1919
1992
|
summary: `${items.length} 个 cannot_verify evidence 任务均在 verify-result.md 体现(满足度由 agent 自报告)`, reason: null }
|
|
1920
1993
|
}
|
|
1921
1994
|
|
|
1922
|
-
/**
|
|
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 不阻断) */
|
|
1923
2094
|
export function printVerifyRequiredEvidenceCheck(result) {
|
|
1924
2095
|
if (result.status === 'skipped') return // 静默:无 evidence 文件 / 无 changeName / 解析失败
|
|
2096
|
+
if (result.status === 'blocked') return // 阻断明细由 gates 接线侧统一输出(本函数不重复打印)
|
|
1925
2097
|
if (result.status === 'passed') {
|
|
1926
2098
|
console.log(`\n✅ required-evidence 对账通过:${result.summary}`)
|
|
1927
2099
|
return
|
|
@@ -2255,24 +2427,35 @@ export function reconcileTargetFiles({ cwd, specBase = null, changeName = null,
|
|
|
2255
2427
|
}
|
|
2256
2428
|
|
|
2257
2429
|
// —— 三类差集 ——
|
|
2258
|
-
|
|
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));
|
|
2259
2442
|
const declaredPaths = [...new Set(decl.declarations.map(d => d.path))].sort()
|
|
2260
|
-
const
|
|
2443
|
+
const declaredKeySet = new Set(declaredPaths.map(pathKey))
|
|
2261
2444
|
// ①交集(计数进 evidence):唯一路径口径(多卡声明同文件只计一次)
|
|
2262
2445
|
for (const path of declaredPaths) {
|
|
2263
|
-
if (
|
|
2446
|
+
if (actualKeySet.has(pathKey(path))) matched.push(path)
|
|
2264
2447
|
}
|
|
2265
2448
|
// ②声明没做(计划落空,ERROR):逐卡逐条报 task-NN + path;NEW: 声明的取剥前缀 path、带
|
|
2266
|
-
// isNew
|
|
2449
|
+
// isNew 标("新建文件没建"与"存量文件没动"的修复指引不同)
|
|
2267
2450
|
for (const d of decl.declarations) {
|
|
2268
|
-
if (!
|
|
2451
|
+
if (!actualKeySet.has(pathKey(d.path))) {
|
|
2269
2452
|
missing.push(d.isNew ? { task: d.task, path: d.path, isNew: true } : { task: d.task, path: d.path })
|
|
2270
2453
|
}
|
|
2271
2454
|
}
|
|
2272
2455
|
// ③做了没声明(scope creep,WARNING):actual − 声明集;suspectTask 尽力归因仅报告(D-002)
|
|
2273
|
-
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !
|
|
2456
|
+
const suspect = attributeSuspectTasks(rt, changeName, actual.files.filter(p => !declaredKeySet.has(pathKey(p))))
|
|
2274
2457
|
for (const path of actual.files) {
|
|
2275
|
-
if (
|
|
2458
|
+
if (declaredKeySet.has(pathKey(path))) continue
|
|
2276
2459
|
const s = suspect.get(normalizeReviewChangedFile(path))
|
|
2277
2460
|
undeclared.push(s ? { path, suspectTask: s } : { path })
|
|
2278
2461
|
}
|
|
@@ -2558,7 +2741,62 @@ export function checkProbeConsistency({ cwd, specBase = null, changeName = null,
|
|
|
2558
2741
|
note: 'contract gap(missing backend)行数与重跑不符(探针5 扫描根/缓存口径环境敏感,WARNING 不阻断)' })
|
|
2559
2742
|
}
|
|
2560
2743
|
|
|
2561
|
-
|
|
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
|
+
}
|
|
2562
2798
|
const severity = mismatches.some(m => m.severity === 'error') ? 'error' : 'warning'
|
|
2563
|
-
|
|
2799
|
+
const mismatchResult = finish('mismatch', severity, mismatches, null)
|
|
2800
|
+
mismatchResult.factsConsistency = factsConsistency
|
|
2801
|
+
return mismatchResult
|
|
2564
2802
|
}
|
package/src/verify-probes.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
|
|
12
12
|
*
|
|
13
13
|
* verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
|
|
15
|
+
* 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
|
|
16
16
|
* P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
|
|
17
17
|
* ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
|
|
18
18
|
* 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
|
|
21
21
|
import { join, dirname, basename } from 'path'
|
|
22
22
|
import { gitQuiet } from './git-helper.js'
|
|
23
|
+
import {
|
|
24
|
+
FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
|
|
25
|
+
} from './verify-facts-schema.js'
|
|
23
26
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
24
27
|
import { parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
25
28
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
@@ -310,8 +313,11 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
310
313
|
const p3 = (result && result.probe3) || {}
|
|
311
314
|
const p5 = (result && result.probe5) || {}
|
|
312
315
|
const p6 = (result && result.probe6) || {}
|
|
316
|
+
// v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
|
|
317
|
+
// runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
|
|
318
|
+
// 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
|
|
313
319
|
return {
|
|
314
|
-
schemaVersion:
|
|
320
|
+
schemaVersion: 2,
|
|
315
321
|
change: changeName,
|
|
316
322
|
generatedAt: now || new Date().toISOString(),
|
|
317
323
|
probes: {
|
|
@@ -350,37 +356,160 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
|
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
/**
|
|
353
|
-
* verify-probes --init 落盘 verify-facts.json(CLI
|
|
354
|
-
*
|
|
355
|
-
*
|
|
359
|
+
* verify-probes --init 落盘 verify-facts.json(CLI 全权写)。v2(2026-09-08-ir-verify-facts)改
|
|
360
|
+
* 分段合并:re-init 刷新机器段(probes/generatedAt),保留既有 slot-backfill/实测固化段
|
|
361
|
+
* (conclusion/tests/requiredEvidence/runtimeEvidence)——旧「无条件覆盖」会抹掉 --done 回填数据
|
|
362
|
+
* (设计审查 P1-5)。agent 勿手改:复跑审计的对照快照,防篡改基准是 verify-result.md 正文。
|
|
356
363
|
* @param {string} changeDir 变更目录(spec 根下 changes/<name>)
|
|
357
364
|
* @param {object} result runVerifyProbes 返回值
|
|
358
365
|
* @param {string} changeName 变更名(统一命令行呈现)
|
|
359
366
|
* @returns {{ facts: object, path: string }}
|
|
360
367
|
*/
|
|
361
368
|
export function writeVerifyFacts(changeDir, result, changeName) {
|
|
362
|
-
const
|
|
369
|
+
const fresh = buildVerifyFacts(result, { changeName })
|
|
363
370
|
const factsPath = join(changeDir, 'verify-facts.json')
|
|
371
|
+
let facts = fresh
|
|
372
|
+
try {
|
|
373
|
+
const prev = JSON.parse(readFileSync(factsPath, 'utf8'))
|
|
374
|
+
if (prev && prev.schemaVersion === FACTS_SCHEMA_VERSION) {
|
|
375
|
+
facts = {
|
|
376
|
+
...fresh,
|
|
377
|
+
conclusion: prev.conclusion,
|
|
378
|
+
tests: prev.tests,
|
|
379
|
+
requiredEvidence: prev.requiredEvidence,
|
|
380
|
+
runtimeEvidence: prev.runtimeEvidence,
|
|
381
|
+
factsConsistency: prev.factsConsistency, // 保留至上次对比结论(下次 --done 重算覆盖)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch { /* 首次落盘/v1/损坏 → 全新快照(v1 无固化段,直接升 v2) */ }
|
|
364
385
|
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
365
|
-
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(CLI 全权写,勿手改)`)
|
|
386
|
+
console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(v2 分段合并,固化段保留;CLI 全权写,勿手改)`)
|
|
366
387
|
return { facts, path: factsPath }
|
|
367
388
|
}
|
|
368
389
|
|
|
390
|
+
/**
|
|
391
|
+
* --done 回填(D-001@v2 slot-backfill):结论枚举槽 + 证据/回执槽解析固化进 facts;
|
|
392
|
+
* testCheckResult 提供时回填 tests 段(实测在 runValidators 之后 → gates 二次回填,FR-03 次序)。
|
|
393
|
+
* 机器段(probes/factsConsistency)不经本函数(writeVerifyFacts / checkProbeConsistency 所有)。
|
|
394
|
+
* fail-soft:facts 缺失时原地升 v2(probes 保留或空);落盘失败 warn 不抛。
|
|
395
|
+
* @param {string} factsPath verify-facts.json 路径
|
|
396
|
+
* @param {{ verifyMd: string, testCheckResult?: object|null }} opts
|
|
397
|
+
* @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
|
|
398
|
+
*/
|
|
399
|
+
export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
|
|
400
|
+
let facts = null
|
|
401
|
+
try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
|
|
402
|
+
// 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
|
|
403
|
+
// (未跑过 --init)若被回填凭空建出 facts.json,checkProbeConsistency 的「有 facts 无探针
|
|
404
|
+
// 子节 → error」判别会把存量兼容 skip 误升 error(run-complete-step-verify e2e 抓出)。
|
|
405
|
+
// 底稿创建唯一入口是 verify-probes --init。
|
|
406
|
+
if (!facts) {
|
|
407
|
+
return { facts: null, skipped: true, conclusion: null, evidenceCount: 0, receiptCount: 0, testsBackfilled: false, reason: '无 verify-facts.json(未跑 --init),回填跳过——底稿创建唯一入口是 --init' }
|
|
408
|
+
}
|
|
409
|
+
if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
|
|
410
|
+
facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
|
|
411
|
+
}
|
|
412
|
+
const slots = parseEvidenceSlots(verifyMd || '')
|
|
413
|
+
// conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
|
|
414
|
+
// (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
|
|
415
|
+
const conclusionSlot = conclusion || null
|
|
416
|
+
if (conclusionSlot) facts.conclusion = conclusionSlot
|
|
417
|
+
if (slots.hasEvidenceSlot && slots.requiredEvidence.length > 0) {
|
|
418
|
+
facts.requiredEvidence = slots.requiredEvidence.map(e => ({
|
|
419
|
+
task: e.task, status: e.status, verifiedFiles: e.verifiedFiles,
|
|
420
|
+
...(e.exempt ? { exempt: true, exemptionReason: e.exemptionReason } : {}),
|
|
421
|
+
}))
|
|
422
|
+
}
|
|
423
|
+
if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
|
|
424
|
+
facts.runtimeEvidence = slots.runtimeEvidence
|
|
425
|
+
}
|
|
426
|
+
let testsBackfilled = false
|
|
427
|
+
if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
|
|
428
|
+
facts.tests = {
|
|
429
|
+
command: testCheckResult.command || null,
|
|
430
|
+
exitCode: typeof testCheckResult.exitCode === 'number' ? testCheckResult.exitCode : null,
|
|
431
|
+
strategy: testCheckResult.mode || testCheckResult.strategy || null,
|
|
432
|
+
failedRemaining: testCheckResult.failureNames || testCheckResult.failedTests || [],
|
|
433
|
+
passedAt: new Date().toISOString(),
|
|
434
|
+
}
|
|
435
|
+
testsBackfilled = true
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
|
|
439
|
+
} catch (e) {
|
|
440
|
+
console.warn(`⚠️ facts 回填落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
|
|
441
|
+
}
|
|
442
|
+
return { facts, conclusion: conclusionSlot, evidenceCount: slots.requiredEvidence.length, receiptCount: slots.runtimeEvidence.length, testsBackfilled }
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* md 槽段缺失补齐(--init 段落级补齐,2026-09-08-ir-verify-facts R-02):已存在的
|
|
447
|
+
* verify-result.md 缺「证据账/集成验证回执」槽段时仅追加骨架(不触碰既有正文),幂等二跑零改动。
|
|
448
|
+
* @param {string} mdPath verify-result.md 路径
|
|
449
|
+
* @param {Array<{task:string,evidence:string[]}>} [requiredEvidenceItems] verify-required-evidence.json items(预填 task 行)
|
|
450
|
+
* @returns {{ added: string[] }} 追加的槽段标题列表
|
|
451
|
+
*/
|
|
452
|
+
export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = []) {
|
|
453
|
+
let text = ''
|
|
454
|
+
try { text = readFileSync(mdPath, 'utf8') } catch { return { added: [] } }
|
|
455
|
+
const normalized = text.replace(/\r\n/g, '\n')
|
|
456
|
+
const added = []
|
|
457
|
+
const blocks = []
|
|
458
|
+
if (!normalized.includes(EVIDENCE_SLOT_HEADING)) {
|
|
459
|
+
const lines = [EVIDENCE_SLOT_HEADING, '[层:人工判断——CLI 核验]', '', '<!-- 无 cannot_verify 任务时本节写「无」 -->']
|
|
460
|
+
if (requiredEvidenceItems.length > 0) {
|
|
461
|
+
for (const it of requiredEvidenceItems) {
|
|
462
|
+
lines.push(`- ${it.task}: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)`)
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
lines.push('无(本次变更无 cannot_verify 任务)')
|
|
466
|
+
}
|
|
467
|
+
lines.push('')
|
|
468
|
+
blocks.push(lines.join('\n'))
|
|
469
|
+
added.push(EVIDENCE_SLOT_HEADING)
|
|
470
|
+
}
|
|
471
|
+
if (!normalized.includes(RECEIPT_SLOT_HEADING)) {
|
|
472
|
+
blocks.push([
|
|
473
|
+
RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
|
|
474
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
475
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
476
|
+
'',
|
|
477
|
+
].join('\n'))
|
|
478
|
+
added.push(RECEIPT_SLOT_HEADING)
|
|
479
|
+
}
|
|
480
|
+
if (added.length > 0) {
|
|
481
|
+
writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + blocks.join('\n') + '\n')
|
|
482
|
+
}
|
|
483
|
+
return { added }
|
|
484
|
+
}
|
|
485
|
+
|
|
369
486
|
/**
|
|
370
487
|
* 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
|
|
371
|
-
*
|
|
488
|
+
* 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
|
|
489
|
+
* 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
|
|
490
|
+
* `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
|
|
372
491
|
* 章节标题行末尾带 claims 层标注(P3b):探针结果=可复跑探针 / 测试结果=确定性检查 / 其余=
|
|
373
|
-
*
|
|
492
|
+
* 人工判断——纯渲染层后缀,不新增行;结论解析槽优先于标题关键词窗口(存量兼容)。
|
|
374
493
|
* @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
|
|
375
494
|
*/
|
|
376
495
|
export function generateVerifyResultSkeleton(result) {
|
|
377
496
|
const L = [
|
|
378
497
|
'# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
|
|
379
498
|
'',
|
|
380
|
-
'> 探针结果已机械预填;其余章节把 `<!--TODO-->`
|
|
381
|
-
'>
|
|
499
|
+
'> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
|
|
500
|
+
'> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
|
|
501
|
+
'',
|
|
502
|
+
'## 结论 [层:人工判断]',
|
|
503
|
+
'',
|
|
504
|
+
'结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
|
|
505
|
+
'',
|
|
506
|
+
'## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
|
|
507
|
+
'<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
|
|
508
|
+
'- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
|
|
382
509
|
'',
|
|
383
|
-
'##
|
|
510
|
+
'## 集成验证回执 [层:自述声明——CLI 一致性校验]',
|
|
511
|
+
'<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
|
|
512
|
+
'- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
|
|
384
513
|
'',
|
|
385
514
|
'## 任务完成度 [层:人工判断]',
|
|
386
515
|
'<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
|
package/src/workflow.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
|
|
13
13
|
import { join, resolve, basename } from 'path'
|
|
14
14
|
import jsYaml from 'js-yaml'
|
|
15
|
+
import { pruneTimestampedEntries } from './runtime-hygiene.js'
|
|
15
16
|
import { WORKFLOW_STATUS } from './constants.js'
|
|
16
17
|
import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
|
|
17
18
|
import { collectInvalidDocRefs } from './docs-check.js'
|
|
@@ -626,6 +627,11 @@ export function saveWorkflowRun(result, options = {}) {
|
|
|
626
627
|
|
|
627
628
|
try {
|
|
628
629
|
writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
|
|
630
|
+
// 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):workflow-runs/ 只写不收,
|
|
631
|
+
// 实证本仓 21 份累积。文件名以零填充时间戳开头,name 字典序 == 时间序。fail-open 静默。
|
|
632
|
+
try {
|
|
633
|
+
pruneTimestampedEntries({ dir: runDir, keep: 30 })
|
|
634
|
+
} catch { /* 卫生动作 fail-open */ }
|
|
629
635
|
return filepath
|
|
630
636
|
} catch (e) {
|
|
631
637
|
console.warn('⚠️ 保存 workflow run 失败:', e.message)
|