sillyspec 3.32.1 → 3.32.3
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-explore/SKILL.md +6 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -0
- package/package.json +2 -2
- package/src/cross-repo-reconcile.js +33 -0
- package/src/doctor-diagnostics.js +84 -4
- package/src/flow-parity.js +19 -26
- package/src/flow-review.js +4 -0
- package/src/flow.js +17 -10
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +23 -11
- package/src/init.js +5 -1
- package/src/knowledge-graph.js +918 -0
- package/src/knowledge-match.js +11 -6
- package/src/knowledge-vector.js +17 -3
- package/src/module-impact.js +3 -1
- package/src/progress/stage-machine.js +8 -1
- package/src/run/code-face-key.js +84 -0
- package/src/run/complete.js +32 -9
- package/src/run/gate-snapshot.js +19 -0
- package/src/run/gates.js +109 -67
- package/src/run/green-cache.js +11 -19
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/sentinel-assertions.js +27 -2
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/stages/explore.js +5 -1
- package/src/stages/knowledge.js +11 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/watcher.js +6 -2
- package/src/wt-commit.js +82 -3
package/src/knowledge-match.js
CHANGED
|
@@ -62,7 +62,7 @@ function isDecisionRoute(entry) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const DECISION_HEADER_RE = /^##\s+(D-\d+@v\d+)\s*(.*)$/
|
|
65
|
-
const DECISION_FIELD_RE = /^(
|
|
65
|
+
const DECISION_FIELD_RE = /^(状态|文件|理由|否决理由|复潮条件|supersedes)\s*[::]\s*(.*)$/
|
|
66
66
|
// 「锚点:」单独标签精确匹配读入(仿 docs-check.js parseDecisionEntries 先例)——绝不并入
|
|
67
67
|
// DECISION_FIELD_RE:其 else-if 链末尾的 reason 回填(else if (!cur.reason))会把锚点值
|
|
68
68
|
// 误吞进 reason(Grill X-002 隐性回归)。
|
|
@@ -87,15 +87,19 @@ const ANCHOR_FILE_TOKEN_RE = /[\w./-]+\.(?:mjs|cjs|jsx|tsx|js|ts|py|go|java|rs)/
|
|
|
87
87
|
* 锚点值 → 代码文件路径集(条目无「文件:」字段时的兜底命中源)。
|
|
88
88
|
* 真实先例:D-905 形态「锚点:src/quicklog.js:493」→ 提取 src/quicklog.js
|
|
89
89
|
* (token 字符类不含 : 故止于行号前,再防御性剥 :line/:line-line/:符号 后缀)。
|
|
90
|
-
* 「未记录」/无路径形态 token → []
|
|
90
|
+
* 「未记录」/无路径形态 token → []。导出供 knowledge-graph.js 采集 anchors 边复用。
|
|
91
91
|
*/
|
|
92
|
-
function anchorFilePaths(anchor) {
|
|
92
|
+
export function anchorFilePaths(anchor) {
|
|
93
93
|
const a = String(anchor || '').trim().replace(/\\/g, '/')
|
|
94
94
|
if (!a || a === '未记录') return []
|
|
95
95
|
const out = new Set()
|
|
96
96
|
for (const token of a.match(ANCHOR_FILE_TOKEN_RE) || []) {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// 前导斜杠剥除(2026-10-09-graph-docrefs-noise):repo:// URL 被正则吃成 //sillyhub/... 碎片,
|
|
98
|
+
// 剥后 sillyhub/ 顶级由图侧按跨仓口径处理;纯数字/裸扩展段丢弃(解析噪声)
|
|
99
|
+
const p = token
|
|
100
|
+
.replace(/:(?:\d+(?:-\d+)?|[A-Za-z_$][A-Za-z0-9_$]*)$/, '')
|
|
101
|
+
.replace(/^\/+/, '')
|
|
102
|
+
if (p && !/^\d+$/.test(p)) out.add(p)
|
|
99
103
|
}
|
|
100
104
|
return [...out]
|
|
101
105
|
}
|
|
@@ -129,7 +133,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
129
133
|
const h = line.match(DECISION_HEADER_RE)
|
|
130
134
|
if (h) {
|
|
131
135
|
flush()
|
|
132
|
-
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '' }
|
|
136
|
+
cur = { file, id: h[1], title: h[2].trim(), status: '', reason: '', revisitWhen: '', files: [], anchor: '', change: '', supersedesText: '' }
|
|
133
137
|
continue
|
|
134
138
|
}
|
|
135
139
|
if (!cur) continue
|
|
@@ -145,6 +149,7 @@ function parseDecisionFile(filePath, file) {
|
|
|
145
149
|
const value = f[2].trim()
|
|
146
150
|
if (f[1] === '状态') cur.status = value.toLowerCase()
|
|
147
151
|
else if (f[1] === '文件') cur.files = splitDecisionFiles(value)
|
|
152
|
+
else if (f[1] === 'supersedes') cur.supersedesText = value
|
|
148
153
|
else if (f[1] === '否决理由') cur.reason = value
|
|
149
154
|
else if (f[1] === '复潮条件') cur.revisitWhen = value
|
|
150
155
|
else if (!cur.reason) cur.reason = value // 理由:仅作 implemented(或否决理由缺失)时的回填
|
package/src/knowledge-vector.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* knowledge-vector.js — 平台向量召回层(CLI 侧,2026-09-29-knowledge-vector-recall)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* 检索四层(2026-10-08-knowledge-graph 起):路由 tag 命中 →(零命中)平台向量召回(本模块,
|
|
5
|
+
* 已连接平台时)→ scope 遍历召回(knowledge-graph.js,scopeFiles 在场时)→ 本地词片复现窗口
|
|
6
|
+
* (knowledge-match.js fallbackByQueryShingles)→ 空。
|
|
6
7
|
*
|
|
7
8
|
* 原则(用户裁决 2026-09-29):平台只做语义召回——返回「哪个文件哪个条目」的候选
|
|
8
9
|
* (spec_path+anchor+score);条目 status/deathPath/回显资格等全部策略面在本地解析
|
|
@@ -143,8 +144,11 @@ function buildResultFromPlatform(indexDir, pvResults) {
|
|
|
143
144
|
|
|
144
145
|
/**
|
|
145
146
|
* 异步三层检索入口(四消费方使用:flow 注入段/complete 门/prompt {DECISION_HITS}/
|
|
146
|
-
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ 本地词片 → 空。
|
|
147
|
+
* knowledge search CLI):路由 → 平台向量(已连接且开关开)→ scope 遍历 → 本地词片 → 空。
|
|
147
148
|
* opts.cwd = 仓根(平台配置与 local.yaml 定位用);缺省由 indexDir 上推两级。
|
|
149
|
+
* opts.scopeFiles(2026-10-08-knowledge-graph,FR-04 承接 FR-cli-entry-234):变更触碰文件集
|
|
150
|
+
* ——机器算的图查询键(D-004:flow 注入段传 touched、complete 门传 decisions.md 锚点提取)。
|
|
151
|
+
* 缺省/空数组 → 遍历层整体跳过,行为与不携带该参数的旧行为逐字段一致(回归钉死)。
|
|
148
152
|
*/
|
|
149
153
|
export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
150
154
|
const r = matchByRouting(indexDir, taskContext)
|
|
@@ -155,6 +159,16 @@ export async function matchKnowledgeHybrid(indexDir, taskContext, opts = {}) {
|
|
|
155
159
|
const built = pv ? buildResultFromPlatform(indexDir, pv.results) : null
|
|
156
160
|
if (built) return built
|
|
157
161
|
}
|
|
162
|
+
// scope 遍历层(第三层):只在 scope 非空且向量零命中后接管;fail-soft(图构建异常静默降级词片)
|
|
163
|
+
const scopeFiles = Array.isArray(opts.scopeFiles) ? opts.scopeFiles.filter(Boolean) : []
|
|
164
|
+
if (scopeFiles.length > 0) {
|
|
165
|
+
try {
|
|
166
|
+
const { buildKnowledgeGraph, scopeRecall, buildScopeRecallResult } = await import('./knowledge-graph.js')
|
|
167
|
+
const specRoot = String(indexDir).replace(/[\\/]knowledge[\\/]?$/, '')
|
|
168
|
+
const built = buildScopeRecallResult(scopeRecall(buildKnowledgeGraph(specRoot), scopeFiles))
|
|
169
|
+
if (built) return built
|
|
170
|
+
} catch { /* 图构建 fail-soft——降级词片层 */ }
|
|
171
|
+
}
|
|
158
172
|
const fb = fallbackByQueryShingles(indexDir, taskContext)
|
|
159
173
|
return fb || r
|
|
160
174
|
}
|
package/src/module-impact.js
CHANGED
|
@@ -155,7 +155,9 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
|
|
|
155
155
|
const cardPath = join(modulesDir, `${mod}.md`)
|
|
156
156
|
try {
|
|
157
157
|
const card = readFileSync(cardPath, 'utf8')
|
|
158
|
-
|
|
158
|
+
// 全量 ISO(机器可比时间戳,scan-postcheck 同字段同口径)——旧式 UTC 数字拼硬编码
|
|
159
|
+
// '+08:00' 的瞬间恒早真实时刻 8h(数字取 UTC、偏移写死,机器无关地错)
|
|
160
|
+
const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString()}`)
|
|
159
161
|
if (stamped !== card) writeFileSync(cardPath, stamped)
|
|
160
162
|
} catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
|
|
161
163
|
synced.push(proj ? `${proj}/${mod}` : mod)
|
|
@@ -369,12 +369,19 @@ export class StageMachine {
|
|
|
369
369
|
if (!(isProgress || f.startsWith('spec-sync-conflict-')) || !f.endsWith('.json')) continue;
|
|
370
370
|
let change = f.replace(/^spec-sync-conflict-|^sync-conflict-/, '').replace(/\.json$/, '');
|
|
371
371
|
let createdAt = null;
|
|
372
|
+
let kind = null;
|
|
372
373
|
try {
|
|
373
374
|
const cf = JSON.parse(readFileSync(join(runtimeDir, f), 'utf8'));
|
|
374
375
|
change = cf.change || change;
|
|
375
376
|
createdAt = cf.created_at || null;
|
|
377
|
+
kind = typeof cf.kind === 'string' ? cf.kind : null;
|
|
376
378
|
} catch { /* 损坏文件按文件名兜底,不崩 */ }
|
|
377
|
-
|
|
379
|
+
// 2026-10-09-tombstone-conflict-root-fix FR-01:kind='tombstone' 优先透传
|
|
380
|
+
// (归因记账新形态——daemon 摘要/前端据此区分墓碑冲突与版本冲突);文件名
|
|
381
|
+
// 前缀判定保留兜底(旧格式/损坏文件)。
|
|
382
|
+
const type = kind === 'tombstone' ? 'tombstone'
|
|
383
|
+
: isProgress ? 'progress' : 'spec-tree';
|
|
384
|
+
conflicts.push({ change, created_at: createdAt, type });
|
|
378
385
|
}
|
|
379
386
|
return conflicts;
|
|
380
387
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-face-key.js — 代码面口径单点(2026-10-09-verify-reuse-friction FR-03 / D-002@v1)。
|
|
3
|
+
*
|
|
4
|
+
* 问题:两类复用指纹(green-cache 的 computeGateFingerprint / 质量扫描的
|
|
5
|
+
* computeQualityScanFingerprint)原先以 git HEAD 为「代码态」分量——收口窗口内任何提交
|
|
6
|
+
* (哪怕纯文档的声明修正)都换 HEAD → 缓存必 miss(2026-10-09 multi-agent-platform
|
|
7
|
+
* tombstone 取证:13:35-14:10 五笔提交每笔击穿,2-3 轮 ×3.5min 重测纯浪费)。
|
|
8
|
+
*
|
|
9
|
+
* 修复:HEAD 分量替换为「代码树内容键」——`git ls-tree -r -z HEAD` 条目按既有非代码
|
|
10
|
+
* 路径口径(.sillyspec/、docs/、*.md——与 filterCodePorcelain 同源判据)过滤后哈希。
|
|
11
|
+
* ls-tree 条目自带 git 对象哈希(内容寻址):纯文档提交不进过滤集 → 键不变 → 缓存存活;
|
|
12
|
+
* 代码提交必进/必变 → 键变 → 必失配。整树 oid(HEAD^{tree})作快路径:oid 与上次相同
|
|
13
|
+
* → 直接沿用上次键(纯缓存优化,语义不变——oid 相同 ⇔ 整树逐字节相同)。
|
|
14
|
+
*
|
|
15
|
+
* 红线合规:非代码判据是**路径口径**(沿既有单点),不是语言/框架/扩展名枚举;
|
|
16
|
+
* ls-tree 是 git 对象枚举,与语言无关。-z(NUL 分隔)输出不加引号不转义——非 ASCII
|
|
17
|
+
* 路径的 endsWith 判据不被尾部引号击穿(独立审查指出的 quotepath 边界)。
|
|
18
|
+
*
|
|
19
|
+
* fail-open:git 不可用/任何异常 → null(调用方按 miss 处理,多跑一轮不误复用——
|
|
20
|
+
* 与两指纹既有的 git 不可用语义同向)。
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
24
|
+
|
|
25
|
+
function safeGit(cwd, args) {
|
|
26
|
+
try {
|
|
27
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true });
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 非代码路径判据(口径单点):.sillyspec/、docs/ 与 *.md——verify 窗口内的合法产出
|
|
35
|
+
* (规范文档/声明修正)不算代码面。green-cache.filterCodePorcelain 与质量扫描
|
|
36
|
+
* porcelainCodeLines 均消费本判据,不再各自实现。
|
|
37
|
+
*/
|
|
38
|
+
export function isNonCodePath(p) {
|
|
39
|
+
return p.startsWith('.sillyspec/') || p.startsWith('docs/') || /\.md$/i.test(p);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* porcelain 行过滤为「代码面」(原 green-cache.js 实现迁入,语义不变):
|
|
44
|
+
* rename 行「old -> new」两侧任一是文档面即整行剔除(防 rename 伪装穿透);排序保序。
|
|
45
|
+
*/
|
|
46
|
+
export function filterCodePorcelain(porcelain) {
|
|
47
|
+
return String(porcelain || '')
|
|
48
|
+
.split(/\r?\n/)
|
|
49
|
+
.filter((line) => {
|
|
50
|
+
if (!line.trim()) return false;
|
|
51
|
+
const raw = line.slice(3).trim();
|
|
52
|
+
if (!raw) return false;
|
|
53
|
+
const parts = raw.split(' -> ').map((p) => p.replace(/^"|"$/g, ''));
|
|
54
|
+
return !parts.every((p) => isNonCodePath(p));
|
|
55
|
+
})
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 整树 oid 快路径缓存:cwd → { treeOid, key }(模块级;进程短命令生命周期内有效) */
|
|
60
|
+
const treeKeyMemo = new Map();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 代码树内容键:`git ls-tree -r -z HEAD` 条目(mode type oid\tpath)过滤非代码路径后
|
|
64
|
+
* 对逐条目原文(含 git 对象哈希——内容寻址)做 sha256。
|
|
65
|
+
* @returns {string|null} git 不可用/无 HEAD → null(调用方 fail-open miss)
|
|
66
|
+
*/
|
|
67
|
+
export function computeCodeTreeKey({ cwd }) {
|
|
68
|
+
const treeOid = safeGit(cwd, ['rev-parse', 'HEAD^{tree}']);
|
|
69
|
+
if (treeOid === null) return null;
|
|
70
|
+
const oid = treeOid.trim();
|
|
71
|
+
const memo = treeKeyMemo.get(cwd);
|
|
72
|
+
if (memo && memo.treeOid === oid) return memo.key;
|
|
73
|
+
const out = safeGit(cwd, ['ls-tree', '-r', '-z', 'HEAD']);
|
|
74
|
+
if (out === null) return null;
|
|
75
|
+
const entries = String(out).split('\0').filter((e) => {
|
|
76
|
+
if (!e) return false;
|
|
77
|
+
const tab = e.indexOf('\t');
|
|
78
|
+
if (tab === -1) return false;
|
|
79
|
+
return !isNonCodePath(e.slice(tab + 1));
|
|
80
|
+
});
|
|
81
|
+
const key = createHash('sha256').update(entries.join('\n')).digest('hex');
|
|
82
|
+
treeKeyMemo.set(cwd, { treeOid: oid, key });
|
|
83
|
+
return key;
|
|
84
|
+
}
|
package/src/run/complete.js
CHANGED
|
@@ -479,10 +479,23 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
479
479
|
_kgQuery = _kgQuery.slice(0, 4000)
|
|
480
480
|
if (_kgQuery.trim()) {
|
|
481
481
|
const { matchKnowledgeHybrid } = await import('../knowledge-vector.js')
|
|
482
|
-
const
|
|
482
|
+
const { scopeFromDecisionsMd } = await import('../knowledge-graph.js')
|
|
483
|
+
// scope 键=机器算(D-004 需求期入口):decisions.md「锚点:」字段路径提取——方案步的
|
|
484
|
+
// 结构化声明面,不解析自由文本。词面零命中时沿强边保底防复潮(FR-04)。
|
|
485
|
+
const km = await matchKnowledgeHybrid(join(specBase, 'knowledge'), _kgQuery, { cwd, scopeFiles: scopeFromDecisionsMd(_dm) })
|
|
483
486
|
// 零分不弹(2026-09-28-knowledge-gate-denoise):score 零且非死路的 rejected 与查询零主题
|
|
484
487
|
// 重叠——空标题条目靠状态蹭进回显是行为实测三例的噪音源;死路条目不受限(防复潮先验)。
|
|
485
488
|
const _hits = (km.decisionHits || []).filter((h) => h.deathPath || (h.status === 'rejected' && h.score > 0))
|
|
489
|
+
// impact 防复潮可达集(2026-10-09-brainstorm-impact-antirevival):锚点+模块域双结构键
|
|
490
|
+
// → graphImpact 闭包内 rejected/死路保底进场——方案步锚点常空(confirmed 才必填),
|
|
491
|
+
// 模块域是生成规范文件步硬校验的在场键;viaImpact 条目绕过 score 门槛(结构可达即
|
|
492
|
+
// 防复潮先验,与死路同待遇)。图构建 fail-soft(失败不阻断门)。
|
|
493
|
+
try {
|
|
494
|
+
const { buildKnowledgeGraph, impactFromDecisionsMd } = await import('../knowledge-graph.js')
|
|
495
|
+
const _viaImpact = impactFromDecisionsMd(buildKnowledgeGraph(specBase), _dm)
|
|
496
|
+
.filter((h) => !_hits.some((x) => x.file === h.file && x.id === h.id && (x.change || '') === (h.change || '')))
|
|
497
|
+
_hits.push(..._viaImpact)
|
|
498
|
+
} catch { /* 图构建/键解析 fail-soft——词面命中面不受影响 */ }
|
|
486
499
|
// 已回应不重弹:decisions.md 正文已含「命中 id+域文件名」共现(小白鼠回应形态
|
|
487
500
|
// 「unmapped.md D-001@v1」)→ 视为已甄别过,静默;同轮查询串仍含其词面属预期。
|
|
488
501
|
const _answered = _dm
|
|
@@ -500,7 +513,8 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
500
513
|
const _note = h.status === 'rejected'
|
|
501
514
|
? ` 否决理由:${h.reason || '(未记录)'}`
|
|
502
515
|
: h.deathPath ? ` ⚰️死路注记:${deathPathNote(h.reason)}` : ''
|
|
503
|
-
|
|
516
|
+
const _via = h.viaImpact ? ` 🧭impact 可达(键:${h.impactKey})` : ''
|
|
517
|
+
console.warn(` - ${h.id} ${h.title}(${h.file})status=${h.status || '?'}${_note}${_via}`)
|
|
504
518
|
}
|
|
505
519
|
for (const e of _entries.slice(0, 3)) {
|
|
506
520
|
const _base = e.anchor ? `${e.file}#${e.anchor}` : e.file
|
|
@@ -1112,7 +1126,8 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1112
1126
|
snap.closedBy = 'execute --done'
|
|
1113
1127
|
// 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
|
|
1114
1128
|
// unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
|
|
1115
|
-
// 经共用 writeCloseTraceArtifacts 与 thin flow done
|
|
1129
|
+
// 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处单套写两件(2026-10-09-
|
|
1130
|
+
// close-trace-single-set:对账面并入 change-patch.json 的 scopeAudit 子对象);
|
|
1116
1131
|
// 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
|
|
1117
1132
|
const face = projectTraceFaceRows(snap.rows)
|
|
1118
1133
|
const closeTrace = writeCloseTraceArtifacts({
|
|
@@ -1129,7 +1144,7 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
1129
1144
|
note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
|
|
1130
1145
|
},
|
|
1131
1146
|
})
|
|
1132
|
-
console.log(` 📦
|
|
1147
|
+
console.log(` 📦 范围快照落盘(单套):change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}(对账面并入 scopeAudit 子对象${closeTrace.patchStatus === 'ok' ? ',收尾时点冻结 sha256 已锚' : '——patch 采集失败已留痕'})`)
|
|
1133
1148
|
} catch (e) {
|
|
1134
1149
|
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1135
1150
|
}
|
|
@@ -1147,13 +1162,21 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
|
|
|
1147
1162
|
return
|
|
1148
1163
|
}
|
|
1149
1164
|
let snapshot = null
|
|
1150
|
-
// 快照读取链(
|
|
1165
|
+
// 快照读取链(2026-10-09-close-trace-single-set):change-patch.json.scopeAudit(单套
|
|
1166
|
+
// 新形态)→ 旧 scope-audit.json(存量归档)→ .runtime/scope-audit-<change>.json(更早
|
|
1167
|
+
// 存量兼容)——scopeAuditPathSet 只吃 rows,子对象与旧顶级形态同构直用
|
|
1151
1168
|
try {
|
|
1152
|
-
|
|
1153
|
-
|
|
1169
|
+
const merged = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'change-patch.json'), 'utf8'))
|
|
1170
|
+
if (merged && typeof merged.scopeAudit === 'object' && merged.scopeAudit) snapshot = merged.scopeAudit
|
|
1171
|
+
} catch { /* 新形态缺失 → 旧名兜底 */ }
|
|
1172
|
+
if (!snapshot) {
|
|
1154
1173
|
try {
|
|
1155
|
-
snapshot = JSON.parse(readFileSync(join(
|
|
1156
|
-
} catch {
|
|
1174
|
+
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
1175
|
+
} catch {
|
|
1176
|
+
try {
|
|
1177
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
1178
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
1179
|
+
}
|
|
1157
1180
|
}
|
|
1158
1181
|
if (!snapshot || snapshot.ok !== true) {
|
|
1159
1182
|
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -41,6 +41,25 @@ function git(cwd, args) {
|
|
|
41
41
|
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true }).trim()
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* 快照回退主仓的可见性 + 摩擦记账(2026-10-09-verify-reuse-friction FR-02)。
|
|
46
|
+
* 2026-10-09 取证:快照创建慢性失败静默回退主仓口径——apply 前实测的是「无变更代码」的
|
|
47
|
+
* HEAD 且绿结论可被 --done 复用消费(正确性风险),全程零可见零记账,只能靠 .runtime 考古
|
|
48
|
+
* 发现。本 helper 统一两处调用点(gates.js verify 实测门 / verify-quality-scan.js noAI 动作)
|
|
49
|
+
* 的告警块与 gate_snapshot_fallback 摩擦事件;记账 best-effort 不影响门禁。
|
|
50
|
+
*/
|
|
51
|
+
export async function reportGateSnapshotFallback({ cwd, changeName, platformOpts = null, where, reason }) {
|
|
52
|
+
console.warn(`⚠️ [gate-snapshot-fallback] ${where}:隔离快照创建失败——实测回退主仓口径。`)
|
|
53
|
+
console.warn(` 影响并行 WIP 可能混入判定;apply 前主仓 HEAD 可能不含本变更代码——实测结论须按主仓口径解读,收口前确认变更代码已落主仓。`)
|
|
54
|
+
console.warn(` 原因:${reason}`)
|
|
55
|
+
console.warn(` 排查:git worktree 可用性(.git/worktrees 可写)与变更文件集非空;确要主仓口径显式设 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF=1(口径切换会使复用指纹失配一次后收敛)。`)
|
|
56
|
+
try {
|
|
57
|
+
const { recordFrictionEvent } = await import('../friction-tally.js')
|
|
58
|
+
await recordFrictionEvent({ cwd, platformOpts, changeName, type: 'gate_snapshot_fallback', detail: `${where}: ${String(reason).slice(0, 160)}` })
|
|
59
|
+
} catch { /* 记账失败不影响门禁 */ }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
/** worktree 注册是否仍登记该 root(跨平台路径归一 + win32 大小写不敏感) */
|
|
45
64
|
function worktreeListContains(cwd, snapshotRoot) {
|
|
46
65
|
const out = git(cwd, ['worktree', 'list', '--porcelain'])
|
package/src/run/gates.js
CHANGED
|
@@ -872,6 +872,78 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
872
872
|
}
|
|
873
873
|
}
|
|
874
874
|
|
|
875
|
+
// 前移(2026-10-09-verify-reuse-friction FR-05):reconcileRuntimeRoot 提升为函数级——探针一致性/draft 门等后续消费方与对账门同源口径
|
|
876
|
+
const reconcileRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
877
|
+
if (stageName === 'verify' && changeName) {
|
|
878
|
+
// ── 纯事实门前移(2026-10-09-verify-reuse-friction FR-05 / D-004@v1):required-evidence
|
|
879
|
+
// 与 target_files 对账门只依赖 git/文档事实(存在×mtime×diff 交集 / git 三源差集),
|
|
880
|
+
// 与实测结果零数据依赖——移到实测门(test/lint)之前并入文档面收集(R16 聚合一次全列)。
|
|
881
|
+
// 2026-10-09 取证:target_files 声明缺失时每处偏差先付 3.5 分钟实测再被拦(14:00-14:10
|
|
882
|
+
// 每轮先测后拦);前移后秒级失败零测试执行。依赖实测结果的门(PASS 封顶/parity/超时
|
|
883
|
+
// 降档)位置不动。
|
|
884
|
+
// ── required-evidence 对账(2026-09-08-ir-verify-facts FR-03:v2 起 cannot_verify 不闭环阻断)──
|
|
885
|
+
// verify-required-evidence.json 由退役前的 execute Task Review Gate 写入
|
|
886
|
+
// (2026-09-26-task-review-retire 起停写;在场则消费=历史变更兼容读,缺席=无 cannot_verify 任务)。
|
|
887
|
+
// v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
|
|
888
|
+
// (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
|
|
889
|
+
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck, trackVerifyResultRegression } = await import('../verify-postcheck.js')
|
|
890
|
+
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName, verifyStartAt: verifyStartAtIso })
|
|
891
|
+
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
892
|
+
// verify-result.md 内容回退检测(2026-09-10 用户反馈③:报告被平台同步覆盖回旧版一次,
|
|
893
|
+
// 只能靠人眼发现重写):高水位指纹(hash+mtime)检出「内容变且 mtime 回退」→ ⚠️ 告警。
|
|
894
|
+
// 纯 advisory——真回退也由 agent 决定重写,CLI 只保证可见。
|
|
895
|
+
try {
|
|
896
|
+
const vr = trackVerifyResultRegression(specBase, changeName, join(specBase, 'changes', changeName, 'verify-result.md'))
|
|
897
|
+
if (vr.regressed) {
|
|
898
|
+
console.warn(`\n⚠️ verify-result.md 疑被覆盖回旧版(内容变化 + mtime 回退至 ${new Date(vr.prevMtime).toLocaleString('zh-CN')} 之前)——平台/daemon 按服务端旧版本回写的指纹。`)
|
|
899
|
+
console.warn(` 若你刚重写过请忽略本行(下次读取刷新高水位);否则当前盘上可能是旧内容,需对照记忆/git 重新补写。`)
|
|
900
|
+
}
|
|
901
|
+
} catch { /* 检测失败静默(advisory) */ }
|
|
902
|
+
if (evidenceCheck.status === 'blocked') {
|
|
903
|
+
console.error(`\n❌ verify 阶段被阻断:cannot_verify 证据账未闭环(${evidenceCheck.summary})。`)
|
|
904
|
+
for (const d of (evidenceCheck.detailed || [])) {
|
|
905
|
+
console.error(` - ${d.task} [${d.status}${d.exempt ? '/豁免' : ''}] ${d.reason}`)
|
|
906
|
+
for (const v of (d.verification || [])) {
|
|
907
|
+
if (v && v.reason) console.error(` · ${v.path || '(无路径)'} [${v.pathClass || '?'}] ${v.reason}(filesExist=${v.filesExist} mtimeOk=${v.mtimeOk} diffHit=${v.diffHit})`)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
|
|
911
|
+
console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
|
|
912
|
+
console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
|
|
913
|
+
docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: `cannot_verify 证据账未闭环(${evidenceCheck.summary})` })
|
|
914
|
+
}
|
|
915
|
+
// ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
|
|
916
|
+
// 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
|
|
917
|
+
// 事实源,execute 的 scope creep 只有 symbol-impact 试图抓。reconcileTargetFiles 用 git 三源
|
|
918
|
+
// 口径亲自取 actual,与 plan 阶段 task 卡 target_files 声明做三类差集:②类(声明未做)在场
|
|
919
|
+
// 即阻断——verify 的语义是「按计划交付且客观核验」,声明没做不能盖章归档;③类(做了没
|
|
920
|
+
// 声明=scope creep)启发式噪音面大(并行 WIP 剔除后仍可能有工具产物),WARNING 放行留审计;
|
|
921
|
+
// skipped/degraded(无 task 卡 / 全无声明 / git 不可用)降级不误红(存量变更零红门禁)。
|
|
922
|
+
// 阻断形态照 runVerifyTestCheck 先例:逐条列 task + path + 修复提示 + rollbackCompletionAndReturn。
|
|
923
|
+
const { reconcileTargetFiles, isStrictChange } = await import('../verify-postcheck.js')
|
|
924
|
+
const reconcileCheck = reconcileTargetFiles({
|
|
925
|
+
cwd, specBase, changeName,
|
|
926
|
+
// runtimeRoot 口径与上方 parity 对账同源(B3 apply-pathspec 兜底源在此根下;平台模式下
|
|
927
|
+
// specBase 与 .runtime 分离,不传会读不到兜底清单 → 假降级)
|
|
928
|
+
runtimeRoot: reconcileRuntimeRoot,
|
|
929
|
+
// IR 严格档(2026-09-07-ir-hardening D-003@v1):主仓卡全零声明 → strictViolation → ERROR
|
|
930
|
+
strictMode: isStrictChange({ pm, cwd, changeName }),
|
|
931
|
+
})
|
|
932
|
+
const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
|
|
933
|
+
const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
|
|
934
|
+
// 跨仓 per-repo 对账明细渲染(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004
|
|
935
|
+
// 分期):reconcileTargetFiles 的 crossRepo 字段(advisory 不阻断)——各仓 对上/②缺/③多
|
|
936
|
+
// 逐仓一行 + ②缺明细(跨仓声明落空是该去对应仓干活的信号,但不翻主仓状态——锚点窗口
|
|
937
|
+
// 脆弱期会假信号)。无跨仓卡时 crossRepo 为空数组零输出。
|
|
938
|
+
printCrossRepoReconcile(reconcileCheck.crossRepo)
|
|
939
|
+
// 结果落盘(design Wave 2 承诺:对齐 .runtime/verify-runs/<ts>/ 先例)——五状态全落(含
|
|
940
|
+
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
941
|
+
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
942
|
+
if (reconcileBlocked) {
|
|
943
|
+
writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
|
|
944
|
+
docGateFailures.push({ type: 'gate_rollback', detail: 'verify-contract', label: 'target_files 对账阻断(②类「声明未做」——计划落空,明细见上)' })
|
|
945
|
+
}
|
|
946
|
+
}
|
|
875
947
|
// ── 收集模式检查点 A(verify):纯文档门全清后才进入实测门(test/lint/parity/probe 保持
|
|
876
948
|
// fail-fast 不变)。brainstorm/plan/execute 不走本检查点(其实测面为空,检查点 B 收口)。──
|
|
877
949
|
if (stageName === 'verify' && docGateFailures.length > 0) {
|
|
@@ -926,8 +998,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
926
998
|
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
927
999
|
if (verifyGateSnap) {
|
|
928
1000
|
console.log(`🧪 Verify 实测隔离快照(根:${verifyGateSnap.snapshotRoot}):HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
1001
|
+
} else {
|
|
1002
|
+
// FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账
|
|
1003
|
+
try {
|
|
1004
|
+
const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
|
|
1005
|
+
if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'verify --done 实测门', reason: 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)' })
|
|
1006
|
+
} catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
|
|
929
1007
|
}
|
|
930
|
-
} catch {
|
|
1008
|
+
} catch (e) {
|
|
1009
|
+
try {
|
|
1010
|
+
const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
|
|
1011
|
+
if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'verify --done 实测门', reason: e && e.message ? e.message : String(e) })
|
|
1012
|
+
} catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
|
|
1013
|
+
}
|
|
931
1014
|
}
|
|
932
1015
|
const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
|
|
933
1016
|
const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
|
|
@@ -970,6 +1053,20 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
970
1053
|
gateFingerprint = computeGateFingerprint({ cwd, specBase })
|
|
971
1054
|
if (gateFingerprint) greenHit = lookupGreenCache({ runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), scope: changeName, kind: 'test', fingerprint: gateFingerprint })
|
|
972
1055
|
} catch { /* fail-open:缓存链异常回退真跑 */ }
|
|
1056
|
+
// ── 复用观测(2026-10-09-verify-reuse-friction FR-04):真跑轮随 test-result.json 落
|
|
1057
|
+
// fingerprint/reuse_decision;复用命中轮落 reuse-decisions-<change>.jsonl——取证直读,
|
|
1058
|
+
// 不再依赖 stdout 考古(tombstone 取证只能靠 sqlite 会话记录的教训)。──
|
|
1059
|
+
let _obsQualityScanFp = null
|
|
1060
|
+
try {
|
|
1061
|
+
const { computeQualityScanFingerprint } = await import('./verify-quality-scan.js')
|
|
1062
|
+
_obsQualityScanFp = computeQualityScanFingerprint({ cwd, specBase })
|
|
1063
|
+
} catch { /* 观测 best-effort */ }
|
|
1064
|
+
const _obsFingerprint = { gate: gateFingerprint, quality_scan: _obsQualityScanFp }
|
|
1065
|
+
const _obsJournal = (layer, hit, reason) => {
|
|
1066
|
+
import('../verify-postcheck.js').then(({ appendReuseDecision }) =>
|
|
1067
|
+
appendReuseDecision({ specBase, changeName, decision: { layer, hit, reason, fingerprint: _obsFingerprint } }))
|
|
1068
|
+
.catch(() => { /* 观测失败不阻断 */ })
|
|
1069
|
+
}
|
|
973
1070
|
if (ledgerReuse) {
|
|
974
1071
|
testCheck = {
|
|
975
1072
|
status: 'passed',
|
|
@@ -980,10 +1077,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
980
1077
|
}
|
|
981
1078
|
console.log(`\n♻️ Verify 测试对账:P2 账本复用(三键全等,免重跑;实测于 ${ledgerReuse.result.ranAt})`)
|
|
982
1079
|
printVerifyTestCheck(testCheck)
|
|
1080
|
+
_obsJournal('ledger', true, null)
|
|
983
1081
|
} else if (reusableScan && reusableScan.testResult) {
|
|
984
1082
|
testCheck = reusableScan.testResult
|
|
985
1083
|
console.log(`\n♻️ Verify 测试对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑;实测于 ${reusableScan.ranAt || '本变更 verify 期间'}${testCheck.resultPath ? `,台账 ${testCheck.resultPath}` : ''})`)
|
|
986
1084
|
printVerifyTestCheck(testCheck)
|
|
1085
|
+
_obsJournal('quality-scan', true, null)
|
|
987
1086
|
} else if (greenHit) {
|
|
988
1087
|
testCheck = {
|
|
989
1088
|
status: 'passed',
|
|
@@ -992,8 +1091,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
992
1091
|
exitCode: 0, durationMs: 0, outputTail: null,
|
|
993
1092
|
resultPath: null, strategy: null, cached: true,
|
|
994
1093
|
}
|
|
995
|
-
console.log(`\n♻️ Verify 测试对账:green-cache
|
|
1094
|
+
console.log(`\n♻️ Verify 测试对账:green-cache 同指纹复用(代码树键+代码脏面+local.yaml 全等,近期真跑绿——P2 账本未命中时的第二层免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
|
|
996
1095
|
printVerifyTestCheck(testCheck)
|
|
1096
|
+
_obsJournal('green-cache', true, null)
|
|
997
1097
|
} else {
|
|
998
1098
|
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
999
1099
|
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
@@ -1007,7 +1107,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1007
1107
|
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
1008
1108
|
_restrict = resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase: gateSpecBase })
|
|
1009
1109
|
} catch { /* 解析异常走全量(restrictFiles 不传) */ }
|
|
1010
|
-
|
|
1110
|
+
const _missReason = [
|
|
1111
|
+
`账本: ${traceHasActiveRows ? '停用(active trace 行——fail-closed 护栏)' : '未命中/不可用'}`,
|
|
1112
|
+
`质量扫描: ${reusableScan ? '命中(上层未消费)' : '指纹失配/无记录'}`,
|
|
1113
|
+
`green-cache: ${greenHit ? '命中(上层未消费)' : '指纹失配/过期/无记录'}`,
|
|
1114
|
+
].join(';')
|
|
1115
|
+
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, observability: { fingerprint: _obsFingerprint, reuse_decision: { layer: 'real-run', hit: false, reason: _missReason } }, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
|
|
1011
1116
|
printVerifyTestCheck(testCheck)
|
|
1012
1117
|
// 自动绑定补全(full-autopilot-parity,R21 thin 迁移):测试已跑、test-result.json 已落盘,
|
|
1013
1118
|
// verify --done 收口时从测试结果自动补全 requirements.md 的空绑定槽——与 thin 同逻辑(逐行扫描)。
|
|
@@ -1231,69 +1336,6 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1231
1336
|
const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
|
|
1232
1337
|
const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
|
|
1233
1338
|
printVerifyDeletionCheck(deletionCheck)
|
|
1234
|
-
// ── required-evidence 对账(2026-09-08-ir-verify-facts FR-03:v2 起 cannot_verify 不闭环阻断)──
|
|
1235
|
-
// verify-required-evidence.json 由退役前的 execute Task Review Gate 写入
|
|
1236
|
-
// (2026-09-26-task-review-retire 起停写;在场则消费=历史变更兼容读,缺席=无 cannot_verify 任务)。
|
|
1237
|
-
// v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
|
|
1238
|
-
// (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
|
|
1239
|
-
const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck, trackVerifyResultRegression } = await import('../verify-postcheck.js')
|
|
1240
|
-
const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName, verifyStartAt: verifyStartAtIso })
|
|
1241
|
-
printVerifyRequiredEvidenceCheck(evidenceCheck)
|
|
1242
|
-
// verify-result.md 内容回退检测(2026-09-10 用户反馈③:报告被平台同步覆盖回旧版一次,
|
|
1243
|
-
// 只能靠人眼发现重写):高水位指纹(hash+mtime)检出「内容变且 mtime 回退」→ ⚠️ 告警。
|
|
1244
|
-
// 纯 advisory——真回退也由 agent 决定重写,CLI 只保证可见。
|
|
1245
|
-
try {
|
|
1246
|
-
const vr = trackVerifyResultRegression(specBase, changeName, join(specBase, 'changes', changeName, 'verify-result.md'))
|
|
1247
|
-
if (vr.regressed) {
|
|
1248
|
-
console.warn(`\n⚠️ verify-result.md 疑被覆盖回旧版(内容变化 + mtime 回退至 ${new Date(vr.prevMtime).toLocaleString('zh-CN')} 之前)——平台/daemon 按服务端旧版本回写的指纹。`)
|
|
1249
|
-
console.warn(` 若你刚重写过请忽略本行(下次读取刷新高水位);否则当前盘上可能是旧内容,需对照记忆/git 重新补写。`)
|
|
1250
|
-
}
|
|
1251
|
-
} catch { /* 检测失败静默(advisory) */ }
|
|
1252
|
-
if (evidenceCheck.status === 'blocked') {
|
|
1253
|
-
console.error(`\n❌ verify 阶段被阻断:cannot_verify 证据账未闭环(${evidenceCheck.summary})。`)
|
|
1254
|
-
for (const d of (evidenceCheck.detailed || [])) {
|
|
1255
|
-
console.error(` - ${d.task} [${d.status}${d.exempt ? '/豁免' : ''}] ${d.reason}`)
|
|
1256
|
-
for (const v of (d.verification || [])) {
|
|
1257
|
-
if (v && v.reason) console.error(` · ${v.path || '(无路径)'} [${v.pathClass || '?'}] ${v.reason}(filesExist=${v.filesExist} mtimeOk=${v.mtimeOk} diffHit=${v.diffHit})`)
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
|
|
1261
|
-
console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
|
|
1262
|
-
console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
|
|
1263
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1264
|
-
}
|
|
1265
|
-
// ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
|
|
1266
|
-
// 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
|
|
1267
|
-
// 事实源,execute 的 scope creep 只有 symbol-impact 试图抓。reconcileTargetFiles 用 git 三源
|
|
1268
|
-
// 口径亲自取 actual,与 plan 阶段 task 卡 target_files 声明做三类差集:②类(声明未做)在场
|
|
1269
|
-
// 即阻断——verify 的语义是「按计划交付且客观核验」,声明没做不能盖章归档;③类(做了没
|
|
1270
|
-
// 声明=scope creep)启发式噪音面大(并行 WIP 剔除后仍可能有工具产物),WARNING 放行留审计;
|
|
1271
|
-
// skipped/degraded(无 task 卡 / 全无声明 / git 不可用)降级不误红(存量变更零红门禁)。
|
|
1272
|
-
// 阻断形态照 runVerifyTestCheck 先例:逐条列 task + path + 修复提示 + rollbackCompletionAndReturn。
|
|
1273
|
-
const { reconcileTargetFiles, isStrictChange } = await import('../verify-postcheck.js')
|
|
1274
|
-
const reconcileRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
1275
|
-
const reconcileCheck = reconcileTargetFiles({
|
|
1276
|
-
cwd, specBase, changeName,
|
|
1277
|
-
// runtimeRoot 口径与上方 parity 对账同源(B3 apply-pathspec 兜底源在此根下;平台模式下
|
|
1278
|
-
// specBase 与 .runtime 分离,不传会读不到兜底清单 → 假降级)
|
|
1279
|
-
runtimeRoot: reconcileRuntimeRoot,
|
|
1280
|
-
// IR 严格档(2026-09-07-ir-hardening D-003@v1):主仓卡全零声明 → strictViolation → ERROR
|
|
1281
|
-
strictMode: isStrictChange({ pm, cwd, changeName }),
|
|
1282
|
-
})
|
|
1283
|
-
const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
|
|
1284
|
-
const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
|
|
1285
|
-
// 跨仓 per-repo 对账明细渲染(坑 cross-repo-reconcile-blindness,2026-09-15 复盘兑现 D-004
|
|
1286
|
-
// 分期):reconcileTargetFiles 的 crossRepo 字段(advisory 不阻断)——各仓 对上/②缺/③多
|
|
1287
|
-
// 逐仓一行 + ②缺明细(跨仓声明落空是该去对应仓干活的信号,但不翻主仓状态——锚点窗口
|
|
1288
|
-
// 脆弱期会假信号)。无跨仓卡时 crossRepo 为空数组零输出。
|
|
1289
|
-
printCrossRepoReconcile(reconcileCheck.crossRepo)
|
|
1290
|
-
// 结果落盘(design Wave 2 承诺:对齐 .runtime/verify-runs/<ts>/ 先例)——五状态全落(含
|
|
1291
|
-
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
1292
|
-
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
1293
|
-
if (reconcileBlocked) {
|
|
1294
|
-
writeVerifyGatePointer({ runtimeRoot: reconcileRuntimeRoot, changeName, blocked: true, note: 'target_files ②类(missing_declared)阻断' })
|
|
1295
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
1296
|
-
}
|
|
1297
1339
|
// ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
|
|
1298
1340
|
// 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
|
|
1299
1341
|
// 基准是正文锚点而非 verify-facts.json(删底稿绕不过防护)。checkProbeConsistency 重跑只读
|
|
@@ -1302,7 +1344,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1302
1344
|
// skipped(存量旧格式报告/无 changeName)/degraded(重跑异常 fail-soft)→ 放行提示。
|
|
1303
1345
|
// 接线形态照上方 P3a reconcile 先例:envelope + print(ERROR 时 console.error 逐条 mismatch +
|
|
1304
1346
|
// supportedFixes)+ verify-runs 落盘 + rollbackCompletionAndReturn。
|
|
1305
|
-
const { checkProbeConsistency } = await import('../verify-postcheck.js')
|
|
1347
|
+
const { checkProbeConsistency, isStrictChange } = await import('../verify-postcheck.js')
|
|
1306
1348
|
const probeCheck = checkProbeConsistency({
|
|
1307
1349
|
// 四参与 reconcile 同源取值:cwd/specBase/changeName 原样透传;runtimeRoot 复用上方
|
|
1308
1350
|
// reconcileRuntimeRoot(resolveRuntimeRoot(platformOpts, specBase))——checkProbeConsistency
|
package/src/run/green-cache.js
CHANGED
|
@@ -39,36 +39,28 @@ function safeGit(cwd, args) {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* porcelain 行过滤为「代码面」:剔 .sillyspec/、docs/ 与 *.md(含 rename 两侧任一命中)。
|
|
42
|
-
*
|
|
42
|
+
* 实现已迁 code-face-key.js(口径单点,2026-10-09-verify-reuse-friction FR-03)——此处
|
|
43
|
+
* re-export 保持既有 import 路径兼容。
|
|
43
44
|
*/
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
.split(/\r?\n/)
|
|
47
|
-
.filter((line) => {
|
|
48
|
-
if (!line.trim()) return false;
|
|
49
|
-
const raw = line.slice(3).trim();
|
|
50
|
-
if (!raw) return false;
|
|
51
|
-
// rename 行「old -> new」两侧任一是文档面即整行剔除(防 rename 伪装穿透)
|
|
52
|
-
const parts = raw.split(' -> ').map((p) => p.replace(/^"|"$/g, ''));
|
|
53
|
-
return !parts.every((p) => p.startsWith('.sillyspec/') || p.startsWith('docs/') || /\.md$/i.test(p));
|
|
54
|
-
})
|
|
55
|
-
.sort();
|
|
56
|
-
}
|
|
45
|
+
export { filterCodePorcelain } from './code-face-key.js';
|
|
46
|
+
import { filterCodePorcelain, computeCodeTreeKey } from './code-face-key.js';
|
|
57
47
|
|
|
58
48
|
/**
|
|
59
|
-
*
|
|
49
|
+
* 计算门禁绿缓存指纹:代码树内容键 + 代码脏面 + local.yaml 内容。
|
|
50
|
+
* 2026-10-09-verify-reuse-friction(D-002@v1):HEAD 分量替换为代码树内容键(ls-tree
|
|
51
|
+
* 过滤哈希)——纯文档提交不再击穿缓存,代码提交必击穿;整树 oid 快路径见 code-face-key。
|
|
60
52
|
* git 不可用(非仓目录)返回 null → 调用方按 miss 处理。
|
|
61
53
|
*/
|
|
62
54
|
export function computeGateFingerprint({ cwd, specBase }) {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
55
|
+
const treeKey = computeCodeTreeKey({ cwd });
|
|
56
|
+
if (treeKey === null) return null;
|
|
65
57
|
const porcelain = safeGit(cwd, ['status', '--porcelain']) ?? '';
|
|
66
58
|
let localYaml = '';
|
|
67
59
|
try {
|
|
68
60
|
const yamlPath = join(specBase, 'local.yaml');
|
|
69
61
|
if (existsSync(yamlPath)) localYaml = readFileSync(yamlPath, 'utf8');
|
|
70
|
-
} catch { /*
|
|
71
|
-
return sha1([
|
|
62
|
+
} catch { /* 读不到按空——指纹仍含树键+脏面,miss 风险方向安全 */ }
|
|
63
|
+
return sha1([treeKey, filterCodePorcelain(porcelain).join('\n'), localYaml].join('\u0000'));
|
|
72
64
|
}
|
|
73
65
|
|
|
74
66
|
function greenCacheFile({ runtimeRoot, scope, kind }) {
|