sillyspec 3.32.1 → 3.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-explore/SKILL.md +6 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -0
- package/package.json +2 -2
- package/src/cross-repo-reconcile.js +33 -0
- package/src/doctor-diagnostics.js +84 -4
- package/src/flow-parity.js +19 -26
- package/src/flow-review.js +4 -0
- package/src/flow.js +17 -10
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +23 -11
- package/src/init.js +5 -1
- package/src/knowledge-graph.js +918 -0
- package/src/knowledge-match.js +11 -6
- package/src/knowledge-vector.js +17 -3
- package/src/module-impact.js +3 -1
- package/src/progress/stage-machine.js +8 -1
- package/src/run/code-face-key.js +84 -0
- package/src/run/complete.js +32 -9
- package/src/run/gate-snapshot.js +19 -0
- package/src/run/gates.js +109 -67
- package/src/run/green-cache.js +11 -19
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/sentinel-assertions.js +27 -2
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/stages/explore.js +5 -1
- package/src/stages/knowledge.js +11 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/watcher.js +6 -2
- package/src/wt-commit.js +82 -3
|
@@ -50,9 +50,10 @@ export function qualityScanRecordPath(specBase, changeName) {
|
|
|
50
50
|
* 指纹非代码面排除:verify 阶段步骤 6 实测 → --done 窗口内的合法产出只有规范文档——
|
|
51
51
|
* 排除后这些写入不再触发失配重跑;src 一动(护栏外行为)立即失配照旧亲测。
|
|
52
52
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
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 行 +
|
|
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
|
-
|
|
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 (
|
|
93
|
+
if (treeKey === null || porcelain === null) return null
|
|
90
94
|
const codeLines = porcelainCodeLines(porcelain)
|
|
91
95
|
return createHash('sha256')
|
|
92
|
-
.update([configText,
|
|
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
|
-
// ──
|
|
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:
|
|
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:
|
|
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
|
-
|
|
677
|
-
|
|
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 档
|
package/src/scope-audit.js
CHANGED
|
@@ -237,12 +237,26 @@ function resolveDiffRoot(sb, changeName, form, cwd) {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
240
|
+
* 收口时点快照读取(归档记录态数据源):结构即 computeFullFlowAudit 返回值 + savedAt。
|
|
241
|
+
* 读取链(2026-10-09-close-trace-single-set):变更目录 change-patch.json 的 scopeAudit
|
|
242
|
+
* 子对象(单套新形态——顶级 patchSha256/patchStatus/savedAt 原位注入返回值,下游回放
|
|
243
|
+
* 注记与 getFileDiff 防篡改锚口径不变)→ 旧 scope-audit.json(存量归档)→ .runtime/
|
|
244
|
+
* scope-audit-<change>.json(更早存量兼容)→ null(调用方降级,不出伪数据)。
|
|
243
245
|
*/
|
|
244
246
|
function readScopeSnapshot(changeDir, runtimeRoot, changeName) {
|
|
245
247
|
if (changeDir) {
|
|
248
|
+
try {
|
|
249
|
+
const merged = JSON.parse(readFileSync(join(changeDir, 'change-patch.json'), 'utf8'))
|
|
250
|
+
if (merged && typeof merged === 'object' && merged.scopeAudit && typeof merged.scopeAudit === 'object'
|
|
251
|
+
&& Array.isArray(merged.scopeAudit.rows)) {
|
|
252
|
+
return {
|
|
253
|
+
...merged.scopeAudit,
|
|
254
|
+
...(merged.patchSha256 ? { patchSha256: merged.patchSha256 } : {}),
|
|
255
|
+
...(merged.patchStatus ? { patchStatus: merged.patchStatus } : {}),
|
|
256
|
+
...(merged.savedAt ? { savedAt: merged.savedAt } : {}),
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch { /* 新形态缺失/损坏 → 旧名兜底 */ }
|
|
246
260
|
try {
|
|
247
261
|
const snap = JSON.parse(readFileSync(join(changeDir, 'scope-audit.json'), 'utf8'))
|
|
248
262
|
if (snap && typeof snap === 'object' && Array.isArray(snap.rows)) return snap
|
|
@@ -255,18 +269,6 @@ function readScopeSnapshot(changeDir, runtimeRoot, changeName) {
|
|
|
255
269
|
return null
|
|
256
270
|
}
|
|
257
271
|
|
|
258
|
-
/**
|
|
259
|
-
* 冻结 patch 读取(审计级存储,--file 已收尾切片的数据源):变更目录 scope-audit.patch。
|
|
260
|
-
* @returns {string|null}
|
|
261
|
-
*/
|
|
262
|
-
function readFrozenPatch(changeDir) {
|
|
263
|
-
if (!changeDir) return null
|
|
264
|
-
try {
|
|
265
|
-
const text = readFileSync(join(changeDir, 'scope-audit.patch'), 'utf8')
|
|
266
|
-
return text && text.trim() ? text : null
|
|
267
|
-
} catch { return null }
|
|
268
|
-
}
|
|
269
|
-
|
|
270
272
|
/**
|
|
271
273
|
* flow done 冻结记录读取(2026-10-07-scope-audit-thin-patch-replay):change-patch.json 是
|
|
272
274
|
* thin 轻量变更 flow done 时点冻结的归属面(files/baseline/head/totals/patchSha256)——thin
|
|
@@ -846,10 +848,19 @@ async function computeFullFlowAudit({ cwd, specBase, changeName, platformOpts, c
|
|
|
846
848
|
// 审查 C-F02:预执行判定必须排除归档(归档=流程走完,58/77 归档变更三信号全缺被误判
|
|
847
849
|
// 「未执行」实证——tag 只在分支被 review 引用时才打)+ 补四类证据:快照文件本身 /
|
|
848
850
|
// execute-runs change 戳(task-review readExecuteRunChangeStamp 同口径)/ apply-pathspec /
|
|
849
|
-
// execute-cleanup 回执。任一命中 = 进过 execute
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
851
|
+
// execute-cleanup 回执。任一命中 = 进过 execute。快照存在性(2026-10-09-close-trace-
|
|
852
|
+
// single-set):新形态读 change-patch.json 的 scopeAudit 子对象(非文件裸存在——旧 thin
|
|
853
|
+
// 冻结件无子对象不误判),旧形态 scope-audit.json / .runtime 快照照旧。
|
|
854
|
+
const hasMergedScopeAudit = changeDirInfo ? (() => {
|
|
855
|
+
try {
|
|
856
|
+
const merged = JSON.parse(readFileSync(join(changeDirInfo.dir, 'change-patch.json'), 'utf8'))
|
|
857
|
+
return !!(merged && typeof merged === 'object' && merged.scopeAudit && typeof merged.scopeAudit === 'object')
|
|
858
|
+
} catch { return false }
|
|
859
|
+
})() : false
|
|
860
|
+
const hasSnapshotFile = hasMergedScopeAudit
|
|
861
|
+
|| (changeDirInfo
|
|
862
|
+
? existsSync(join(changeDirInfo.dir, 'scope-audit.json')) || existsSync(join(runtimeRoot, `scope-audit-${changeName}.json`))
|
|
863
|
+
: existsSync(join(runtimeRoot, `scope-audit-${changeName}.json`)))
|
|
853
864
|
const hasApplyPathspec = existsSync(join(runtimeRoot, `apply-pathspec-${changeName}.txt`))
|
|
854
865
|
const hasCleanupReceipt = existsSync(join(runtimeRoot, `execute-cleanup-${changeName}.json`))
|
|
855
866
|
let hasExecuteRunStamp = false
|
|
@@ -1575,19 +1586,34 @@ export async function getFileDiff({ cwd, specBase, changeName, platformOpts, fil
|
|
|
1575
1586
|
}
|
|
1576
1587
|
|
|
1577
1588
|
// —— 真·当时内容比对(quick-359a48f1):已收尾变更优先冻结 patch 切片 ——
|
|
1578
|
-
// ① quick 记录态自带 frozenPatchPath;② full-flow 变更目录
|
|
1589
|
+
// ① quick 记录态自带 frozenPatchPath;② full-flow 变更目录 change.patch(单套新形态,
|
|
1590
|
+
// 2026-10-09-close-trace-single-set)→ scope-audit.patch(存量 heavy 归档兜底)。
|
|
1579
1591
|
// patch 冻结在收尾时点,不含后续演进——实时锚 diff 只作 patch 缺失的兜底(口径标注)。
|
|
1580
1592
|
// A-F01:读取时按 json 记录的 patchSha256 校验,不匹配显式告警(防低级篡改)。
|
|
1581
1593
|
const patchRecord = result.frozenPatchPath
|
|
1582
1594
|
? { path: result.frozenPatchPath, sha: result.patchSha256 || null }
|
|
1583
|
-
: (() => {
|
|
1595
|
+
: (() => {
|
|
1596
|
+
const d = resolveChangeDir(sb, changeName)
|
|
1597
|
+
if (!d) return null
|
|
1598
|
+
for (const name of ['change.patch', 'scope-audit.patch']) {
|
|
1599
|
+
const p = join(d.dir, name)
|
|
1600
|
+
if (existsSync(p)) return { path: p, sha: null }
|
|
1601
|
+
}
|
|
1602
|
+
return null
|
|
1603
|
+
})()
|
|
1584
1604
|
let recordedSha = patchRecord ? patchRecord.sha : null
|
|
1585
1605
|
if (patchRecord && !recordedSha && result.rows) {
|
|
1586
|
-
// json 里未直接带回 hash(记录态捷径不透传)→ 读 json 原文取(快照/quick
|
|
1606
|
+
// json 里未直接带回 hash(记录态捷径不透传)→ 读 json 原文取(快照/quick 记录都存该
|
|
1607
|
+
// 字段)。full-flow 伴生 json:change-patch.json(新形态顶级键)优先,旧形态 heavy
|
|
1608
|
+
// 归档(无 change-patch.json)兜底读 scope-audit.json。
|
|
1587
1609
|
try {
|
|
1588
|
-
const
|
|
1610
|
+
const d = resolveChangeDir(sb, changeName)
|
|
1611
|
+
const companionPath = result.frozenPatchPath
|
|
1589
1612
|
? result.frozenPatchPath.replace(/\.patch$/, '.json')
|
|
1590
|
-
: join(
|
|
1613
|
+
: (d && existsSync(join(d.dir, 'change-patch.json'))
|
|
1614
|
+
? join(d.dir, 'change-patch.json')
|
|
1615
|
+
: join(d.dir, 'scope-audit.json'))
|
|
1616
|
+
const snapRaw = JSON.parse(readFileSync(companionPath, 'utf8'))
|
|
1591
1617
|
recordedSha = snapRaw.patchSha256 || null
|
|
1592
1618
|
} catch { /* json 不可读 → 无 hash 可校验(null=不校验) */ }
|
|
1593
1619
|
}
|
|
@@ -25,6 +25,24 @@ function taskTokenRe(id) {
|
|
|
25
25
|
return new RegExp(`${id}(?!\\d)`);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 提交消息连写 token 组展开(坑 flow-done-sentinel-token-split,2026-10-08 实证):
|
|
30
|
+
* `task-01/02/03`、`task-1、2,03` 等自然语言连写组只含首个完整 token——字面匹配判
|
|
31
|
+
* 其余为零完成证据误拒收。展开为独立规范 token(补零到两位,与判集行锚同形)后再判;
|
|
32
|
+
* 尾数不跟数字(防 task-013 版本串误切)。纯函数,供哨兵判定与 autopilot 勾选提取共用。
|
|
33
|
+
*/
|
|
34
|
+
export function expandTaskShorthand(messages) {
|
|
35
|
+
return (Array.isArray(messages) ? messages : []).map((msg) =>
|
|
36
|
+
String(msg ?? '').replace(
|
|
37
|
+
/task-(\d{1,2})((?:\s*[\/、,,;;]\s*\d{1,2}(?!\d))+)/gi,
|
|
38
|
+
(full, first, rest) =>
|
|
39
|
+
' ' + [first, ...(rest.match(/\d{1,2}/g) || [])]
|
|
40
|
+
.map((n) => `task-${String(Number(n)).padStart(2, '0')}`)
|
|
41
|
+
.join(' ') + ' '
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* review.json 在场证据清单(execute-runs 两级遍历;异常/缺失 → [],commit 证据照判)。
|
|
30
48
|
*/
|
|
@@ -84,11 +102,13 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
84
102
|
return { status: 'none', claimTotal, checked, missing: [] };
|
|
85
103
|
}
|
|
86
104
|
const reviews = listReviewEvidence(changeDir, opts);
|
|
105
|
+
// 连写组先展开(坑 flow-done-sentinel-token-split):task-01/02/03 → 独立 token 再字面判
|
|
106
|
+
const expanded = expandTaskShorthand(messages);
|
|
87
107
|
const missing = entries
|
|
88
108
|
.filter((e) => {
|
|
89
109
|
if (!e.checked) return false;
|
|
90
110
|
const re = taskTokenRe(e.id);
|
|
91
|
-
const byCommit =
|
|
111
|
+
const byCommit = expanded.some((msg) => re.test(msg));
|
|
92
112
|
const byReview = reviews.some((p) => p.includes(`/tasks/${e.id}/`));
|
|
93
113
|
return !(byCommit || byReview);
|
|
94
114
|
})
|
|
@@ -109,7 +129,12 @@ export function detectFakeCheckCompletion({ changeDir, tasksMd, commits, opts =
|
|
|
109
129
|
export function detectBatchCheckCadence(events) {
|
|
110
130
|
const taskEvents = [];
|
|
111
131
|
for (const e of events || []) {
|
|
112
|
-
if (e
|
|
132
|
+
if (!e || e.kind !== 'task-done') continue;
|
|
133
|
+
// 节奏域过滤(2026-10-08-batch-tick-false-positive):只认任务队列面(stage='tasks',缺省
|
|
134
|
+
// 视同 tasks 兼容 legacy 流)。其他阶段的 task-done(如 design.md 自审预勾的首现计数)不是
|
|
135
|
+
// tick 信号——混入会把逐格勾选误判成单拍大跳(实证:design 0→8 拒收六次独立 tick)。
|
|
136
|
+
if (e.stage != null && e.stage !== 'tasks') continue;
|
|
137
|
+
taskEvents.push(e);
|
|
113
138
|
}
|
|
114
139
|
const parse = (e) => {
|
|
115
140
|
const m = /^checked (\d+)→(\d+)$/.exec(String(e.detail || ''));
|
package/src/spec-sync.js
CHANGED
|
@@ -463,6 +463,94 @@ function clearSpecConflictMarker(specRoot, changeName) {
|
|
|
463
463
|
} catch { /* 清理失败不影响同步结果 */ }
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
// ── 2026-10-09-tombstone-conflict-root-fix FR-01:纯墓碑拒收按被删变更归因记账 ──
|
|
467
|
+
// 背景(91 条实证):整树同步被墓碑整批拒收时按「当轮同步标签」落冲突文件——一个
|
|
468
|
+
// 根因(changes/salv 两路径)伪装成 91 个变更的冲突;该批变更归档后永不再以同名
|
|
469
|
+
// 同步成功,clearSpecConflictMarker 永远轮不到 → 记录单调累积。归因记账:从
|
|
470
|
+
// platform_deleted 路径剥被删变更名(真凶),每被删变更一条记录幂等维护。
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 从 platform_deleted 路径剥被删变更名:活跃区 changes/<name>/… 与归档区
|
|
474
|
+
* changes/archive/<name>/… 两前缀;剥不出的路径归 __unattributed__ 聚合桶
|
|
475
|
+
* (正常不出现——守卫只对 changes/ 前缀写墓碑;出现即提示人工核查)。
|
|
476
|
+
*/
|
|
477
|
+
export function extractTombstonedChanges(platformDeleted) {
|
|
478
|
+
const byChange = new Map();
|
|
479
|
+
for (const p of platformDeleted) {
|
|
480
|
+
let name = '__unattributed__';
|
|
481
|
+
const m = typeof p === 'string' ? p.match(/^changes\/(?:archive\/)?([^/]+)\//) : null;
|
|
482
|
+
if (m) name = m[1];
|
|
483
|
+
const list = byChange.get(name) || [];
|
|
484
|
+
list.push(p);
|
|
485
|
+
byChange.set(name, list);
|
|
486
|
+
}
|
|
487
|
+
return byChange;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 归因记账落盘(幂等合并):spec-sync-conflict-<被删变更>.json,created_at 首见
|
|
492
|
+
* 保持、platform_deleted 路径并集、last_seen 每轮刷新;kind='tombstone' 供
|
|
493
|
+
* progress show / daemon 摘要按形态区分(stage-machine 优先读 kind 透传 type)。
|
|
494
|
+
* 返回 { byChange, written } 供横幅单根因叙事。
|
|
495
|
+
*/
|
|
496
|
+
export function writeTombstoneAttribution(specRoot, platformDeleted) {
|
|
497
|
+
const byChange = extractTombstonedChanges(platformDeleted);
|
|
498
|
+
const written = [];
|
|
499
|
+
for (const [name, paths] of byChange) {
|
|
500
|
+
const filePath = join(specRoot, '.runtime', `spec-sync-conflict-${name}.json`);
|
|
501
|
+
let prev = null;
|
|
502
|
+
try { prev = JSON.parse(readFileSync(filePath, 'utf8')); } catch { /* 首见 */ }
|
|
503
|
+
const record = {
|
|
504
|
+
change: name,
|
|
505
|
+
kind: 'tombstone',
|
|
506
|
+
created_at: (prev && prev.created_at) || new Date().toISOString(),
|
|
507
|
+
last_seen: new Date().toISOString(),
|
|
508
|
+
server_versions: {},
|
|
509
|
+
conflicting_paths: [],
|
|
510
|
+
platform_deleted: [...new Set([...((prev && prev.platform_deleted) || []), ...paths])],
|
|
511
|
+
note: '路径被平台删除墓碑拒收(非版本冲突,resolve 重推无效)。恢复通道:平台 manifest-heal 清墓碑后重推;或平台删除变更时 tombstone_cleanup 收敛本机目录(.runtime/tombstone-quarantine/ 隔离区)。',
|
|
512
|
+
};
|
|
513
|
+
try {
|
|
514
|
+
mkdirSync(join(specRoot, '.runtime'), { recursive: true });
|
|
515
|
+
writeFileSync(filePath, JSON.stringify(record, null, 2) + '\n', 'utf8');
|
|
516
|
+
written.push(filePath);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
console.warn(`[spec-sync] 墓碑冲突文件写入失败(信息仅打印): ${e.message}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { byChange, written };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* 全绿同步清陈旧纯墓碑记录(2026-10-09 FR-01):判定式 = conflicting_paths 为空
|
|
526
|
+
* 且 platform_deleted 非空(kind 无关——覆盖 kind:'tombstone'、现行 kind:'spec-tree'
|
|
527
|
+
* 纯墓碑与存量旧格式 91 条);混合形态(conflicting_paths 非空)永不清理。返回
|
|
528
|
+
* 清理条数。挂在两个全绿点:POST 成功无冲突分支与无差异跳过分支。
|
|
529
|
+
*/
|
|
530
|
+
export function clearStaleTombstoneMarkers(specRoot) {
|
|
531
|
+
const runtimeDir = join(specRoot, '.runtime');
|
|
532
|
+
let files = [];
|
|
533
|
+
try { files = readdirSync(runtimeDir); } catch { return 0; }
|
|
534
|
+
let cleared = 0;
|
|
535
|
+
for (const f of files) {
|
|
536
|
+
if (!f.startsWith('spec-sync-conflict-') || !f.endsWith('.json')) continue;
|
|
537
|
+
const p = join(runtimeDir, f);
|
|
538
|
+
try {
|
|
539
|
+
const j = JSON.parse(readFileSync(p, 'utf8'));
|
|
540
|
+
const cp = Array.isArray(j.conflicting_paths) ? j.conflicting_paths : null;
|
|
541
|
+
const pd = Array.isArray(j.platform_deleted) ? j.platform_deleted : null;
|
|
542
|
+
if (cp !== null && cp.length === 0 && pd !== null && pd.length > 0) {
|
|
543
|
+
unlinkSync(p);
|
|
544
|
+
cleared++;
|
|
545
|
+
}
|
|
546
|
+
} catch { /* 损坏文件不动 */ }
|
|
547
|
+
}
|
|
548
|
+
if (cleared > 0) {
|
|
549
|
+
console.log(`[spec-sync] 全绿同步清理 ${cleared} 条陈旧墓碑冲突记录(被删变更已收敛)`);
|
|
550
|
+
}
|
|
551
|
+
return cleared;
|
|
552
|
+
}
|
|
553
|
+
|
|
466
554
|
// follower/stale 提示的跨轮去重(坑 spec-sync-follow-banner-spam,2026-09-08 用户反馈③):
|
|
467
555
|
// 「N 个本地未改动文件自动跟随服务器」的信息只在集合**首次出现/变化**时有价值。基线快照锚定
|
|
468
556
|
// local-at-last-sync(见 writeBaseSnapshot 注释),本地落后于服务器的文件每轮都重新算出同样的
|
|
@@ -584,6 +672,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
584
672
|
|
|
585
673
|
if (ops.length === 0) {
|
|
586
674
|
debugLog(`[spec-sync] 无差异,跳过同步: ${changeName}`);
|
|
675
|
+
clearStaleTombstoneMarkers(specRoot);
|
|
587
676
|
return { synced: 0 };
|
|
588
677
|
}
|
|
589
678
|
|
|
@@ -634,27 +723,15 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
634
723
|
// 纯墓碑形态:版本冲突面为空、拒收全是墓碑 → 不写/不清通用冲突标记(clear 会把
|
|
635
724
|
// 状态洗成「已消解」误导 status),单独落冲突文件 + 正确恢复指引。
|
|
636
725
|
if (realPaths.length === 0 && tombstoned.length > 0) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
mkdirSync(runtimeDir, { recursive: true });
|
|
641
|
-
tombConflictPath = join(runtimeDir, `spec-sync-conflict-${changeName}.json`);
|
|
642
|
-
writeFileSync(tombConflictPath, JSON.stringify({
|
|
643
|
-
change: changeName,
|
|
644
|
-
kind: 'spec-tree',
|
|
645
|
-
created_at: new Date().toISOString(),
|
|
646
|
-
server_versions: {},
|
|
647
|
-
conflicting_paths: [],
|
|
648
|
-
platform_deleted: tombstoned,
|
|
649
|
-
note: '路径被平台删除墓碑拒收(非版本冲突,resolve 重推无效)。恢复通道:平台 manifest-heal 清墓碑后重推,或联系管理员确认删除是否有意。',
|
|
650
|
-
}, null, 2) + '\n', 'utf8');
|
|
651
|
-
} catch (e) {
|
|
652
|
-
console.warn(`[spec-sync] 墓碑冲突文件写入失败(信息仅打印): ${e.message}`);
|
|
653
|
-
}
|
|
726
|
+
// 归因记账(2026-10-09 FR-01):按被删变更落 spec-sync-conflict-<被删变更>.json
|
|
727
|
+
// (不再用当轮同步标签——91 条累积根因),幂等合并;横幅按被删变更单根因叙事。
|
|
728
|
+
const { byChange, written } = writeTombstoneAttribution(specRoot, tombstoned);
|
|
654
729
|
console.warn('');
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
730
|
+
for (const [name, paths] of byChange) {
|
|
731
|
+
console.warn(`⚠️ [spec-sync] 变更 ${name} 被平台删除墓碑拒收(${paths.length} 个路径,非版本冲突,resolve --keep-local 重推无效): ${paths.slice(0, 5).join(', ')}${paths.length > 5 ? ' 等' : ''}`);
|
|
732
|
+
}
|
|
733
|
+
console.warn(`⚠️ 恢复通道:平台 manifest-heal(清墓碑后重推即闭环)或 tombstone_cleanup 收敛本机目录;详情: ${written.join(', ') || '(写入失败)'}`);
|
|
734
|
+
return { synced: 0, conflict: true, serverVersions: {}, tombstoned, conflictPath: written[0] || null, tombstonedChanges: [...byChange.keys()] };
|
|
658
735
|
}
|
|
659
736
|
const tombNote = tombstoned.length > 0
|
|
660
737
|
? `\n⚠️ 另有 ${tombstoned.length} 个路径被平台删除墓碑拒收(非版本冲突,resolve 无效;恢复走平台 manifest-heal): ${tombstoned.slice(0, 5).join(', ')}${tombstoned.length > 5 ? ' 等' : ''}`
|
|
@@ -717,6 +794,7 @@ export async function syncSpecTree(specRoot, platform, changeName, opts = {}) {
|
|
|
717
794
|
// 本轮无冲突 = 此前的未决 spec 树冲突已不存在,清残留标记(否则 status 永久红标、
|
|
718
795
|
// 横幅去重单行提示不消——坑 spec-sync-conflict-banner-spam)
|
|
719
796
|
clearSpecConflictMarker(specRoot, changeName);
|
|
797
|
+
clearStaleTombstoneMarkers(specRoot);
|
|
720
798
|
writeLastSyncTs(specRoot);
|
|
721
799
|
writeBaseSnapshot(specRoot, localFiles);
|
|
722
800
|
return { synced: ops.length };
|
package/src/stages/brainstorm.js
CHANGED
|
@@ -220,6 +220,7 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<动机与背景>
|
|
|
220
220
|
3. 定稿前机制词检索(固定动作):方案引入新机制词(如 分类/词表/枚举/门/协议/状态机——例词为举例非机制,不构成封闭清单)时,先跑 \`sillyspec knowledge search --query "<机制词>"\`,命中条目必读(尤其 status=rejected 的防复潮条目)再定稿方案
|
|
221
221
|
4. 给出推荐方案和理由
|
|
222
222
|
5. 用户选定方案后,把选择结果**当场追加**到 \`{SPEC_ROOT}/changes/<change-name>/decisions.md\`(增量落盘规则同「对话式探索」步:目录/文件缺失先创建、按 D-xxx@vN 判重):type 用 architecture、source 用 user、question 记方案取舍问题、answer 记用户所选方案与理由要点、evidence 记方案选择轮次——方案选择是本变更最关键的用户决策,必须随答随落盘,不能只留在对话里
|
|
223
|
+
6. 落选方案**各记一条 rejected 条目**(防复潮——蒸馏链 rejected 通道与 knowledge 否决注入只认独立条目,落选理由埋在所选方案 answer 散文里进不了通道):question 复用同一取舍问句,type 用 architecture、status 用 rejected,否决理由记该方案当轮的劣势与成本,复潮条件记劣势消解或成本反转的前提(如「依赖 X 下线」「改动面降到 Y 以下」)——两字段必填性见「写设计文档」步对账规则
|
|
223
224
|
|
|
224
225
|
### 铁律 — 必须等待用户选择方案
|
|
225
226
|
- 列出方案对比表和推荐后必须暂停等用户选择,**不要自己说"推荐方案 A"然后当用户选了**(命令由 CLI 在下方注入)
|
|
@@ -382,7 +383,7 @@ design.md 第一行标题必须用中文:# 设计文档(Design)— <变更
|
|
|
382
383
|
- decisions.md 是本次变更的决策台账,不是长期术语表
|
|
383
384
|
- 只记录有实现/验收影响的决策,闲聊和低风险偏好不记录
|
|
384
385
|
- 已落盘条目按 D-xxx@vN 判重,不重复追加;补全缺失的九字段与可选字段
|
|
385
|
-
- 前序遗漏未落盘、确有实现影响的决策(增量规则漏网的),此时补写;每条记录必须有稳定版本 ID:D-001@v1、D-002@v1
|
|
386
|
+
- 前序遗漏未落盘、确有实现影响的决策(增量规则漏网的),此时补写;每条记录必须有稳定版本 ID:D-001@v1、D-002@v1 ...;放弃方案属此类——「提出方案」步的落选方案若未各记 rejected 条目(否决理由/复潮条件必填),此时补记
|
|
386
387
|
- 若后续 Design Grill 修正该决策,新记录使用 D-001@v2,并写明 supersedes: D-001@v1
|
|
387
388
|
- 每条记录必须包含:type、status、source、question、answer、normalized_requirement、impacts、evidence、priority
|
|
388
389
|
- 九字段之外可另加四个可选字段,按需填写、不强制全填(旧格式决策记录缺这些字段不受影响):
|
|
@@ -18,7 +18,11 @@ import { join, isAbsolute } from 'path'
|
|
|
18
18
|
// 提取 `npm|pnpm|yarn run <script>` 命令。
|
|
19
19
|
// 对齐 scan-postcheck:136 的 `/npm run (\S+)/g`,扩展包管理器 + 容忍多空白。
|
|
20
20
|
// 捕获组 1 = 包管理器(npm|pnpm|yarn),捕获组 2 = script 名。
|
|
21
|
-
|
|
21
|
+
// script 名收敛为合法字符集(2026-10-09-verify-papercuts ①):原 \S+ 会把紧跟的全角标点
|
|
22
|
+
// (;。,等)/句读拼进 script 名——『verify: npm run test;模块卡 diff 一致』被提取为
|
|
23
|
+
// script=test;模块卡 → 误报「命令不存在」逼 agent 改写法(plan postcheck 实证多跑 3 轮)。
|
|
24
|
+
// npm script 名本身只含字母数字与 : _ - .,按此截断即安全(&& / | / 全角标点自然止步)。
|
|
25
|
+
const SCRIPT_CMD_RE = /(npm|pnpm|yarn)\s+run\s+([A-Za-z0-9:_.\-]+)/g
|
|
22
26
|
|
|
23
27
|
// 识别紧贴命令前的 `cd <subdir> &&` 前缀(anchored at end of the slice before the command)。
|
|
24
28
|
// subdir token 不含空白与 & ; | 等分隔符;末尾的 `&&` 可带任意空白后接命令。
|
package/src/stages/explore.js
CHANGED
|
@@ -15,7 +15,11 @@ export const definition = {
|
|
|
15
15
|
- \`.sillyspec/docs/<project>/scan/ARCHITECTURE.md\`
|
|
16
16
|
- \`.sillyspec/docs/<project>/scan/CONVENTIONS.md\`
|
|
17
17
|
- \`.sillyspec/changes/<change-name>/design.md\`
|
|
18
|
-
3.
|
|
18
|
+
3. 调查兵器按话题选(话题→兵器映射,全只读):
|
|
19
|
+
- 词面考古("这段代码在哪/怎么写的")→ \`rg\` / \`ls\` / \`cat\`
|
|
20
|
+
- 坑史检索("这话题以前踩过什么坑")→ \`sillyspec knowledge search --query "<关键词>"\`(四层召回)
|
|
21
|
+
- 关系面(影响面/历史决策/需求谱系)→ \`sillyspec knowledge graph impact <文件|模块|变更名>\` 多跳闭包与防复潮可达的已否决决策、\`neighbors <锚点>\` / \`path <A> <B>\` 推理链、\`summary\` 知识面健康度——比较方案前先看 impact 的 rejected/⚰️死路条目,已否决的路不重新兜售;全部支持 \`--json\`
|
|
22
|
+
- 在途面(出方向建议前)→ \`sillyspec status\` 活跃变更一览——结论别跟在途工作撞车
|
|
19
23
|
4. 输出 2-3 个有价值方向、关键风险和下一步建议
|
|
20
24
|
5. 如果用户要求保存结论,先明确保存位置,再写入对应文档
|
|
21
25
|
6. 讨论 UI/交互方案且用户明确要求原型时,按下方「HTML 原型生成」执行
|
package/src/stages/knowledge.js
CHANGED
|
@@ -609,6 +609,16 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
609
609
|
return cmdRefresh(dir, args.slice(1), opts)
|
|
610
610
|
case 'propose':
|
|
611
611
|
return cmdPropose(dir, args.slice(1), opts)
|
|
612
|
+
case 'graph': {
|
|
613
|
+
// 知识图谱查询面(2026-10-08-knowledge-graph):neighbors/path/impact/orphans/dangling,
|
|
614
|
+
// --edges 边型筛选;全 JSON 出口(与 search/validate 同族)。
|
|
615
|
+
const mod = await tryImportSubcommandImpl('../knowledge-graph.js', 'knowledge-graph.js')
|
|
616
|
+
if (!mod || typeof mod.cmdKnowledgeGraph !== 'function') {
|
|
617
|
+
output(false, {}, { code: 'not_implemented', subcommand: 'graph' })
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
return mod.cmdKnowledgeGraph(dir, args.slice(1), opts)
|
|
621
|
+
}
|
|
612
622
|
case 'classify': {
|
|
613
623
|
const mod = await tryImportSubcommandImpl('../knowledge-classify.js', 'knowledge-classify.js')
|
|
614
624
|
if (!mod || typeof mod.cmdKnowledgeClassify !== 'function') {
|
|
@@ -665,7 +675,7 @@ export async function cmdKnowledge(args, dir, opts = {}) {
|
|
|
665
675
|
output(false, {}, {
|
|
666
676
|
code: 'unknown_subcommand',
|
|
667
677
|
subcommand: subCommand,
|
|
668
|
-
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest'],
|
|
678
|
+
available: ['search', 'inspect', 'validate', 'refresh', 'propose', 'classify', 'inbox', 'stats', 'digest', 'graph'], // dump 随 graph 路由面(2026-10-09-graph-dump-layout)
|
|
669
679
|
})
|
|
670
680
|
}
|
|
671
681
|
}
|
package/src/test-bindings.js
CHANGED
|
@@ -157,6 +157,8 @@ export function normalizeRow(row, opts = {}) {
|
|
|
157
157
|
confirmed_at: row.confirmed_at && row.confirmed_at !== 'null' ? row.confirmed_at : null,
|
|
158
158
|
reconfirm: ['unchanged', 'rebound'].includes(row.reconfirm) ? row.reconfirm : null,
|
|
159
159
|
status: row.status === 'superseded' ? 'superseded' : 'active',
|
|
160
|
+
// FR-06(2026-10-09-verify-reuse-friction):行 repo 归属(additive,缺省不落键=主仓)
|
|
161
|
+
...(row.repo && row.repo !== 'main' ? { repo: String(row.repo) } : {}),
|
|
160
162
|
source_change: row.source_change && row.source_change !== 'null' ? row.source_change : null,
|
|
161
163
|
}
|
|
162
164
|
}
|