sillyspec 3.28.5 → 3.28.6
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/package.json +1 -1
- package/sa-rename-probe/src/new.js +4 -0
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +8 -2
- package/src/docs-check.js +9 -5
- package/src/friction-tally.js +221 -0
- package/src/index.js +4 -1
- package/src/progress/change-registry.js +2 -0
- package/src/progress.js +3 -0
- package/src/run/command.js +17 -1
- package/src/run/complete-handlers.js +74 -1
- package/src/run/complete.js +39 -8
- package/src/run/gate-snapshot.js +73 -2
- package/src/run/gates.js +53 -16
- package/src/run/quick-audit.js +40 -3
- package/src/run/shared.js +29 -1
- package/src/run/stage.js +14 -0
- package/src/run/verify-quality-scan.js +52 -5
- package/src/scope-audit.js +231 -25
- package/src/stages/brainstorm.js +3 -3
package/src/run/gate-snapshot.js
CHANGED
|
@@ -28,7 +28,7 @@ function git(cwd, args) {
|
|
|
28
28
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
29
29
|
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
30
30
|
*/
|
|
31
|
-
export function createGateSnapshot({ cwd, files }) {
|
|
31
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
32
32
|
let snapshotRoot = null
|
|
33
33
|
try {
|
|
34
34
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
@@ -38,16 +38,19 @@ export function createGateSnapshot({ cwd, files }) {
|
|
|
38
38
|
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
39
39
|
|
|
40
40
|
// 会话文件 overlay:主仓工作区版本覆盖进快照(本会话的最新态;文件不存在=已删,跳过)。
|
|
41
|
+
// sourceRoot(verify 门定向跑用,2026-09-10 驾驭小结第八批):overlay 源切换为本变更
|
|
42
|
+
// worktree 根——「--worktree 定向跑本变更分支内容」的快照实现(缺省仍是主仓 cwd)。
|
|
41
43
|
// .sillyspec/ 跳过面收窄(P2-d 落地实证的快照盲区):原一刀切跳过整个 .sillyspec/ ——
|
|
42
44
|
// 会话声明的 tracked docs(module-map 补录/知识库/模块卡)进不了快照,门禁读到 HEAD
|
|
43
45
|
// 旧版恒误报(src 未录 module-map 的修复在主仓生效、快照里仍然失败)。真正需要隔离的
|
|
44
46
|
// 是共享运行时面:.runtime/(sqlite 锁/在途 marker)与 quicklog/(跨会话共享账本);
|
|
45
47
|
// local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
|
|
48
|
+
const overlayRoot = sourceRoot || cwd
|
|
46
49
|
let overlaid = 0
|
|
47
50
|
for (const f of files) {
|
|
48
51
|
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
49
52
|
if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
|
|
50
|
-
const src = join(
|
|
53
|
+
const src = join(overlayRoot, f)
|
|
51
54
|
if (!existsSync(src)) continue
|
|
52
55
|
const dst = join(snapshotRoot, f)
|
|
53
56
|
mkdirSync(dirname(dst), { recursive: true })
|
|
@@ -86,3 +89,71 @@ export function createGateSnapshot({ cwd, files }) {
|
|
|
86
89
|
return null
|
|
87
90
|
}
|
|
88
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* verify 门专用快照(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
95
|
+
* main 工作区,多会话并发任何人的 WIP 都能弄红别人的门)。等价「--worktree 定向跑」:
|
|
96
|
+
* overlay 文件集 = resolveVerifyChangedFiles(worktree-aware:含 merge-base 补齐与
|
|
97
|
+
* working-tree),overlay 源 = 本变更 worktree 根(meta 感知;in-place 退主仓 cwd)——
|
|
98
|
+
* 并行会话的在途文件物理不进快照。变更文档(changes/<name>/**,test_strategy 的
|
|
99
|
+
* module-impact.md 等消费)从主仓 specBase 随快照复制。
|
|
100
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
101
|
+
* @returns {Promise<{snapshotRoot: string, cleanup: () => void, overlaid: number, changeFileCount: number}|null>}
|
|
102
|
+
*/
|
|
103
|
+
export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
104
|
+
try {
|
|
105
|
+
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
106
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
107
|
+
const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
|
|
108
|
+
let changeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
|
|
109
|
+
specBase, includeWorkingTree: true,
|
|
110
|
+
}) || []
|
|
111
|
+
if (changeFiles.length === 0) return null // 无变更文件集 → 无从定向,回退主仓
|
|
112
|
+
// 他者声明归属过滤(坑 verify-reconcile-foreign-wip 同款):in-place 模式 working-tree
|
|
113
|
+
// 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
|
|
114
|
+
// 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
|
|
115
|
+
try {
|
|
116
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
117
|
+
const { foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, changeFiles, { specBase, runtimeRoot })
|
|
118
|
+
if (foreign.length > 0) {
|
|
119
|
+
const foreignSet = new Set(foreign.map(x => x.file))
|
|
120
|
+
changeFiles = changeFiles.filter(f => !foreignSet.has(f))
|
|
121
|
+
if (changeFiles.length === 0) return null
|
|
122
|
+
}
|
|
123
|
+
} catch { /* 过滤失败退全量(fail-closed:宁可多 overlay 不漏本变更文件) */ }
|
|
124
|
+
|
|
125
|
+
// overlay 源:worktree meta 感知(native worktree 在则从 worktree 根取「本变更分支内容」)
|
|
126
|
+
let sourceRoot = null
|
|
127
|
+
try {
|
|
128
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
129
|
+
const wm = new WorktreeManager({ cwd })
|
|
130
|
+
const meta = wm.getMeta(changeName)
|
|
131
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
132
|
+
sourceRoot = meta.worktreePath
|
|
133
|
+
}
|
|
134
|
+
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
135
|
+
|
|
136
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot })
|
|
137
|
+
if (!snap) return null
|
|
138
|
+
|
|
139
|
+
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|
|
140
|
+
// 在 .sillyspec(gitignore)不进 HEAD,从主仓 specBase 复制 changes/<name>/**
|
|
141
|
+
try {
|
|
142
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
143
|
+
if (existsSync(changeDir)) {
|
|
144
|
+
const copyDir = (srcDir, dstDir) => {
|
|
145
|
+
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
146
|
+
const s = join(srcDir, e.name); const d = join(dstDir, e.name)
|
|
147
|
+
if (e.isDirectory()) { mkdirSync(d, { recursive: true }); copyDir(s, d) }
|
|
148
|
+
else { mkdirSync(dirname(d), { recursive: true }); copyFileSync(s, d) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
copyDir(changeDir, join(snap.snapshotRoot, '.sillyspec', 'changes', changeName))
|
|
152
|
+
}
|
|
153
|
+
} catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
|
|
154
|
+
|
|
155
|
+
return { ...snap, changeFileCount: changeFiles.length, sourceRoot }
|
|
156
|
+
} catch {
|
|
157
|
+
return null // 任一链路异常 → 回退主仓现行为
|
|
158
|
+
}
|
|
159
|
+
}
|
package/src/run/gates.js
CHANGED
|
@@ -30,6 +30,7 @@ import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './comple
|
|
|
30
30
|
import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
|
|
31
31
|
import { stageRegistry } from '../stages/index.js'
|
|
32
32
|
import { normalizeTaskId } from '../taskcard.js'
|
|
33
|
+
import { recordFrictionEvent } from '../friction-tally.js'
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* 从任务注册表(tasks.md)提取全部 task id(task-XX)——符号影响面覆盖度校验用。
|
|
@@ -506,7 +507,11 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
506
507
|
* 漏写 triggerSync / 写错 return 结构导致行为分裂。返回 nextPendingIdx=currentIdx,
|
|
507
508
|
* 让上层走「完成但不推进」分支,--done 被拒、agent 修复产物后重跑。
|
|
508
509
|
*/
|
|
509
|
-
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts) {
|
|
510
|
+
function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, friction) {
|
|
511
|
+
// friction 尾参(friction-signal-hint task-02):gate 失败摩擦埋点标签 {type, detail}——各调用点
|
|
512
|
+
// 按所属 gate 段传来源标签(审查两段走 review_rejected),未传兜底通用 gate-cascade。
|
|
513
|
+
// recordFrictionEvent 自带静默降级(非法类型/写失败返回 null),不影响回滚/返回语义。
|
|
514
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
510
515
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
511
516
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
512
517
|
pm._write(cwd, progress, changeName)
|
|
@@ -596,7 +601,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
596
601
|
// 产物校验失败必须阻断完成 —— 否则 validator 形同虚设,
|
|
597
602
|
// verify 会带着 FAIL/缺 verify-result.md 被 ✅ 标记完成(历史教训)。
|
|
598
603
|
// plan/execute 的专项契约校验(下方)在产物齐全后才需要继续跑,故此处先 return。
|
|
599
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
604
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'validators' })
|
|
600
605
|
}
|
|
601
606
|
if (contractResult.warnings.length > 0) {
|
|
602
607
|
console.warn(`\n⚠️ 阶段 ${stageName} 校验警告:`)
|
|
@@ -630,6 +635,23 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
630
635
|
// 亲测(本路径语义与产物逐字不变)。
|
|
631
636
|
const { loadReusableQualityScan } = await import('./verify-quality-scan.js')
|
|
632
637
|
const reusableScan = loadReusableQualityScan({ specBase, cwd, changeName })
|
|
638
|
+
// ── 隔离快照定向跑(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
639
|
+
// main 工作区,多会话并发任何人的 WIP 都能弄红别人的门):HEAD 干净基线 + 本变更文件集
|
|
640
|
+
// overlay(worktree meta 在则源=worktree 根——等价「--worktree 定向跑本变更分支内容」)+
|
|
641
|
+
// 变更文档随快照(test_strategy 消费)。并行会话在途文件物理不进快照、不参与本门判定。
|
|
642
|
+
// 快照基建失败 → 回退主仓现行为(失败路径沿用 renderVerifyTestAttribution 归因提示)。
|
|
643
|
+
let verifyGateSnap = null
|
|
644
|
+
if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
|
|
645
|
+
try {
|
|
646
|
+
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
647
|
+
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
648
|
+
if (verifyGateSnap) {
|
|
649
|
+
console.log(`🧪 Verify 实测隔离快照:HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
650
|
+
}
|
|
651
|
+
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
652
|
+
}
|
|
653
|
+
const gateCwd = verifyGateSnap ? verifyGateSnap.snapshotRoot : cwd
|
|
654
|
+
const gateSpecBase = verifyGateSnap ? join(verifyGateSnap.snapshotRoot, '.sillyspec') : specBase
|
|
633
655
|
let testCheck
|
|
634
656
|
if (reusableScan && reusableScan.testResult) {
|
|
635
657
|
testCheck = reusableScan.testResult
|
|
@@ -638,7 +660,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
638
660
|
} else {
|
|
639
661
|
// 测试实测是同步 execSync,长套件可跑 2~10min 且中途无输出——先预告避免 agent 误判卡死
|
|
640
662
|
console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
|
|
641
|
-
testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
|
|
663
|
+
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName, ctx })
|
|
642
664
|
printVerifyTestCheck(testCheck)
|
|
643
665
|
}
|
|
644
666
|
// tests 段二次回填(2026-09-08-ir-verify-facts FR-03 次序:实测在 runValidators 之后,
|
|
@@ -657,6 +679,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
657
679
|
}
|
|
658
680
|
} catch { /* fail-soft:回填失败不影响门禁 */ }
|
|
659
681
|
if (testCheck.status === 'failed') {
|
|
682
|
+
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
660
683
|
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
661
684
|
// 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):单一实现迁至 verify-quality-scan.js
|
|
662
685
|
// renderVerifyTestAttribution(P0-1 前置二:noAI 质量扫描失败路径与本 --done 路径共用,
|
|
@@ -667,7 +690,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
667
690
|
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
668
691
|
} catch { /* 归因提示失败不影响阻断语义 */ }
|
|
669
692
|
console.error(' 请修复失败的测试并更新 verify-result.md 后重新完成此步骤。')
|
|
670
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
693
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-test' })
|
|
671
694
|
}
|
|
672
695
|
// lint 对账(2026-08-21 审查 CLI-1):test 侧"自报告 PASS 但实测失败→阻断"已闭环,
|
|
673
696
|
// lint 侧此前纯口头——CLI 亲自执行 commands.lint,advisory 起步(失败打印不阻断,观察期后升级)
|
|
@@ -677,11 +700,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
677
700
|
if (lintCheck) {
|
|
678
701
|
console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
|
|
679
702
|
} else {
|
|
680
|
-
lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
703
|
+
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase })
|
|
681
704
|
if (lintCheck.status !== 'skipped') {
|
|
682
705
|
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
683
706
|
}
|
|
684
707
|
}
|
|
708
|
+
// 快照收尾(test+lint 两检查都已完成;parity/删除探针走主仓 git 事实不依赖快照)
|
|
709
|
+
const usedSnapshot = Boolean(verifyGateSnap)
|
|
710
|
+
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
685
711
|
printVerifyLintCheck(lintCheck)
|
|
686
712
|
// lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
|
|
687
713
|
// 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
|
|
@@ -693,8 +719,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
693
719
|
console.error(' 输出(末尾):')
|
|
694
720
|
for (const line of tail.split('\n')) console.error(` | ${line}`)
|
|
695
721
|
}
|
|
722
|
+
// 污染归属提示(2026-09-10 驾驭小结第八批建议②,对齐 test 侧 renderVerifyTestAttribution
|
|
723
|
+
// 先例):实测跑在主仓共享工作区(快照不可用/被 ENV 关闭的 fallback 路径)且存在非本变更
|
|
724
|
+
// 的未提交文件时,失败可能是并行会话污染——先做归属鉴定,不静默背锅。
|
|
725
|
+
if (!usedSnapshot) {
|
|
726
|
+
try {
|
|
727
|
+
const { renderVerifyTestAttribution } = await import('./verify-quality-scan.js')
|
|
728
|
+
console.error(' ℹ️ 本次实测跑在主仓共享工作区(未走隔离快照):')
|
|
729
|
+
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
|
|
730
|
+
console.error(' 亦可跑 sillyspec scope-audit --change ' + changeName + ' 做污染归属鉴定(计划×实际三态全表)。')
|
|
731
|
+
} catch { /* 归因提示失败不影响阻断语义 */ }
|
|
732
|
+
}
|
|
696
733
|
console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
|
|
697
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
734
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-lint' })
|
|
698
735
|
}
|
|
699
736
|
// 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
|
|
700
737
|
// 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
|
|
@@ -734,7 +771,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
734
771
|
console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
|
|
735
772
|
console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
|
|
736
773
|
console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
|
|
737
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
774
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
738
775
|
}
|
|
739
776
|
// ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
|
|
740
777
|
// 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
|
|
@@ -760,7 +797,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
760
797
|
// 放行态),fail-soft 不影响 gate 判定;②类阻断回执同样留档供平台/审计消费
|
|
761
798
|
writeReconcileRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: reconcileEnvelope, result: reconcileCheck })
|
|
762
799
|
if (reconcileBlocked) {
|
|
763
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
800
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
764
801
|
}
|
|
765
802
|
// ── verify-result.md 探针预填段一致性抽查(task-03 / ir-stage-p3b D-002@v1 方案A)──
|
|
766
803
|
// 「#### 探针 N」机械预填段(verify-probes --init 生成)长在 agent 可编辑的正文里,其防篡改
|
|
@@ -789,7 +826,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
789
826
|
// 两检查间耦合;各写各的回执、按文件名订阅更干净。fail-soft 不影响 gate 判定
|
|
790
827
|
writeProbeConsistencyRunResult({ runtimeRoot: reconcileRuntimeRoot, changeName, envelope: probeEnvelope, result: probeCheck })
|
|
791
828
|
if (probeBlocked) {
|
|
792
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
829
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
793
830
|
}
|
|
794
831
|
// ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
|
|
795
832
|
// 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
|
|
@@ -823,7 +860,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
823
860
|
console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
|
|
824
861
|
for (const row of pendingRows) console.error(` - ${row}`)
|
|
825
862
|
console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
|
|
826
|
-
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
863
|
+
return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'verify-contract' })
|
|
827
864
|
}
|
|
828
865
|
}
|
|
829
866
|
}
|
|
@@ -852,7 +889,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
852
889
|
for (const err of planValidation.errors) console.error(` - ${err}`)
|
|
853
890
|
console.error(`\n tasks.md/plan.md 不满足 execute 契约,请修复后重新完成此步骤。`)
|
|
854
891
|
// 阻断 completed
|
|
855
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
892
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'plan-execute-contract' })
|
|
856
893
|
}
|
|
857
894
|
if (planValidation.warnings.length > 0) {
|
|
858
895
|
console.warn(`\n⚠️ Plan contract 警告(不阻断完成):`)
|
|
@@ -913,7 +950,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
913
950
|
const reviewResult = gated.result
|
|
914
951
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
915
952
|
if (!reviewResult.ok) {
|
|
916
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
953
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
|
|
917
954
|
}
|
|
918
955
|
// 降级自审留痕(2026-09-10 用户反馈①:PI agent 等宿主无 Agent tool,prompt 降级条款允许
|
|
919
956
|
// 当前 agent 自审产出 review.json)。放行但独立性折损必须可见:⚠️ 审计行区分降级 review
|
|
@@ -933,7 +970,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
933
970
|
} catch (e) {
|
|
934
971
|
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
935
972
|
console.error('❌ Stage Review Gate 异常,阻断 ' + stageName + ' 完成: ' + e.message)
|
|
936
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
973
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'stage-review' })
|
|
937
974
|
}
|
|
938
975
|
}
|
|
939
976
|
|
|
@@ -1042,7 +1079,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1042
1079
|
console.error('\n⚠️ 部分任务已在 tasks.md 中勾选,但 review.json 不存在。')
|
|
1043
1080
|
console.error(` 请取消勾选这些任务的 checkbox,或补充对应的 review.json(execute run ID: ${executeRunId})。`)
|
|
1044
1081
|
}
|
|
1045
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1082
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
|
|
1046
1083
|
}
|
|
1047
1084
|
|
|
1048
1085
|
// cannot_verify 的 requiredEvidence 写入 change 目录,供 verify 阶段消费
|
|
@@ -1058,7 +1095,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1058
1095
|
// fail-closed:Gate 自身异常时不能默认放行,否则异常成了绕过评审的通道
|
|
1059
1096
|
console.error(`❌ Task Review Gate 异常,阻断 execute 完成: ${e.message}`)
|
|
1060
1097
|
console.error(' 请检查 review.json / plan.md 是否可读,修复后重新完成此步骤。')
|
|
1061
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1098
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'review_rejected', detail: 'task-review' })
|
|
1062
1099
|
}
|
|
1063
1100
|
}
|
|
1064
1101
|
return null
|
|
@@ -1312,7 +1349,7 @@ export async function completeStageGates({ stageName, cwd, changeName, platformO
|
|
|
1312
1349
|
}
|
|
1313
1350
|
} catch (e) {
|
|
1314
1351
|
console.error(`\n❌ ${stageName} 阶段完成收尾异常(已 rollback 为 in-progress,请修复后重新 --done):${e?.message || e}`)
|
|
1315
|
-
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
|
|
1352
|
+
return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts, { type: 'gate_rollback', detail: 'internal-error' })
|
|
1316
1353
|
}
|
|
1317
1354
|
|
|
1318
1355
|
// execute worktree cleanup(completeStep / continueStep 完成分支统一调用,避免双清理)
|
package/src/run/quick-audit.js
CHANGED
|
@@ -33,9 +33,38 @@ export function printQuickAuditReview(review) {
|
|
|
33
33
|
console.error(` sillyspec run quick --done --allow-delete --change <id> --output "..."`)
|
|
34
34
|
console.error(` (--force-baseline / --allow-new 不能解锁删除;不确认请 git restore 撤回删除后再 --done,或走完整流程 execute 由 review 把关)`)
|
|
35
35
|
} else {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// 分流点名(2026-09-11 用户实证:allowedFiles 推断漏了三个实改文件,被与「危险文件」
|
|
37
|
+
// 混在一起推 --force-baseline——语义过宽且吓人)。按 reason 类别给精确最小解锁:
|
|
38
|
+
// 超出 allowedFiles(非危险)→ 追加 --files 并入边界即可;危险/baseline → --force-baseline;
|
|
39
|
+
// 新增 → --allow-new。未命中的 flag 不再出现在咒语里。
|
|
40
|
+
const prot = review.reasons.filter((r) => r.startsWith('危险文件变更') || r.startsWith('覆盖 baseline'))
|
|
41
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
42
|
+
const news = review.reasons.filter((r) => r.startsWith('新增文件'))
|
|
43
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
44
|
+
const undecl = review.reasons.filter((r) => r.startsWith('超出 allowedFiles'))
|
|
45
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
46
|
+
const flags = []
|
|
47
|
+
if (undecl.length > 0) {
|
|
48
|
+
flags.push('--files <原声明文件>,' + undecl.join(','))
|
|
49
|
+
console.error(` 📋 边界归属未声明(非危险变更):${undecl.join(', ')}——本会话实际改动但未在 --files 边界内(自动推断漏掉),追加声明即可:`)
|
|
50
|
+
console.error(` sillyspec run quick --files ${undecl.join(',')} --change <id>(恢复会话追加边界,追加不替换)`)
|
|
51
|
+
}
|
|
52
|
+
if (news.length > 0) {
|
|
53
|
+
flags.push('--allow-new')
|
|
54
|
+
console.error(` 📋 新增文件待放行:${news.join(', ')}——确认收进本会话带 --allow-new。`)
|
|
55
|
+
}
|
|
56
|
+
if (prot.length > 0) {
|
|
57
|
+
flags.push('--force-baseline')
|
|
58
|
+
console.error(` ⛔ 受保护/危险文件变更:${prot.join(', ')}——放行开关唯 --force-baseline(--files 只声明归属不改变危险判定)。`)
|
|
59
|
+
}
|
|
60
|
+
if (flags.length > 0) {
|
|
61
|
+
console.error(` 解锁命令(按上方分类携带最小 flag 集):`)
|
|
62
|
+
console.error(` sillyspec run quick --done ${flags.join(' ')} --change <id> --output "..."`)
|
|
63
|
+
} else {
|
|
64
|
+
console.error(` 如确认接受这些变更,重新运行 --done 时带上对应 flag 即可解锁:`)
|
|
65
|
+
console.error(` sillyspec run quick --done --force-baseline --allow-new --change <id> --output "..."`)
|
|
66
|
+
console.error(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件;--allow-delete 允许删除文件)`)
|
|
67
|
+
}
|
|
39
68
|
// 两套开关明示(坑 files-flag-not-unlock-protected,2026-08-22 实证:模块文档被判危险
|
|
40
69
|
// 文件,追加 --files 边界不解锁仍被拦——「追加边界」与「解锁拦截」是两套开关,交互上易误解
|
|
41
70
|
// 前者能解决后者)。命中危险文件 reason 时点名:--files 只改归属口径(哪些文件计入本
|
|
@@ -109,6 +138,14 @@ export function printQuickAuditReview(review) {
|
|
|
109
138
|
}
|
|
110
139
|
if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
111
140
|
console.warn(`\n📎 文档引用失效(docs check):本次改动的文档含 ${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处失效 file:line 引用。`)
|
|
141
|
+
// 逐条指名(2026-09-11 用户实证:只报计数不指名哪处,四轮复现才定位——门禁输出
|
|
142
|
+
// 直接带 文件:行号,另跑命令是复现不是定位);封顶 10 与生产端对齐,超出提示全量口径
|
|
143
|
+
for (const i of (review.docsCheckHint.invalidRefs || [])) {
|
|
144
|
+
console.warn(` ❌ [${i.doc}:${i.docLine}] ${i.ref} → ${i.reason}`)
|
|
145
|
+
}
|
|
146
|
+
if (review.docsCheckHint.invalidTruncated) {
|
|
147
|
+
console.warn(` … 共 ${review.docsCheckHint.invalid} 处(上方封顶 10,全量跑 sillyspec docs check)`)
|
|
148
|
+
}
|
|
112
149
|
console.warn(` 行号漂移 → 更新到当前源码;文件删改名 → 更新引用路径。跑 sillyspec docs check 可看完整清单。`)
|
|
113
150
|
console.warn(` 引用格式:\`src/foo.js:42\`(或 42-48)+ 同行反引号代码符号(如 \`runDocsCheck\`)——符号可让 --suggest 给出候选行号。`)
|
|
114
151
|
}
|
package/src/run/shared.js
CHANGED
|
@@ -70,6 +70,28 @@ export function assertSafeChangeName(name, label = '变更名') {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// ── 变更名日期前缀门禁(brainstorm step6 规则 CLI 化)──
|
|
74
|
+
// 规则出处:stages/brainstorm.js step6「变更名格式 YYYY-MM-DD-<简短描述>」此前只是 prompt 约束,
|
|
75
|
+
// agent 自拟/重命名丢日期前缀时 CLI 照单全收静默物化(2026-09-11 实证:friction-signal-hint)。
|
|
76
|
+
// 语义:只拦「新建/改名」——调用方须先确认名字未物化(无 DB 行、无 changes/ 目录含归档),
|
|
77
|
+
// 历史 DB/目录已存在的旧变更不追诉(归档里 auto-flow-optimization 等 10 个无前缀名照常可读)。
|
|
78
|
+
// 豁免:quick-<8hex> 会话 key 与 default 兜底 key 是系统生成名,非 agent 自拟描述名。
|
|
79
|
+
// 日期只校验形状(月 01-12 日 01-31),不校验「当天」——跨天续跑/次日重命名不应被拦。
|
|
80
|
+
const DATED_CHANGE_NAME_RE = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])-[a-zA-Z0-9][\w.\-]*$/
|
|
81
|
+
export function assertDatedChangeName(name, label = '变更名') {
|
|
82
|
+
if (name == null) return
|
|
83
|
+
const s = String(name)
|
|
84
|
+
if (s === 'default' || /^quick-[0-9a-f]{8}$/.test(s)) return
|
|
85
|
+
if (DATED_CHANGE_NAME_RE.test(s)) return
|
|
86
|
+
const now = new Date()
|
|
87
|
+
const pad = n => String(n).padStart(2, '0')
|
|
88
|
+
const example = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-<简短描述>`
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${label}「${s}」缺少日期前缀或格式非法——新变更名必须形如 YYYY-MM-DD-<简短描述>(例如 ${example})。` +
|
|
91
|
+
`此为 brainstorm step6 既定规则,现由 CLI 强制:格式不合规不予执行。已存在的旧名变更不受影响`
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
import { buildExecuteSteps } from '../stages/execute.js'
|
|
74
96
|
import { buildPlanSteps } from '../stages/plan.js'
|
|
75
97
|
import { stageRegistry } from '../stages/index.js'
|
|
@@ -1427,7 +1449,13 @@ export async function auditQuickCompletion(cwd, guard, options = {}) {
|
|
|
1427
1449
|
const dc = runDocsCheck({ projectRoot: cwd, docs: mdChanged, crossRepoRoots: quickCfg.crossRepoRoots })
|
|
1428
1450
|
if (!dc.ok) {
|
|
1429
1451
|
result.reasons.push(`本次改动文档含 ${dc.invalid.length} 处失效 file:line 引用(sillyspec docs check 可复现)`)
|
|
1430
|
-
|
|
1452
|
+
// invalidRefs 明细(2026-09-11 用户实证:只报计数 N/M 不指名哪处,四轮才定位到一处
|
|
1453
|
+
// 预存债——门禁输出直接带 文件:行号,另跑 docs check 复现是逃生不是定位):封顶 10 条
|
|
1454
|
+
result.docsCheckHint = {
|
|
1455
|
+
invalid: dc.invalid.length, total: dc.total,
|
|
1456
|
+
invalidRefs: dc.invalid.slice(0, 10).map(i => ({ doc: i.doc, docLine: i.docLine, ref: i.ref, reason: String(i.reason || '').slice(0, 120) })),
|
|
1457
|
+
invalidTruncated: dc.invalid.length > 10,
|
|
1458
|
+
}
|
|
1431
1459
|
if (result.status === 'safe') result.status = 'warning'
|
|
1432
1460
|
}
|
|
1433
1461
|
} catch { /* docs-check 不可用(老版本包等)→ 静默跳过,不阻断 quick */ }
|
package/src/run/stage.js
CHANGED
|
@@ -476,6 +476,20 @@ export async function runStage(pm, progress, stageName, cwd, changeName, skipApp
|
|
|
476
476
|
if (allowNew) parts.push('允许新增文件')
|
|
477
477
|
if (allowDelete) parts.push('允许删除文件')
|
|
478
478
|
console.log(`🛡️ quick 变更边界已记录: ${parts.join(', ')}`)
|
|
479
|
+
// 归属点名(2026-09-11 用户实证:--done 审计的 allowedFiles 自动推断可漏实改文件,
|
|
480
|
+
// 被拦后与危险文件混推 --force-baseline。启动未显式 --files 时,把工作区 src/test 前缀
|
|
481
|
+
// 脏文件点名让用户当场确认归属——预声明即精确边界,免去收尾被拦一轮)。
|
|
482
|
+
if (allowedFiles.length === 0 && Array.isArray(baselineFiles)) {
|
|
483
|
+
const declCandidates = baselineFiles
|
|
484
|
+
.map((p) => String(p).split('\\').join('/'))
|
|
485
|
+
.filter((p) => p.startsWith('src/') || p.startsWith('test/'))
|
|
486
|
+
if (declCandidates.length > 0) {
|
|
487
|
+
console.log(`👀 工作区已有 ${declCandidates.length} 个 src/test 脏文件(本会话如会改到其中部分,建议现在声明边界;并行会话的他者改动勿声明):`)
|
|
488
|
+
for (const p of declCandidates.slice(0, 8)) console.log(` - ${p}`)
|
|
489
|
+
if (declCandidates.length > 8) console.log(` … 共 ${declCandidates.length} 个`)
|
|
490
|
+
console.log(` 声明:sillyspec run quick --files <a.js,b.js> --change ${changeName}(恢复会话追加边界)`)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
479
493
|
console.log(`📝 QUICKLOG 条目已创建: ${qlId}`)
|
|
480
494
|
// 空壳会话探测(坑 quick-duplicate-empty-shell,2026-09-03 用户实证:一次输出被吞 →
|
|
481
495
|
// agent 误判失败重跑 → 重复空壳会话,此前只能手工 reset + 删骨架)。新会话起步时点名
|
|
@@ -27,8 +27,9 @@ import { existsSync, readFileSync, mkdirSync } from 'node:fs'
|
|
|
27
27
|
import { join, dirname } from 'node:path'
|
|
28
28
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
29
29
|
import { gitQuiet } from '../git-helper.js'
|
|
30
|
-
import { resolveRuntimeRoot } from './shared.js'
|
|
30
|
+
import { resolveRuntimeRoot, parsePorcelainPath } from './shared.js'
|
|
31
31
|
import { detectChangeRisk } from '../change-risk-profile.js'
|
|
32
|
+
import { recordFrictionEvent } from '../friction-tally.js'
|
|
32
33
|
|
|
33
34
|
const RECORD_SCHEMA_VERSION = 1
|
|
34
35
|
|
|
@@ -243,6 +244,20 @@ export function renderCoverageExistenceReport(cov) {
|
|
|
243
244
|
return lines.join('\n')
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
|
|
248
|
+
/** fallback 口径可见性(2026-09-11 驾驭第十二批):快照不可用回退主仓且主仓存在非 .sillyspec
|
|
249
|
+
* 脏文件时,明示实测判定面含并行 WIP——第三撞根因是「静默 fallback 看不出走了哪个口径」。 */
|
|
250
|
+
function warnIfMainRepoDirtyForGate(cwd) {
|
|
251
|
+
try {
|
|
252
|
+
const st = gitQuiet(cwd, ['status', '--porcelain'])
|
|
253
|
+
if (!st) return
|
|
254
|
+
const dirty = st.split('\n').map(l => parsePorcelainPath(l)).filter(p => p && !p.startsWith('.sillyspec/'))
|
|
255
|
+
if (dirty.length > 0) {
|
|
256
|
+
console.warn(`⚠️ 本次实测未走隔离快照(基建不可用/变更文件集为空),判定面 = main 工作区——当前存在 ${dirty.length} 个非 .sillyspec 脏文件(${dirty.slice(0, 3).join('、')}${dirty.length > 3 ? ' 等' : ''}),并行会话 WIP 可能影响判定;失败时先做污染归属鉴定再修。`)
|
|
257
|
+
}
|
|
258
|
+
} catch { /* git 不可用 → 静默(原本也无快照) */ }
|
|
259
|
+
}
|
|
260
|
+
|
|
246
261
|
/**
|
|
247
262
|
* noAI 动作本体:提前实测 test+lint → 指纹落盘 → 全绿返回(stage.js 盖 completed 自动前进);
|
|
248
263
|
* 失败(test failed / lint 硬门档 failed)打印归因提示后 throw——步骤不推进,agent 修复后
|
|
@@ -250,26 +265,58 @@ export function renderCoverageExistenceReport(cov) {
|
|
|
250
265
|
*/
|
|
251
266
|
export async function executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts }) {
|
|
252
267
|
const { runVerifyTestCheck, printVerifyTestCheck, runVerifyLintCheck, printVerifyLintCheck, shouldBlockVerifyLint } = await import('../verify-postcheck.js')
|
|
268
|
+
// ── 隔离快照定向跑(2026-09-11 驾驭第十二批,用户第三次撞上:noAI 质量扫描是 verify 的
|
|
269
|
+
// 第一实测执行点,第八批只接了 gates.js verify 块——本步仍跑 main 工作区,并行 WIP 在此弄红
|
|
270
|
+
// 无辜变更)。与 gates.js 同款:createVerifyGateSnapshot(HEAD + 本变更文件集,native worktree
|
|
271
|
+
// 定向源),ENV 同 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF,基建失败 fallback 主仓 + 脏文件在场时
|
|
272
|
+
// ⚠️ 口径可见。指纹(storeQualityScan)仍按主仓 cwd 算——--done 对账复用的指纹匹配口径不变。
|
|
273
|
+
let snap = null
|
|
274
|
+
if (!process.env.SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF) {
|
|
275
|
+
try {
|
|
276
|
+
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
277
|
+
snap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
278
|
+
if (snap) {
|
|
279
|
+
console.log(`🧪 noAI 扫描隔离快照:HEAD + 本变更 ${snap.changeFileCount} 个文件${snap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与判定)`)
|
|
280
|
+
}
|
|
281
|
+
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
282
|
+
}
|
|
283
|
+
const gateCwd = snap ? snap.snapshotRoot : cwd
|
|
284
|
+
const gateSpecBase = snap ? join(snap.snapshotRoot, '.sillyspec') : specBase
|
|
285
|
+
if (!snap) {
|
|
286
|
+
warnIfMainRepoDirtyForGate(cwd)
|
|
287
|
+
}
|
|
253
288
|
console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.test(同步,长套件 2~10 分钟无输出属正常)——结果按代码指纹落盘,最终 --done 对账直接复用免重跑。`)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
let testCheck
|
|
290
|
+
let lintCheck
|
|
291
|
+
let coverageCheck = null
|
|
292
|
+
try {
|
|
293
|
+
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
|
|
294
|
+
printVerifyTestCheck(testCheck)
|
|
295
|
+
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase })
|
|
257
296
|
if (lintCheck.status !== 'skipped') {
|
|
258
297
|
console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.lint…`)
|
|
259
298
|
}
|
|
260
299
|
printVerifyLintCheck(lintCheck)
|
|
261
300
|
// P2(noai-ir-roadmap §5):coverage 存在性事实——commands.coverage 显式配置才采集,
|
|
262
301
|
// 只证「有覆盖记录」不证「覆盖了行为」(渲染自带语义边界),fail-soft 不阻断。
|
|
263
|
-
let coverageCheck = null
|
|
264
302
|
try {
|
|
265
303
|
coverageCheck = await runCoverageExistenceCheck({ cwd, specBase, changeName })
|
|
266
304
|
console.log(renderCoverageExistenceReport(coverageCheck))
|
|
267
305
|
} catch (e) {
|
|
268
306
|
console.warn(`⚠️ coverage 存在性事实采集异常(fail-soft): ${e && e.message ? e.message : e}`)
|
|
269
307
|
}
|
|
308
|
+
} finally {
|
|
309
|
+
if (snap) { try { snap.cleanup() } catch {} }
|
|
310
|
+
}
|
|
270
311
|
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, coverageCheck })
|
|
271
312
|
const testFailed = testCheck.status === 'failed'
|
|
272
313
|
const lintBlocked = shouldBlockVerifyLint(lintCheck)
|
|
314
|
+
// 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档
|
|
315
|
+
// lint 失败(SILLYSPEC_VERIFY_LINT_GATE=advisory 时不 throw)同样是摩擦信号,计数不应随
|
|
316
|
+
// 逃生阀漂移。recordFrictionEvent 自带静默降级,不影响本步推进/throw 语义。
|
|
317
|
+
if (testFailed || lintCheck.status === 'failed') {
|
|
318
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'verify_run_failed', detail: testFailed ? 'test' : 'lint' })
|
|
319
|
+
}
|
|
273
320
|
if (testFailed || lintBlocked) {
|
|
274
321
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
275
322
|
await renderVerifyTestAttribution({ cwd, changeName, specBase, runtimeRoot })
|