sillyspec 3.28.8 → 3.28.9
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-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +32 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +188 -17
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +155 -14
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +107 -12
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +54 -16
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gates.js +50 -9
- package/src/run/prompt.js +134 -5
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +76 -10
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +52 -3
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +135 -7
- package/src/task-review.js +292 -21
- package/src/verify-postcheck.js +20 -0
- package/src/verify-probes.js +340 -4
- package/src/worktree-apply.js +538 -44
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +117 -5
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/src/worktree-apply.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 1. 读取 meta.json 获取 baseHash
|
|
6
6
|
* 2. git diff --name-only baseHash 获取 worktree 中所有变更文件
|
|
7
7
|
* 3. 从 design.md 解析文件变更清单(无清单 = 允许所有)
|
|
8
|
-
* 4. 校验:变更文件 ⊆
|
|
8
|
+
* 4. 校验:变更文件 ⊆ 清单(review 声明与 allow 面相交过滤,D-003:外来声明不并入放行面)
|
|
9
9
|
* 5. 校验:主工作区文件 base hash 一致
|
|
10
10
|
* 6. --check-only 模式只输出检查结果
|
|
11
11
|
* 7. 非 checkOnly:生成 patch → apply --check → apply --3way
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync, readFileSync, mkdirSync } from 'fs';
|
|
16
16
|
import { join, resolve } from 'path';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
|
+
import { createHash } from 'crypto';
|
|
18
19
|
import { WorktreeManager } from './worktree.js';
|
|
19
20
|
import { getCrossWorktreeMeta, cleanupCrossWorktrees } from './worktree-cross.js';
|
|
20
21
|
import { parseFileChangeList, parseFileChangeListDetailed, pathMatches } from './change-list.js';
|
|
21
22
|
import { parseAllowedPaths, parseRepo } from './stages/plan-postcheck.js';
|
|
22
|
-
import { git, gitQuiet } from './git-helper.js';
|
|
23
|
+
import { git, gitQuiet, safeGit } from './git-helper.js';
|
|
23
24
|
import { resolveLatestExecuteRunId, resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } from './task-review.js';
|
|
25
|
+
import { collectActiveQuickGuardFiles } from './quicklog.js';
|
|
26
|
+
import { detectCommittedDrift, formatCommittedDriftWarning } from './run/concurrent-detect.js';
|
|
24
27
|
|
|
25
28
|
const CHANGES_REL = '.sillyspec/changes';
|
|
26
29
|
|
|
@@ -151,7 +154,133 @@ export function mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash
|
|
|
151
154
|
} finally {
|
|
152
155
|
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
153
156
|
}
|
|
154
|
-
|
|
157
|
+
// 批末统一暂存(task-01 / FR-01 / D-001@v1 前半,2026-09-14-apply-conflict-hardening):
|
|
158
|
+
// 本写回点曾是全链路唯一未暂存的自动写点——未暂存文件对主仓工作区级 git 操作
|
|
159
|
+
// (restore / clean / checkout)零免疫(§64 事故的实际缺口)。对全部 clean 写回文件
|
|
160
|
+
// 显式 pathspec git add(safeGit 数组形式不经 shell;chunkPaths 分批防 Windows argv
|
|
161
|
+
// 上限,批 D-③ 同款)。fail-open:add 失败不抛(合并成果保留),console.warn 留痕 +
|
|
162
|
+
// 返回 stagedOk:false——manifest 侧对该批文件自动回退 readFile+归一 approx 口径。
|
|
163
|
+
let stagedOk = true
|
|
164
|
+
if (merged.length > 0) {
|
|
165
|
+
for (const batch of chunkPaths(merged)) {
|
|
166
|
+
const r = safeGit(projectRoot, ['add', '--', ...batch])
|
|
167
|
+
if (r && r.error) {
|
|
168
|
+
stagedOk = false
|
|
169
|
+
console.warn(`⚠️ merge 写回批末 git add 失败(fail-open:${merged.length} 个合并文件成果保留,但未暂存对 git restore/clean 不免疫,请尽快人工 git add):${String(r.error).split('\n')[0]}`)
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { merged, conflicts, stagedOk }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 字节级 CRLF→LF 归一(ql-20260915-001 修复①,mergeMismatchThreeWay 专用)。
|
|
179
|
+
*
|
|
180
|
+
* 动机:本仓 core.autocrlf=true(2026-09-15 实测),git checkout 出的 worktree 磁盘内容是
|
|
181
|
+
* CRLF、blob(git show 取出)是 LF——ours(blob LF) 与 theirs(disk CRLF) 混形喂 merge-file 会
|
|
182
|
+
* 逐行假冲突(每一行都呈现为两侧各改一次)。三输入统一 LF 域合并,写回后 git add 按
|
|
183
|
+
* autocrlf 属性归一回 blob 域(磁盘 LF 形态 git status 亦不报幻影改动)。
|
|
184
|
+
*
|
|
185
|
+
* 字节级替换(latin1 语义,不经 utf8 解码)保任意非 UTF-8 文本字节不被替换字符污染;含 NUL
|
|
186
|
+
* (二进制标志)返回 null——二进制不可文本合并,调用方按冲突处理。
|
|
187
|
+
*
|
|
188
|
+
* @param {Buffer} buf
|
|
189
|
+
* @returns {Buffer|null} 归一后内容;二进制返回 null
|
|
190
|
+
*/
|
|
191
|
+
function _toLfBuffer(buf) {
|
|
192
|
+
if (!buf || buf.length === 0) return buf
|
|
193
|
+
if (buf.indexOf(0) !== -1) return null // 二进制
|
|
194
|
+
if (buf.indexOf(Buffer.from('\r\n')) === -1) return buf
|
|
195
|
+
const out = []
|
|
196
|
+
for (let i = 0; i < buf.length; i++) {
|
|
197
|
+
if (buf[i] === 0x0d && i + 1 < buf.length && buf[i + 1] === 0x0a) { out.push(0x0a); i++ }
|
|
198
|
+
else out.push(buf[i])
|
|
199
|
+
}
|
|
200
|
+
return Buffer.from(out)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* EXCLUDE-MISMATCH 前置自动三方合并(ql-20260915-001 修复①,坑 apply-archived-mismatch-no-merge:
|
|
205
|
+
* 2026-09-14 用户实证归档后补 apply 时主仓文件已被并行会话提交推进 → rescue EXCLUDE-MISMATCH
|
|
206
|
+
* 跳过不落地,被迫手工 cp + 锚点合并)。
|
|
207
|
+
*
|
|
208
|
+
* 与 mergeDirtyOverlapThreeWay 同实现形态,base/ours 取源不同:
|
|
209
|
+
* - base = apply 锚点(baseHash 形参,调用方传 deliverableBase=baselineCommit||baseHash)blob
|
|
210
|
+
* - **ours = 主仓 HEAD blob(已提交推进态,非脏读——mismatch 面主仓无未提交改动,
|
|
211
|
+
* 调用方已按 pre-stash dirty 集排除,HEAD 即主仓当前态)**
|
|
212
|
+
* - theirs = worktree 文件内容(磁盘读,子代理未 commit 的交付)
|
|
213
|
+
*
|
|
214
|
+
* clean 合并才写回主仓(两侧增量都保留)+ 批末 git add(chunkPaths 分批)+ 调用方从
|
|
215
|
+
* changedFiles/hashMismatchFiles 剔除(patch 不再重放,防 staged 内容与 patch 预像互踩);
|
|
216
|
+
* conflict / 异常 fail-soft 维持原路径(rescue EXCLUDE-MISMATCH 新文案 / step7 --3way 实测)。
|
|
217
|
+
* 覆写前备份主仓磁盘原文至 .sillyspec/.runtime/merge-backups/(与脏读版同一惯例)。
|
|
218
|
+
*
|
|
219
|
+
* @param {{ projectRoot: string, worktreePath: string, baseHash: string }} ctx
|
|
220
|
+
* @param {string[]} mismatchFiles mismatch ∩ changedFiles 且主仓无未提交改动的文件(仓库根相对路径)
|
|
221
|
+
* @returns {{ merged: string[], conflicts: string[], stagedOk: boolean }}
|
|
222
|
+
*/
|
|
223
|
+
export function mergeMismatchThreeWay({ projectRoot, worktreePath, baseHash }, mismatchFiles) {
|
|
224
|
+
const merged = []
|
|
225
|
+
const conflicts = []
|
|
226
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-3way-m-'))
|
|
227
|
+
try {
|
|
228
|
+
for (const f of mismatchFiles) {
|
|
229
|
+
try {
|
|
230
|
+
// ours/HEAD 缺失(主干侧删除等 modify/delete 形态)→ git show 抛错 → conflicts(fail-soft,
|
|
231
|
+
// 删除语义 merge-file 无法表达,交既有出口 --3way/--merge 裁决)
|
|
232
|
+
const baseRaw = git(projectRoot, ['show', `${baseHash}:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
233
|
+
const oursRaw = git(projectRoot, ['show', `HEAD:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
234
|
+
const wtRaw = readFileSync(join(worktreePath, f))
|
|
235
|
+
const baseN = _toLfBuffer(baseRaw)
|
|
236
|
+
const oursN = _toLfBuffer(oursRaw)
|
|
237
|
+
const theirsN = _toLfBuffer(wtRaw)
|
|
238
|
+
if (!baseN || !oursN || !theirsN) { conflicts.push(f); continue } // 二进制 → 原路径
|
|
239
|
+
const p = (tag) => join(tmpDir, `${tag}-${Math.random().toString(36).slice(2, 8)}`)
|
|
240
|
+
const baseP = p('base'); const oursP = p('ours'); const theirsP = p('theirs')
|
|
241
|
+
writeFileSync(baseP, baseN); writeFileSync(oursP, oursN); writeFileSync(theirsP, theirsN)
|
|
242
|
+
let outBuf, code = 0
|
|
243
|
+
try {
|
|
244
|
+
outBuf = git(projectRoot, ['merge-file', '-p', oursP, baseP, theirsP], { encoding: 'buffer', timeout: 15000 })
|
|
245
|
+
} catch (e) {
|
|
246
|
+
code = (typeof e.status === 'number') ? e.status : -1
|
|
247
|
+
outBuf = (e.stdout instanceof Buffer) ? e.stdout : Buffer.alloc(0)
|
|
248
|
+
if (code < 0) { conflicts.push(f); continue }
|
|
249
|
+
}
|
|
250
|
+
if (code === 0) {
|
|
251
|
+
// 覆写前备份主仓磁盘原文(merge-backups 惯例;ours=HEAD 理论可经 git show 重取,仍留备份保审计)
|
|
252
|
+
try {
|
|
253
|
+
const backupDir = join(projectRoot, '.sillyspec', '.runtime', 'merge-backups')
|
|
254
|
+
mkdirSync(backupDir, { recursive: true })
|
|
255
|
+
let pre = oursRaw
|
|
256
|
+
try { pre = readFileSync(join(projectRoot, f)) } catch { /* 磁盘缺文件用 HEAD 原文 */ }
|
|
257
|
+
writeFileSync(join(backupDir, `${Date.now()}-${f.replace(/[\\/]/g, '__')}`), pre)
|
|
258
|
+
} catch { /* 备份失败不阻断合并 */ }
|
|
259
|
+
writeFileSync(join(projectRoot, f), outBuf)
|
|
260
|
+
merged.push(f)
|
|
261
|
+
console.log(`🔀 EXCLUDE-MISMATCH 三方合并成功(clean): ${f}——主仓已提交推进与 worktree 交付均已保留(覆写前原文已备份至 .sillyspec/.runtime/merge-backups/)`)
|
|
262
|
+
} else {
|
|
263
|
+
conflicts.push(f)
|
|
264
|
+
}
|
|
265
|
+
} catch { conflicts.push(f) }
|
|
266
|
+
}
|
|
267
|
+
} finally {
|
|
268
|
+
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
269
|
+
}
|
|
270
|
+
// 批末统一暂存(与 mergeDirtyOverlapThreeWay 同款:safeGit 数组形式 + chunkPaths 分批,
|
|
271
|
+
// fail-open:add 失败不抛、stagedOk:false 供调用方 warning 提示人工补 add)
|
|
272
|
+
let stagedOk = true
|
|
273
|
+
if (merged.length > 0) {
|
|
274
|
+
for (const batch of chunkPaths(merged)) {
|
|
275
|
+
const r = safeGit(projectRoot, ['add', '--', ...batch])
|
|
276
|
+
if (r && r.error) {
|
|
277
|
+
stagedOk = false
|
|
278
|
+
console.warn(`⚠️ mismatch merge 写回批末 git add 失败(fail-open:${merged.length} 个合并文件成果保留,但未暂存对 git restore/clean 不免疫,请尽快人工 git add):${String(r.error).split('\n')[0]}`)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { merged, conflicts, stagedOk }
|
|
155
284
|
}
|
|
156
285
|
|
|
157
286
|
export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchFiles, deletedFiles = [], worktreePath, projectRoot }) {
|
|
@@ -177,7 +306,9 @@ export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchF
|
|
|
177
306
|
continue;
|
|
178
307
|
}
|
|
179
308
|
if (mismatchSet.has(f)) {
|
|
180
|
-
|
|
309
|
+
// ql-20260915-001 修复①:文案更新——前置三方合并已尝试过(4.5b),此处剩的是合并冲突面,
|
|
310
|
+
// 给出两条实际出路(手动锚点合并 / 先 commit 后 --3way),不再误导「正常 apply 走 --3way」即可解
|
|
311
|
+
warnings.push(`跳过 ${f}:EXCLUDE-MISMATCH(主干已提交推进该文件,已尝试自动三方合并,冲突——手动锚点合并,或先 commit 主仓未提交改动后正常 apply 走 --3way)`);
|
|
181
312
|
excludedCount++;
|
|
182
313
|
continue;
|
|
183
314
|
}
|
|
@@ -185,6 +316,15 @@ export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchF
|
|
|
185
316
|
cpFileCount++;
|
|
186
317
|
}
|
|
187
318
|
|
|
319
|
+
// D-003@v1(task-01,2026-09-14-apply-conflict-hardening):rescue 是人工落地路径,落地文件
|
|
320
|
+
// 不进 manifest 也不自动暂存——主仓工作区级 git 操作(restore/clean/checkout)可静默冲掉
|
|
321
|
+
// 未暂存落地(§64 事故实际路径)。commands 末尾追加一行「落地后立即 git add 锁定」指引
|
|
322
|
+
// (# 注释形态,整块复制粘贴仍是合法 shell;纯文案,commands/warnings 结构语义与
|
|
323
|
+
// cpFileCount/excludedCount 口径不变;commands 为空 = 无可落地文件,不加指引)。
|
|
324
|
+
if (commands.length > 0) {
|
|
325
|
+
commands.push(`# 全部执行后立即 git add -- <上方 cp 落地的各文件路径> 锁定暂存区(staged 对 git restore / git clean 免疫,防止并行会话的工作区级操作冲掉刚落地的文件;rm 删除的文件无需 add)`);
|
|
326
|
+
}
|
|
327
|
+
|
|
188
328
|
return { commands, warnings, cpFileCount, excludedCount };
|
|
189
329
|
}
|
|
190
330
|
|
|
@@ -374,14 +514,127 @@ function _silentPointerSpecRoot(projectRoot) {
|
|
|
374
514
|
} catch { return null; }
|
|
375
515
|
}
|
|
376
516
|
|
|
517
|
+
/**
|
|
518
|
+
* apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半,
|
|
519
|
+
* 2026-09-14-apply-conflict-hardening)。
|
|
520
|
+
*
|
|
521
|
+
* 目的:apply 后交付文件在主仓被并行会话的工作区级 git 操作冲掉(新文件删除/修改回退)时
|
|
522
|
+
* 靠指纹可检测(doctor 漂移检查项消费,Wave 3)。CLI 全权写(agent 勿手改,verify-facts
|
|
523
|
+
* 同款契约);rescue 人工落地文件不进 manifest(design 非目标边界)。
|
|
524
|
+
*
|
|
525
|
+
* 哈希口径(design 接口定义定案,AGENTS.md 规则 13 Windows 兼容):**staged blob 内容**
|
|
526
|
+
* (git show :<path>,autocrlf 归一后的 LF 规范态)——写侧在 git add / apply --3way(隐含
|
|
527
|
+
* --index)之后取值,与 doctor 的 staged 态比对天然同基。文件未进暂存(如 merge 写回批末
|
|
528
|
+
* add 失败的 fail-open 面)→ 回退 readFile 后 CRLF→LF 归一计算(latin1 往返保字节),条目
|
|
529
|
+
* 加 approx:true 标记;staged 与磁盘皆无(apply 删除的文件——无内容可指纹)→ 不进 manifest。
|
|
530
|
+
*
|
|
531
|
+
* 落点:<changeDir>/apply-manifest.json(resolveActiveOrArchiveChangeDir 归档感知——与
|
|
532
|
+
* resolveApplyAllowSet 同源,归档后补 apply 的重放写在归档目录)。已存在则覆盖(重放 apply
|
|
533
|
+
* 以最新为准)。manifest 落 .sillyspec/changes/ 面 → filterDeliverableFiles 排除,不构成
|
|
534
|
+
* apply 交付物、不进自身 files。
|
|
535
|
+
*
|
|
536
|
+
* 本函数自身不 catch 写失败(交调用方 fail-open warning);files 为空时跳过不写(无落盘面
|
|
537
|
+
* 的成功出口不产 manifest,doctor 按存在性跳过)。
|
|
538
|
+
*
|
|
539
|
+
* @param {{ projectRoot: string, specBase?: string, changeName: string, baseHash?: string, files: string[] }} args
|
|
540
|
+
* specBase 缺省时与 resolveApplyAllowSet 同链回退(指针 specRoot > 本地 .sillyspec);
|
|
541
|
+
* baseHash 记 apply 锚点(deliverableBase 语义:baselineCommit || baseHash)
|
|
542
|
+
* @returns {{ written: boolean, file?: string, files: Array<{path:string, sha256:string, approx?:true}>, skipped?: string }}
|
|
543
|
+
*/
|
|
544
|
+
function writeApplyManifest({ projectRoot, specBase, changeName, baseHash, files }) {
|
|
545
|
+
const face = [...new Set((Array.isArray(files) ? files : []).filter(Boolean))];
|
|
546
|
+
if (face.length === 0) return { written: false, files: [], skipped: 'empty-files' };
|
|
547
|
+
const base = specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
548
|
+
const changeDir = resolveActiveOrArchiveChangeDir(base, changeName);
|
|
549
|
+
const entries = [];
|
|
550
|
+
for (const f of face) {
|
|
551
|
+
let sha256 = null;
|
|
552
|
+
let approx = false;
|
|
553
|
+
try {
|
|
554
|
+
// staged blob(LF 规范态):encoding buffer 保二进制原样;不在 index(删除/未 add)→ 抛错走回退
|
|
555
|
+
const blob = git(projectRoot, ['show', `:${f}`], { encoding: 'buffer', timeout: 15000 });
|
|
556
|
+
sha256 = createHash('sha256').update(blob).digest('hex');
|
|
557
|
+
} catch {
|
|
558
|
+
try {
|
|
559
|
+
const raw = readFileSync(join(projectRoot, f));
|
|
560
|
+
// CRLF→LF 归一(latin1 往返保字节,防 utf8 解码破坏二进制内容)
|
|
561
|
+
const norm = Buffer.from(raw.toString('latin1').replace(/\r\n/g, '\n'), 'latin1');
|
|
562
|
+
sha256 = createHash('sha256').update(norm).digest('hex');
|
|
563
|
+
approx = true;
|
|
564
|
+
} catch { continue; } // staged 与磁盘皆无(apply 删除面)→ 无内容可指纹,不进 manifest
|
|
565
|
+
}
|
|
566
|
+
entries.push(approx ? { path: f, sha256, approx: true } : { path: f, sha256 });
|
|
567
|
+
}
|
|
568
|
+
const manifest = {
|
|
569
|
+
schemaVersion: 1,
|
|
570
|
+
change: changeName,
|
|
571
|
+
appliedAt: new Date().toISOString(),
|
|
572
|
+
baseHash: baseHash || null,
|
|
573
|
+
files: entries,
|
|
574
|
+
};
|
|
575
|
+
mkdirSync(changeDir, { recursive: true });
|
|
576
|
+
const manifestPath = join(changeDir, 'apply-manifest.json');
|
|
577
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
578
|
+
return { written: true, file: manifestPath, files: entries };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* 坑 mixed-baseline-drift-hint(ql-20260915-004):apply 成功尾声混合基线 advisory。
|
|
583
|
+
*
|
|
584
|
+
* 主仓自本变更基点(baseHash)后的已提交推进若触及本变更相关文件(含 .test./.spec. 变体),
|
|
585
|
+
* worktree 快照内验证过的测试断言可能基于旧内容——apply 落地的是混合基线,提示 agent 合并态
|
|
586
|
+
* 复跑相关测试(非阻断,result.committedDrift 留痕)。head 缺省 HEAD:patch 路径自身交付
|
|
587
|
+
* staged 未提交,baseHash..HEAD 即纯他者推进面;merge 路径传 preMergeHead(merge 提交后
|
|
588
|
+
* HEAD 含自身交付,不剔会把「自己合自己」误报成他者推进)。fail-soft:异常零影响 apply。
|
|
589
|
+
*/
|
|
590
|
+
function annotateCommittedDrift(result, { projectRoot, baseHash, files, head }) {
|
|
591
|
+
try {
|
|
592
|
+
if (!baseHash || !files || files.length === 0) return;
|
|
593
|
+
const drift = detectCommittedDrift({ projectRoot, baseHash, touchedFiles: files, head });
|
|
594
|
+
if (drift && drift.drift) {
|
|
595
|
+
result.committedDrift = drift;
|
|
596
|
+
console.warn(formatCommittedDriftWarning(drift));
|
|
597
|
+
}
|
|
598
|
+
} catch { /* fail-soft:drift 检测异常不影响 apply 结果 */ }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* guard 相交预检(task-02 / FR-03 / D-002@v1,内部函数)。
|
|
603
|
+
*
|
|
604
|
+
* 活跃 quick 会话 guard.allowedFiles 与本次 apply 实际落盘集求交集——非空说明本变更文件正被
|
|
605
|
+
* 并行 quick 会话声明在途改动,此刻 apply 会互冲(2026-09-14 实证事故:apply 交付文件被并行
|
|
606
|
+
* 会话工作区 git 操作冲掉)。交集口径:字符串等值(guard.allowedFiles 与 changedFiles 同为
|
|
607
|
+
* 仓库根相对路径)。无活跃会话/无声明/交集空 → blocked=false(零行为变化)。
|
|
608
|
+
*
|
|
609
|
+
* @param {{ activeGuards: Map<string, string[]>, applyFiles: string[], selfChange?: string }} p
|
|
610
|
+
* - activeGuards:collectActiveQuickGuardFiles 产物(已按 excludeChange 排除自身)
|
|
611
|
+
* - applyFiles:changedFiles∪newPatchFiles(result 实际字段口径)
|
|
612
|
+
* - selfChange:防御性再排除(sessionId == 本次 changeName 不拦自身)
|
|
613
|
+
* @returns {{ overlaps: Array<{sessionId: string, file: string}>, blocked: boolean }}
|
|
614
|
+
*/
|
|
615
|
+
function checkGuardOverlap({ activeGuards, applyFiles, selfChange }) {
|
|
616
|
+
const overlaps = [];
|
|
617
|
+
const fileSet = new Set((applyFiles || []).filter(Boolean));
|
|
618
|
+
if (!activeGuards || fileSet.size === 0) return { overlaps, blocked: false };
|
|
619
|
+
for (const [sessionId, allowedFiles] of activeGuards) {
|
|
620
|
+
if (selfChange && sessionId === selfChange) continue;
|
|
621
|
+
for (const f of (allowedFiles || [])) {
|
|
622
|
+
if (fileSet.has(f)) overlaps.push({ sessionId, file: f });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return { overlaps, blocked: overlaps.length > 0 };
|
|
626
|
+
}
|
|
627
|
+
|
|
377
628
|
/**
|
|
378
629
|
* 收集最新 execute run 各 task review.json 的 changedFiles 声明 → Map<repoKey, string[]>。
|
|
379
630
|
*
|
|
380
631
|
* 坑 apply-undeclared-deviation-block(2026-08-24 用户反馈四期③):执行期有据越界文件
|
|
381
632
|
* (facade 转发/名单测试)不在 design §6 也不在 allowed_paths,Gate1 拦 apply 只能回改
|
|
382
633
|
* design.md。review.json changedFiles 是 reviewer 对实际改动的声明(Task Review Gate 的
|
|
383
|
-
* verifyReviewGitEvidence 已交叉校验其与真实 git diff
|
|
384
|
-
*
|
|
634
|
+
* verifyReviewGitEvidence 已交叉校验其与真实 git diff 相交)。D-003@v1 收紧(2026-09-14
|
|
635
|
+
* change-ownership-guards task-03):声明只与 allow 面相交(pathMatches 容差)才计入,
|
|
636
|
+
* 不相交的外来声明剔除出放行面、进 violations 报告行——本函数仍是声明收集口(口径不变),
|
|
637
|
+
* 相交过滤在主仓 3b/跨仓清单校验两消费点执行。口径与 complete-handlers 的
|
|
385
638
|
* collectExecuteChangedFiles 一致:change 戳归属 run、.sillyspec//meta.json 过滤、
|
|
386
639
|
* review.repo 切片(跨仓声明不进 main 集)。读不到 run/review → 空 Map(fail-closed 回退旧行为)。
|
|
387
640
|
*
|
|
@@ -510,9 +763,12 @@ function validateCrossRepoNoOp(ctx, projectRoot, changeName) {
|
|
|
510
763
|
* 可清,wm.cleanup 只作用于主仓 worktree)。主仓 task 走原 A5 完整 apply 不动(GOAL-2 单仓零回归)。
|
|
511
764
|
*
|
|
512
765
|
* @param {string} changeName - 变更名
|
|
513
|
-
* @param {{ cwd?: string, checkOnly?: boolean, merge?: boolean, ctx?: object }} opts
|
|
766
|
+
* @param {{ cwd?: string, checkOnly?: boolean, merge?: boolean, ctx?: object, force?: boolean, autoApply?: boolean }} opts
|
|
514
767
|
* - ctx:可选 MultiRepoContext(design §7.1)。缺省=单仓退化(仅主仓 apply,零行为变化,GOAL-2)。
|
|
515
768
|
* 提供 ctx 且含跨仓 entry 时,触发跨仓 no-op 校验(校验 review.head 真实 + 不 cleanup 跨仓)。
|
|
769
|
+
* - force(task-02 / FR-03):guard 相交预检命中时显式放行并留痕 result.overlapForced(仅人工 CLI --force)。
|
|
770
|
+
* - autoApply(task-02 / FR-03):assess 自动入口置 true——相交命中软跳过(result.overlapSkipped + warning,
|
|
771
|
+
* 不抛错不落盘);自动路径永不 force。
|
|
516
772
|
* @returns {{
|
|
517
773
|
* ok: boolean,
|
|
518
774
|
* changedFiles: string[],
|
|
@@ -583,7 +839,7 @@ export async function withMainRepoLock(projectRoot, changeName, purpose, fn, opt
|
|
|
583
839
|
*
|
|
584
840
|
* 对有 worktree meta 的跨仓仓(worktree-cross.js 创建)执行与主仓 A5 同构的 patch 回落:
|
|
585
841
|
* deliverables(diff baselineCommit||baseHash + untracked,filterDeliverableFiles 排除基础设施)
|
|
586
|
-
* → 清单校验(resolveApplyAllowSet 按 repo 切片
|
|
842
|
+
* → 清单校验(resolveApplyAllowSet 按 repo 切片 + review 声明相交过滤,D-003:不相交剔除)
|
|
587
843
|
* → 跨仓主工作副本 dirty 重叠拦截
|
|
588
844
|
* → patch(tracked diff + untracked add/diff --cached/reset,Buffer 防二进制损坏)
|
|
589
845
|
* → git apply --3way 回跨仓主工作副本;成功后 cleanup 该跨仓 worktree(分支保留作锚)。
|
|
@@ -641,24 +897,45 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
641
897
|
continue;
|
|
642
898
|
}
|
|
643
899
|
|
|
644
|
-
// 清单校验(per-repo 切片 + review
|
|
900
|
+
// 清单校验(per-repo 切片 + review 声明相交过滤,与主仓 Gate1/3b 同语义)。
|
|
645
901
|
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
902
|
+
// review 声明收紧(D-003@v1,2026-09-14-change-ownership-guards task-03):声明文件须与
|
|
903
|
+
// allow 面(design 清单 ∪ 各 task target_files/allowed_paths ∪ linked-change 声明 =
|
|
904
|
+
// resolveApplyAllowSet 现有并集)相交(pathMatches 容差)才计入;不相交的外来声明剔除出
|
|
905
|
+
// 放行面、进 violations 报告行(「review 声明了越权文件」嫌疑标注),admission 增量归零。
|
|
646
906
|
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
647
|
-
|
|
907
|
+
const allowSet = allowMap.get(repoKey) || new Set();
|
|
648
908
|
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
649
|
-
|
|
650
|
-
|
|
909
|
+
const reviewForeign = [];
|
|
910
|
+
if (reviewDeclared && allowSet.size > 0) {
|
|
911
|
+
const face = [...allowSet];
|
|
651
912
|
for (const f of reviewDeclared) {
|
|
652
|
-
if (!
|
|
913
|
+
if (!face.some(ap => pathMatches(f, ap))) reviewForeign.push(f);
|
|
653
914
|
}
|
|
654
|
-
|
|
915
|
+
}
|
|
916
|
+
if (reviewForeign.length > 0) {
|
|
917
|
+
out.warnings.push(
|
|
918
|
+
`跨仓 ${repoKey}:${reviewForeign.length} 个 review 声明文件不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),已剔除出放行面(review 声明了越权文件——嫌疑:review 失实或并行会话在途文件混入声明):${reviewForeign.join(', ')}`
|
|
919
|
+
);
|
|
655
920
|
}
|
|
656
921
|
if (allowSet.size > 0) {
|
|
657
922
|
const violations = classifyAllowListViolations(changedFiles, allowSet);
|
|
658
923
|
if (violations.length > 0) {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
);
|
|
924
|
+
const foreignSet = new Set(reviewForeign);
|
|
925
|
+
const foreignViolations = violations.filter(f => foreignSet.has(f));
|
|
926
|
+
const undeclaredViolations = violations.filter(f => !foreignSet.has(f));
|
|
927
|
+
const lines = [];
|
|
928
|
+
if (undeclaredViolations.length > 0) {
|
|
929
|
+
lines.push(
|
|
930
|
+
`跨仓 ${repoKey}:文件清单校验失败——以下变更文件不在 design.md 清单、也不在该仓 task review.json changedFiles 声明中:\n ${undeclaredViolations.join('\n ')}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (foreignViolations.length > 0) {
|
|
934
|
+
lines.push(
|
|
935
|
+
`跨仓 ${repoKey}:review 声明了越权文件——以下变更文件已被 review.json changedFiles 声明但不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),不予放行:\n ${foreignViolations.join('\n ')}\n(确属本变更交付:补进 design §6 清单或任务卡 allowed_paths 后重跑)`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
out.errors.push(lines.join('\n'));
|
|
662
939
|
if (!checkOnly) continue;
|
|
663
940
|
}
|
|
664
941
|
}
|
|
@@ -765,7 +1042,7 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
765
1042
|
return out;
|
|
766
1043
|
}
|
|
767
1044
|
|
|
768
|
-
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false, stashDirty = false } = {}) {
|
|
1045
|
+
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false, stashDirty = false, force = false, autoApply = false } = {}) {
|
|
769
1046
|
const projectRoot = cwd || process.cwd();
|
|
770
1047
|
const wm = new WorktreeManager({ cwd: projectRoot });
|
|
771
1048
|
const meta = wm.getMeta(changeName);
|
|
@@ -912,27 +1189,30 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
912
1189
|
// (主仓 worktree diff 只含主仓文件,跨仓文件在跨仓仓不进主仓 worktree diff)。跨仓 allowed_paths
|
|
913
1190
|
// 仅作 Map 切片返回供上游用,主流程不消费(跨仓 apply=no-op,D-009)。
|
|
914
1191
|
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase: allowSpecBase });
|
|
915
|
-
// --- 3b. review.json
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
1192
|
+
// --- 3b. review.json 声明偏差文件相交过滤(坑 apply-undeclared-deviation-block → D-003@v1 收紧,
|
|
1193
|
+
// 2026-09-14-change-ownership-guards task-03 / FR-02)---
|
|
1194
|
+
// 旧语义:reviewer 声明的 changedFiles 整体并入 allow set(声明即放行)——troubleshooting §65
|
|
1195
|
+
// 事件②实证该通道无 allowed_paths 相交校验,11 个并行会话在途文件经 review 声明真实放行。
|
|
1196
|
+
// 新语义(D-003 翻转):review 声明只承认与 allow 面(design 清单 ∪ 各 task
|
|
1197
|
+
// target_files/allowed_paths ∪ linked-change 声明 = resolveApplyAllowSet 现有并集)相交的文件
|
|
1198
|
+
// (pathMatches 容差);不相交的外来声明剔除出放行面、进 violations 报告行(「review 声明了
|
|
1199
|
+
// 越权文件」嫌疑标注)——admission 增量归零,通道保留作审计报告位(外来声明显式列出)。
|
|
919
1200
|
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: allowRuntimeRoot });
|
|
920
|
-
const
|
|
1201
|
+
const reviewForeignMain = [];
|
|
921
1202
|
for (const [repoKey, files] of reviewDeclaredByRepo) {
|
|
922
1203
|
const repoSet = allowMap.get(repoKey);
|
|
923
|
-
if (!repoSet) continue; // 该 repo 无 design
|
|
924
|
-
const
|
|
1204
|
+
if (!repoSet || repoSet.size === 0) continue; // 该 repo 无 design/任务卡清单(无清单=不校验),无相交可判
|
|
1205
|
+
const face = [...repoSet];
|
|
925
1206
|
for (const f of files) {
|
|
926
|
-
if (!
|
|
927
|
-
|
|
928
|
-
if (repoKey === 'main') reviewAdmittedFiles.push(f);
|
|
1207
|
+
if (!face.some(ap => pathMatches(f, ap))) {
|
|
1208
|
+
if (repoKey === 'main') reviewForeignMain.push(f);
|
|
929
1209
|
}
|
|
930
1210
|
}
|
|
931
1211
|
}
|
|
932
|
-
if (
|
|
933
|
-
result.
|
|
1212
|
+
if (reviewForeignMain.length > 0) {
|
|
1213
|
+
result.reviewOverdeclaredFiles = reviewForeignMain;
|
|
934
1214
|
result.warnings = (result.warnings || []).concat([
|
|
935
|
-
`${
|
|
1215
|
+
`${reviewForeignMain.length} 个 review 声明文件不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),已剔除出放行面(review 声明了越权文件——嫌疑:review 失实或并行会话在途文件混入声明):${reviewForeignMain.join(', ')};确属本变更交付请补进 design §6 清单或任务卡 allowed_paths`
|
|
936
1216
|
]);
|
|
937
1217
|
}
|
|
938
1218
|
const allowSet = allowMap.get('main') || new Set();
|
|
@@ -972,16 +1252,90 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
972
1252
|
if (hasAllowList) {
|
|
973
1253
|
const violations = classifyAllowListViolations(changedFiles, allowSet);
|
|
974
1254
|
if (violations.length > 0) {
|
|
1255
|
+
// D-003:violation 分流——已被 review 声明但不在 allow 面(越权声明)与完全未声明分报,
|
|
1256
|
+
// 前者带「review 声明了越权文件」嫌疑标注(外来声明剔除后的落地拦截报告行)。
|
|
1257
|
+
const foreignSet = new Set(result.reviewOverdeclaredFiles || []);
|
|
1258
|
+
const foreignViolations = violations.filter(f => foreignSet.has(f));
|
|
1259
|
+
const undeclaredViolations = violations.filter(f => !foreignSet.has(f));
|
|
975
1260
|
result.extraFiles.push(...violations);
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1261
|
+
const errLines = [];
|
|
1262
|
+
if (undeclaredViolations.length > 0) {
|
|
1263
|
+
errLines.push(
|
|
1264
|
+
`文件清单校验失败:以下变更文件不在 design.md 清单、也不在 task review.json changedFiles 声明中:\n ${undeclaredViolations.join('\n ')}\n(若属执行期合理偏差:在 design.md §6 清单或任务卡 allowed_paths 补行——review.json 的 changedFiles 声明已不再放行清单外文件,仅作审计对照)`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
if (foreignViolations.length > 0) {
|
|
1268
|
+
errLines.push(
|
|
1269
|
+
`review 声明了越权文件:以下变更文件已被 task review.json changedFiles 声明,但不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明)——嫌疑:review 失实或并行会话在途文件混入声明,不予放行:\n ${foreignViolations.join('\n ')}\n(确属本变更交付:补进 design.md §6 清单或任务卡 allowed_paths 后重跑)`
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
result.errors.push(errLines.join('\n'));
|
|
979
1273
|
// checkOnly(assess)模式不短路:继续跑 Gate3,收集所有道供一次报全(坑 worktree-execute-apply-friction 坑4)。
|
|
980
1274
|
// 真实 apply(checkOnly=false)仍短路,保安全。
|
|
981
1275
|
if (!checkOnly) return result;
|
|
982
1276
|
}
|
|
983
1277
|
}
|
|
984
1278
|
|
|
1279
|
+
// --- 4.3 guard 相交预检(task-02 / FR-03 / D-002@v1):活跃 quick 会话 guard.allowedFiles ∩ 本次落盘集 ---
|
|
1280
|
+
// 内嵌 applyWorktree 主流程:CLI apply(index.js apply 分支)与 assess 自动 apply(index.js assess
|
|
1281
|
+
// 分支)全经此覆盖;真实 apply 由调用方 withMainRepoLock 包裹——判定与落盘同在锁内,无 TOCTOU。
|
|
1282
|
+
// 位置在一切主仓写动作之前(4.4 stash / 4.5 合并写回 / 4.6 --merge / step7 patch / ENOBUFS 降级)。
|
|
1283
|
+
// applyFiles=changedFiles∪newPatchFiles:newPatchFiles⊆patchFiles⊆changedFiles(resolvePatchFiles
|
|
1284
|
+
// 是 changedFiles 的过滤),并集即 changedFiles∪patchFiles,此处显式两集合并忠于 result 实际字段口径。
|
|
1285
|
+
// specBase/sessionsDir 复用上方 allowlist 同一解析链(allowSpecBase > 平台指针 > 本地),勿新造配置读取。
|
|
1286
|
+
// checkOnly 只读零写盘 → 跳过(零行为变化)。无活跃会话/无声明/交集空 → 全段空转零路径变化。
|
|
1287
|
+
if (!checkOnly) {
|
|
1288
|
+
const guardSpecBase = allowSpecBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
1289
|
+
const guardSessionsDir = allowRuntimeRoot ? join(allowRuntimeRoot, 'quick-sessions') : undefined;
|
|
1290
|
+
const activeGuards = collectActiveQuickGuardFiles(guardSpecBase, { excludeChange: changeName, sessionsDir: guardSessionsDir });
|
|
1291
|
+
const guardApplyFiles = [...new Set([...changedFiles, ...resolvePatchFiles(changedFiles, allowSet, hasAllowList)])];
|
|
1292
|
+
const guardCheck = checkGuardOverlap({ activeGuards, applyFiles: guardApplyFiles, selfChange: changeName });
|
|
1293
|
+
if (guardCheck.blocked) {
|
|
1294
|
+
const sessions = [...new Set(guardCheck.overlaps.map(o => o.sessionId))];
|
|
1295
|
+
const files = [...new Set(guardCheck.overlaps.map(o => o.file))];
|
|
1296
|
+
const pairLines = guardCheck.overlaps.map(o => ` [${o.sessionId}] ${o.file}`);
|
|
1297
|
+
if (force) {
|
|
1298
|
+
// 显式解锁:放行并留痕(审计可见——自动路径永不带 force,overlapForced 只可能来自人工 --force)
|
|
1299
|
+
result.overlapForced = { sessions, files };
|
|
1300
|
+
result.warnings = (result.warnings || []).concat([
|
|
1301
|
+
`--force 越过 guard 相交预检:${guardCheck.overlaps.length} 个会话×文件对与活跃 quick 会话声明重叠——` +
|
|
1302
|
+
pairLines.join(';') + `(对方会话在途改动可能被覆盖,overlapForced 留痕)`
|
|
1303
|
+
]);
|
|
1304
|
+
} else if (autoApply) {
|
|
1305
|
+
// 软跳过:无人值守不越权(不落地互冲)也不阻断审计流——不抛错,指引人工评估
|
|
1306
|
+
result.overlapSkipped = true;
|
|
1307
|
+
result.warnings = (result.warnings || []).concat([
|
|
1308
|
+
`自动 apply 软跳过(未落盘):${files.length} 个文件与活跃 quick 会话在途声明重叠——` +
|
|
1309
|
+
pairLines.join(';') + `——请人工评估后显式 sillyspec worktree apply ${changeName}(确认无冲突),或等对方会话 --done 后重试`
|
|
1310
|
+
]);
|
|
1311
|
+
return result; // 安全返回:result.ok 保持 false,主仓零写动作(本预检位于一切写动作之前)
|
|
1312
|
+
} else {
|
|
1313
|
+
// 人工 CLI 入口(fail-closed):结构化错误含会话×文件对清单 + 串行化指引 + --force 提示
|
|
1314
|
+
result.errors.push(
|
|
1315
|
+
`guard 相交拦截:本次 apply 文件与 ${sessions.length} 个活跃 quick 会话的在途声明重叠(会话×文件对):\n` +
|
|
1316
|
+
pairLines.join('\n') +
|
|
1317
|
+
`\n串行化指引:等对方会话 sillyspec run quick --done 完成(guard 目录随之清理)后重试;` +
|
|
1318
|
+
`或确认无实际冲突后显式解锁:sillyspec worktree apply ${changeName} --force(overlapForced 留痕)`
|
|
1319
|
+
);
|
|
1320
|
+
return result; // index.js apply 分支对 result.errors 走 exit 1 展示
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// ── ql-20260915-001 修复①:mismatch 前置合并的 dirty 排除集(4.4 stash 前取样)──
|
|
1326
|
+
// 4.5b 的 EXCLUDE-MISMATCH 前置合并只碰「主仓无未提交改动」的 mismatch 文件——dirty∩mismatch
|
|
1327
|
+
// 交 4.5 脏读合并处理(其 ours=主仓磁盘脏内容,已含已提交推进,语义更全)。排除集必须在
|
|
1328
|
+
// 4.4 --stash-dirty 之前取样:stash 会把脏文件收走,事后取 diff 恒空,会把在途文件误判成
|
|
1329
|
+
// 干净 mismatch 用 ours=HEAD 合并写回(stash 恢复时与合并写回互踩)。checkOnly 只读不合并,
|
|
1330
|
+
// 无 mismatch 时零开销,均不取样。untracked 不可能是 mismatch(不在 HEAD,mainBlob=null
|
|
1331
|
+
// 天然被合并函数的 ours=git show HEAD:f 失败挡住),此处只取 tracked-modified 口径即可。
|
|
1332
|
+
let preStashDirtySet = null
|
|
1333
|
+
if (!checkOnly && (result.hashMismatchFiles || []).length > 0) {
|
|
1334
|
+
preStashDirtySet = new Set(
|
|
1335
|
+
(gitQuiet(projectRoot, ['diff', '--name-only', 'HEAD']) || '').split('\n').filter(Boolean)
|
|
1336
|
+
)
|
|
1337
|
+
}
|
|
1338
|
+
|
|
985
1339
|
// --- 4.4 --stash-dirty:主仓在途改动自动 stash(坑 apply-main-dirty-no-first-class, ---
|
|
986
1340
|
// 2026-08-24 用户反馈四期①:主仓并行在途改动下默认 / --skip-overlap(全重叠「无可应用子集」)/
|
|
987
1341
|
// --merge(git 拒在脏树启动合并)三路死锁,手工 stash→3way→pop 流程未内置。flag 显式 opt-in
|
|
@@ -1023,7 +1377,45 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1023
1377
|
// 用 --merge 时跳过未提交 dirty 拦截——merge 同样要求工作区相对干净,此处仅提示风险,真正失败由 applyByMerge 报告。
|
|
1024
1378
|
// keepConflicts:true——显式 --merge 冲突时保留现场供手工解决(不再直接 abort 丢上下文)。
|
|
1025
1379
|
if (merge && !checkOnly) {
|
|
1026
|
-
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: true });
|
|
1380
|
+
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: true, specBase: allowSpecBase });
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// --- 4.5b EXCLUDE-MISMATCH 前置三方合并(ql-20260915-001 修复①,坑 apply-archived-mismatch-no-merge,
|
|
1384
|
+
// 2026-09-14 用户实证:归档后补 apply,主仓已被并行会话提交推进 → rescue EXCLUDE-MISMATCH 跳过
|
|
1385
|
+
// 不落地,被迫手工 cp+锚点合并)---
|
|
1386
|
+
// 对 hashMismatch ∩ changedFiles 且主仓无未提交改动(pre-stash dirty 集排除,dirty 面交 4.5
|
|
1387
|
+
// 脏读合并)的文件逐个 merge-file:base=checkpoint(deliverableBase)blob、ours=主仓 HEAD blob、
|
|
1388
|
+
// theirs=worktree 文件。clean → 写回主仓 + 批末 add + mergedMismatchFiles 留痕 + 从
|
|
1389
|
+
// changedFiles/hashMismatchFiles/deletedFiles 剔除(后续 patch 不再重放——staged 合并内容若再进
|
|
1390
|
+
// patch,apply --3way 对 staged≠HEAD 的文件报 does not match index);conflict/异常 → 留原集走
|
|
1391
|
+
// 既有出口(rescue EXCLUDE-MISMATCH 新文案 / step7 --3way 实测 / --merge 兜底)。
|
|
1392
|
+
// checkOnly(assess 只读)零写盘不试合并;--merge 已在 4.6 早退不达此;位于 4.3 guard 预检
|
|
1393
|
+
// (一切主仓写动作之前)与 Gate1 清单校验之后,越权文件不会被合并落地。
|
|
1394
|
+
if (!checkOnly && (result.hashMismatchFiles || []).length > 0 && worktreePath && worktreePath !== projectRoot) {
|
|
1395
|
+
const mismatchCandidates = result.hashMismatchFiles.filter(f =>
|
|
1396
|
+
changedFiles.includes(f) && !(preStashDirtySet && preStashDirtySet.has(f)))
|
|
1397
|
+
if (mismatchCandidates.length > 0) {
|
|
1398
|
+
try {
|
|
1399
|
+
const m3m = mergeMismatchThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, mismatchCandidates)
|
|
1400
|
+
if (m3m.merged.length > 0) {
|
|
1401
|
+
result.mergedMismatchFiles = (result.mergedMismatchFiles || []).concat(m3m.merged)
|
|
1402
|
+
if (m3m.stagedOk === false) {
|
|
1403
|
+
result.warnings = (result.warnings || []).concat([
|
|
1404
|
+
`EXCLUDE-MISMATCH 三方合并 ${m3m.merged.length} 个文件已写回主仓,但批末 git add 暂存失败(未暂存对 git restore / git clean 不免疫)——请尽快人工 git add -- <该批文件> 锁定`
|
|
1405
|
+
])
|
|
1406
|
+
}
|
|
1407
|
+
result.warnings = (result.warnings || []).concat([
|
|
1408
|
+
`EXCLUDE-MISMATCH 前置三方合并:${m3m.merged.length} 个主干已推进文件 clean 合并已写回并暂存(主仓已提交推进与 worktree 交付均保留,覆写前原文备份于 .sillyspec/.runtime/merge-backups/)`
|
|
1409
|
+
])
|
|
1410
|
+
const mergedMisSet = new Set(m3m.merged)
|
|
1411
|
+
changedFiles = changedFiles.filter(f => !mergedMisSet.has(f))
|
|
1412
|
+
result.changedFiles = changedFiles
|
|
1413
|
+
result.deletedFiles = deletedFiles.filter(f => !mergedMisSet.has(f))
|
|
1414
|
+
result.hashMismatchFiles = result.hashMismatchFiles.filter(f => !mergedMisSet.has(f))
|
|
1415
|
+
}
|
|
1416
|
+
// m3m.conflicts 面留在 result.hashMismatchFiles 原样(rescue 分类 / step7 --3way 既有路径)
|
|
1417
|
+
} catch { /* 合并链路异常 → mismatch 集原样,走既有出口(fail-soft 不阻断 apply) */ }
|
|
1418
|
+
}
|
|
1027
1419
|
}
|
|
1028
1420
|
|
|
1029
1421
|
// --- 4.5 校验:主工作区「未提交」脏文件是否与本次变更重叠(overlap-only 拦截)---
|
|
@@ -1052,10 +1444,14 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1052
1444
|
// hash 必变,若仍比对会永久死锁(须手改 meta.baselineHash)。改判「排除规则下当前是否有未提交 dirty」。
|
|
1053
1445
|
const hasUncommittedDirty = staged !== '' || unstaged !== '' || untracked !== '';
|
|
1054
1446
|
if (hasUncommittedDirty) {
|
|
1447
|
+
// ql-20260915-001 修复①:4.5b 已合并写回并暂存的 mismatch 文件不出现在此脏清单——它们
|
|
1448
|
+
// 是本次 apply 自己的产物(非他者无关脏文件),列进「无关未提交文件」警告会误导。
|
|
1449
|
+
// 对 overlapDirty 无影响(合并文件已从 changedFiles 剔除,交集恒空);集合空时为恒等过滤。
|
|
1450
|
+
const mergedMisSet45 = new Set(result.mergedMismatchFiles || [])
|
|
1055
1451
|
const dirtyFiles = [...new Set(
|
|
1056
1452
|
((gitQuiet(projectRoot, ['diff', '--name-only', 'HEAD']) || '').split('\n').filter(Boolean))
|
|
1057
1453
|
.concat((gitQuiet(projectRoot, ['ls-files', '--others', '--exclude-standard']) || '').split('\n').filter(Boolean))
|
|
1058
|
-
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
|
|
1454
|
+
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json' && !mergedMisSet45.has(f));
|
|
1059
1455
|
const overlapDirty = dirtyFiles.filter(f => changedFiles.includes(f));
|
|
1060
1456
|
if (overlapDirty.length > 0) {
|
|
1061
1457
|
// 自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭小结第六批③):先对
|
|
@@ -1068,6 +1464,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1068
1464
|
const m3 = mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, overlapDirty);
|
|
1069
1465
|
if (m3.merged.length > 0) {
|
|
1070
1466
|
result.mergedDirtyFiles = (result.mergedDirtyFiles || []).concat(m3.merged);
|
|
1467
|
+
if (m3.stagedOk === false) {
|
|
1468
|
+
// 批末 git add fail-open 留痕(task-01 / FR-01):写回成功但未暂存——result 面
|
|
1469
|
+
// 可见(console.warn 已在 mergeDirtyOverlapThreeWay 内打),提示人工补 add。
|
|
1470
|
+
result.warnings = (result.warnings || []).concat([
|
|
1471
|
+
`EXCLUDE-DIRTY 三方合并 ${m3.merged.length} 个文件已写回主仓,但批末 git add 暂存失败(未暂存对 git restore / git clean 不免疫)——请尽快人工 git add -- <该批文件> 锁定`
|
|
1472
|
+
]);
|
|
1473
|
+
}
|
|
1071
1474
|
const mergedSet = new Set(m3.merged);
|
|
1072
1475
|
changedFiles = changedFiles.filter(f => !mergedSet.has(f));
|
|
1073
1476
|
result.changedFiles = changedFiles;
|
|
@@ -1131,11 +1534,15 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1131
1534
|
} else {
|
|
1132
1535
|
// 无关脏文件放行(只校验重叠文件):--3way 若因 autocrlf 报 does not match index 会被
|
|
1133
1536
|
// step7 catch 回滚(交集空前提下无损,见上注释),stash 后重试即可——不再硬挡 rescue 手动路径
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1537
|
+
// (ql-20260915-001 修复①:dirtyFiles 已剔除本次合并写回面,剔除后为空 = 探针命中的
|
|
1538
|
+
// 全是本 apply 自身 staged 产物——不打「0 个无关文件」的空警告)
|
|
1539
|
+
if (dirtyFiles.length > 0) {
|
|
1540
|
+
result.warnings = (result.warnings || []).concat([
|
|
1541
|
+
`主工作区有 ${dirtyFiles.length} 个与本次 apply 无关的未提交文件,已放行(只校验重叠文件):` +
|
|
1542
|
+
`${dirtyFiles.slice(0, 5).join(', ')}${dirtyFiles.length > 5 ? ' 等' : ''}` +
|
|
1543
|
+
`——若 apply --3way 因 CRLF/autocrlf 报 does not match index,可 git stash 后重试`
|
|
1544
|
+
])
|
|
1545
|
+
}
|
|
1139
1546
|
}
|
|
1140
1547
|
}
|
|
1141
1548
|
}
|
|
@@ -1259,6 +1666,26 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1259
1666
|
if (patchContent.length === 0) {
|
|
1260
1667
|
// patch 为空(清单中部分文件可能没实际变更)
|
|
1261
1668
|
result.ok = true;
|
|
1669
|
+
// apply-manifest(task-01 出口③变体):4.5 三方合并全部消解重叠时,merge 写回面是
|
|
1670
|
+
// 本出口唯一落盘面——manifest 仍须覆盖(写回批末 add 已先行,指纹取 staged 态)。
|
|
1671
|
+
// ql-20260915-001 修复①:4.5b mismatch 前置合并同样可能消解全部 changedFiles(本出口
|
|
1672
|
+
// 另一来源),合并面并入 manifest files。
|
|
1673
|
+
const emptyPatchManifestFace = [...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])]
|
|
1674
|
+
if (emptyPatchManifestFace.length > 0) {
|
|
1675
|
+
try {
|
|
1676
|
+
const mf = writeApplyManifest({
|
|
1677
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
1678
|
+
baseHash: deliverableBase, files: emptyPatchManifestFace,
|
|
1679
|
+
});
|
|
1680
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
1681
|
+
} catch (e) {
|
|
1682
|
+
result.warnings = (result.warnings || []).concat([
|
|
1683
|
+
`apply-manifest.json 写入失败(不影响 apply 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
1684
|
+
]);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):三方合并消解型成功出口的混合基线提示
|
|
1688
|
+
annotateCommittedDrift(result, { projectRoot, baseHash: deliverableBase, files: emptyPatchManifestFace });
|
|
1262
1689
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
1263
1690
|
return result;
|
|
1264
1691
|
}
|
|
@@ -1309,12 +1736,43 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1309
1736
|
|
|
1310
1737
|
result.ok = true;
|
|
1311
1738
|
|
|
1739
|
+
// --- 7.2 apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半)---
|
|
1740
|
+
// 三条成功出口之一(patch 主路径;含 4.5 三方合并写回后主流程继续至此的出口③)。指纹
|
|
1741
|
+
// 口径 = staged blob(git show :<path> 的 LF 规范态——apply --3way 隐含 --index 已暂存,
|
|
1742
|
+
// merge 写回批末 add 亦已先行)→ 与 doctor 漂移检测的 staged 态比对天然同基。files 面 =
|
|
1743
|
+
// patch 实际落盘集 ∪ 4.5 三方合并写回集(mergeDirtyFiles 已从 changedFiles 剔除,须并回);
|
|
1744
|
+
// apply 删除的文件 staged/磁盘皆无,函数内自然跳过。fail-open:写失败不阻断 apply
|
|
1745
|
+
// (warning 留痕)。已存在则覆盖(重放 apply 以最新为准)。锁内(调用方 withMainRepoLock)。
|
|
1746
|
+
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示
|
|
1747
|
+
// 在 manifest 写点后消费同面,try 内 const 出块即失域)
|
|
1748
|
+
const manifestFace = [...new Set([...patchFiles, ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])];
|
|
1749
|
+
try {
|
|
1750
|
+
// ql-20260915-001 修复①:mergedMismatchFiles(4.5b 前置合并写回面)与 mergedDirtyFiles
|
|
1751
|
+
// 同为 staged 落盘集,一并进 manifest 指纹面与提交复用 pathspec(缺列 = doctor 漂移检测
|
|
1752
|
+
// 盲区 + agent 按 pathspec 提交漏文件)
|
|
1753
|
+
const mf = writeApplyManifest({
|
|
1754
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
1755
|
+
baseHash: deliverableBase, files: manifestFace,
|
|
1756
|
+
});
|
|
1757
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
result.warnings = (result.warnings || []).concat([
|
|
1760
|
+
`apply-manifest.json 写入失败(不影响 apply 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
1761
|
+
]);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):patch 主路径成功出口的混合基线提示
|
|
1765
|
+
//(自身交付 staged 未提交 → baseHash..HEAD 即纯他者推进面,精确/变体命中均为真信号)
|
|
1766
|
+
annotateCommittedDrift(result, { projectRoot, baseHash: deliverableBase, files: manifestFace });
|
|
1767
|
+
|
|
1312
1768
|
// --- 7.5 提交复用 pathspec(坑 apply-commit-pathspec-sweep,2026-08-21 实证)---
|
|
1313
1769
|
// apply 后主仓常混有无关未提交文件(他者会话/并行 quick),agent 习惯 `git add <目录>/`
|
|
1314
1770
|
// 会把无关文件扫进暂存需手工剔除。落盘本变更精确 pathspec 供提交直接复用:
|
|
1315
1771
|
// 文件 + result 字段(CLI 输出可照抄 git add -- … / 长清单用 --pathspec-from-file)。
|
|
1316
1772
|
try {
|
|
1317
|
-
|
|
1773
|
+
// ql-20260915-001 修复①:提交复用 pathspec 补并两个三方合并写回面(均为 staged 落盘、
|
|
1774
|
+
// 属本变更交付——缺列时 agent 按此 pathspec 提交会漏掉合并成果)
|
|
1775
|
+
const commitFiles = [...new Set([...patchFiles, ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])].sort().filter(Boolean);
|
|
1318
1776
|
result.commitPathspec = commitFiles;
|
|
1319
1777
|
const runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
|
|
1320
1778
|
mkdirSync(runtimeRoot, { recursive: true });
|
|
@@ -1361,7 +1819,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1361
1819
|
);
|
|
1362
1820
|
try {
|
|
1363
1821
|
// keepConflicts:false——自动降级路径无人善后冲突状态,维持 abort 回滚干净态
|
|
1364
|
-
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: false });
|
|
1822
|
+
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: false, specBase: allowSpecBase });
|
|
1365
1823
|
} catch (mergeErr) {
|
|
1366
1824
|
result.errors.push(`ENOBUFS 降级 merge 也失败: ${mergeErr.message}`);
|
|
1367
1825
|
return result;
|
|
@@ -1715,8 +2173,11 @@ function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
|
|
|
1715
2173
|
// git add + git commit;或 git merge --abort 放弃)。worktree/分支/meta 本就保留。
|
|
1716
2174
|
// false(ENOBUFS 自动降级路径,默认)→ 维持 abort——自动降级无人善后冲突状态,回滚到
|
|
1717
2175
|
// 干净态让用户显式重来更安全。
|
|
2176
|
+
// opts.specBase(task-01,2026-09-14-apply-conflict-hardening):applyWorktree 调用方透传的
|
|
2177
|
+
// spec 基准目录(平台模式 ctx.platformOpts.specRoot),成功尾声 writeApplyManifest 用;缺省
|
|
2178
|
+
// 时函数内同链回退(指针 specRoot > 本地 .sillyspec)——外部直接调用零必填。
|
|
1718
2179
|
export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
1719
|
-
const { keepConflicts = false } = opts;
|
|
2180
|
+
const { keepConflicts = false, specBase } = opts;
|
|
1720
2181
|
const meta = wm.getMeta(changeName);
|
|
1721
2182
|
const changedFiles = result.changedFiles || [];
|
|
1722
2183
|
// 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
|
|
@@ -1729,6 +2190,10 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
1729
2190
|
// 它跳过 dirty 文件(不覆盖 WIP)且只提交对齐集;WIP commit 在后,两者不混提交 ---
|
|
1730
2191
|
autoCommitWorktreeWip({ meta, result, changeName });
|
|
1731
2192
|
|
|
2193
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):merge 前 HEAD = 纯他者推进面锚点——
|
|
2194
|
+
// merge 提交后 HEAD 含自身交付,drift 检测须以 preMergeHead 为推进面终点(防自污染误报)
|
|
2195
|
+
const preMergeHead = gitQuiet(projectRoot, ['rev-parse', 'HEAD']);
|
|
2196
|
+
|
|
1732
2197
|
try {
|
|
1733
2198
|
git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
|
|
1734
2199
|
} catch (e) {
|
|
@@ -1793,6 +2258,32 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
1793
2258
|
}
|
|
1794
2259
|
|
|
1795
2260
|
result.ok = true;
|
|
2261
|
+
// apply 成功尾声写 apply-manifest.json(task-01 出口②:applyWorktree 显式 --merge 与
|
|
2262
|
+
// ENOBUFS 自动降级两条提前 return 入口共用本成功出口)。merge 已 commit → staged 态 =
|
|
2263
|
+
// HEAD = 合并内容,指纹口径(git show :<path> LF 规范态)与 patch 路径同基;files 面 =
|
|
2264
|
+
// changedFiles(落地校验过的交付集)∪ mergedDirtyFiles(ENOBUFS 路径下 4.5 可能已写回)
|
|
2265
|
+
// ∪ mergedMismatchFiles(ql-20260915-001 修复①:4.5b 前置合并写回面,同口径并入),
|
|
2266
|
+
// merge 删除面 staged/磁盘皆无、函数内自然跳过。fail-open:写失败不阻断 merge(warning 留痕)。
|
|
2267
|
+
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示在
|
|
2268
|
+
// manifest 写点后消费同面,try 内 const 出块即失域)
|
|
2269
|
+
const manifestFace = [...new Set([...(result.changedFiles || []), ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])];
|
|
2270
|
+
try {
|
|
2271
|
+
const mf = writeApplyManifest({
|
|
2272
|
+
projectRoot, specBase, changeName,
|
|
2273
|
+
baseHash: meta.baselineCommit || meta.baseHash, files: manifestFace,
|
|
2274
|
+
});
|
|
2275
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
2276
|
+
} catch (e) {
|
|
2277
|
+
result.warnings = (result.warnings || []).concat([
|
|
2278
|
+
`apply-manifest.json 写入失败(不影响 merge 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
2279
|
+
]);
|
|
2280
|
+
}
|
|
2281
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):merge 成功出口的混合基线提示(推进面
|
|
2282
|
+
// 终点 = preMergeHead,剥离本 merge 自身交付,保精确命中为真信号)
|
|
2283
|
+
annotateCommittedDrift(result, {
|
|
2284
|
+
projectRoot, baseHash: meta.baselineCommit || meta.baseHash,
|
|
2285
|
+
files: manifestFace, head: preMergeHead || 'HEAD',
|
|
2286
|
+
});
|
|
1796
2287
|
try { result.mergeSummary = git(projectRoot, ['log', '--oneline', '-1']); } catch {}
|
|
1797
2288
|
try {
|
|
1798
2289
|
consumeCleanupResult(wm.cleanup(changeName, { force: true }), result);
|
|
@@ -1880,6 +2371,9 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
1880
2371
|
);
|
|
1881
2372
|
// review.json 声明偏差文件(坑 apply-undeclared-deviation-block):与顺带修复同等待遇——
|
|
1882
2373
|
// reviewer 声明(已过 git 证据校验)的执行期偏差豁免 allowed_paths 严格校验,降 warning 注明来源。
|
|
2374
|
+
// D-003@v1(task-03)后本豁免仅作用于 Gate2 的 allowed_paths 归属道——allow 面(design∪task
|
|
2375
|
+
// 清单∪linked)之外的外来声明已在 Gate1(applyWorktree 3b 相交过滤)被剔除拦截,assess 整体
|
|
2376
|
+
// 判定以 Gate1 收集的 errors 为准(BLOCKED);两道口径不再重叠放行。
|
|
1883
2377
|
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(assessSpecBase, '.runtime') }).get('main') || []);
|
|
1884
2378
|
|
|
1885
2379
|
// 检查 2: 变更在 allowed_paths 内(仅在 TaskCard 存在时);顺带修复/review 声明文件豁免。
|