sillyspec 3.28.5 → 3.28.7
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/attachments/2ab2fd87cfa568ca6acadbb934a651d293a0ad8f3227c66a70b56dcf53a0ea3d.pptx +0 -0
- package/attachments/dc4c2b314ee0c295e1fe5243073eb5c7df7e429074ad7c14424523fbe86e6193.pptx +0 -0
- package/meta.json +11 -0
- package/package.json +1 -1
- package/sa-rename-probe/src/new.js +4 -0
- package/src/config-schema.js +26 -0
- package/src/decision-distill.js +14 -4
- package/src/dispatch/probe.js +8 -1
- package/src/docs-check.js +9 -5
- package/src/friction-tally.js +221 -0
- package/src/index.js +4 -1
- package/src/knowledge-match.js +90 -6
- package/src/progress/change-registry.js +28 -0
- package/src/progress.js +6 -0
- package/src/review-dispatch.js +7 -1
- package/src/run/command.js +17 -1
- package/src/run/complete-handlers.js +105 -2
- package/src/run/complete.js +53 -10
- package/src/run/gate-snapshot.js +73 -2
- package/src/run/gates.js +53 -16
- package/src/run/prompt.js +53 -1
- package/src/run/quick-audit.js +95 -5
- 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 +308 -27
- package/src/semantic-guard.js +207 -0
- package/src/sillyhub-mcp/client.js +14 -0
- package/src/stages/brainstorm.js +3 -3
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/prompt.js
CHANGED
|
@@ -22,7 +22,8 @@ import { basename, join } from 'node:path'
|
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
23
23
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
24
|
import { stageRegistry } from '../stages/index.js'
|
|
25
|
-
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
25
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
26
|
+
import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-guard.js'
|
|
26
27
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
27
28
|
import { nowWallClock } from '../datetime.js'
|
|
28
29
|
import { parseModuleMapSimple } from '../modules.js'
|
|
@@ -181,6 +182,44 @@ export function buildQuickContextDigest(specBase, projectName) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
/**
|
|
186
|
+
* quick step1 语义护栏进场注入拼装(task-05,FR-03,D-001@v1 模块四):开关 → 候选文件采集 →
|
|
187
|
+
* renderSemanticGuardBlock 反查。候选 = 会话声明文件(guard.allowedFiles,readQuickGuardField
|
|
188
|
+
* 同源读取:session guard 优先 / legacy 单文件回退)∪ git status --porcelain 脏文件
|
|
189
|
+
* (parsePorcelainPath 同款解析口径:引号剥离 / -> rename / 反斜杠归一),反斜杠归一去重封顶 20。
|
|
190
|
+
* 返回 ''(开关关 / 无候选 / 双零命中——prompt 与现状字节一致,零命中静默)或 advisory 段;
|
|
191
|
+
* 全链 fail-soft:异常返回单行降级说明,不阻断 quick 启动。导出供注入单测直测
|
|
192
|
+
* (test/semantic-guard-prompt-inject.test.mjs)。
|
|
193
|
+
*/
|
|
194
|
+
export function buildQuickSemanticGuardInjection({ specBase, cwd, changeName } = {}) {
|
|
195
|
+
try {
|
|
196
|
+
// 开关首行(Grill X-012):false 直接零输出——不采候选文件、不跑反查(开关语义全停非半停)
|
|
197
|
+
if (!readSemanticGuardEnabled(specBase)) return ''
|
|
198
|
+
// 会话声明文件(--files 落 guard.allowedFiles;缺失 / 形态异常按空)
|
|
199
|
+
const declaredRaw = readQuickGuardField(changeName, specBase, 'allowedFiles')
|
|
200
|
+
const declared = Array.isArray(declaredRaw) ? declaredRaw : []
|
|
201
|
+
// git 脏文件:porcelain 解析同源 quick-audit(trim:false——首列前导空格是状态码,trim 会削掉
|
|
202
|
+
// 致 parsePorcelainPath 丢首字符;.sillyspec/ 运行时产物过滤同款——候选面是代码文件)
|
|
203
|
+
let dirty = []
|
|
204
|
+
const statusResult = safeGit(cwd, ['status', '--porcelain'], { trim: false })
|
|
205
|
+
if (!statusResult.error) {
|
|
206
|
+
dirty = (statusResult.value || '').split('\n').filter(Boolean)
|
|
207
|
+
.map(l => parsePorcelainPath(l))
|
|
208
|
+
.filter(Boolean)
|
|
209
|
+
.filter(f => !f.startsWith('.sillyspec/'))
|
|
210
|
+
}
|
|
211
|
+
// 去重(反斜杠归一后;renderSemanticGuardBlock 内侧亦有同款归一,此处为封顶前的口径)
|
|
212
|
+
// 封顶 20 —— 与 collectRecentForeignDelivery 内侧文件封顶同数
|
|
213
|
+
const candidateFiles = [...new Set(
|
|
214
|
+
[...declared, ...dirty].map(f => String(f || '').trim().replace(/\\/g, '/')).filter(Boolean)
|
|
215
|
+
)].slice(0, 20)
|
|
216
|
+
if (candidateFiles.length === 0) return ''
|
|
217
|
+
return renderSemanticGuardBlock({ specBase, cwd, candidateFiles, currentChange: changeName })
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return `【语义护栏】注入失败(${e && e.message ? e.message : e})——跳过反查,不阻断本步骤;需要时自查 knowledge/INDEX.md 与 git log`
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
184
223
|
// parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
|
|
185
224
|
// 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
|
|
186
225
|
/**
|
|
@@ -1061,6 +1100,19 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1061
1100
|
}
|
|
1062
1101
|
}
|
|
1063
1102
|
|
|
1103
|
+
// ── 语义护栏进场注入(task-05,FR-03,D-001@v1 模块四)──
|
|
1104
|
+
// 判定口径独立:与上方模块上下文注入同锚(quickFirstStep——quick step1「理解任务」),不嵌进
|
|
1105
|
+
// {QUICK_CONTEXT_DIGEST} 占位符守卫内——模板改版去占位符时护栏静默失效(plan 审查 N2)。
|
|
1106
|
+
// 非空追加到 step1 prompt 末尾;空串(开关关 / 零命中)prompt 与现状字节一致(零命中静默)。
|
|
1107
|
+
// 渲染层注入,src/stages/quick.js 模板零变更(design 明确不改清单)。全链 fail-soft 见 helper。
|
|
1108
|
+
if (quickFirstStep) {
|
|
1109
|
+
const sgSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1110
|
+
const semanticGuardBlock = buildQuickSemanticGuardInjection({ specBase: sgSpecBase, cwd, changeName })
|
|
1111
|
+
if (semanticGuardBlock) {
|
|
1112
|
+
promptText = promptText + '\n\n' + semanticGuardBlock
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1064
1116
|
// 平台模式 prompt 自检:确保没有裸相对输出路径
|
|
1065
1117
|
// 只匹配正向写入指令中的裸路径,避免误杀「禁止写入 .sillyspec/」等安全说明
|
|
1066
1118
|
if ((platformOpts?.specRoot || platformOpts?.runtimeRoot) && stageName === 'scan') {
|
package/src/run/quick-audit.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import { parsePorcelainPath, safeGit } from './shared.js'
|
|
16
|
+
import { collectRecentForeignDelivery, detectAssertionRewrites, readSemanticGuardEnabled } from '../semantic-guard.js'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* 打印 quick 完成审计结论(按 review.status 输出 SAFE/WARNING/BLOCKED)。
|
|
@@ -33,9 +34,38 @@ export function printQuickAuditReview(review) {
|
|
|
33
34
|
console.error(` sillyspec run quick --done --allow-delete --change <id> --output "..."`)
|
|
34
35
|
console.error(` (--force-baseline / --allow-new 不能解锁删除;不确认请 git restore 撤回删除后再 --done,或走完整流程 execute 由 review 把关)`)
|
|
35
36
|
} else {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// 分流点名(2026-09-11 用户实证:allowedFiles 推断漏了三个实改文件,被与「危险文件」
|
|
38
|
+
// 混在一起推 --force-baseline——语义过宽且吓人)。按 reason 类别给精确最小解锁:
|
|
39
|
+
// 超出 allowedFiles(非危险)→ 追加 --files 并入边界即可;危险/baseline → --force-baseline;
|
|
40
|
+
// 新增 → --allow-new。未命中的 flag 不再出现在咒语里。
|
|
41
|
+
const prot = review.reasons.filter((r) => r.startsWith('危险文件变更') || r.startsWith('覆盖 baseline'))
|
|
42
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
43
|
+
const news = review.reasons.filter((r) => r.startsWith('新增文件'))
|
|
44
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
45
|
+
const undecl = review.reasons.filter((r) => r.startsWith('超出 allowedFiles'))
|
|
46
|
+
.map((r) => r.split(': ')[1]).filter(Boolean)
|
|
47
|
+
const flags = []
|
|
48
|
+
if (undecl.length > 0) {
|
|
49
|
+
flags.push('--files <原声明文件>,' + undecl.join(','))
|
|
50
|
+
console.error(` 📋 边界归属未声明(非危险变更):${undecl.join(', ')}——本会话实际改动但未在 --files 边界内(自动推断漏掉),追加声明即可:`)
|
|
51
|
+
console.error(` sillyspec run quick --files ${undecl.join(',')} --change <id>(恢复会话追加边界,追加不替换)`)
|
|
52
|
+
}
|
|
53
|
+
if (news.length > 0) {
|
|
54
|
+
flags.push('--allow-new')
|
|
55
|
+
console.error(` 📋 新增文件待放行:${news.join(', ')}——确认收进本会话带 --allow-new。`)
|
|
56
|
+
}
|
|
57
|
+
if (prot.length > 0) {
|
|
58
|
+
flags.push('--force-baseline')
|
|
59
|
+
console.error(` ⛔ 受保护/危险文件变更:${prot.join(', ')}——放行开关唯 --force-baseline(--files 只声明归属不改变危险判定)。`)
|
|
60
|
+
}
|
|
61
|
+
if (flags.length > 0) {
|
|
62
|
+
console.error(` 解锁命令(按上方分类携带最小 flag 集):`)
|
|
63
|
+
console.error(` sillyspec run quick --done ${flags.join(' ')} --change <id> --output "..."`)
|
|
64
|
+
} else {
|
|
65
|
+
console.error(` 如确认接受这些变更,重新运行 --done 时带上对应 flag 即可解锁:`)
|
|
66
|
+
console.error(` sillyspec run quick --done --force-baseline --allow-new --change <id> --output "..."`)
|
|
67
|
+
console.error(` (--force-baseline 覆盖受保护/危险文件如 src/run.js;--allow-new 允许新增文件;--allow-delete 允许删除文件)`)
|
|
68
|
+
}
|
|
39
69
|
// 两套开关明示(坑 files-flag-not-unlock-protected,2026-08-22 实证:模块文档被判危险
|
|
40
70
|
// 文件,追加 --files 边界不解锁仍被拦——「追加边界」与「解锁拦截」是两套开关,交互上易误解
|
|
41
71
|
// 前者能解决后者)。命中危险文件 reason 时点名:--files 只改归属口径(哪些文件计入本
|
|
@@ -109,6 +139,14 @@ export function printQuickAuditReview(review) {
|
|
|
109
139
|
}
|
|
110
140
|
if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
111
141
|
console.warn(`\n📎 文档引用失效(docs check):本次改动的文档含 ${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处失效 file:line 引用。`)
|
|
142
|
+
// 逐条指名(2026-09-11 用户实证:只报计数不指名哪处,四轮复现才定位——门禁输出
|
|
143
|
+
// 直接带 文件:行号,另跑命令是复现不是定位);封顶 10 与生产端对齐,超出提示全量口径
|
|
144
|
+
for (const i of (review.docsCheckHint.invalidRefs || [])) {
|
|
145
|
+
console.warn(` ❌ [${i.doc}:${i.docLine}] ${i.ref} → ${i.reason}`)
|
|
146
|
+
}
|
|
147
|
+
if (review.docsCheckHint.invalidTruncated) {
|
|
148
|
+
console.warn(` … 共 ${review.docsCheckHint.invalid} 处(上方封顶 10,全量跑 sillyspec docs check)`)
|
|
149
|
+
}
|
|
112
150
|
console.warn(` 行号漂移 → 更新到当前源码;文件删改名 → 更新引用路径。跑 sillyspec docs check 可看完整清单。`)
|
|
113
151
|
console.warn(` 引用格式:\`src/foo.js:42\`(或 42-48)+ 同行反引号代码符号(如 \`runDocsCheck\`)——符号可让 --suggest 给出候选行号。`)
|
|
114
152
|
}
|
|
@@ -205,6 +243,32 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
205
243
|
|
|
206
244
|
// ============ quick --done test+lint 硬门禁(2026-09-02 跨 agent 工单 P0-2)============
|
|
207
245
|
|
|
246
|
+
/**
|
|
247
|
+
* 语义护栏命中组装(change: 2026-09-11-cross-change-decision-guard,task-06,FR-04,
|
|
248
|
+
* D-001@v1):断言重写检测 ∩ 近因他者交付归因 → hits 清单,gate 内一行调用拼进返回对象。
|
|
249
|
+
* 单独抽出导出是为了可测性(runQuickTestLintGate 会真跑 test/lint 命令,检测组装层
|
|
250
|
+
* 独立测可避免测试里真跑 npm test——卡内可测性豁免)。
|
|
251
|
+
* - 步骤 0 总开关:readSemanticGuardEnabled false → 跳过检测,hits 恒空(fail-open 同 task-03)
|
|
252
|
+
* - 纯新增断言(detect 不命中)/ 无他者归因标记 / 非测试文件 → 落不到 hits
|
|
253
|
+
* - WARNING 级非阻断:本函数只组装数据,不参与 gate action/failed 判定
|
|
254
|
+
* @param {{ cwd?: string, specBase?: string, files?: string[], changeName?: string|null }} opts
|
|
255
|
+
* @returns {{ hits: Array<{ file: string, deliveredBy: string, sampleLines: string[] }> }}
|
|
256
|
+
*/
|
|
257
|
+
export function buildSemanticGuardHits({ cwd, specBase, files, changeName } = {}) {
|
|
258
|
+
if (!readSemanticGuardEnabled(specBase)) return { hits: [] }
|
|
259
|
+
const rewrites = detectAssertionRewrites({ cwd, files: Array.isArray(files) ? files : [] })
|
|
260
|
+
if (Object.keys(rewrites).length === 0) return { hits: [] }
|
|
261
|
+
// 归因只查断言命中文件(省 git log 调用;collectRecentForeignDelivery 内部封顶 20 文件)
|
|
262
|
+
const delivery = collectRecentForeignDelivery({ cwd, files: Object.keys(rewrites), currentChange: changeName })
|
|
263
|
+
const hits = []
|
|
264
|
+
for (const file of Object.keys(rewrites)) {
|
|
265
|
+
// 无归因不点名:纯新增断言 / 无标记提交 / 交付者是本变更(currentChange 透传跳过)同落空
|
|
266
|
+
if (!delivery[file]) continue
|
|
267
|
+
hits.push({ file, deliveredBy: delivery[file], sampleLines: rewrites[file] })
|
|
268
|
+
}
|
|
269
|
+
return { hits }
|
|
270
|
+
}
|
|
271
|
+
|
|
208
272
|
/**
|
|
209
273
|
* quick --done 内置 test+lint 实测门禁:把 CLAUDE.md 规则 8「触及 src/test 的改动先
|
|
210
274
|
* npm test + npm run lint」从 agent 自律下沉为 CLI 卡点。
|
|
@@ -228,8 +292,11 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
|
|
|
228
292
|
* @param {string} opts.specBase - .sillyspec 目录(local.yaml 读取源,信任边界见 runVerifyTestCheck SEC-02 注释)
|
|
229
293
|
* @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null → 回退 declaredFiles)
|
|
230
294
|
* @param {string[]} [opts.declaredFiles] - 会话声明边界(guard.allowedFiles;changedFiles 为空时的兜底判定源)
|
|
231
|
-
* @param {string} [opts.changeName] - quick 会话名(evidence-auto
|
|
232
|
-
* @returns {Promise<{action:'pass'|'fail'|'skip', failed:string[], reason:string, test:object|null, lint:object|null
|
|
295
|
+
* @param {string} [opts.changeName] - quick 会话名(evidence-auto 策略解析用 + 语义护栏归因 currentChange 透传)
|
|
296
|
+
* @returns {Promise<{action:'pass'|'fail'|'skip', failed:string[], reason:string, test:object|null, lint:object|null,
|
|
297
|
+
* semanticGuard?:{hits:Array<{file:string,deliveredBy:string,sampleLines:string[]}>}}>}
|
|
298
|
+
* semanticGuard 仅检测跑过(三条早退 skip 路径不跑检测、无字段)时挂载;WARNING 级
|
|
299
|
+
* 非阻断——action/failed 判定不受它影响(调用方 complete-handlers.js 只读 action/failed,零改动)。
|
|
233
300
|
*/
|
|
234
301
|
export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null }) {
|
|
235
302
|
if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
|
|
@@ -247,6 +314,13 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
247
314
|
return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
|
|
248
315
|
}
|
|
249
316
|
|
|
317
|
+
// ── 语义护栏检测(task-06,FR-04 断言重写 WARNING,D-001@v1 非阻断)──
|
|
318
|
+
// 三条早退(env skip / 无文件 / 纯 doc)已在上文 return,检测不跑——语义一致。
|
|
319
|
+
// 检测对主仓 cwd 执行而非 gateCwd 隔离快照(下方才建快照):检测对象是本会话工作树
|
|
320
|
+
// 未提交改动(git diff HEAD 须见它),快照语义是 test/lint 实测隔离——两者不同层
|
|
321
|
+
// (Grill X-001 附注)。specBase 同理用主仓的(快照副本 local.yaml 不参与开关判定)。
|
|
322
|
+
const semanticGuard = buildSemanticGuardHits({ cwd, specBase, files, changeName })
|
|
323
|
+
|
|
250
324
|
const { runVerifyTestCheck, runVerifyLintCheck } = await import('../verify-postcheck.js')
|
|
251
325
|
|
|
252
326
|
// ── 隔离快照执行(2026-09-10 驾驭小结第六批②,用户实证「lint 实测对账在主仓跑被并行
|
|
@@ -282,6 +356,8 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
282
356
|
: `触及 src/test 共 ${codeFiles.length} 个文件,test+lint 实测通过(或未配置命令自动跳过)${snapshot ? '(隔离快照口径:并行会话脏文件不计入)' : ''}`,
|
|
283
357
|
test,
|
|
284
358
|
lint,
|
|
359
|
+
// 语义护栏 WARNING(task-06):只增可选字段,action/failed 判定与调用方读取面均不受影响
|
|
360
|
+
semanticGuard,
|
|
285
361
|
}
|
|
286
362
|
} finally {
|
|
287
363
|
if (snapshot) { try { snapshot.cleanup() } catch { /* 清理失败不连坐门禁结论 */ } }
|
|
@@ -313,4 +389,18 @@ export function printQuickTestLintGate(gate) {
|
|
|
313
389
|
} else {
|
|
314
390
|
console.log(`\n✅ quick test+lint 门禁 — PASS(${gate.reason})`)
|
|
315
391
|
}
|
|
392
|
+
|
|
393
|
+
// 语义护栏 WARNING(task-06 / FR-04,非阻断——D-001@v1:误报校准数据为零先 advisory,
|
|
394
|
+
// 复潮条件:误报率实证后可升 block):他者近因交付的测试文件既有断言行被改 → 点名
|
|
395
|
+
// 文件 + 交付变更 + 被改样例行(上游 detectAssertionRewrites 已封顶 5/文件,渲染侧
|
|
396
|
+
// 再 slice(0,5) 兜底)。hits 空 / 字段缺失(三条早退 skip 路径不跑检测)→ 零输出。
|
|
397
|
+
const sgHits = gate.semanticGuard && Array.isArray(gate.semanticGuard.hits) ? gate.semanticGuard.hits : []
|
|
398
|
+
if (sgHits.length > 0) {
|
|
399
|
+
console.warn(`\n⚠️ 语义护栏 — 断言重写 WARNING(非阻断):他者近因交付的测试文件既有断言被改`)
|
|
400
|
+
for (const h of sgHits) {
|
|
401
|
+
console.warn(` - ${h.file} ← ${h.deliveredBy} 交付`)
|
|
402
|
+
for (const s of (h.sampleLines || []).slice(0, 5)) console.warn(` 被改: ${String(s).trim()}`)
|
|
403
|
+
}
|
|
404
|
+
console.warn(` ➜ 若确需重写:重写理由写进 quicklog --solution(--done 四参数收尾);并复查 ${[...new Set(sgHits.map(h => h.deliveredBy))].join('、')} 的关联决策——勿以改断言重定义绿灯。`)
|
|
405
|
+
}
|
|
316
406
|
}
|
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 })
|