sillyspec 3.32.3 → 3.32.5
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/README.md +4 -4
- package/SKILL.md +2 -4
- package/package.json +1 -1
- package/src/command-cards.js +1 -1
- package/src/config-schema.js +8 -0
- package/src/cross-placement.js +246 -0
- package/src/cross-repo-reconcile.js +8 -5
- package/src/doctor-diagnostics.js +21 -1
- package/src/flow-parity.js +41 -10
- package/src/flow-review.js +12 -2
- package/src/flow.js +45 -2
- package/src/hooks/worktree-guard.js +35 -2
- package/src/index.js +53 -5
- package/src/init.js +58 -61
- package/src/run/command.js +19 -0
- package/src/run/multi-repo-context.js +11 -5
- package/src/run/shared.js +85 -4
- package/src/scope-audit.js +130 -85
- package/src/spec-sync.js +45 -16
- package/src/stages/plan-postcheck.js +67 -9
- package/src/sync.js +3 -0
- package/src/verify-postcheck.js +56 -8
- package/src/verify-probes.js +14 -7
- package/src/worktree-apply.js +62 -8
- package/src/worktree-cross.js +188 -27
- package/src/worktree-deps.js +50 -9
- package/src/worktree.js +43 -9
- package/assets/command-cards/run-quick.md +0 -20
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
|
|
|
85
85
|
|
|
86
86
|
**也可以跳过完整流程:**
|
|
87
87
|
|
|
88
|
-
- 小改动:`/sillyspec:
|
|
88
|
+
- 小改动:`/sillyspec:flow "修复 xxx"`
|
|
89
89
|
- 不确定做什么:`/sillyspec:explore`
|
|
90
90
|
- 中断恢复:`/sillyspec:resume`
|
|
91
91
|
- 不知道下一步:`/sillyspec:continue`
|
|
@@ -112,7 +112,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
|
|
|
112
112
|
| `/sillyspec:status` · `/sillyspec:state` | 查看进度 / 当前工作状态 |
|
|
113
113
|
| `/sillyspec:continue` | 自动判断并执行下一步 |
|
|
114
114
|
| `/sillyspec:explore` | 自由思考:画图、讨论、调研、HTML 原型 |
|
|
115
|
-
| `/sillyspec:
|
|
115
|
+
| `/sillyspec:flow` | 轻量变更(默认快道):需求明确的小改动两步收口 |
|
|
116
116
|
| `/sillyspec:resume` | 恢复工作(支持大模块阶段进度) |
|
|
117
117
|
| `/sillyspec:doctor` | 项目自检与状态修复 |
|
|
118
118
|
| `/sillyspec:commit` | 智能提交 |
|
|
@@ -122,7 +122,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
|
|
|
122
122
|
## CLI 命令
|
|
123
123
|
|
|
124
124
|
```bash
|
|
125
|
-
sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/
|
|
125
|
+
sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/explore)
|
|
126
126
|
sillyspec run <stage> --done 完成当前步骤并推进到下一步
|
|
127
127
|
sillyspec run <stage> --status 查看阶段进度
|
|
128
128
|
sillyspec progress show 显示当前项目状态
|
|
@@ -140,7 +140,7 @@ sillyspec change-delete <变更名> 删除变更(DB status=deleted 与归
|
|
|
140
140
|
**第一层:护城河(外部状态,模型能力不可替代)**
|
|
141
141
|
|
|
142
142
|
- **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ID 分配 + 双占用 fail-closed 硬拦;`--files` 边界声明切分"这是谁改的";`--done` 前并发写预检告警他者未提交改动;平台同步乐观锁(base_ts + 身份 + 服务器权威钟)+ 冲突血统归属(自回声不误报)
|
|
143
|
-
- **实测验收门禁** — verify
|
|
143
|
+
- **实测验收门禁** — verify 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
|
|
144
144
|
- **持久账本 + 断点恢复** — SQLite(node:sqlite)单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账可检索
|
|
145
145
|
- **跨仓变更管理** — task 卡声明 `repo:` 字段即可管理跨仓任务:同构 worktree 隔离、base 锚快照、apply 永不覆盖用户在途改动
|
|
146
146
|
|
package/SKILL.md
CHANGED
|
@@ -51,7 +51,6 @@ Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini 通用。
|
|
|
51
51
|
| `/sillyspec:continue` | 自动判断并执行下一步 |
|
|
52
52
|
| `/sillyspec:explore` | 自由思考模式 |
|
|
53
53
|
| `/sillyspec:flow` | 轻量变更(默认快道):flow start → 干活 → flow done |
|
|
54
|
-
| `/sillyspec:quick` | (已退役,重定向到 flow)仅存量在途会话收尾 |
|
|
55
54
|
| `/sillyspec:resume` | 恢复工作 |
|
|
56
55
|
| `/sillyspec:doctor` | 项目自检 |
|
|
57
56
|
| `/sillyspec:commit` | 智能提交 |
|
|
@@ -70,7 +69,6 @@ sillyspec run execute 执行开发阶段(子代理并行 + worktree
|
|
|
70
69
|
sillyspec run verify 执行验证阶段
|
|
71
70
|
sillyspec run archive 执行归档阶段
|
|
72
71
|
sillyspec flow start/done/status 轻量变更(默认快道,2 次调用收口)
|
|
73
|
-
sillyspec run quick (已退役,仅存量在途会话 --done/--cancel 收尾)
|
|
74
72
|
sillyspec run explore 自由探索
|
|
75
73
|
sillyspec progress show 显示当前项目状态
|
|
76
74
|
sillyspec setup 安装推荐 MCP 工具
|
|
@@ -81,12 +79,12 @@ sillyspec init 初始化(零交互,自动检测工具)
|
|
|
81
79
|
|
|
82
80
|
**护城河层(外部状态,模型能力不可替代)**
|
|
83
81
|
- **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ql-ID 分配 + 双占用 fail-closed 硬拦;边界声明归属切分(窗口∩声明);并发写预检告警;平台乐观锁(base_ts + 身份头 + 服务器权威钟)
|
|
84
|
-
- **实测验收门禁** — verify
|
|
82
|
+
- **实测验收门禁** — verify 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
|
|
85
83
|
- **持久账本 + 断点恢复** — SQLite 单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账
|
|
86
84
|
- **跨仓变更管理** — task 卡声明 `repo:` 字段,跨仓同构 worktree 隔离,base 锚快照,apply 不覆盖用户在途改动
|
|
87
85
|
|
|
88
86
|
**确定性对账层(机械核验,零模型轮次)**
|
|
89
|
-
- **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive
|
|
87
|
+
- **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive 三处收尾注入
|
|
90
88
|
- **API 契约矩阵** — provider/consumer 识别、endpoint 工件提取、前后端 API parity 对账
|
|
91
89
|
- **文档一致性棘轮** — 源码引用真实性校验(HEAD 模式),失败数只降不升
|
|
92
90
|
- **noAI 下沉 / IR 事实层** — 机械轮次下沉 CLI(骨架预生成、注入、代算对账),判断层留给 agent;同一契约清单事前渲染预览、事后引擎核验
|
package/package.json
CHANGED
package/src/command-cards.js
CHANGED
|
@@ -10,7 +10,7 @@ import { writeAtomicSync } from './fs-atomic.js'
|
|
|
10
10
|
|
|
11
11
|
export const COMMAND_CARD_TARGETS = { zcode: '.zcode/commands/sillyspec', claude: '.claude/commands/sillyspec' }
|
|
12
12
|
// flow=轻量变更默认道入口卡(2026-09-25-quick-channel-retire 配套:quick 退役后快道首次有卡)
|
|
13
|
-
export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', '
|
|
13
|
+
export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'status']
|
|
14
14
|
|
|
15
15
|
const ANCHOR_TAIL_RE = /\n?<!-- sillyspec-card: v(\S+) sha256=([0-9a-f]{64}) -->\s*$/
|
|
16
16
|
const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
|
package/src/config-schema.js
CHANGED
|
@@ -157,6 +157,7 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
157
157
|
note: 'gitignore 生成物(如构建期产出的 src/build-id.ts)不在 git 树也不进 untracked overlay(ls-files --others --exclude-standard 尊重 .gitignore),worktree 缺失致构建炸 Failed to load url——create 期按本清单从主仓复制供给(2026-09-15-worktree-dual-truth-gates 坑③,D-003@v1)。块列表或 inline flow 数组均可解析。',
|
|
158
158
|
keys: [
|
|
159
159
|
{ path: 'worktree.supplyFiles', type: 'array', optional: true, status: 'live', readers: ['readSupplyFilesConfig + _supplyGeneratedFiles (src/worktree.js — create step 5.9 供给步)'], desc: '随 worktree create 从主仓复制的生成物清单:精确路径或 glob(* 单层 / ** 多层),相对仓根。展开上限 200 文件超出截断警告;实供清单记 meta.supplyFiles;缺省 [] = 供给步空转零行为变化。', example: 'src/build-id.ts' },
|
|
160
|
+
{ path: 'worktree.crossPlacement.<repoKey>', type: 'string', optional: true, status: 'live', readers: ['readCrossPlacementConfig + resolvePlacementRoot (src/cross-placement.js) → ensureCrossWorktrees 落位 (src/worktree-cross.js)'], desc: '跨仓 worktree 按仓落位根目录(坑 cross-wt-toolchain-split:主仓在 WSL /root、跨仓在 /mnt/<盘>/ Windows 盘时,默认落位与该仓构建工具链分居互不可访文件系统,隔离形同虚设——2026-10-10 fire-equipment D-007 实证)。worktree 落 <目录>/<change>--<repoKey>;相对路径相对主仓根解析;落位根在跨仓仓根内 = 配置错拒绝创建;落位注册表 cross-placements.json 保 meta/list/cleanup/verify 对账全链寻回。缺省 = 默认公式 <specBase>/.runtime/worktrees/(零回归)。【推荐迁移】repos 条目内联 worktree 子键(repos.<key>: {path: <仓根>, worktree: <落位根>},2026-10-10 配置简化)——优先级 repos 内联 > 本键 > 默认;本键兼容保留不废。', example: '/mnt/e/sillyspec-worktrees' },
|
|
160
161
|
{ path: 'worktree.policy.adopt_branch', type: 'boolean', optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → WorktreeManager.create 分支冲突守卫 (src/worktree.js)'], desc: '分支已存在冲突的选择记忆:true = 自动收编既有分支(等效常备 --adopt-branch,meta 记 adoptViaPolicy 留痕)。缺省 false = 既有三选一 fail-closed 菜单。显式 flag 优先于 policy。', example: 'true' },
|
|
161
162
|
{ path: 'worktree.policy.apply_overlap', type: 'enum', values: ['manual', 'force', 'skip'], optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → applyWorktree guard 相交拦截 (src/worktree-apply.js)'], desc: 'apply 与活跃 quick 会话在途声明相交时的处置记忆:manual(缺省)= 既有 fail-closed 报错;force = 放行并留痕(overlapForced.via 记 policy 来源);skip = 无人值守软跳过(同 autoApply 路径)。--force 显式 flag 优先。', example: 'manual' },
|
|
162
163
|
{ path: 'worktree.policy.stash_dirty', type: 'boolean', optional: true, status: 'live', readers: ['readWorktreePolicy (src/worktree-policy.js) → applyWorktree 4.4 stash 段 (src/worktree-apply.js)'], desc: '主仓在途改动自动 stash 的选择记忆:true = 等效常备 --stash-dirty(stash SHA 显著打印 + 失败绝不 drop)。缺省 false = 显式 flag opt-in。', example: 'false' },
|
|
@@ -412,6 +413,13 @@ dispatch:
|
|
|
412
413
|
# # adopt_branch: true # 分支已存在冲突自动收编(等效常备 --adopt-branch)
|
|
413
414
|
# # apply_overlap: manual # manual=fail-closed(缺省)| force=放行留痕 | skip=软跳过
|
|
414
415
|
# # stash_dirty: false # true=主仓在途改动自动 stash(等效常备 --stash-dirty)
|
|
416
|
+
# crossPlacement: # 跨仓 worktree 按仓落位(坑 cross-wt-toolchain-split:主仓 WSL + 跨仓
|
|
417
|
+
# # urgent: /mnt/e/wt # Windows 盘时工具链够不着默认落位——配到跨仓工具链可达的目录)
|
|
418
|
+
# # ↑推荐改用 repos 条目内联(一处声明注册+落位):
|
|
419
|
+
# # repos:
|
|
420
|
+
# # urgent:
|
|
421
|
+
# # path: /mnt/e/Java/crrc/qmdp/urgent
|
|
422
|
+
# # worktree: /mnt/e/sillyspec-worktrees
|
|
415
423
|
|
|
416
424
|
# ── 测试策略(2026-09-26-dynamic-test-inference 起)──
|
|
417
425
|
# 缺省=动态子集:本变更测试 ∪ FR 关联回归(active FR 覆盖面∩触碰文件→绑定 tests)∪ import 依赖,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cross-placement.js — 跨仓 worktree 落位配置 / 注册表 / 路径解析(零依赖叶子模块)
|
|
3
|
+
*
|
|
4
|
+
* 坑 cross-wt-toolchain-split(2026-10-10 用户实证,fire-equipment D-007):跨仓 worktree 固定
|
|
5
|
+
* 落位主仓 specBase 下——主仓在 WSL /root、跨仓在 Windows 盘时,worktree 与该仓唯一构建工具链
|
|
6
|
+
* 分居互不可访文件系统,隔离形同虚设(agent 被迫绕开 worktree 回主副本直写主干)。
|
|
7
|
+
*
|
|
8
|
+
* 本模块承载三件事:
|
|
9
|
+
* 1. worktree.crossPlacement 配置读取(local.yaml,块式/inline 双形态);
|
|
10
|
+
* 2. 落位注册表 cross-placements.json(create 写 / cleanup 删,read-modify-merge 原子写)——
|
|
11
|
+
* 挪位后 listCrossWorktreeMetas/cleanup/verify 对账仍可寻址;
|
|
12
|
+
* 3. crossWorktreePath(placementRoot?) 路径公式 + resolveCrossWorktreePath(注册表优先、公式兜底)。
|
|
13
|
+
*
|
|
14
|
+
* 依赖纪律(评审吸收①):本模块只 import fs/path/fs-atomic——worktree-cross.js 与
|
|
15
|
+
* run/multi-repo-context.js 都 import 本模块。multi-repo-context 此前刻意不 import worktree-cross
|
|
16
|
+
* (经 run/shared 的既有环),落位逻辑下沉叶子模块后两侧共用不引新环。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { join, resolve as resolvePath, isAbsolute, dirname } from 'path';
|
|
21
|
+
import { writeAtomicSync } from './fs-atomic.js';
|
|
22
|
+
|
|
23
|
+
const PLACEMENT_REGISTRY_FILE = 'cross-placements.json';
|
|
24
|
+
|
|
25
|
+
/** 跨仓 worktree 目录名(默认落位与自定义落位同形态——isCrossWorktreeDir 的双连字符判定依赖它) */
|
|
26
|
+
export function crossWorktreeDirName(changeName, repoKey) {
|
|
27
|
+
return `${changeName}--${repoKey}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 路径公式:默认落位 <specBase>/.runtime/worktrees/<change>--<repoKey>;
|
|
32
|
+
* placementRoot 给定时 <placementRoot>/<change>--<repoKey>(与默认同目录名形态)。
|
|
33
|
+
* @param {string} specBase 主仓 spec 根(<主仓>/.sillyspec)
|
|
34
|
+
* @param {string} changeName
|
|
35
|
+
* @param {string} repoKey
|
|
36
|
+
* @param {string|null} [placementRoot] 已解析的落位根目录(null/缺省走默认公式——三参调用零回归)
|
|
37
|
+
*/
|
|
38
|
+
export function crossWorktreePath(specBase, changeName, repoKey, placementRoot = null) {
|
|
39
|
+
const dirName = crossWorktreeDirName(changeName, repoKey);
|
|
40
|
+
if (placementRoot) return join(placementRoot, dirName);
|
|
41
|
+
return join(specBase, '.runtime', 'worktrees', dirName);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 解析 crossPlacement 配置值为落位根目录:相对路径相对主仓根解析;拒绝 .. 逃逸出主仓根
|
|
46
|
+
* 之外的形态不在此处判(主仓根外合法——/mnt/e/wt-root 本就在主仓外),只归一绝对路径。
|
|
47
|
+
* @param {string} raw local.yaml 配置原文
|
|
48
|
+
* @param {string} mainRoot 主仓根(相对路径解析基准)
|
|
49
|
+
* @returns {string} 绝对落位根
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePlacementRoot(raw, mainRoot) {
|
|
52
|
+
const trimmed = String(raw || '').trim().replace(/^['"]|['"]$/g, '');
|
|
53
|
+
if (isAbsolute(trimmed)) return resolvePath(trimmed);
|
|
54
|
+
return resolvePath(mainRoot, trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 读主仓 local.yaml 的 worktree.crossPlacement 段 → Map<repoKey, 原始配置值>。
|
|
59
|
+
* 兼容两种写法(沿 worktree.js readSupplyFilesConfig / hooks/worktree-guard.js parseSimpleYaml
|
|
60
|
+
* 的轻量手写风格,不引 yaml 依赖):
|
|
61
|
+
* worktree:
|
|
62
|
+
* crossPlacement:
|
|
63
|
+
* urgent: /mnt/e/wt-root # 块式(indent 4)
|
|
64
|
+
* worktree:
|
|
65
|
+
* crossPlacement: {urgent: /mnt/e/wt-root, front: D:/wt} # inline flow
|
|
66
|
+
* 文件缺失/段缺失/解析异常 → 空 Map(fail-open,落位步空转走默认公式)。
|
|
67
|
+
* @param {string} mainCwd 主仓根
|
|
68
|
+
* @returns {Map<string, string>}
|
|
69
|
+
*/
|
|
70
|
+
export function readCrossPlacementConfig(mainCwd) {
|
|
71
|
+
const out = new Map();
|
|
72
|
+
let content;
|
|
73
|
+
try {
|
|
74
|
+
content = readFileSync(join(mainCwd, '.sillyspec', 'local.yaml'), 'utf8');
|
|
75
|
+
} catch {
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
const stripQuotes = (v) => (
|
|
79
|
+
v.length >= 2 && ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) ? v.slice(1, -1) : v
|
|
80
|
+
);
|
|
81
|
+
let inWorktree = false;
|
|
82
|
+
let inCrossPlacement = false;
|
|
83
|
+
for (const rawLine of content.split('\n')) {
|
|
84
|
+
const noComment = rawLine.replace(/\s+#.*$/, ''); // 尾注释剥离(与 worktree-guard 同口径)
|
|
85
|
+
const trimmed = noComment.trim();
|
|
86
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
87
|
+
const indent = noComment.length - noComment.trimStart().length;
|
|
88
|
+
if (indent === 0) {
|
|
89
|
+
inWorktree = /^worktree:\s*$/.test(trimmed);
|
|
90
|
+
inCrossPlacement = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!inWorktree) continue;
|
|
94
|
+
if (indent === 2) {
|
|
95
|
+
const m = trimmed.match(/^crossPlacement:\s*(.*)$/);
|
|
96
|
+
if (!m) { inCrossPlacement = false; continue; }
|
|
97
|
+
inCrossPlacement = true;
|
|
98
|
+
const rest = m[1].trim();
|
|
99
|
+
// inline flow 形态:{urgent: /a, front: D:/b}——逗号拆(值内逗号不支持,落位路径含逗号用块式)
|
|
100
|
+
if (rest.startsWith('{') && rest.endsWith('}')) {
|
|
101
|
+
for (const part of rest.slice(1, -1).split(',')) {
|
|
102
|
+
const kv = part.split(':');
|
|
103
|
+
if (kv.length >= 2) {
|
|
104
|
+
const k = kv[0].trim();
|
|
105
|
+
const v = stripQuotes(kv.slice(1).join(':').trim());
|
|
106
|
+
if (k && v) out.set(k, v);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
inCrossPlacement = false; // inline 一行吃尽
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (inCrossPlacement && trimmed.includes(':')) {
|
|
114
|
+
const idx = trimmed.indexOf(':');
|
|
115
|
+
const k = trimmed.slice(0, idx).trim();
|
|
116
|
+
const v = stripQuotes(trimmed.slice(idx + 1).trim());
|
|
117
|
+
if (k && v) out.set(k, v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 注册表路径 <specBase>/.runtime/worktrees/cross-placements.json */
|
|
124
|
+
export function placementRegistryPath(specBase) {
|
|
125
|
+
return join(specBase, '.runtime', 'worktrees', PLACEMENT_REGISTRY_FILE);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 读注册表(整对象)。文件缺失/损坏半截 JSON → {}(fail-open:读取方降级默认公式)。
|
|
130
|
+
* @returns {Record<string, {repoKey: string, changeName: string, worktreePath: string, placementRoot: string}>}
|
|
131
|
+
*/
|
|
132
|
+
export function readPlacementRegistry(specBase) {
|
|
133
|
+
const p = placementRegistryPath(specBase);
|
|
134
|
+
if (!existsSync(p)) return {};
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(readFileSync(p, 'utf8'));
|
|
137
|
+
return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
|
|
138
|
+
} catch {
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 写/更新一条注册表(read-modify-merge + 原子写——多变更并发各改各键,merge 防整文件覆盖丢他键;
|
|
145
|
+
* 极小窗口仍可能丢并发写,后果=该变更降级公式寻址,见 design R-02)。
|
|
146
|
+
* @param {string} specBase
|
|
147
|
+
* @param {string} changeName
|
|
148
|
+
* @param {string} repoKey
|
|
149
|
+
* @param {{worktreePath: string, placementRoot: string}} entry
|
|
150
|
+
*/
|
|
151
|
+
export function updatePlacementRegistry(specBase, changeName, repoKey, entry) {
|
|
152
|
+
const registry = readPlacementRegistry(specBase);
|
|
153
|
+
registry[crossWorktreeDirName(changeName, repoKey)] = {
|
|
154
|
+
repoKey,
|
|
155
|
+
changeName,
|
|
156
|
+
worktreePath: entry.worktreePath,
|
|
157
|
+
placementRoot: entry.placementRoot,
|
|
158
|
+
};
|
|
159
|
+
writeAtomicSync(placementRegistryPath(specBase), JSON.stringify(registry, null, 2) + '\n');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 删除一条注册表键(无键幂等 no-op;目录被并发清理后再删同键安全)。
|
|
164
|
+
*/
|
|
165
|
+
export function removePlacementRegistryEntry(specBase, changeName, repoKey) {
|
|
166
|
+
const registry = readPlacementRegistry(specBase);
|
|
167
|
+
const key = crossWorktreeDirName(changeName, repoKey);
|
|
168
|
+
if (!(key in registry)) return;
|
|
169
|
+
delete registry[key];
|
|
170
|
+
writeAtomicSync(placementRegistryPath(specBase), JSON.stringify(registry, null, 2) + '\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 差集 sweep(评审 P2 吸收):本变更名下的全部注册表键(<change>--*),凡 worktreePath 处
|
|
175
|
+
* 无 meta.json 的悬挂键一律删除——覆盖「cleanup 迭代源=listCrossWorktreeMetas 只列有 meta
|
|
176
|
+
* 条目」够不着的残留形态(worktree remove 连目录带 meta 同亡、仅键存)。
|
|
177
|
+
* @returns {string[]} 实际删除的键
|
|
178
|
+
*/
|
|
179
|
+
export function sweepPlacementRegistry(specBase, changeName) {
|
|
180
|
+
const registry = readPlacementRegistry(specBase);
|
|
181
|
+
const prefix = `${changeName}--`;
|
|
182
|
+
const removed = [];
|
|
183
|
+
for (const [key, entry] of Object.entries(registry)) {
|
|
184
|
+
if (!key.startsWith(prefix)) continue;
|
|
185
|
+
const wtPath = entry && typeof entry.worktreePath === 'string' ? entry.worktreePath : '';
|
|
186
|
+
if (!wtPath || !existsSync(join(wtPath, 'meta.json'))) {
|
|
187
|
+
delete registry[key];
|
|
188
|
+
removed.push(key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (removed.length > 0) {
|
|
192
|
+
writeAtomicSync(placementRegistryPath(specBase), JSON.stringify(registry, null, 2) + '\n');
|
|
193
|
+
}
|
|
194
|
+
return removed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 寻址候选有序列表(去重):注册表条目 → 仓内新默认公式(传 repoRoot 时)→ 主仓 specBase
|
|
199
|
+
* 旧默认公式(legacy 存量兜底)。需要「找真实在场位置」的读取方(meta 读取 / reconcile B' 档)
|
|
200
|
+
* 逐候选探测;单地址语义(resolve)取首候选。
|
|
201
|
+
* @returns {string[]}
|
|
202
|
+
*/
|
|
203
|
+
export function resolveCrossWorktreePathCandidates(specBase, changeName, repoKey, repoRoot = null) {
|
|
204
|
+
const out = [];
|
|
205
|
+
const registry = readPlacementRegistry(specBase);
|
|
206
|
+
const entry = registry[crossWorktreeDirName(changeName, repoKey)];
|
|
207
|
+
if (entry && typeof entry.worktreePath === 'string' && entry.worktreePath) {
|
|
208
|
+
out.push(entry.worktreePath);
|
|
209
|
+
}
|
|
210
|
+
if (repoRoot) {
|
|
211
|
+
out.push(crossWorktreePath(specBase, changeName, repoKey, join(repoRoot, '.sillyspec', '.runtime', 'worktrees')));
|
|
212
|
+
}
|
|
213
|
+
out.push(crossWorktreePath(specBase, changeName, repoKey));
|
|
214
|
+
return [...new Set(out.map((p) => resolvePath(p)))];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 寻址:注册表优先(键 <change>--<repoKey>)→ 仓内新默认公式(<repoRoot>/.sillyspec/
|
|
219
|
+
* .runtime/worktrees/<change>--<repoKey>,传 repoRoot 时)→ 主仓 specBase 旧默认公式(legacy
|
|
220
|
+
* 兜底——存量 worktree 仍可达)。
|
|
221
|
+
* 注册表只提供位置——条目指向处是否真有 worktree 由调用方(meta 读取/目录判定)裁决,
|
|
222
|
+
* 本函数不做存在性校验(create 前调用时目录尚不存在是合法态)。
|
|
223
|
+
* @param {string} specBase
|
|
224
|
+
* @param {string} changeName
|
|
225
|
+
* @param {string} repoKey
|
|
226
|
+
* @param {string|null} [repoRoot] 跨仓仓根(知道时传——寻址仓内新默认;缺省跳过该级)
|
|
227
|
+
* @returns {string} worktree 应在/所在路径
|
|
228
|
+
*/
|
|
229
|
+
export function resolveCrossWorktreePath(specBase, changeName, repoKey, repoRoot = null) {
|
|
230
|
+
return resolveCrossWorktreePathCandidates(specBase, changeName, repoKey, repoRoot)[0];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* WSL 跨文件系统分裂判定(坑 cross-wt-toolchain-split,advisory 依据):
|
|
235
|
+
* linux 平台且 repoRoot 匹配 /mnt/<盘>/(WSL automount Windows 盘)而 worktreePath 不匹配
|
|
236
|
+
* → 该仓构建工具链(Windows 侧)与 worktree(WSL 原生 fs)分居。win32/darwin 恒 false 不误报。
|
|
237
|
+
* @param {string} repoRoot 跨仓仓根
|
|
238
|
+
* @param {string} worktreePath worktree 落位路径
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
const WSL_AUTOMOUNT_RE = /^\/mnt\/[A-Za-z](?:\/|$)/;
|
|
242
|
+
export function isWslSplit(repoRoot, worktreePath) {
|
|
243
|
+
return process.platform === 'linux'
|
|
244
|
+
&& WSL_AUTOMOUNT_RE.test(String(repoRoot || ''))
|
|
245
|
+
&& !WSL_AUTOMOUNT_RE.test(String(worktreePath || ''));
|
|
246
|
+
}
|
|
@@ -33,7 +33,7 @@ import { join, isAbsolute, resolve } from 'path'
|
|
|
33
33
|
import { gitQuiet } from './git-helper.js'
|
|
34
34
|
import { parseRepoRegistry } from './stages/plan-postcheck.js'
|
|
35
35
|
import { filterDeliverableFiles, classifyToolScaffold } from './worktree-apply.js'
|
|
36
|
-
import {
|
|
36
|
+
import { resolveCrossWorktreePathCandidates } from './cross-placement.js'
|
|
37
37
|
import { resolveLatestExecuteRunIdWithTasks, readReview } from './task-review.js'
|
|
38
38
|
|
|
39
39
|
// 与 verify-postcheck normalizeReconcilePath 同口径(本地实现防环):剥 ./ 前缀、反斜杠归一
|
|
@@ -235,10 +235,13 @@ export function collectRepoActual({ repoKey, specBase, cwd, runtimeRoot = null,
|
|
|
235
235
|
// 提交,对 worktree 多笔交付是假信号(2026-10-09 tombstone 取证:声明 4 实测 0)。
|
|
236
236
|
// meta 缺失/worktree 不在场/不可读 → 回退 B 档(不回退语义)。
|
|
237
237
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
238
|
+
// 寻址候选逐级探测(注册表 → 仓内新默认 → 主仓 specBase 旧默认)——任一候选 meta 在场即成窗
|
|
239
|
+
let wtPath = null
|
|
240
|
+
for (const cand of resolveCrossWorktreePathCandidates(specBase, changeName, repoKey, repoPath)) {
|
|
241
|
+
if (existsSync(join(cand, 'meta.json'))) { wtPath = cand; break }
|
|
242
|
+
}
|
|
243
|
+
if (wtPath) {
|
|
244
|
+
const meta = JSON.parse(readFileSync(join(wtPath, 'meta.json'), 'utf8'))
|
|
242
245
|
const base = meta && meta.baseHash
|
|
243
246
|
if (base && typeof base === 'string' && /^[0-9a-f]{7,40}$/i.test(base)) {
|
|
244
247
|
const union2 = new Set()
|
|
@@ -1147,7 +1147,7 @@ export function detectSelfMaintenanceTax(cwd, authoritySpecDir) {
|
|
|
1147
1147
|
* 语义:WARNING advisory 不阻断、只读无修复(钩子/门禁挂载是后续独立策略裁决);
|
|
1148
1148
|
* 无 archive 目录 / 无 task 行 → pass(无任务视为完备,同 OpenSpec "nothing to complete")。
|
|
1149
1149
|
*/
|
|
1150
|
-
function detectArchiveIntegrity(cwd, authoritySpecDir) {
|
|
1150
|
+
export function detectArchiveIntegrity(cwd, authoritySpecDir) {
|
|
1151
1151
|
const base = { name: 'archive_integrity', label: '归档完整性重扫', safe_actions: [] };
|
|
1152
1152
|
if (!authoritySpecDir) {
|
|
1153
1153
|
return { ...base, pass: true, severity: null, findings: ['无权威 specDir,跳过'] };
|
|
@@ -1169,6 +1169,26 @@ function detectArchiveIntegrity(cwd, authoritySpecDir) {
|
|
|
1169
1169
|
if (names.length === 0) {
|
|
1170
1170
|
return { ...base, pass: true, severity: null, findings: ['changes/archive 为空(无已归档变更)'], archive_count: 0, offenders: [] };
|
|
1171
1171
|
}
|
|
1172
|
+
// 幽灵复活检测(坑 archived-change-resurrect-by-merge,2026-10-10 实证:远端 merge 把
|
|
1173
|
+
// 已归档变更的旧四件套带回活跃路径,run <stage> 自动注册幽灵行并推上平台):活跃目录与
|
|
1174
|
+
// 归档同名并存 = 高危形态。doctor 只报不治(勿建议 align——会给幽灵补 execute 戳)。
|
|
1175
|
+
const activeChangesDir = join(authoritySpecDir, 'changes');
|
|
1176
|
+
const resurrected = names.filter((name) => existsSync(join(activeChangesDir, name)));
|
|
1177
|
+
if (resurrected.length > 0) {
|
|
1178
|
+
return {
|
|
1179
|
+
...base,
|
|
1180
|
+
pass: false,
|
|
1181
|
+
severity: CHECK_SEVERITY.FAILED,
|
|
1182
|
+
findings: [
|
|
1183
|
+
`活跃目录与归档同名并存(${resurrected.length} 个):${resurrected.join('、')}——疑似 merge 带回的幽灵复活(run 命令已拒绝对其注册)`,
|
|
1184
|
+
'处置(先递归 diff 活跃 vs 归档确认归档侧为超集零信息损失):认归档则 git rm -r 活跃目录提交;确要重开则换新变更名',
|
|
1185
|
+
'勿用 --align-execute-progress(会给幽灵目录补 execute 戳)',
|
|
1186
|
+
],
|
|
1187
|
+
archive_count: names.length,
|
|
1188
|
+
resurrected,
|
|
1189
|
+
offenders: [],
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1172
1192
|
const offenders = [];
|
|
1173
1193
|
// FR 索引面(D14 第四检查数据源,循环外一次扫描复用;fr/ 不存在=仓未启用,全跳不误报)
|
|
1174
1194
|
const frKnowledgeRoot = join(authoritySpecDir, 'knowledge');
|
package/src/flow-parity.js
CHANGED
|
@@ -221,7 +221,7 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
|
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
223
|
* patch 冻结面漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):处置重入审计
|
|
224
|
-
*
|
|
224
|
+
* 时点错位——评审发现处置涉及代码修改后重跑 flow done,patch 子步幂等跳过让 change.patch/
|
|
225
225
|
* review.json 停在处置前时点(2026-10-05-review-promise-negation 先例 905397ef;
|
|
226
226
|
* 2026-10-05-flowdone-lintfail-output 收编活体复现:评审 P1 处置提交后不带 --refreeze 重跑,
|
|
227
227
|
* 归档件缺处置面)。
|
|
@@ -230,23 +230,41 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
|
|
|
230
230
|
* 漂移——按提交 message 变更名归属(parseChangeNamesFromSubject,与 filterCommittedFace
|
|
231
231
|
* 同口径的既成事实切分,非声明抢文件)。仅他侧后缀/裸提交不触发(他侧面归属切分另有防线,
|
|
232
232
|
* 裸提交保守不动作)。检测自身失败按无漂移(退现状幂等跳过行为,fail-safe 不新造阻断面)。
|
|
233
|
+
*
|
|
234
|
+
* 文件面三字段(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
|
|
235
|
+
* ownCommits 只答「有没有漂移」,不答「评审结论该不该作废」——调用方(flow.js 漂移分支)
|
|
236
|
+
* 据文件面区分治理面等价(评审保留)与交付/承诺面(隔离重评):
|
|
237
|
+
* ownFiles 窗口内本变更提交触及的文件集(--name-only 采集,正斜杠归一;merge 提交
|
|
238
|
+
* 无 -m 展开不计文件——漏采只可能让 governanceOnly 偏 false,回落隔离
|
|
239
|
+
* 重评,fail-safe 宁可多评)
|
|
240
|
+
* governanceOnly own 提交全在 .sillyspec/** 下且文件集非空(治理文件等价漂移)
|
|
241
|
+
* touchedPromiseFace 含本变更 requirements.md / design.md(承诺面也是评审对象,破豁免)
|
|
233
242
|
* @param {{ cwd: string, change: string, freezeHead: string|null }} opts
|
|
234
|
-
* @returns {{ drifted: boolean, ownCommits: string[], head: string|null }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
|
|
243
|
+
* @returns {{ drifted: boolean, ownCommits: string[], head: string|null, ownFiles: string[], governanceOnly: boolean, touchedPromiseFace: boolean }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
|
|
235
244
|
*/
|
|
236
245
|
export function detectPatchDrift({ cwd, change, freezeHead }) {
|
|
237
246
|
try {
|
|
238
|
-
if (!freezeHead) return { drifted: false, ownCommits: [], head: null }
|
|
247
|
+
if (!freezeHead) return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
|
|
239
248
|
const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true }).trim()
|
|
240
|
-
if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head }
|
|
241
|
-
const log = execFileSync('git', ['log', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
|
|
249
|
+
if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
|
|
250
|
+
const log = execFileSync('git', ['log', '--name-only', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
|
|
242
251
|
const own = []
|
|
252
|
+
const ownFileSet = new Set()
|
|
243
253
|
for (const block of String(log).split('\x00')) {
|
|
244
|
-
const
|
|
254
|
+
const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
|
|
255
|
+
const subject = lines[0]
|
|
245
256
|
if (!subject) continue
|
|
246
|
-
if (parseChangeNamesFromSubject(subject).includes(change))
|
|
257
|
+
if (parseChangeNamesFromSubject(subject).includes(change)) {
|
|
258
|
+
own.push(subject.slice(0, 80))
|
|
259
|
+
for (const f of lines.slice(1)) ownFileSet.add(f.replace(/\\/g, '/'))
|
|
260
|
+
}
|
|
247
261
|
}
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
const ownFiles = [...ownFileSet]
|
|
263
|
+
const promiseFace = [`.sillyspec/changes/${change}/requirements.md`, `.sillyspec/changes/${change}/design.md`]
|
|
264
|
+
const governanceOnly = own.length > 0 && ownFiles.length > 0 && ownFiles.every((f) => f.startsWith('.sillyspec/'))
|
|
265
|
+
const touchedPromiseFace = ownFiles.some((f) => promiseFace.includes(f))
|
|
266
|
+
return { drifted: own.length > 0, ownCommits: own, head, ownFiles, governanceOnly, touchedPromiseFace }
|
|
267
|
+
} catch { return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false } }
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
/**
|
|
@@ -290,10 +308,16 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
290
308
|
* stats(collectNumstatByPath 产物 Map)缺档落 null 行数(不出伪数据);planEntries 空/缺省
|
|
291
309
|
* → 实际侧 only 行(无 verdict,与主链路计划侧降级同语义)。清单条目未命中冻结面 →
|
|
292
310
|
* untouched 0/0 补行(跨仓条目带 crossRepo 保持 ⊘ 形态,主链路补行同语义)。
|
|
311
|
+
* crossRepoRows(2026-10-10-cross-repo-patch-freeze D-002@v1 可选第 4 参):scope-audit
|
|
312
|
+
* reconcileCrossRepoPlan 的跨仓对账行(真实三态或 degraded ⊘,均带 crossRepo 标记)——
|
|
313
|
+
* 提供时带 repo 的声明条目若已被对账行覆盖(crossRepo+pathMatches 双判)不再落 ⊘ 补行
|
|
314
|
+
* (跨仓实改不谎报「未动」);缺省 undefined → 现行为逐字节零回归(单仓变更/对账失败
|
|
315
|
+
* fail-soft 退路)。
|
|
293
316
|
* @returns {{ rows: Array<object>, totals: { files: number, additions: number, deletions: number } }}
|
|
294
317
|
*/
|
|
295
|
-
export function buildThinSnapshotRows({ ownFiles, stats, planEntries }) {
|
|
318
|
+
export function buildThinSnapshotRows({ ownFiles, stats, planEntries, crossRepoRows }) {
|
|
296
319
|
const plan = Array.isArray(planEntries) ? planEntries : []
|
|
320
|
+
const crossRows = (Array.isArray(crossRepoRows) ? crossRepoRows : []).filter((r) => r && typeof r === 'object')
|
|
297
321
|
const statMap = stats instanceof Map ? stats : new Map()
|
|
298
322
|
const rowFiles = filterDeliverableFiles([...new Set((Array.isArray(ownFiles) ? ownFiles : [])
|
|
299
323
|
.map((f) => String(f).replace(/\\/g, '/')).filter(Boolean))]).sort()
|
|
@@ -324,12 +348,19 @@ export function buildThinSnapshotRows({ ownFiles, stats, planEntries }) {
|
|
|
324
348
|
}
|
|
325
349
|
for (const e of plan) {
|
|
326
350
|
if (!e || matched.has(e.path)) continue
|
|
351
|
+
// 跨仓对账覆盖判定(D-002@v1):crossRepoRows 提供且该声明条目已被对账行覆盖(同 repoKey
|
|
352
|
+
// + pathMatches 双判——对账行是真实三态或 degraded ⊘,覆盖即不再补恒 ⊘ 行);对账行缺失
|
|
353
|
+
// (fail-soft 退路/降级仓无行)→ 照旧补 ⊘ 行
|
|
354
|
+
if (e.repo && crossRows.some((r) => r.crossRepo === e.repo && pathMatches(String(r.path || ''), e.path))) continue
|
|
327
355
|
rows.push({
|
|
328
356
|
path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
|
|
329
357
|
kind: 'modified', verdict: 'untouched',
|
|
330
358
|
...(e.repo ? { crossRepo: e.repo } : {}),
|
|
331
359
|
})
|
|
332
360
|
}
|
|
361
|
+
// 跨仓对账行并入总表(D-002@v1,heavy 通道同款语义——「跨仓行并入后自然计入 totals」):
|
|
362
|
+
// 主仓行在前、跨仓行按仓追加;真实三态行(实 +/- 行数)替换恒 ⊘ 谎报
|
|
363
|
+
rows.push(...crossRows)
|
|
333
364
|
let additions = 0
|
|
334
365
|
let deletions = 0
|
|
335
366
|
for (const r of rows) {
|
package/src/flow-review.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 豁免 ≠ 免责:全信号不命中而豁免的变更按 1/4 定额抽查采样(确定性哈希分桶),采样命中照评
|
|
17
17
|
* 并进遥测——误豁免率可测量可校准(先例:review-dispatch shadow 影子期 N=10 转正判据)。
|
|
18
18
|
*/
|
|
19
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
20
20
|
import { join } from 'node:path'
|
|
21
21
|
import { DESIGN_QUESTIONS as DESIGN_QUESTIONS_FALLBACK } from './flow-draft.js'
|
|
22
22
|
|
|
@@ -203,7 +203,17 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
|
|
|
203
203
|
// (首评任务书与现状逐字一致)。
|
|
204
204
|
let priorFindingsMd = ''
|
|
205
205
|
try {
|
|
206
|
-
|
|
206
|
+
// 前轮产物读取(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop ②):
|
|
207
|
+
// review.json 在场优先;漂移隔离后已改名 review.json.superseded-<ts>——缺席时回退字典序最新
|
|
208
|
+
// 隔离件(时间戳后缀同格式可排序),否则处置重评读不到前轮 findings,「已报项只验修复+
|
|
209
|
+
// 回归」复审语义丢失,退化为全量首评整轮重报(2026-10-10 用户实证)。changeDir 不存在/
|
|
210
|
+
// 无隔离件 → readFileSync/readdirSync 抛错走外层 catch,首评形态零注入不变。
|
|
211
|
+
let prevPath = join(changeDir, 'review.json')
|
|
212
|
+
if (!existsSync(prevPath)) {
|
|
213
|
+
const supers = readdirSync(changeDir).filter((f) => /^review\.json\.superseded-/.test(f)).sort()
|
|
214
|
+
if (supers.length > 0) prevPath = join(changeDir, supers[supers.length - 1])
|
|
215
|
+
}
|
|
216
|
+
const prev = JSON.parse(readFileSync(prevPath, 'utf8'))
|
|
207
217
|
if (Array.isArray(prev.findings) && prev.findings.length > 0) {
|
|
208
218
|
priorFindingsMd = [
|
|
209
219
|
``,
|
package/src/flow.js
CHANGED
|
@@ -1272,6 +1272,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1272
1272
|
} catch { /* 锚定判定失败按未锚定处理 */ }
|
|
1273
1273
|
if (anchoredToHead) {
|
|
1274
1274
|
console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(drift.head || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
|
|
1275
|
+
} else if (drift.governanceOnly && !drift.touchedPromiseFace) {
|
|
1276
|
+
// 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
|
|
1277
|
+
// 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
|
|
1278
|
+
// 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
|
|
1279
|
+
// 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结吸收治理面增量
|
|
1280
|
+
// (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
|
|
1281
|
+
// P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
|
|
1282
|
+
console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结吸收治理面增量`)
|
|
1275
1283
|
} else {
|
|
1276
1284
|
const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
|
|
1277
1285
|
try {
|
|
@@ -1478,10 +1486,34 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1478
1486
|
let planEntries = []
|
|
1479
1487
|
try {
|
|
1480
1488
|
const { parseFileChangeListDetailed } = await import('./change-list.js')
|
|
1481
|
-
|
|
1489
|
+
// repoKeys(2026-10-10-cross-repo-patch-freeze D-002@v1):注册仓清单传入——跨仓
|
|
1490
|
+
// 子段/cross-repo: 前缀条目才带 repo 标注(不传则跨仓声明全落主仓口径, thin 跨仓
|
|
1491
|
+
// 对账无从谈起)
|
|
1492
|
+
const { loadRegisteredRepoKeys } = await import('./scope-audit.js')
|
|
1493
|
+
planEntries = parseFileChangeListDetailed(join(changeDir, 'design.md'), { keepSillyspecDocs: true, repoKeys: loadRegisteredRepoKeys(specBase) })
|
|
1482
1494
|
} catch { /* 清单缺失/不可解析 → 实际侧 only 快照行(无 verdict,与主链路降级同语义) */ }
|
|
1495
|
+
// 跨仓声明在场 → 共享集成层分仓对账 + patch 冻结(D-001/D-002@v1):与 heavy 通道
|
|
1496
|
+
// (execute --done)同源 reconcileCrossRepoPlan,repos[](锚点/计数/patch 正文)
|
|
1497
|
+
// 随 snapObj 进 change-patch.json;任何异常 fail-soft 退 ⊘ 补行现行为(不阻断收口)
|
|
1498
|
+
let crossRepos = []
|
|
1499
|
+
let crossRepoRows = undefined
|
|
1500
|
+
if (planEntries.some((e) => e && e.repo)) {
|
|
1501
|
+
try {
|
|
1502
|
+
const { reconcileCrossRepoPlan } = await import('./scope-audit.js')
|
|
1503
|
+
const recon = reconcileCrossRepoPlan({
|
|
1504
|
+
cwd, specBase,
|
|
1505
|
+
runtimeRoot: resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase),
|
|
1506
|
+
changeName: change,
|
|
1507
|
+
planEntries,
|
|
1508
|
+
collectPatch: true,
|
|
1509
|
+
})
|
|
1510
|
+
crossRepoRows = recon.rows
|
|
1511
|
+
crossRepos = recon.repos
|
|
1512
|
+
for (const n of recon.notes) console.log(` 📎 ${n}`)
|
|
1513
|
+
} catch { /* 跨仓对账失败 → 退 ⊘ 补行(crossRepoRows 保持 undefined) */ }
|
|
1514
|
+
}
|
|
1483
1515
|
const { buildThinSnapshotRows, writeCloseTraceArtifacts } = await import('./flow-parity.js')
|
|
1484
|
-
const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles, stats, planEntries })
|
|
1516
|
+
const { rows: snapRows, totals: snapTotals } = buildThinSnapshotRows({ ownFiles, stats, planEntries, crossRepoRows })
|
|
1485
1517
|
const closeTrace = writeCloseTraceArtifacts({
|
|
1486
1518
|
changeDir,
|
|
1487
1519
|
change,
|
|
@@ -1499,6 +1531,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1499
1531
|
excluded: { foreignDeclared: [] },
|
|
1500
1532
|
note: 'flow done 时点冻结(本文件落盘时采集)',
|
|
1501
1533
|
closedBy: 'flow done',
|
|
1534
|
+
...(crossRepos.length > 0 ? { repos: crossRepos } : {}),
|
|
1502
1535
|
},
|
|
1503
1536
|
meta: {
|
|
1504
1537
|
note: 'flow done 时点冻结(本变更可归属面:baseline..HEAD 提交面过滤 .sillyspec/ 非本变更目录但保留 .sillyspec/docs/ 交付文档 + 本变更目录工作树件;含未提交与 untracked,排除 flow-state.yaml 运行态)',
|
|
@@ -1511,6 +1544,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1511
1544
|
},
|
|
1512
1545
|
})
|
|
1513
1546
|
console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
|
|
1547
|
+
if (crossRepos.length > 0) {
|
|
1548
|
+
const frozenRepos = crossRepos.filter((r) => r && r.patch).length
|
|
1549
|
+
const degradedRepos = crossRepos.filter((r) => r && r.degraded).length
|
|
1550
|
+
console.log(` 📦 跨仓 patch 冻结:${crossRepos.length} 仓(正文 ${frozenRepos}、降级 ${degradedRepos})——锚点/计数/diff 正文见 change-patch.json scopeAudit.repos[]`)
|
|
1551
|
+
}
|
|
1514
1552
|
// design 声明面自证(2026-09-25-platform-feedback-batch2 E):design.md 文件变更清单
|
|
1515
1553
|
// 声明的交付文件是否都在冻结面——不在=承诺改了但没交付(承诺未兑现面,advisory 不
|
|
1516
1554
|
// 阻断——可能是范围裁剪了但 design 没同步更新)
|
|
@@ -1598,6 +1636,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
|
|
|
1598
1636
|
if (others.length > 0) {
|
|
1599
1637
|
console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
|
|
1600
1638
|
for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
|
|
1639
|
+
// 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
|
|
1640
|
+
// P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
|
|
1641
|
+
// requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
|
|
1642
|
+
// 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
|
|
1643
|
+
console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
|
|
1601
1644
|
}
|
|
1602
1645
|
console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
|
|
1603
1646
|
reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
|