sillyspec 3.29.4 → 3.30.0
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +2 -2
- package/.claude/skills/sillyspec-commit/SKILL.md +1 -1
- package/.claude/skills/sillyspec-flow/SKILL.md +57 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +18 -116
- package/AGENTS.md +38 -20
- package/SKILL.md +9 -3
- package/assets/command-cards/flow.md +32 -0
- package/assets/command-cards/run-archive.md +26 -0
- package/assets/command-cards/run-brainstorm.md +33 -0
- package/assets/command-cards/run-execute.md +29 -0
- package/assets/command-cards/run-plan.md +27 -0
- package/assets/command-cards/run-quick.md +17 -0
- package/assets/command-cards/run-verify.md +29 -0
- package/assets/command-cards/status.md +27 -0
- package/package.json +2 -1
- package/packages/dashboard/server/executor.js +2 -1
- package/packages/dashboard/server/parser.js +7 -7
- package/r7-test-env/README-R7-TEST.md +93 -0
- package/r7-test-env/demo-project/lib/calc.js +6 -0
- package/r7-test-env/demo-project/package.json +1 -0
- package/r7-test-env/demo-project/test.js +10 -0
- package/r7-test-env/prompt-R7-SMOKE.md +41 -0
- package/r7-test-env/sillyspec-r7.cmd +3 -0
- package/round4/account.mjs +98 -0
- package/round4/b1-analyze-task08.mjs +40 -0
- package/round4/b1-b1-target-validation.md +32 -0
- package/round4/b3a-prompt-audit-result.md +66 -0
- package/round4/b3a-prompt-audit.mjs +89 -0
- package/round4/brief-autocompact.md +34 -0
- package/round4/brief-session-replay.md +50 -0
- package/round4/deep-read-synthesis.md +38 -0
- package/round4/extract-full.mjs +38 -0
- package/round4/extract-timeline.mjs +43 -0
- package/round4/find-sessions.mjs +13 -0
- package/round4/full-r4-l.md +4707 -0
- package/round4/full-r4-o-l.md +4300 -0
- package/round4/full-r4-os.md +1681 -0
- package/round4/full-r4-sf.md +4255 -0
- package/round4/full-r4-sq.md +1734 -0
- package/round4/gate-audit-relay-receipt.md +30 -0
- package/round4/gate-value-audit.md +52 -0
- package/round4/optimization-plan.md +165 -0
- package/round4/prereg.md +67 -0
- package/round4/prompt-R4-L.md +63 -0
- package/round4/prompt-R4-O-L.md +59 -0
- package/round4/prompt-R4-O-S.md +56 -0
- package/round4/prompt-R4-S-F.md +60 -0
- package/round4/prompt-R4-S-Q.md +66 -0
- package/round4/r4-final-report.html +347 -0
- package/round4/r4-os-timeline.txt +297 -0
- package/round4/r4-sf-timeline.txt +738 -0
- package/round4/r4-sq-timeline.txt +322 -0
- package/round4/r4l-timeline.txt +793 -0
- package/round4/r4ol-timeline.txt +729 -0
- package/round4/r4sf-timeline.txt +852 -0
- package/round4/r5-scenario-matrix.md +46 -0
- package/round4/result-S.md +171 -0
- package/round4/result-final.md +46 -0
- package/round4/result-quality.md +63 -0
- package/src/change-list.js +9 -0
- package/src/classify-change.js +6 -0
- package/src/command-cards.js +74 -0
- package/src/commit-guard.js +2 -0
- package/src/config-schema.js +64 -12
- package/src/contract-matrix.js +26 -5
- package/src/db.js +35 -4
- package/src/decision-distill.js +9 -3
- package/src/diagnostic-codes.js +2 -1
- package/src/docs-check.js +1 -1
- package/src/doctor-diagnostics.js +44 -2
- package/src/flow-draft.js +748 -0
- package/src/flow-parity.js +236 -0
- package/src/flow-review.js +184 -0
- package/src/flow.js +1492 -0
- package/src/foreign-declared.js +17 -1
- package/src/fr-index.js +396 -22
- package/src/fs-atomic.js +5 -1
- package/src/git-helper.js +2 -2
- package/src/greenfield-bootstrap.js +98 -0
- package/src/handoff.js +120 -7
- package/src/hunk-attribution.js +191 -0
- package/src/index.js +602 -21
- package/src/init.js +25 -8
- package/src/knowledge-classify.js +47 -1
- package/src/knowledge-digest.js +162 -0
- package/src/knowledge-quicklog.js +246 -0
- package/src/machine-draft.js +128 -0
- package/src/machine-interface.js +194 -7
- package/src/mcp-server.js +1 -0
- package/src/module-impact.js +51 -22
- package/src/module-resolve.js +3 -1
- package/src/preview-progress.js +160 -0
- package/src/progress/change-registry.js +11 -3
- package/src/progress/shared.js +3 -2
- package/src/progress.js +56 -13
- package/src/quicklog.js +44 -6
- package/src/redomain.js +147 -0
- package/src/review-material-pack.js +204 -1
- package/src/run/archive-distill.js +4 -2
- package/src/run/bg-sync.js +67 -0
- package/src/run/command.js +127 -10
- package/src/run/complete-handlers.js +363 -73
- package/src/run/complete.js +356 -74
- package/src/run/gate-snapshot-ledger.js +213 -0
- package/src/run/gate-snapshot.js +260 -38
- package/src/run/gates.js +482 -268
- package/src/run/green-cache.js +123 -0
- package/src/run/junction-rm.js +70 -0
- package/src/run/next.js +14 -0
- package/src/run/prompt.js +185 -55
- package/src/run/quick-audit.js +213 -14
- package/src/run/shared.js +123 -4
- package/src/run/stage.js +182 -216
- package/src/run/test-ledger.js +264 -0
- package/src/run/verify-quality-scan.js +84 -7
- package/src/scope-audit.js +6 -1
- package/src/sentinel-assertions.js +114 -0
- package/src/setup.js +3 -3
- package/src/spec-sync.js +41 -1
- package/src/stage-contract.js +26 -8
- package/src/stage-review-checklist.js +2 -0
- package/src/stage-review.js +7 -2
- package/src/stage-templates.js +157 -0
- package/src/stages/archive.js +2 -0
- package/src/stages/brainstorm.js +24 -106
- package/src/stages/execute.js +222 -85
- package/src/stages/knowledge.js +74 -1
- package/src/stages/plan-postcheck.js +385 -20
- package/src/stages/plan.js +23 -1
- package/src/stages/quick.js +10 -2
- package/src/stages/verify.js +4 -2
- package/src/sync.js +81 -23
- package/src/task-done.js +181 -0
- package/src/task-review.js +8 -55
- package/src/taskcard-frontmatter.js +57 -0
- package/src/test-bindings.js +549 -0
- package/src/timeline.js +218 -0
- package/src/ui-visual.js +175 -0
- package/src/verify-draft.js +214 -0
- package/src/verify-postcheck.js +919 -159
- package/src/verify-probes.js +103 -25
- package/src/watcher.js +1031 -0
- package/src/worktree-apply.js +44 -4
- package/src/worktree-deps.js +22 -8
- package/src/worktree-policy.js +42 -0
- package/src/worktree.js +102 -15
- package/templates/agents-instruction.md +37 -18
package/src/run/gate-snapshot.js
CHANGED
|
@@ -11,16 +11,103 @@
|
|
|
11
11
|
* junction 复用主仓依赖(零拷贝),local.yaml 从主仓复制(gitignore 不进 HEAD),在快照
|
|
12
12
|
* cwd 跑对账。快照基建任何失败 → 返回 null,调用方回退主仓现行为(零回归兜底)。
|
|
13
13
|
*
|
|
14
|
-
* 快照生命周期:一次门禁一批(create → run → cleanup
|
|
15
|
-
*
|
|
14
|
+
* 快照生命周期:一次门禁一批(create → run → cleanup);崩溃残留(进程被杀时 cleanup
|
|
15
|
+
* 不跑)由账本回收路径兜底——gate-snapshot-ledger.js 登记 create、cleanup 双清确认后销账,
|
|
16
|
+
* 下个门禁建快照前按 TTL×pid 双闸自愈回收(账本落 runtime 域 .sillyspec/.runtime/,
|
|
17
|
+
* 不进 git 不上平台)。
|
|
16
18
|
*/
|
|
17
19
|
import { execFileSync, spawnSync } from 'node:child_process'
|
|
18
|
-
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync,
|
|
19
|
-
import { join, dirname, relative, resolve } from 'node:path'
|
|
20
|
+
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, existsSync, readdirSync, statSync, symlinkSync, readFileSync } from 'node:fs'
|
|
21
|
+
import { join, dirname, relative, resolve, isAbsolute } from 'node:path'
|
|
20
22
|
import { tmpdir } from 'node:os'
|
|
23
|
+
import { registerGateSnapshot, unregisterGateSnapshot, reclaimStaleGateSnapshots } from './gate-snapshot-ledger.js'
|
|
24
|
+
import { safeRemoveDirWithLinks, unlinkLinkPath, collectLinkPathsUnder } from './junction-rm.js'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* worktree 会话跳快照判定(2026-09-23 R9/R10 双实证根治):cwd 位于 sillyspec 会话专属
|
|
28
|
+
* execute worktree(.sillyspec/.runtime/worktrees/<change>)时,隔离快照的立命之本——
|
|
29
|
+
* 「主仓共享工作区并行会话脏文件不污染本会话门禁」——天然不成立问题不存在:worktree
|
|
30
|
+
* 本身就是单会话独占的隔离环境。而快照在此场景只剩纯成本:venv/node_modules junction
|
|
31
|
+
* 冻结与慢 I/O 面(R9 3MB/0CPU 冻结、R10 15min 慢跑均出自 Temp 快照内)、快照构建时延、
|
|
32
|
+
* 双层 overlay。跳过 = 直接在 worktree cwd 实测(口径与主仓 fallback 同构)。
|
|
33
|
+
* 判定(跨平台):路径分隔符先归一为 /,再匹配 /.sillyspec/.runtime/worktrees/<name>。
|
|
34
|
+
*/
|
|
35
|
+
export function shouldSkipGateSnapshotForWorktree(cwd) {
|
|
36
|
+
const p = String(cwd || '').replace(/\\/g, '/')
|
|
37
|
+
return /\/\.sillyspec\/\.runtime\/worktrees\/[^/]+/.test(p)
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
function git(cwd, args) {
|
|
23
|
-
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000 }).trim()
|
|
41
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true }).trim()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** worktree 注册是否仍登记该 root(跨平台路径归一 + win32 大小写不敏感) */
|
|
45
|
+
function worktreeListContains(cwd, snapshotRoot) {
|
|
46
|
+
const out = git(cwd, ['worktree', 'list', '--porcelain'])
|
|
47
|
+
const norm = (s) => {
|
|
48
|
+
const v = String(s).replace(/\\/g, '/')
|
|
49
|
+
return process.platform === 'win32' ? v.toLowerCase() : v
|
|
50
|
+
}
|
|
51
|
+
const target = norm(snapshotRoot)
|
|
52
|
+
return out.split('\n').some((line) => line.startsWith('worktree ') && norm(line.slice('worktree '.length)) === target)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 快照目录 + worktree 注册的清理体(2026-09-24-gate-snapshot-lifecycle task-02,FR-02)——
|
|
57
|
+
* Windows 硬化:rmSync 带重试(EPERM/junction 锁是本次 41 个残留目录的实证成因),
|
|
58
|
+
* worktree remove 失败后补 prune 清 prunable 注册(本次实证 2 条)。
|
|
59
|
+
* git/删目录/存在性判定的依赖均可注入(故障注入测试见 test/gate-snapshot-cleanup.test.mjs)。
|
|
60
|
+
* 返回双清结果供调用方决定账本销账(D-004@v2);全失败不抛(fail-open)。
|
|
61
|
+
*/
|
|
62
|
+
const REMOVE_DIR_RETRY = { maxRetries: 5, retryDelay: 100 }
|
|
63
|
+
|
|
64
|
+
export function cleanupSnapshot({
|
|
65
|
+
snapshotRoot,
|
|
66
|
+
cwd,
|
|
67
|
+
runGit = (c, a) => git(c, a),
|
|
68
|
+
// junction 安全删除(坑 git-worktree-remove-pierce,2026-09-24 实证):`git worktree remove
|
|
69
|
+
// --force` 的递归删在 Windows **跟随 junction 删目标内容**(实测 victim 复现;worktree.js
|
|
70
|
+
// cleanup 链的 unlinkNodeModulesLinks 先解链正是防它)——快照内 node_modules/venv/copy 面/
|
|
71
|
+
// 钉定缓存链接全部指向主仓真身,必须**先解链再交给 git/rmSync 删目录本体**。解链失败 →
|
|
72
|
+
// 保护性放弃本次删除(目录留账本待回收——双清契约天然兼容)。
|
|
73
|
+
removeDir = (p, opts) => safeRemoveDirWithLinks(p, opts),
|
|
74
|
+
dirExists = (p) => existsSync(p),
|
|
75
|
+
worktreeRegistered = (c, p) => worktreeListContains(c, p),
|
|
76
|
+
}) {
|
|
77
|
+
let worktreeCleaned = true
|
|
78
|
+
// 0. 先解链快照内全部 junction/reparse 点(含深层:venv 族与 gate_snapshot.copy 面在
|
|
79
|
+
// 子目录层)。解链失败不删目录(穿透风险下,保护主仓优先于清理彻底性)。
|
|
80
|
+
let linksCleared = true
|
|
81
|
+
try {
|
|
82
|
+
for (const link of collectLinkPathsUnder(snapshotRoot)) unlinkLinkPath(link)
|
|
83
|
+
} catch (e) {
|
|
84
|
+
linksCleared = false
|
|
85
|
+
console.warn(`⚠️ 快照 junction 解链失败(保护主仓链接目标,本次不删快照目录——条目留账本待下轮回收): ${e && e.message ? e.message : e}`)
|
|
86
|
+
}
|
|
87
|
+
if (linksCleared) {
|
|
88
|
+
// 注:worktree remove/prune 无 --quiet 选项(非法 flag 会必抛并被 catch 吞掉——旧实现
|
|
89
|
+
// 正是因此让 git remove 每次都失败、只剩无重试 rmSync 兜底,41 个残留目录的根因之一)
|
|
90
|
+
try {
|
|
91
|
+
runGit(cwd, ['worktree', 'remove', '--force', snapshotRoot])
|
|
92
|
+
} catch {
|
|
93
|
+
worktreeCleaned = false
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
worktreeCleaned = false
|
|
97
|
+
}
|
|
98
|
+
let dirRemoved = !dirExists(snapshotRoot)
|
|
99
|
+
if (linksCleared && !dirRemoved) {
|
|
100
|
+
try { removeDir(snapshotRoot, REMOVE_DIR_RETRY) } catch { /* 删不掉→下方按实际存在性判定 */ }
|
|
101
|
+
dirRemoved = !dirExists(snapshotRoot)
|
|
102
|
+
}
|
|
103
|
+
if (!worktreeCleaned) {
|
|
104
|
+
// remove 失败:prune 清掉「目录已不在但注册悬空」的 prunable 条目
|
|
105
|
+
try { runGit(cwd, ['worktree', 'prune']) } catch { /* prune 失败→下方复核 */ }
|
|
106
|
+
}
|
|
107
|
+
// 双清契约(D-004@v2):worktreeCleaned 只认 git worktree list 的确认答复——remove 退出码
|
|
108
|
+
// 为零不等于注册已清(prune 未生效或 list 仍含该 root 时仍可能残留)。查询异常按未清。
|
|
109
|
+
try { worktreeCleaned = !worktreeRegistered(cwd, snapshotRoot) } catch { worktreeCleaned = false }
|
|
110
|
+
return { dirRemoved, worktreeCleaned }
|
|
24
111
|
}
|
|
25
112
|
|
|
26
113
|
/**
|
|
@@ -50,7 +137,7 @@ function runOverlayImportSmoke(pythonBin, snapshotRoot, overlaidFiles) {
|
|
|
50
137
|
let acted = false
|
|
51
138
|
for (const f of overlaidFiles) {
|
|
52
139
|
if (reverted.has(f)) continue
|
|
53
|
-
const bad = smokeImportPython(pythonBin, snapshotRoot, f)
|
|
140
|
+
const bad = smokeImportPython(pythonBin, snapshotRoot, f, byModule)
|
|
54
141
|
if (!bad) continue
|
|
55
142
|
acted = true
|
|
56
143
|
// 依赖归因:报错点名另一 overlay 文件 → 回退依赖( importer 自身往往无恙)
|
|
@@ -82,32 +169,66 @@ function findSnapshotPython(envRelDirs, snapshotRoot) {
|
|
|
82
169
|
}
|
|
83
170
|
|
|
84
171
|
/**
|
|
85
|
-
* 单文件 import 冒烟:`python -c "import <mod>"
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
172
|
+
* 单文件 import 冒烟:`python -c "import <mod>"`,包根从浅到深全尝试后才定性。
|
|
173
|
+
*
|
|
174
|
+
* 坑 verify-gate-worktree-crossrepo-three-defects 缺陷一(2026-09-21 实证):旧实现首个
|
|
175
|
+
* import-class 错误立即判坏——浅根(快照根)下 `No module named 'backend'`(backend 不是
|
|
176
|
+
* 快照根顶层包,包根本就该在 backend/)属 **cwd 假阳性**,却触发交付文件被回退 HEAD →
|
|
177
|
+
* 快照内丢 stats() 等新增 → 模块子集 AttributeError 连环假红。
|
|
178
|
+
*
|
|
179
|
+
* 定性规则(全根耗尽后):
|
|
180
|
+
* - 任一根 import 成功 → 文件健康(null);
|
|
181
|
+
* - SyntaxError → 立即判坏(语法坏与包根无关);
|
|
182
|
+
* - `cannot import name X`(模块已解析、缺名)→ 判坏(真部分态,根无关);
|
|
183
|
+
* - `ModuleNotFoundError: No module named 'X'` → 仅当 X 末段映射到**另一 overlay .py**
|
|
184
|
+
* (byModule)才判坏(overlay 依赖缺失);第三方依赖缺失/浅根前缀 → 环境问题不判坏
|
|
185
|
+
* (回退语义只针对「部分态坏文件」,坑原文口径)。
|
|
186
|
+
*
|
|
187
|
+
* @param {string} pythonBin 解释器
|
|
188
|
+
* @param {string} snapshotRoot 快照根
|
|
189
|
+
* @param {string} relFile overlay 文件(仓库根相对 POSIX)
|
|
190
|
+
* @param {Map<string,string>} byModule 末段模块名 → overlay 文件(缺失归因用)
|
|
89
191
|
* @returns {string|null} 失败原因(判坏时);通过/环境性失败返回 null
|
|
90
192
|
*/
|
|
91
|
-
function smokeImportPython(pythonBin, snapshotRoot, relFile) {
|
|
193
|
+
function smokeImportPython(pythonBin, snapshotRoot, relFile, byModule) {
|
|
92
194
|
const noExt = relFile.replace(/\.py$/, '')
|
|
93
195
|
const parts = noExt.split('/')
|
|
94
196
|
while (parts.length > 0 && parts[parts.length - 1] === '__init__') parts.pop()
|
|
95
197
|
if (parts.length === 0) return null
|
|
96
|
-
|
|
198
|
+
let nameErr = null
|
|
199
|
+
let missingOverlayErr = null
|
|
97
200
|
for (let cut = 0; cut < parts.length; cut++) {
|
|
98
201
|
const mod = parts.slice(cut).join('.')
|
|
99
202
|
if (!mod || !/^[A-Za-z_][\w.]*$/.test(mod)) continue
|
|
100
203
|
const pkgRoot = join(snapshotRoot, ...parts.slice(0, cut))
|
|
101
204
|
const r = spawnSync(pythonBin, ['-c', `import ${mod}`], {
|
|
102
|
-
cwd: pkgRoot, encoding: 'utf-8', timeout: 10_000,
|
|
205
|
+
cwd: pkgRoot, encoding: 'utf-8', timeout: 10_000, windowsHide: true,
|
|
103
206
|
})
|
|
104
207
|
if (r.status === 0) return null
|
|
105
208
|
const err = String((r.stderr || '') + (r.stdout || ''))
|
|
106
|
-
if (/
|
|
107
|
-
|
|
209
|
+
if (/SyntaxError/i.test(err)) {
|
|
210
|
+
// 语法坏与包根无关:该文件任何根下都不可导入,立即判坏
|
|
211
|
+
return (err.match(/SyntaxError[^\n]*/) || ['语法错误'])[0].slice(0, 120)
|
|
212
|
+
}
|
|
213
|
+
if (/cannot import name/i.test(err)) {
|
|
214
|
+
// 模块已解析、缺名 = 真部分态(依赖 overlay 兄弟缺符号);记录,全根耗尽后判坏
|
|
215
|
+
if (!nameErr) nameErr = (err.match(/cannot import name[^\n]*/) || ['cannot import name'])[0].slice(0, 120)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
const mnf = err.match(/ModuleNotFoundError: No module named '([A-Za-z_][\w.]*)'/)
|
|
219
|
+
if (mnf) {
|
|
220
|
+
const missingLast = mnf[1].split('.').pop()
|
|
221
|
+
if (byModule.has(missingLast)) {
|
|
222
|
+
// 缺的模块是 overlay 兄弟(如 crud.py 未进快照)→ 部分态候选,记录
|
|
223
|
+
if (!missingOverlayErr) missingOverlayErr = (err.match(/ModuleNotFoundError[^\n]*/) || ['module not found'])[0].slice(0, 120)
|
|
224
|
+
}
|
|
225
|
+
// else:第三方依赖缺失 / 浅根前缀(cwd 假阳性)→ 环境问题,继续试更深包根
|
|
226
|
+
continue
|
|
108
227
|
}
|
|
109
|
-
//
|
|
228
|
+
// 其余失败(超时/崩溃等)→ 环境性,继续
|
|
110
229
|
}
|
|
230
|
+
if (nameErr) return nameErr
|
|
231
|
+
if (missingOverlayErr) return missingOverlayErr
|
|
111
232
|
return null
|
|
112
233
|
}
|
|
113
234
|
|
|
@@ -240,7 +361,7 @@ export function runGateSnapshotCommands(snapshotRoot, commands) {
|
|
|
240
361
|
ran++
|
|
241
362
|
let r = null
|
|
242
363
|
try {
|
|
243
|
-
r = spawnSync(cmd, { shell: true, cwd: snapshotRoot, timeout: 300_000, encoding: 'utf8' })
|
|
364
|
+
r = spawnSync(cmd, { shell: true, cwd: snapshotRoot, timeout: 300_000, encoding: 'utf8', windowsHide: true })
|
|
244
365
|
} catch (e) {
|
|
245
366
|
const reason = `spawn 异常:${e && e.message ? e.message : e}`
|
|
246
367
|
failed.push({ cmd, reason })
|
|
@@ -332,7 +453,9 @@ export function applyGateSnapshotCopy(cwd, snapshotRoot) {
|
|
|
332
453
|
viaLink = existsSync(dst)
|
|
333
454
|
} catch { viaLink = false }
|
|
334
455
|
if (!viaLink) {
|
|
335
|
-
|
|
456
|
+
// 假链接残留清理:dst 是 junction 本体——rmSync(dst) 在 Node v24 Windows 会穿透删
|
|
457
|
+
// 链接目标(主仓 copy 面真身,坑 junction-pierce-rm-sync),必须 rmdir 解链语义
|
|
458
|
+
try { unlinkLinkPath(dst) } catch { /* 假链接残留清不掉 → 下方复制报错走 warn */ }
|
|
336
459
|
if (srcStat.isDirectory()) copyDirRecursive(src, dst)
|
|
337
460
|
else copyFileSync(src, dst)
|
|
338
461
|
}
|
|
@@ -352,12 +475,25 @@ export function applyGateSnapshotCopy(cwd, snapshotRoot) {
|
|
|
352
475
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
353
476
|
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
354
477
|
*/
|
|
355
|
-
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null }) {
|
|
478
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null, runtimeRoot = null }) {
|
|
356
479
|
let snapshotRoot = null
|
|
357
480
|
try {
|
|
358
481
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
359
482
|
git(cwd, ['rev-parse', 'HEAD'])
|
|
360
483
|
|
|
484
|
+
// 陈旧快照自愈(2026-09-24-gate-snapshot-lifecycle,D-002@v1/D-003@v2/D-004@v2):
|
|
485
|
+
// 建快照前同步回收账本内「守卫通过 ∧ 超 TTL ∧ pid 明确死」的条目(目录+worktree 注册
|
|
486
|
+
// 双清,删不掉的保留待下轮)。全 try/catch 恒吞——回收异常零阻断建快照;reclaimed 空
|
|
487
|
+
// 时零输出(正常路径逐字节不变)。runtimeRoot 缺失→账本链路退 no-op(禁 cwd 猜)。
|
|
488
|
+
try {
|
|
489
|
+
if (runtimeRoot) {
|
|
490
|
+
const { reclaimed, skipped } = reclaimStaleGateSnapshots({ runtimeRoot, cwd })
|
|
491
|
+
if (reclaimed.length > 0) {
|
|
492
|
+
console.log(`🧹 门禁快照自愈:回收 ${reclaimed.length} 个失活残留${skipped.length > 0 ? `(${skipped.length} 个删不掉,留账本待下轮)` : ''}`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch { /* 回收异常 fail-open 退现状 */ }
|
|
496
|
+
|
|
361
497
|
// symlink-store 布局探测(坑 gate-snapshot-pnpm-store-break,2026-09-12 驾驭第十七批②,
|
|
362
498
|
// 用户第三次踩「lint 沙箱临时目录跑 pnpm 必假败」):pnpm/bun/lerna 的 node_modules 内部
|
|
363
499
|
// 是指向 store 的符号链接网——junction 进临时目录后跨根解析失效,实测必假败。
|
|
@@ -370,6 +506,8 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
370
506
|
|
|
371
507
|
snapshotRoot = mkdtempSync(join(tmpdir(), 'sillyspec-gate-'))
|
|
372
508
|
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
509
|
+
// 登记账本(D-004@v2):进程被杀时 cleanup 不跑,条目是残留唯一的可追踪凭据
|
|
510
|
+
registerGateSnapshot({ runtimeRoot, snapshotRoot })
|
|
373
511
|
|
|
374
512
|
// 会话文件 overlay:主仓工作区版本覆盖进快照(本会话的最新态;文件不存在=已删,跳过)。
|
|
375
513
|
// sourceRoot(verify 门定向跑用,2026-09-10 驾驭小结第八批):overlay 源切换为本变更
|
|
@@ -389,15 +527,21 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
389
527
|
// 两份都变于祖先且互不相等(真双写分叉)→ 显式警告 + 取 cwd(主仓直写=本会话最新编辑位约定)。
|
|
390
528
|
// 行尾归一(坑 autocrlf-byte-misjudge):本机 autocrlf=true 时快照 checkout 是 CRLF、
|
|
391
529
|
// 工作区文件常是 LF——字节直比会误判「两边都改」。比较一律 CRLF→LF 归一后做。
|
|
530
|
+
// 对称 trim(坑 gate-snapshot-ancestor-trim,2026-09-20 taskcard-yaml-hardgate verify 实证):
|
|
531
|
+
// git() 助手对全部输出 .trim()(rev-parse 场景需要),git show 的祖先内容因此被剥掉尾换行——
|
|
532
|
+
// 与磁盘读取(仅 CRLF 归一)不对称,已存在文件恒 anc!==cwd 误判「双写分叉」取主仓旧版,
|
|
533
|
+
// worktree 交付的修改进不了 verify 门快照→门假红(新文件因主仓不存在幸免)。比较三侧
|
|
534
|
+
// 统一 normalizeEol+trim 同口径——首尾空白差异不参与分叉判定(语义修改不会只差首尾空白)。
|
|
392
535
|
const normalizeEol = (buf) => String(buf).replace(/\r\n/g, '\n')
|
|
536
|
+
const cmpText = (t) => normalizeEol(t).trim()
|
|
393
537
|
const headFileContent = (rel) => {
|
|
394
538
|
if (mergeBase) {
|
|
395
539
|
try {
|
|
396
540
|
const anc = git(cwd, ['show', `${mergeBase}:${rel}`])
|
|
397
|
-
if (typeof anc === 'string') return
|
|
541
|
+
if (typeof anc === 'string') return cmpText(anc)
|
|
398
542
|
} catch { /* 祖先无此文件(新增文件)→ 落到快照 HEAD 口径 */ }
|
|
399
543
|
}
|
|
400
|
-
try { return
|
|
544
|
+
try { return cmpText(readFileSync(join(snapshotRoot, rel))) } catch { return null }
|
|
401
545
|
}
|
|
402
546
|
for (const f of files) {
|
|
403
547
|
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
@@ -407,8 +551,8 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
407
551
|
const wtPath = join(sourceRoot, f), cwdPath = join(cwd, f)
|
|
408
552
|
if (existsSync(wtPath) && existsSync(cwdPath)) {
|
|
409
553
|
let wtText = null, cwdText = null
|
|
410
|
-
try { wtText =
|
|
411
|
-
try { cwdText =
|
|
554
|
+
try { wtText = cmpText(readFileSync(wtPath)) } catch { /* 读失败不介入 */ }
|
|
555
|
+
try { cwdText = cmpText(readFileSync(cwdPath)) } catch { /* 读失败不介入 */ }
|
|
412
556
|
if (wtText !== null && cwdText !== null && wtText !== cwdText) {
|
|
413
557
|
const base = headFileContent(f)
|
|
414
558
|
const wtDiff = base === null || wtText !== base
|
|
@@ -416,8 +560,12 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
416
560
|
if (cwdDiff && !wtDiff) {
|
|
417
561
|
src = cwdPath // 主仓直写新、worktree 停在基线 → 取主仓
|
|
418
562
|
} else if (!(wtDiff && !cwdDiff)) {
|
|
419
|
-
|
|
420
|
-
|
|
563
|
+
// M2(2026-09-21-r5-efficiency-batch2 D-002@v2):双写分叉改取 worktree 分支版——sourceRoot=worktree
|
|
564
|
+
// 定向跑的语义=验证本变更分支交付(batch1 实证:分叉取主仓拿走无本变更代码的版本,配上
|
|
565
|
+
// worktree 新增测试文件 → src/test 血统断裂 → module 实测假红一轮)。主仓侧同文件异动是
|
|
566
|
+
// 干扰源,其内容在 apply/对齐后由主仓单源复核兜底。
|
|
567
|
+
console.warn(`⚠️ 快照双写分叉:${f} 在主仓与 worktree 均有不同修改——取 worktree 分支版(本变更交付血统);主仓侧改动若需保留请 apply/对齐后复跑`)
|
|
568
|
+
src = wtPath
|
|
421
569
|
} // wtDiff && !cwdDiff → 保持 worktree(正常流零改变)
|
|
422
570
|
}
|
|
423
571
|
}
|
|
@@ -459,11 +607,36 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
459
607
|
const envMissing = envDirsLinked(cwd, snapshotRoot)
|
|
460
608
|
if (envMissing.length > 0) {
|
|
461
609
|
console.warn(`⚠️ 门禁快照环境不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测(宁可主仓口径不可假沙箱硬挂;回退后失败先做污染归属鉴定)`)
|
|
462
|
-
|
|
463
|
-
|
|
610
|
+
// 与正常 cleanup/失败 catch 同一清理体:双清确认才销账(该早退曾残留旧式 --quiet
|
|
611
|
+
// 清理与无重试 rmSync——独立审查 P1-1 根因未除尽的漏网面)
|
|
612
|
+
try {
|
|
613
|
+
const { dirRemoved: dGone, worktreeCleaned: wClean } = cleanupSnapshot({ snapshotRoot, cwd })
|
|
614
|
+
if (dGone && wClean) unregisterGateSnapshot({ runtimeRoot, snapshotRoot })
|
|
615
|
+
} catch {}
|
|
464
616
|
return null
|
|
465
617
|
}
|
|
466
618
|
|
|
619
|
+
// env 钉定缓存链接面(r5l 方案 5 / P17,通用机制见 resolvePinnedCacheLinks jsdoc):
|
|
620
|
+
// 工具把缓存钉在仓内(UV_CACHE_DIR=.uv-cache 类)时 gitignored 不进 HEAD,快照缺它 →
|
|
621
|
+
// 构建隔离环境(含已装构建后端)缺失 → 快照内构建假红。junction 同相对路径进快照,
|
|
622
|
+
// 与环境目录族同款链接原语。整段 fail-soft:链接失败只 warn 不作废快照(缓存面缺失的
|
|
623
|
+
// 影响是工具自身的构建失败,失败输出自带归因提示);未钉定/钉在仓外 → 全段零行为零输出。
|
|
624
|
+
const pinnedCacheLinks = []
|
|
625
|
+
for (const link of resolvePinnedCacheLinks({ cwd })) {
|
|
626
|
+
if (!existsSync(link.srcAbs)) continue
|
|
627
|
+
const dst = join(snapshotRoot, link.rel)
|
|
628
|
+
try {
|
|
629
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
630
|
+
symlinkSync(link.srcAbs, dst, 'junction')
|
|
631
|
+
pinnedCacheLinks.push({ envName: link.envName, tool: link.tool, rel: link.rel })
|
|
632
|
+
} catch (e) {
|
|
633
|
+
console.warn(`⚠️ 门禁快照构建缓存链接失败(${link.envName}=${link.rel}):${e && e.message ? e.message : e}——快照内该工具构建可能缺缓存假红,失败先对照主仓口径`)
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (pinnedCacheLinks.length > 0) {
|
|
637
|
+
console.log(`🔬 门禁快照构建缓存面:${pinnedCacheLinks.map((x) => `${x.envName}=${x.rel}`).join('、')} 已 junction 进快照(P17:gitignored 仓内缓存携带构建隔离环境——缺它快照内构建假红;⚠️ 活链接,快照内构建写穿回主仓缓存)`)
|
|
638
|
+
}
|
|
639
|
+
|
|
467
640
|
// gate_snapshot.commands 命令面(D-002@v2,2026-09-17-feedback-hardening R1,用户
|
|
468
641
|
// 2026-09-17 负面①实证「全量快照必挂」):主仓侧生成物缺失(fresh clone 未跑
|
|
469
642
|
// postinstall)或过期时,copy 面只搬「主仓现有态」供给缺失/陈旧态——本段在环境链接
|
|
@@ -520,16 +693,23 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
520
693
|
}
|
|
521
694
|
|
|
522
695
|
const cleanup = () => {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
696
|
+
// 残留不再「交 OS tmp 清理」(%TEMP% 不会被 OS 清)——删不掉的快照由账本回收路径
|
|
697
|
+
// (下个 create 前按 TTL×pid 双闸自愈,2026-09-24-gate-snapshot-lifecycle)兜底;
|
|
698
|
+
// 销账只在「目录+worktree 注册双清确认」后执行(D-004@v2),删不掉保留条目待下轮
|
|
699
|
+
try {
|
|
700
|
+
const { dirRemoved, worktreeCleaned } = cleanupSnapshot({ snapshotRoot, cwd })
|
|
701
|
+
if (dirRemoved && worktreeCleaned) unregisterGateSnapshot({ runtimeRoot, snapshotRoot })
|
|
702
|
+
} catch { /* 双失败已由 cleanupSnapshot 吞掉;条目保留 */ }
|
|
526
703
|
}
|
|
527
|
-
return { snapshotRoot, cleanup, overlaid }
|
|
704
|
+
return { snapshotRoot, cleanup, overlaid, pinnedCacheLinks }
|
|
528
705
|
} catch (e) {
|
|
529
|
-
// 基建失败(worktree add
|
|
706
|
+
// 基建失败(worktree add 拒绝/磁盘满/…):尽力清理后回退主仓;销账同走双清确认
|
|
707
|
+
// (D-004@v2:清不掉的条目保留待回收——它不是悬空条目,是待回收记录)
|
|
530
708
|
if (snapshotRoot) {
|
|
531
|
-
try {
|
|
532
|
-
|
|
709
|
+
try {
|
|
710
|
+
const { dirRemoved, worktreeCleaned } = cleanupSnapshot({ snapshotRoot, cwd })
|
|
711
|
+
if (dirRemoved && worktreeCleaned) unregisterGateSnapshot({ runtimeRoot, snapshotRoot })
|
|
712
|
+
} catch {}
|
|
533
713
|
}
|
|
534
714
|
return null
|
|
535
715
|
}
|
|
@@ -556,9 +736,48 @@ export function detectSymlinkStoreLayout(cwd) {
|
|
|
556
736
|
/** 环境目录名集(根与子包通用) */
|
|
557
737
|
const ENV_DIR_NAMES = new Set(['node_modules', '.venv', 'venv', 'env'])
|
|
558
738
|
|
|
739
|
+
/**
|
|
740
|
+
* 「env 钉定缓存目录」注册表(r5l-forensic-verdict 方案 5 / P17,用户裁定走通用机制不做
|
|
741
|
+
* 单一工具硬编码):构建/包管理工具用 env var 把缓存钉在**仓内**(相对 cwd 或仓内绝对路径)
|
|
742
|
+
* 时,该目录 gitignored 不进 HEAD——隔离快照(HEAD worktree)天然缺它,快照内构建隔离
|
|
743
|
+
* (uv builds-v0 / pip build 隔离环境)拿不到已装好的构建后端(P17:hatchling EPERM 假红
|
|
744
|
+
* 两轮卡 37min)。机制对任意工具通用:解析「注册的 env var → 仓内路径 → junction 进快照
|
|
745
|
+
* 同相对路径」,工具只是注册表一行数据——扩新工具(cargo/poetry/自定义)加一行即可,
|
|
746
|
+
* 不新增代码分支。家目录缺省形态(~/.cache/uv、%LOCALAPPDATA%/uv 等)**刻意不链接**:
|
|
747
|
+
* 快照内命令继承完整进程 env,家目录绝对路径直达真实缓存,junction 同一批文件零机制增益。
|
|
748
|
+
* pnpm-store 族不入表:符号链接 store 经 junction 跨根失效(detectSymlinkStoreLayout 先例)。
|
|
749
|
+
*/
|
|
750
|
+
const PINNED_CACHE_ENV_VARS = [
|
|
751
|
+
{ env: 'UV_CACHE_DIR', tool: 'uv' },
|
|
752
|
+
{ env: 'PIP_CACHE_DIR', tool: 'pip' },
|
|
753
|
+
{ env: 'POETRY_CACHE_DIR', tool: 'poetry' },
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* 解析 env 钉定的仓内缓存目录(纯解析,不做链接/IO 判存在——可测性)。
|
|
758
|
+
* 规则:注册表逐项取 env 值;相对路径按 cwd 解析(与 uv/pip 相对 cwd 语义一致);解析后
|
|
759
|
+
* 落在仓内(relative 不出仓)才算钉定缓存;仓外路径(家目录/自定义绝对位置)经继承 env
|
|
760
|
+
* 直达,不属于本面。同 var 多次出现取首个命中。
|
|
761
|
+
* @returns {{ envName: string, tool: string, srcAbs: string, rel: string }[]} 仓内钉定缓存清单
|
|
762
|
+
*/
|
|
763
|
+
export function resolvePinnedCacheLinks({ cwd, env = process.env } = {}) {
|
|
764
|
+
const out = []
|
|
765
|
+
for (const entry of PINNED_CACHE_ENV_VARS) {
|
|
766
|
+
const v = env[entry.env]
|
|
767
|
+
if (!v || !String(v).trim()) continue
|
|
768
|
+
const abs = isAbsolute(String(v).trim()) ? String(v).trim() : resolve(cwd, String(v).trim())
|
|
769
|
+
let rel = null
|
|
770
|
+
try { rel = relative(cwd, abs) } catch { continue }
|
|
771
|
+
if (!rel || rel.startsWith('..') || isAbsolute(rel)) continue // 仓外直达,无需链接
|
|
772
|
+
if (out.some((x) => x.rel === rel.split('\\').join('/'))) continue
|
|
773
|
+
out.push({ envName: entry.env, tool: entry.tool, srcAbs: abs, rel: rel.split('\\').join('/') })
|
|
774
|
+
}
|
|
775
|
+
return out
|
|
776
|
+
}
|
|
777
|
+
|
|
559
778
|
/**
|
|
560
779
|
* 递归依赖发现(坑 gate-snapshot-monorepo-layout):深度 ≤3 扫描(跳过环境目录自身内部/
|
|
561
|
-
* .git/dist/build/.sillyspec),返回所有环境目录的仓库根相对 POSIX
|
|
780
|
+
* .git/dist/build/.sillyspec),返回所有环境目录的仓库根相对 POSIX 路径(含根四目录)。
|
|
562
781
|
* pnpm/nx/lerna workspace 的 packages/<pkg>/node_modules 覆盖。
|
|
563
782
|
*/
|
|
564
783
|
export function discoverEnvDirs(cwd, { maxDepth = 3 } = {}) {
|
|
@@ -621,6 +840,10 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
621
840
|
try {
|
|
622
841
|
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
623
842
|
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
843
|
+
// 账本 runtimeRoot(2026-09-24-gate-snapshot-lifecycle task-03):函数级解析一次,
|
|
844
|
+
// 平台/漂移模式口径与本函数内他处一致;解析失败退 null(账本链路 no-op,禁 cwd 猜)
|
|
845
|
+
let verifyRuntimeRoot = null
|
|
846
|
+
try { verifyRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase) } catch { verifyRuntimeRoot = null }
|
|
624
847
|
const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
|
|
625
848
|
let changeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
|
|
626
849
|
specBase, includeWorkingTree: true,
|
|
@@ -630,8 +853,7 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
630
853
|
// 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
|
|
631
854
|
// 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
|
|
632
855
|
try {
|
|
633
|
-
const
|
|
634
|
-
const { foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, changeFiles, { specBase, runtimeRoot })
|
|
856
|
+
const { foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, changeFiles, { specBase, runtimeRoot: verifyRuntimeRoot })
|
|
635
857
|
if (foreign.length > 0) {
|
|
636
858
|
const foreignSet = new Set(foreign.map(x => x.file))
|
|
637
859
|
changeFiles = changeFiles.filter(f => !foreignSet.has(f))
|
|
@@ -664,7 +886,7 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
664
886
|
}
|
|
665
887
|
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
666
888
|
|
|
667
|
-
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase })
|
|
889
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase, runtimeRoot: verifyRuntimeRoot })
|
|
668
890
|
if (!snap) return null
|
|
669
891
|
|
|
670
892
|
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|