sillyspec 3.32.2 → 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/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 同指纹复用(HEAD+代码脏面+local.yaml 全等,近期真跑绿——P2 账本未命中时的第二层免重跑;SILLYSPEC_GREEN_CACHE_OFF=1 可关闭)`)
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
- testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx, ...(Array.isArray(_restrict) && _restrict.length > 0 ? { restrictFiles: _restrict } : {}) })
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
@@ -39,36 +39,28 @@ function safeGit(cwd, args) {
39
39
 
40
40
  /**
41
41
  * porcelain 行过滤为「代码面」:剔 .sillyspec/、docs/ 与 *.md(含 rename 两侧任一命中)。
42
- * 纯函数(口径单点,watcher dirtyCode / quality-scan 同源判据);排序保序(同输入同输出)。
42
+ * 实现已迁 code-face-key.js(口径单点,2026-10-09-verify-reuse-friction FR-03)——此处
43
+ * re-export 保持既有 import 路径兼容。
43
44
  */
44
- export function filterCodePorcelain(porcelain) {
45
- return String(porcelain || '')
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
- * 计算门禁绿缓存指纹:HEAD + 代码脏面 + local.yaml 内容。
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 head = safeGit(cwd, ['rev-parse', 'HEAD']);
64
- if (head === null) return null;
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 { /* 读不到按空——指纹仍含 HEAD+脏面,miss 风险方向安全 */ }
71
- return sha1([head.trim(), filterCodePorcelain(porcelain).join('\n'), localYaml].join('\u0000'));
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 }) {
@@ -50,9 +50,10 @@ export function qualityScanRecordPath(specBase, changeName) {
50
50
  * 指纹非代码面排除:verify 阶段步骤 6 实测 → --done 窗口内的合法产出只有规范文档——
51
51
  * 排除后这些写入不再触发失配重跑;src 一动(护栏外行为)立即失配照旧亲测。
52
52
  */
53
- function isNonCodePath(p) {
54
- return p.startsWith('.sillyspec/') || p.startsWith('docs/') || p.endsWith('.md')
55
- }
53
+ // 非代码路径判据已迁 code-face-key.js(口径单点,2026-10-09-verify-reuse-friction FR-03)。
54
+ // 注意单点版 /\.md$/i 大小写不敏感:本文件原 endsWith('.md') 仅小写——统一后大写 .MD 文档
55
+ // 面同样豁免(两处口径互称同源已久,统一是收敛不是语义翻转)。
56
+ import { isNonCodePath, computeCodeTreeKey } from './code-face-key.js'
56
57
 
57
58
  function porcelainCodeLines(porcelain) {
58
59
  return String(porcelain || '')
@@ -64,7 +65,7 @@ function porcelainCodeLines(porcelain) {
64
65
  }
65
66
 
66
67
  /**
67
- * 代码指纹:sha256(local.yaml commands 段 + test_strategy 行 + git HEAD + 非文档脏集)。
68
+ * 代码指纹:sha256(local.yaml commands 段 + test_strategy 行 + 代码树内容键 + 非文档脏集)。
68
69
  * git 不可用 → null(fail-closed:store 落 null / load 视为无记录,复用窗口关闭)。
69
70
  * 配置面口径与 prompt.js {LOCAL_COMMANDS} 注入的块扫描同款(含 unavailable 行剔除)。
70
71
  */
@@ -84,12 +85,15 @@ export function computeQualityScanFingerprint({ cwd, specBase }) {
84
85
  if (ts) configText += '\n' + ts.trim()
85
86
  }
86
87
  } catch { /* 读不到按空配置面处理 */ }
87
- const head = gitQuiet(cwd, ['rev-parse', 'HEAD'])
88
+ // 2026-10-09-verify-reuse-friction(D-002@v1):HEAD 分量替换为代码树内容键(ls-tree 过滤
89
+ // 哈希,见 code-face-key.js)——纯文档提交不再击穿,代码提交必击穿;git 不可用 → null
90
+ // (fail-closed:复用窗口关闭,语义与原 HEAD 不可用同向)。
91
+ const treeKey = computeCodeTreeKey({ cwd })
88
92
  const porcelain = gitQuiet(cwd, ['status', '--porcelain'])
89
- if (head === null || porcelain === null) return null
93
+ if (treeKey === null || porcelain === null) return null
90
94
  const codeLines = porcelainCodeLines(porcelain)
91
95
  return createHash('sha256')
92
- .update([configText, head.trim(), codeLines.join('|')].join('\n---\n'))
96
+ .update([configText, treeKey, codeLines.join('|')].join('\n---\n'))
93
97
  .digest('hex')
94
98
  }
95
99
 
@@ -100,7 +104,7 @@ export function computeQualityScanFingerprint({ cwd, specBase }) {
100
104
  * 保持 1——存量记录无 smoke 段照常读回(smokeResult 兜底 null);未配置 smoke 时段形态为
101
105
  * configured=false(记录仍写,供 --done PASS 封顶判 not-configured)。
102
106
  */
103
- export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult, usedSnapshot = false }) {
107
+ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult, usedSnapshot = false, missReason = null, scopeDecision = null }) {
104
108
  const path = qualityScanRecordPath(specBase, changeName)
105
109
  if (!path) return null
106
110
  const fingerprint = computeQualityScanFingerprint({ cwd, specBase })
@@ -123,6 +127,11 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
123
127
  // 修改);additive——存量记录无此键 → 闸判 no-passed-key 保守重跑一次后携带
124
128
  passedKey: computeQualityScanPassedKey({ cwd, specBase }),
125
129
  usedSnapshot: Boolean(usedSnapshot),
130
+ // 复用观测(2026-10-09-verify-reuse-friction FR-04,additive——存量记录无键读侧兼容):
131
+ // missReason=本轮真跑前的闸 miss 原因链;scopeDecision=计划口径 vs 实际口径(快照
132
+ // 慢性失败时 planned≠actual 的收敛态可从记录直读,取证不再依赖 stdout 考古)。
133
+ missReason,
134
+ scopeDecision,
126
135
  ranAt: new Date().toISOString(),
127
136
  testResult,
128
137
  lintResult: lintResult || null,
@@ -624,17 +633,66 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
624
633
  console.log('ℹ️ SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1——强制重跑(上次无失败记录在案,签名闸无事可对)')
625
634
  }
626
635
  }
627
- // ── 失败签名去重(ql-20260920-010 修复一 d)──
636
+ // ── 隔离快照定向跑(2026-09-11 驾驭第十二批;2026-10-09-verify-reuse-friction 移到复用闸之前)──
637
+ // noAI 质量扫描是 verify 的第一实测执行点,快照(HEAD + 本变更文件集,native worktree
638
+ // 定向源)防并行 WIP 弄红无辜变更。**先建后判(D-003@v1)**:下方两道复用闸(失败签名
639
+ // 去重 / passed 幂等)以「本轮实际口径」Boolean(snap) 对账 lastRecord.usedSnapshot——快照
640
+ // 创建慢性失败时 false==false 口径一致即收敛命中,断根治「计划口径 vs 实际口径恒失配」的
641
+ // 死循环(2026-10-09 multi-agent-platform tombstone 取证:13 轮实测中 4 轮纯浪费于此,
642
+ // 记录 usedSnapshot=false × planned=true 永不收敛)。指纹(storeQualityScan)仍按主仓 cwd
643
+ // 算——--done 对账复用的指纹匹配口径不变。
644
+ let snap = null
645
+ if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
646
+ // worktree 会话跳快照(2026-09-23 R9/R10 根治):worktree 已是会话独占隔离——快照只剩
647
+ // venv junction 冻结/慢 I/O 纯成本(R9 3MB/0CPU 冻结、R10 15min 慢跑实证),直接工作树实测。
648
+ let skipForWorktree = false
649
+ try {
650
+ const { shouldSkipGateSnapshotForWorktree } = await import('./gate-snapshot.js')
651
+ skipForWorktree = shouldSkipGateSnapshotForWorktree(cwd)
652
+ } catch { /* 判定异常按不跳走原路 */ }
653
+ if (skipForWorktree) {
654
+ console.log('🔀 cwd 是会话专属 worktree——隔离快照冗余(并行会话不在场;快照 venv junction 冻结/慢 I/O 面 R9/R10 双实证),直接在 worktree 实测')
655
+ } else {
656
+ try {
657
+ const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
658
+ snap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
659
+ if (snap) {
660
+ console.log(`🧪 noAI 扫描隔离快照(根:${snap.snapshotRoot}):HEAD + 本变更 ${snap.changeFileCount} 个文件${snap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与判定)`)
661
+ } else {
662
+ // FR-02(2026-10-09-verify-reuse-friction):null 回退与异常回退同等可见 + 记账
663
+ try {
664
+ const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
665
+ if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'noAI 质量扫描步', reason: 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)' })
666
+ } catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
667
+ }
668
+ } catch (e) {
669
+ try {
670
+ const { reportGateSnapshotFallback } = await import('./gate-snapshot.js')
671
+ if (typeof reportGateSnapshotFallback === 'function') await reportGateSnapshotFallback({ cwd, changeName, platformOpts, where: 'noAI 质量扫描步', reason: e && e.message ? e.message : String(e) })
672
+ } catch { /* 报告失败不改变回退行为(含测试 mock 模块缺导出的形态) */ }
673
+ }
674
+ }
675
+ }
676
+ const gateCwd = snap ? snap.snapshotRoot : cwd
677
+ const gateSpecBase = snap ? join(snap.snapshotRoot, '.sillyspec') : specBase
678
+ if (!snap) {
679
+ warnIfMainRepoDirtyForGate(cwd)
680
+ }
681
+ // ── 失败签名去重(ql-20260920-010 修复一 d;2026-10-09 起传实际口径)──
628
682
  // 复入本步前先对账上次失败:代码指纹 + known_failures 豁免面 + 快照口径都没变 → 失败
629
683
  // 必然原样复现,重跑是纯等待(对撞三轮三连 8~10 分钟假红重试的根治)。复用只消费
630
684
  // failed 态记录(passed 态 --done 复用归 loadReusableQualityScan),逃生阀
631
685
  // SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(闸门见上方签名闸)与 =force(无条件旁路)。
686
+ // plannedSnapshot 参数自 2026-10-09 起接收「本轮实际口径」Boolean(snap)(先建后判)。
687
+ // ── 复用观测(2026-10-09-verify-reuse-friction FR-04):判定原因链落盘——miss 原因随
688
+ // 本轮 store 记录(missReason),命中轮落 reuse-decisions jsonl,真跑轮随 test-result.json。──
689
+ let _missReasonParts = []
632
690
  if (rerunMode !== '1' && rerunMode !== 'force') {
633
691
  const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
634
692
  const verdict = shouldReuseLastFailedScan({
635
693
  lastRecord,
636
694
  currentDedupKey: computeQualityScanDedupKey({ cwd, specBase }),
637
- plannedSnapshot: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,
695
+ plannedSnapshot: Boolean(snap),
638
696
  })
639
697
  if (verdict.reuse) {
640
698
  const t = lastRecord.testResult
@@ -651,66 +709,51 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
651
709
  console.error(' (上次运行早于分诊机制——本次重跑会自动做主树口径对照;如需立即重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1)')
652
710
  }
653
711
  console.error(' 出路(按序):① 修代码(指纹即失配,下次自动重测)② 补 known_failures 豁免(豁免面即失配)③ 换快照口径(设/删 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,口径变化即失配)④ 环境确已修好后强制重跑:SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1(环境探针已入失败签名——环境真变即放行;仍被拒用 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=force)')
712
+ if (snap) { try { snap.cleanup() } catch { /* 复用路径快照清理失败不阻断 throw */ } }
713
+ try {
714
+ const { appendReuseDecision } = await import('../verify-postcheck.js')
715
+ appendReuseDecision({ specBase, changeName, decision: { layer: 'noai-failed-gate', hit: true, reason: null, fingerprint: computeQualityScanFingerprint({ cwd, specBase }) } })
716
+ } catch { /* 观测失败不阻断 */ }
654
717
  throw new Error(`noAI 质量扫描复用上次失败结果(失败签名未变,跳过重跑)——${(t.reason || '测试失败').split('\n')[0]}。修复后重跑 sillyspec run verify${changeName ? ` --change ${changeName}` : ''},或按上方出路处置。`)
655
718
  }
719
+ if (!verdict.reuse && verdict.reason) _missReasonParts.push(`failed闸: ${verdict.reason}`)
656
720
  }
657
- // ── passed 幂等闸(2026-09-30-quality-scan-passed-idempotent,与失败签名闸对称)──
721
+ // ── passed 幂等闸(2026-09-30-quality-scan-passed-idempotent,与失败签名闸对称;2026-10-09 起传实际口径)──
658
722
  // 上次 passed 且 passedKey(dedupKey × 非文档脏集内容键——内容敏感,同文件未提交修改
659
723
  // 即失配)/快照口径全等 → 免重跑直接完成本步骤(verify 收敛循环里 agent 修文档反复重入
660
724
  // 本步,码态未变时快照构建+测试+lint 的 ~分钟级全量重跑是纯等待——实证 11 轮 ×~290s)。
661
725
  // 复用只做披露,不重写记录(ranAt 不动,--done 读侧 loadReusableQualityScan 口径不变);
662
- // 逃生阀与失败闸同款 RERUN=1/force。
726
+ // 逃生阀与失败闸同款 RERUN=1/force。plannedSnapshot 参数自 2026-10-09 起接收「本轮实际
727
+ // 口径」Boolean(snap)(先建后判——死循环根治见上方快照块注释)。
663
728
  if (rerunMode !== '1' && rerunMode !== 'force') {
664
729
  const lastRecord = loadLastQualityScanRecord({ specBase, changeName })
665
730
  const passedVerdict = shouldReuseLastPassedScan({
666
731
  lastRecord,
667
732
  currentPassedKey: computeQualityScanPassedKey({ cwd, specBase }),
668
- plannedSnapshot: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,
733
+ plannedSnapshot: Boolean(snap),
669
734
  })
670
735
  if (passedVerdict.reuse) {
671
736
  console.log(`\n♻️ noAI 质量扫描幂等命中:上次实测 passed 且代码指纹/配置/豁免面/快照口径全等${lastRecord.ranAt ? `(实测于 ${lastRecord.ranAt})` : ''}——本轮免重跑,--done 对账继续复用该记录;确需重跑设 SILLYSPEC_VERIFY_QUALITY_SCAN_RERUN=1。`)
672
737
  console.log(`\n✅ noAI 质量扫描全绿(幂等复用)——本步骤自动完成;代码/配置/豁免面任一变化即 dedupKey 失配自动重测。`)
738
+ try {
739
+ const { appendReuseDecision } = await import('../verify-postcheck.js')
740
+ appendReuseDecision({ specBase, changeName, decision: { layer: 'noai-passed-gate', hit: true, reason: null, fingerprint: computeQualityScanFingerprint({ cwd, specBase }) } })
741
+ } catch { /* 观测失败不阻断 */ }
742
+ if (snap) { try { snap.cleanup() } catch { /* 复用路径快照清理失败不阻断返回 */ } }
673
743
  return
674
744
  }
675
- }
676
- // ── 隔离快照定向跑(2026-09-11 驾驭第十二批,用户第三次撞上:noAI 质量扫描是 verify 的
677
- // 第一实测执行点,第八批只接了 gates.js verify 块——本步仍跑 main 工作区,并行 WIP 在此弄红
678
- // 无辜变更)。与 gates.js 同款:createVerifyGateSnapshot(HEAD + 本变更文件集,native worktree
679
- // 定向源),ENV 同 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,基建失败 fallback 主仓 + 脏文件在场时
680
- // ⚠️ 口径可见。指纹(storeQualityScan)仍按主仓 cwd 算——--done 对账复用的指纹匹配口径不变。
681
- let snap = null
682
- if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
683
- // worktree 会话跳快照(2026-09-23 R9/R10 根治):worktree 已是会话独占隔离——快照只剩
684
- // venv junction 冻结/慢 I/O 纯成本(R9 3MB/0CPU 冻结、R10 15min 慢跑实证),直接工作树实测。
685
- let skipForWorktree = false
686
- try {
687
- const { shouldSkipGateSnapshotForWorktree } = await import('./gate-snapshot.js')
688
- skipForWorktree = shouldSkipGateSnapshotForWorktree(cwd)
689
- } catch { /* 判定异常按不跳走原路 */ }
690
- if (skipForWorktree) {
691
- console.log('🔀 cwd 是会话专属 worktree——隔离快照冗余(并行会话不在场;快照 venv junction 冻结/慢 I/O 面 R9/R10 双实证),直接在 worktree 实测')
692
- } else {
693
- try {
694
- const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
695
- snap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
696
- if (snap) {
697
- console.log(`🧪 noAI 扫描隔离快照(根:${snap.snapshotRoot}):HEAD + 本变更 ${snap.changeFileCount} 个文件${snap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与判定)`)
698
- }
699
- } catch { /* 快照链路异常 → 主仓现行为 */ }
745
+ if (passedVerdict.reason) {
746
+ _missReasonParts.push(`passed闸: ${passedVerdict.reason}`)
747
+ console.log(`ℹ️ 质量扫描复用未命中:${passedVerdict.reason}——本轮真跑`)
700
748
  }
701
749
  }
702
- const gateCwd = snap ? snap.snapshotRoot : cwd
703
- const gateSpecBase = snap ? join(snap.snapshotRoot, '.sillyspec') : specBase
704
- if (!snap) {
705
- warnIfMainRepoDirtyForGate(cwd)
706
- }
707
750
  console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.test(同步,长套件 2~10 分钟无输出属正常)——结果按代码指纹落盘,最终 --done 对账直接复用免重跑。`)
708
751
  let testCheck
709
752
  let lintCheck
710
753
  let coverageCheck = null
711
754
  let smokeResult = null
712
755
  try {
713
- testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
756
+ testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, observability: { fingerprint: computeQualityScanFingerprint({ cwd, specBase }), reuse_decision: { layer: 'real-run', hit: false, reason: _missReasonParts.join(';') || null } } })
714
757
  // ── 伪影分诊(ql-20260920-010 修复一 a):失败先分诊,命中 → 主树口径单点复跑对照 ──
715
758
  testCheck = applyArtifactTriageRerun({
716
759
  testCheck,
@@ -752,7 +795,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
752
795
  } finally {
753
796
  if (snap) { try { snap.cleanup() } catch {} }
754
797
  }
755
- storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, usedSnapshot: Boolean(snap) })
798
+ storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck, usedSnapshot: Boolean(snap), missReason: _missReasonParts.join(';') || null, scopeDecision: { planned: !process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF, actual: Boolean(snap) } })
756
799
  const testFailed = testCheck.status === 'failed'
757
800
  const lintBlocked = shouldBlockVerifyLint(lintCheck)
758
801
  // 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档