sillyspec 3.32.2 → 3.32.4
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 +1 -1
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +3 -2
- package/.claude/skills/sillyspec-explore/SKILL.md +0 -2
- package/.claude/skills/sillyspec-flow/SKILL.md +1 -0
- package/README.md +4 -4
- package/SKILL.md +2 -4
- package/package.json +1 -1
- package/src/command-cards.js +1 -1
- package/src/cross-repo-reconcile.js +33 -0
- package/src/flow-parity.js +46 -35
- package/src/flow-review.js +12 -2
- package/src/flow.js +23 -8
- package/src/friction-tally.js +2 -1
- package/src/git-helper.js +23 -1
- package/src/index.js +54 -13
- package/src/init.js +58 -61
- 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 +16 -7
- 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/shared.js +75 -0
- package/src/run/verify-quality-scan.js +88 -45
- package/src/scope-audit.js +50 -24
- package/src/spec-sync.js +98 -20
- package/src/stages/brainstorm.js +2 -1
- package/src/stages/cmd-existence.js +5 -1
- package/src/test-bindings.js +2 -0
- package/src/verify-postcheck.js +141 -18
- package/src/verify-probes.js +141 -0
- package/src/worktree.js +43 -9
- package/src/wt-commit.js +82 -3
- package/.claude/skills/sillyspec-quick/SKILL.md +0 -116
- package/assets/command-cards/run-quick.md +0 -20
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
|
}
|
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/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
|
}
|
package/src/verify-postcheck.js
CHANGED
|
@@ -27,8 +27,8 @@ import { createHash } from 'crypto'
|
|
|
27
27
|
import { IR_STRICT_SINCE } from './constants.js'
|
|
28
28
|
import { gitQuiet } from './git-helper.js'
|
|
29
29
|
import { resolveRuntimeRoot } from './run/shared.js'
|
|
30
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'
|
|
31
|
-
import { join, resolve } from 'path'
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, appendFileSync } from 'fs'
|
|
31
|
+
import { join, resolve, isAbsolute } from 'path'
|
|
32
32
|
import { readChangeTrace, testAnchorFile } from './test-bindings.js'
|
|
33
33
|
import { collectFrLinkedTests } from './fr-index.js'
|
|
34
34
|
import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
|
|
@@ -47,7 +47,8 @@ import { discoverModuleIndex } from './decision-distill.js'
|
|
|
47
47
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
48
48
|
// target_files 声明侧解析(Wave 1 已落地):依赖链已核实无环——plan-postcheck 不反向依赖本模块,
|
|
49
49
|
// 且本模块已经 worktree-apply.js:21 间接依赖 plan-postcheck,此处改直连不引入新环(task-04)
|
|
50
|
-
import { parseTargetFiles, parseRepo } from './stages/plan-postcheck.js'
|
|
50
|
+
import { parseTargetFiles, parseRepo, parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
51
|
+
import { crossWorktreePath } from './worktree-cross.js'
|
|
51
52
|
// 探针重跑(P3b task-02 一致性抽查):依赖方向已核实无环——verify-probes 只依赖
|
|
52
53
|
// fs/path/git-helper/change-list/plan-postcheck/contract-matrix/foreign-declared/run-shared,
|
|
53
54
|
// 不反向依赖本模块,此处直连不引入循环。smoke 机器段一致性(2026-09-17-api-coverage-smoke
|
|
@@ -1878,7 +1879,27 @@ export function resolveVerifyChangedFiles(cwd, changeName, ctx = null, opts = {}
|
|
|
1878
1879
|
let anyAvailable = mainFiles !== null
|
|
1879
1880
|
for (const entry of crossEntries) {
|
|
1880
1881
|
// 跨仓仓 gitDir = 跨仓仓根(MultiRepoContext._buildCrossRepoEntry 已 fail-closed 保证可达)
|
|
1881
|
-
|
|
1882
|
+
// FR-07(2026-10-09-verify-reuse-friction):跨仓 worktree 在场(verify 常在 apply 前)时
|
|
1883
|
+
// 交付面在 worktree——meta.baseHash..工作树 窗口(多笔提交+未提交全覆盖);HEAD~1..HEAD
|
|
1884
|
+
// 只看主副本最近一笔,对多笔交付是假信号。meta 缺失回退现行窗口。
|
|
1885
|
+
let files = null
|
|
1886
|
+
try {
|
|
1887
|
+
const wtPath = crossWorktreePath(opts.specBase || join(cwd, '.sillyspec'), changeName, entry.repoKey)
|
|
1888
|
+
const metaPath = join(wtPath, 'meta.json')
|
|
1889
|
+
if (existsSync(metaPath)) {
|
|
1890
|
+
const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
|
|
1891
|
+
const base = meta && meta.baseHash
|
|
1892
|
+
if (base && /^[0-9a-f]{7,40}$/i.test(base)) {
|
|
1893
|
+
const d1 = gitQuiet(wtPath, ['diff', '--name-only', base])
|
|
1894
|
+
const d2 = gitQuiet(wtPath, ['ls-files', '--others', '--exclude-standard'])
|
|
1895
|
+
const mergedWt = [...new Set(String(d1 || '').split(String.fromCharCode(10)).concat(String(d2 || '').split(String.fromCharCode(10))))].map(x => x.trim()).filter(x => x && x !== 'meta.json' && !x.startsWith('.sillyspec/'))
|
|
1896
|
+
// 与 cross-repo-reconcile B' 档同语义:双查皆失败(d1/d2 双 null)→ 不成窗,
|
|
1897
|
+
// files 保持 null 回退 HEAD~1..HEAD(低报不如口径如实降级)
|
|
1898
|
+
if (d1 !== null || d2 !== null) files = mergedWt
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
} catch { /* 回退 HEAD~1..HEAD */ }
|
|
1902
|
+
if (files === null) files = runGitDiffNameOnly(entry.gitDir, 'HEAD~1..HEAD')
|
|
1882
1903
|
if (files !== null) {
|
|
1883
1904
|
anyAvailable = true
|
|
1884
1905
|
for (const f of files) {
|
|
@@ -1991,7 +2012,7 @@ function resolveMainChangedFiles(cwd, changeName, specBase = null) {
|
|
|
1991
2012
|
* resultPath: string|null,
|
|
1992
2013
|
* }}
|
|
1993
2014
|
*/
|
|
1994
|
-
export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = null, restrictFiles = null, faceOverride = null }) {
|
|
2015
|
+
export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = null, restrictFiles = null, faceOverride = null, observability = null }) {
|
|
1995
2016
|
// restrictFiles(坑 quick-gate-并行全流程变更脏文件误伤,2026-09-21 实证):本会话声明
|
|
1996
2017
|
// 文件集(quick gate 传 allowedFiles)——提供时 module 子集的变更文件面收窄到
|
|
1997
2018
|
// 「实际变更 ∩ restrictFiles」,窗口内未声明的并行全流程变更 WIP 文件不再进模块选择
|
|
@@ -2186,7 +2207,7 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
|
|
|
2186
2207
|
}
|
|
2187
2208
|
} else {
|
|
2188
2209
|
console.log(`ℹ️ 动态测试子集(缺省):本变更测试 ∪ FR 关联回归 ∪ import 依赖 = ${scopeFiles.length} 个(并集去重后的总数;分量 deps ${depsAutoFiles.length}${frCount ? ` + FR 绑定 ${frCount}` : ''},重叠只计一次);runner 自项目结构推断。显式 test_strategy: full 恢复全量`)
|
|
2189
|
-
mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre })
|
|
2210
|
+
mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre, observability })
|
|
2190
2211
|
}
|
|
2191
2212
|
// ledger 记账(无论增/全):head=当前 HEAD(git 失败 null → 下轮自动回全子集);失败批文件
|
|
2192
2213
|
// 落账供下轮增量;passed 后 failedFiles 清空 → 下轮自然回全子集基线
|
|
@@ -2924,21 +2945,54 @@ export function resolveVerifyAnchorSet({ specBase, changeName }) {
|
|
|
2924
2945
|
return [...ids]
|
|
2925
2946
|
}
|
|
2926
2947
|
|
|
2927
|
-
/**
|
|
2948
|
+
/**
|
|
2949
|
+
* trace 行 repo 归属解析根(2026-10-09-verify-reuse-friction FR-06 / D-005@v1):
|
|
2950
|
+
* 行 repo 字段(additive,缺省=主仓)经 local.yaml repos 注册表换根——跨仓 task 卡抄入的
|
|
2951
|
+
* 仓根相对 tests 路径不再按主仓解析恒悬空(2026-10-09 tombstone 取证:3 文件悬空 fail-fast
|
|
2952
|
+
* 连烧 4 轮实测)。注册表缺失/键未注册 → 该行回退主仓根(现状行为),dangling 消息带
|
|
2953
|
+
* repo: 前缀使错配可见。读法同 verify-probes 探针 7 先例(join(specBase,'local.yaml') +
|
|
2954
|
+
* parseRepoRegistry,相对路径按主仓 cwd resolve)。
|
|
2955
|
+
*/
|
|
2956
|
+
function traceRepoRoots({ specBase, cwd }) {
|
|
2957
|
+
try {
|
|
2958
|
+
const yamlPath = join(specBase, 'local.yaml')
|
|
2959
|
+
if (!existsSync(yamlPath)) return new Map()
|
|
2960
|
+
const reg = parseRepoRegistry(readFileSync(yamlPath, 'utf8'))
|
|
2961
|
+
const m = new Map()
|
|
2962
|
+
for (const [k, v] of reg) m.set(k, isAbsolute(v) ? v : resolve(cwd, v))
|
|
2963
|
+
return m
|
|
2964
|
+
} catch { return new Map() }
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/** 残差解析:active·非 superseded·锚命中的 trace 行 → { anchors, rows, files, dangling, perFile } */
|
|
2928
2968
|
export function resolveTraceResidual({ specBase, changeName, cwd }) {
|
|
2929
2969
|
const anchors = resolveVerifyAnchorSet({ specBase, changeName })
|
|
2930
|
-
if (anchors.length === 0) return { anchors, rows: [], files: [], dangling: [] }
|
|
2970
|
+
if (anchors.length === 0) return { anchors, rows: [], files: [], dangling: [], perFile: [] }
|
|
2931
2971
|
const rows = readChangeTrace(join(specBase, 'changes', changeName))
|
|
2932
2972
|
.filter(r => anchors.includes(r.anchor) && r.state === 'active' && r.status !== 'superseded')
|
|
2933
|
-
// tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity
|
|
2934
|
-
|
|
2935
|
-
const
|
|
2936
|
-
|
|
2973
|
+
// tests 条目可携带用例锚(2026-09-26-binding-anchor-fidelity)——文件面/悬空判定走剥锚。
|
|
2974
|
+
// FR-06(2026-10-09):文件按行 repo 归属配对(perFile),悬空判定/残差执行按行根解析。
|
|
2975
|
+
const repoRoots = traceRepoRoots({ specBase, cwd })
|
|
2976
|
+
const seen = new Set()
|
|
2977
|
+
const perFile = []
|
|
2978
|
+
for (const r of rows) {
|
|
2979
|
+
const repoKey = r.repo && r.repo !== 'main' ? r.repo : null
|
|
2980
|
+
for (const t of (r.tests || [])) {
|
|
2981
|
+
const f = testAnchorFile(t)
|
|
2982
|
+
const key = (repoKey ? repoKey + '' : '') + f
|
|
2983
|
+
if (!seen.has(key)) { seen.add(key); perFile.push({ repo: repoKey, file: f }) }
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
const files = [...new Set(perFile.map(x => x.file))].sort()
|
|
2987
|
+
const dangling = perFile
|
|
2988
|
+
.filter(x => !existsSync(resolve((x.repo && repoRoots.get(x.repo)) || cwd, x.file)))
|
|
2989
|
+
.map(x => x.repo ? `${x.repo}:${x.file}` : x.file)
|
|
2990
|
+
return { anchors, rows, files, dangling, perFile }
|
|
2937
2991
|
}
|
|
2938
2992
|
|
|
2939
2993
|
/** 残差执行段:按 buildDepsBatches 同口径组卷(扩展名分语言/pytest 前缀自模块命令推断),
|
|
2940
2994
|
* 30/块分卷破 deps 面的帽(残差是绑定面钦定必须跑的,不适用发现面防膨胀帽)。 */
|
|
2941
|
-
function runTraceResidual({ cwd, files, hits, knownFailures = [] }) {
|
|
2995
|
+
function runTraceResidual({ cwd, files, groups = null, hits, knownFailures = [] }) {
|
|
2942
2996
|
// 嵌套 test-runner 防污染:孙代 node --test 会误连外层 reporter(NODE_TEST_* 环境变量,
|
|
2943
2997
|
// 单测实测:嵌套下挂测被误判 passed)——spawn 前剥除、跑完还原(同步窗口安全;真实门禁
|
|
2944
2998
|
// 进程无此变量,零影响)。
|
|
@@ -2946,12 +3000,17 @@ function runTraceResidual({ cwd, files, hits, knownFailures = [] }) {
|
|
|
2946
3000
|
const strippedKeys = []
|
|
2947
3001
|
for (const k of Object.keys(process.env)) { if (k.startsWith('NODE_TEST')) { strippedKeys.push(k); delete process.env[k] } }
|
|
2948
3002
|
try {
|
|
2949
|
-
|
|
3003
|
+
// FR-06(2026-10-09-verify-reuse-friction):残差按行 repo 分组执行——跨仓组在其仓根
|
|
3004
|
+
// 跑(组卷 cwd=组根),主仓组现状不变;groups 缺席回退旧 files 单主仓组(零迁移)。
|
|
3005
|
+
const execGroups = Array.isArray(groups) && groups.length > 0
|
|
3006
|
+
? groups
|
|
3007
|
+
: [{ repo: null, root: cwd, files }]
|
|
3008
|
+
return execGroups.flatMap(g => runTraceResidualInner({ cwd: g.root, repoLabel: g.repo, files: g.files, hits, knownFailures }))
|
|
2950
3009
|
} finally {
|
|
2951
3010
|
for (const k of strippedKeys) process.env[k] = savedEnv[k]
|
|
2952
3011
|
}
|
|
2953
3012
|
}
|
|
2954
|
-
function runTraceResidualInner({ cwd, files, hits, knownFailures }) {
|
|
3013
|
+
function runTraceResidualInner({ cwd, repoLabel = null, files, hits, knownFailures }) {
|
|
2955
3014
|
const norm = (p) => String(p).replace(/\\/g, '/')
|
|
2956
3015
|
const segments = []
|
|
2957
3016
|
for (let i = 0; i < files.length; i += 30) {
|
|
@@ -2998,6 +3057,19 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
2998
3057
|
const provable = (action === 'deps-auto-subset' || action === 'dynamic-subset')
|
|
2999
3058
|
? new Set((depsAutoFiles || []).map(f => String(f).replace(/\\/g, '/'))) : new Set()
|
|
3000
3059
|
const residualFiles = tr.files.filter(f => !provable.has(f))
|
|
3060
|
+
// FR-06(2026-10-09-verify-reuse-friction):残差执行面按行 repo 分组(provable 差集按
|
|
3061
|
+
// 文件名判,跨仓文件同样成立);组根=注册表仓根,未注册回退主仓(与悬空判定同口径)。
|
|
3062
|
+
const _repoRoots = traceRepoRoots({ specBase, cwd })
|
|
3063
|
+
const _gmap = new Map()
|
|
3064
|
+
for (const x of (tr.perFile || [])) {
|
|
3065
|
+
if (provable.has(String(x.file).replace(/\\/g, '/'))) continue
|
|
3066
|
+
const _rk = x.repo || null
|
|
3067
|
+
const _root = (_rk && _repoRoots.get(_rk)) || cwd
|
|
3068
|
+
const _gk = _rk + '' + _root
|
|
3069
|
+
if (!_gmap.has(_gk)) _gmap.set(_gk, { repo: _rk, root: _root, files: [] })
|
|
3070
|
+
_gmap.get(_gk).files.push(x.file)
|
|
3071
|
+
}
|
|
3072
|
+
const residualGroups = [..._gmap.values()].map(g => ({ ...g, files: [...new Set(g.files)] }))
|
|
3001
3073
|
try {
|
|
3002
3074
|
writeTraceDisclosure({
|
|
3003
3075
|
specBase, changeName,
|
|
@@ -3015,7 +3087,7 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3015
3087
|
if (residualFiles.length === 0) return mainResult
|
|
3016
3088
|
let segs
|
|
3017
3089
|
try {
|
|
3018
|
-
segs = runTraceResidual({ cwd, files: residualFiles, hits, knownFailures })
|
|
3090
|
+
segs = runTraceResidual({ cwd, files: residualFiles, groups: residualGroups, hits, knownFailures })
|
|
3019
3091
|
} catch (e) {
|
|
3020
3092
|
return { ...mainResult, status: 'failed', reason: `trace 残差段执行异常(fail-closed):${e && e.message ? e.message : e}` }
|
|
3021
3093
|
}
|
|
@@ -3042,7 +3114,7 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
|
|
|
3042
3114
|
* export 供直测(buildDepsBatches 同款先例——fixture 需真跑测试文件,不宜经
|
|
3043
3115
|
* runVerifyTestCheck 全链只为断言组卷接线)。
|
|
3044
3116
|
*/
|
|
3045
|
-
export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [], frPre = null }) {
|
|
3117
|
+
export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [], frPre = null, observability = null }) {
|
|
3046
3118
|
const subsetStartedAt = Date.now()
|
|
3047
3119
|
const perModule = hits.map(h => runOneModule(h.name, h.test, cwd, knownFailures))
|
|
3048
3120
|
// 依赖测试伪模块(module-test-face-rot,2026-09-16 本会话实证):硬编码模块测试清单不含
|
|
@@ -3158,6 +3230,9 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
|
|
|
3158
3230
|
changeName,
|
|
3159
3231
|
result,
|
|
3160
3232
|
extra: {
|
|
3233
|
+
// 复用观测(2026-10-09-verify-reuse-friction FR-04):真跑轮携带指纹与判定原因——
|
|
3234
|
+
// 取证直读 test-result.json,不再依赖 stdout 考古。additive:调用方未传则零字段。
|
|
3235
|
+
...(observability || {}),
|
|
3161
3236
|
modules: perModule.map(r => ({
|
|
3162
3237
|
name: r.name,
|
|
3163
3238
|
command: r.command,
|
|
@@ -3218,6 +3293,26 @@ export function persistLintResult({ specBase, changeName, testResultPath, lint }
|
|
|
3218
3293
|
}
|
|
3219
3294
|
}
|
|
3220
3295
|
|
|
3296
|
+
/**
|
|
3297
|
+
* 复用判定追加日志(2026-10-09-verify-reuse-friction FR-04):复用命中轮不产生
|
|
3298
|
+
* test-result.json(无真实执行——那是它的语义),判定落
|
|
3299
|
+
* .runtime/verify-runs/reuse-decisions-<change>.jsonl 逐行追加(ts + layer + hit +
|
|
3300
|
+
* reason + fingerprint 摘要),与真跑轮 test-result.json 的 fingerprint/reuse_decision
|
|
3301
|
+
* 互为对照——取证直读文件,不再依赖 stdout。best-effort:追加失败不阻断任何路径。
|
|
3302
|
+
*/
|
|
3303
|
+
export function appendReuseDecision({ specBase, changeName, decision }) {
|
|
3304
|
+
try {
|
|
3305
|
+
if (!specBase || !changeName || !decision || typeof decision !== 'object') return false
|
|
3306
|
+
const dir = join(specBase, '.runtime', 'verify-runs')
|
|
3307
|
+
mkdirSync(dir, { recursive: true })
|
|
3308
|
+
appendFileSync(join(dir, `reuse-decisions-${changeName}.jsonl`), JSON.stringify({
|
|
3309
|
+
ts: new Date().toISOString(),
|
|
3310
|
+
...decision,
|
|
3311
|
+
}) + '\n')
|
|
3312
|
+
return true
|
|
3313
|
+
} catch { /* 观测失败不阻断 */ return false }
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3221
3316
|
/**
|
|
3222
3317
|
* 结果落盘到 .runtime/verify-runs/<ts>/test-result.json(供追溯与 SillyHub 消费)。
|
|
3223
3318
|
* 多模块时 extra.modules 描述各模块明细。export 供 test 验证台账落盘形状。
|
|
@@ -3994,7 +4089,7 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
3994
4089
|
try {
|
|
3995
4090
|
// 锚定变更分支/审计 tag(非 blanket 20 提交——base 提交文件不得误救)
|
|
3996
4091
|
const rescueRef = (typeof branchHash === 'string' && branchHash.trim()) ? branch : (gitQuiet(cwd, ['rev-parse', '--verify', '--quiet', auditTag + '^{commit}'], { timeout: 30 * 1000 }) ? auditTag : null)
|
|
3997
|
-
if (
|
|
4092
|
+
if (rescueRef) {
|
|
3998
4093
|
// -n 形态计数(2026-10-04-log-window-arity:裸 '20' 在 git ≥2.4x 是 ambiguous argument
|
|
3999
4094
|
// fatal——fail-open 静默吞后救赎 note 路径从未生效,plan-target-files D7 在纯 HEAD 稳定红)
|
|
4000
4095
|
const recent = gitQuiet(cwd, ['log', '-n', '20', '--name-only', '--format=%h', rescueRef], { timeout: 30 * 1000, trim: false })
|
|
@@ -4005,6 +4100,34 @@ export function resolveReconcileActualFiles({ cwd, specBase, runtimeRoot, change
|
|
|
4005
4100
|
sources.push(`main:log-20-commit-window(declared-rescue-only:${rescueRef})`)
|
|
4006
4101
|
}
|
|
4007
4102
|
}
|
|
4103
|
+
} else {
|
|
4104
|
+
// ③ 提交信息锚定窗口(2026-10-09-verify-papercuts ③):主代理直改形态(无 worktree/
|
|
4105
|
+
// 分支/审计 tag——交付直接 commit 到主干、消息含变更名)此前救援窗口恒空 → ②类全量
|
|
4106
|
+
// 假红,只能手工喂 apply-pathspec 才能过(当日收口实证撞 3 轮)。锚定规则:近 30 提交中
|
|
4107
|
+
// 消息含变更名的提交,其 --name-only 触及面进窗口——declared-rescue-only 语义不变
|
|
4108
|
+
// (不进 union、只救「声明而 missing」),他变更提交的文件不满足消息锚定天然不入窗。
|
|
4109
|
+
try {
|
|
4110
|
+
const msgWindow = gitQuiet(cwd, ['log', '-n', '30', '--name-only', '--format=%h %s'], { timeout: 30 * 1000, trim: false })
|
|
4111
|
+
// 边界锚定(审查 P3 修正):子串 includes 会把同日前缀变更名(A=…-x 是 B=…-x2 的
|
|
4112
|
+
// 前缀)误纳——改为变更名两侧不得紧贴 [\w-](消息里的括号/空格/行首尾都是自然边界)。
|
|
4113
|
+
const msgAnchorRe = new RegExp('(?<![\\w-])' + String(changeName).replace(/[\\.*+?^${}()|[\\]]/g, '\\$&') + '(?![\\w-])')
|
|
4114
|
+
if (typeof msgWindow === 'string' && msgAnchorRe.test(msgWindow)) {
|
|
4115
|
+
const files = []
|
|
4116
|
+
let inOwnCommit = false
|
|
4117
|
+
for (const l of msgWindow.split(String.fromCharCode(10))) {
|
|
4118
|
+
const t = l.trim()
|
|
4119
|
+
if (!t) { continue }
|
|
4120
|
+
const header = t.match(/^[0-9a-f]{7,40} /)
|
|
4121
|
+
if (header) { inOwnCommit = msgAnchorRe.test(t); continue }
|
|
4122
|
+
if (inOwnCommit && !/^[0-9a-f]{7,40}$/.test(t)) files.push(t)
|
|
4123
|
+
}
|
|
4124
|
+
if (files.length > 0) {
|
|
4125
|
+
commitWindowFiles = files
|
|
4126
|
+
sources.push('main:log-msg-window(declared-rescue-only:HEAD)')
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
} catch { /* 消息窗口失败静默省略(不干扰主链) */ }
|
|
4130
|
+
}
|
|
4008
4131
|
} catch { /* 窗口源失败静默省略 */ }
|
|
4009
4132
|
// B3 apply-pathspec 兜底(存在则并入,读取失败静默忽略——前两源不受影响)
|
|
4010
4133
|
const pathspecFile = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
|