sillyspec 3.32.4 → 3.32.6
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 +2 -3
- package/.claude/skills/sillyspec-explore/SKILL.md +2 -0
- package/.claude/skills/sillyspec-quick/SKILL.md +116 -0
- package/package.json +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 +14 -1
- package/src/flow-review.js +11 -2
- package/src/flow.js +52 -12
- package/src/hooks/worktree-guard.js +35 -2
- package/src/index.js +22 -3
- package/src/machine-interface.js +10 -5
- package/src/run/command.js +19 -0
- package/src/run/multi-repo-context.js +11 -5
- package/src/run/shared.js +10 -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
|
@@ -67,12 +67,12 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
|
|
|
67
67
|
```
|
|
68
68
|
┌─ scale=large → plan(四件套齐)
|
|
69
69
|
scan → brainstorm ┤
|
|
70
|
-
└─ scale=small →
|
|
70
|
+
└─ scale=small → quick --linked-changes(仅 design.md)
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
|
|
74
74
|
- **large**(多文件/跨模块/有状态机或 schema 变更):四件套齐 + Design Grill 审查通过(tier=independent 时由独立审查子代理产出 stage review.json)→ `sillyspec run plan --change <变更名>`
|
|
75
|
-
- **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec
|
|
75
|
+
- **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec run quick --linked-changes <变更名>`
|
|
76
76
|
|
|
77
77
|
> 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
|
|
78
78
|
|
|
@@ -108,7 +108,6 @@ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还
|
|
|
108
108
|
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
109
109
|
- 只做当前步骤 prompt 描述的操作,不跳过、不自行扩展
|
|
110
110
|
- 产物写入 CLI 输出的 `changeDir` 目录(如 `<changeDir>/design.md`),不要自己拼路径
|
|
111
|
-
- **变更标题(面板显示用):总结一句中文概括,≤50 字、建议 ~20 字**——CLI 从 proposal.md / design.md 首行 H1 提取简述写入 changes.title(剥「提案书(Proposal)— / 设计文档(Design)—」固定前缀后取段),design.md 固定格式 `# 设计文档(Design)— <简述>` 中的简述即面板标题,按此口径写
|
|
112
111
|
- 完成后立即 `--done`,不跳过
|
|
113
112
|
|
|
114
113
|
## 用户指令
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sillyspec:quick
|
|
3
|
+
description: 用于明确、低风险、范围很小的直接任务。适合用户说"直接改、快速修、顺手调整、改个文案、修个小 bug、更新一个文件、不要完整流程"。跳过 brainstorm/plan,但仍按 sillyspec quick 流程执行。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 何时使用
|
|
7
|
+
|
|
8
|
+
- 明确、低风险、范围小的直接修改:改文案、修小 bug、更新单个文件
|
|
9
|
+
- 用户说"直接改、快速修、顺手调整、不要完整流程"
|
|
10
|
+
- brainstorm 判定 `scale=small` 的变更:带 design.md 进 quick(`sillyspec run quick --linked-changes <变更名>`),design 当背景
|
|
11
|
+
- 跳过 brainstorm/plan,但仍走 quick 的 3 步流程(理解任务 → 实现 → 自检提交)
|
|
12
|
+
|
|
13
|
+
## 多变更说明(quick 特殊,务必注意)
|
|
14
|
+
|
|
15
|
+
quick 阶段的 `--change` 语义是「关联变更」**且会触发步骤重置**,**不要用 `--change` 来指定关联变更**。多活跃变更时改用:
|
|
16
|
+
|
|
17
|
+
- `--linked-changes none`:不关联,仅记 QUICKLOG(CLI 启动时写入)
|
|
18
|
+
- `--linked-changes a,b`:显式关联到变更 a、b
|
|
19
|
+
- `--non-interactive`:CI/脚本环境,默认不关联(避免交互 prompt 崩溃)
|
|
20
|
+
|
|
21
|
+
首次 `sillyspec run quick` 选定的关联会持久化到 `.runtime/quick-sessions/<quick-session-id>/guard.json`(每个 quick 会话独立,多会话并发不互覆盖),后续 `--done` 自动复用,**不会重复弹交互 prompt**。
|
|
22
|
+
|
|
23
|
+
## 步骤生命周期(所有阶段通用)
|
|
24
|
+
|
|
25
|
+
> `sillyspec quick` 是 `sillyspec run quick` 的顶层别名,两者等价。
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
sillyspec run quick # 输出当前步骤 prompt(首次会记录 baseline)
|
|
29
|
+
sillyspec run quick --done --output "摘要" # 完成当前步骤
|
|
30
|
+
sillyspec run quick --status # 查看阶段进度
|
|
31
|
+
sillyspec run quick --skip # 跳过可选步骤
|
|
32
|
+
sillyspec run quick --reset # 重置阶段(从头开始)
|
|
33
|
+
sillyspec run quick --reopen --from-step N # 重新打开已完成阶段修订(N=序号或名称)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 已有进行中会话的恢复(sessionId)
|
|
37
|
+
|
|
38
|
+
`sillyspec run quick` / `run quick --done` / `--status` **不带 `--change` 时,是启动一个新 quick 会话**(CLI 分配新 `quick-<hash>` + 新 ql-ID,QUICKLOG 追加「进行中」条目),不是恢复正在进行的旧会话。多会话并发或中断恢复时,必须带上启动时 CLI 打印的 sessionId:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
sillyspec run quick --change quick-<hash> # 恢复查看该会话当前 step prompt
|
|
42
|
+
sillyspec run quick --done --change quick-<hash> --output "…" # 完成该会话步骤
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- 这里的 `--change quick-<hash>` 是「恢复到该 CLI 生成的 session」,与下方 `--linked-changes` 的关联业务变更语义不同:想关联真实业务变更才用 `--linked-changes`。
|
|
46
|
+
- 误启动的空壳会话:`sillyspec run quick --cancel --change quick-<hash>` 一键取消(QUICKLOG 条目翻「已取消」+ tasks.md 挂载行移除 + 会话目录清理 + db 行注销;已完成/已勾选的拒绝取消)。
|
|
47
|
+
|
|
48
|
+
## 通用参数(所有阶段适用)
|
|
49
|
+
|
|
50
|
+
| 参数 | 说明 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `--spec-dir <path>` | 指定规范目录(默认 `<项目>/.sillyspec`) |
|
|
53
|
+
| `--input "<一句话任务描述>"` | 通用参数,**quick 启动时强烈建议带**:作为 QUICKLOG 条目标题,条目从第一分钟即语义可读。不带则落「(quick 任务)」占位标题——平台「快速修复」列表默认隐藏进行中的占位条目,语义标题要到最终 `--done` 才回填(关联变更有 proposal/design 标题时可自动提取,免传) |
|
|
54
|
+
| `--non-interactive` | CI/脚本下禁用交互式 prompt |
|
|
55
|
+
| `--interactive` | 强制交互(即便 stdin 非 TTY) |
|
|
56
|
+
| `--skip-approval` | 跳过阶段转换/审批检查(不能跳产物校验 gate——review.json/文档产物硬校验仍在) |
|
|
57
|
+
|
|
58
|
+
## quick 特有参数
|
|
59
|
+
|
|
60
|
+
| 参数 | 说明 |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `--linked-changes none\|a,b` | **显式关联变更(取代 `--change`,推荐)**。none=不关联,a,b=关联列表 |
|
|
63
|
+
| `--files a.js,b.js` | 显式声明本次允许修改的文件(边界保护 + 声明即归属:多 agent 并发仓防他者窗口文件混入 QUICKLOG 文件行,未声明窗口文件进「审计:」行追溯;**他向亦然**——其他 active 会话已声明的文件在你的 `--done` 审计中自动剔除归该会话,不再误拦、无需 `--force-baseline`)。**中途可追加**:发现要改声明外文件(如新写的测试)时,带新增文件恢复会话即并入边界(追加不替换)——`sillyspec run quick --files <新增> --change <sessionId>`;不带 `--files` 恢复则边界原样保留 |
|
|
64
|
+
| `--file-notes "p::注 \|\| p::注"` | quick `--done` 用:QUICKLOG「文件:」行落盘为多行带括注 bullet(省事后手改文件行)。格式 `path::括注`,`\|\|` 分隔多条;**只随 step3 --done 同命令传**(CLI 短进程,step1/step2 传无效,不带到 step3) |
|
|
65
|
+
| `--allow-new` | 允许新增文件(默认禁止,防意外创建) |
|
|
66
|
+
| `--allow-delete` | 允许删除文件(默认 fail-closed,删除是破坏性操作;确认删除带此 flag 显式解锁) |
|
|
67
|
+
| `--force-baseline` | 允许覆盖 baseline 受保护文件 / 压制 `.sillyspec/` 危险判定(危险,慎用) |
|
|
68
|
+
| `--confirm` | ⚠️ 仅打印变更概览,**不解锁 blocked**(blocked 仍 exit 1)。真正解锁用 `--force-baseline`/`--allow-new`/`--allow-delete` |
|
|
69
|
+
|
|
70
|
+
## 审计与并发变更(`--done` 边界审计)
|
|
71
|
+
|
|
72
|
+
`--done` 收尾时对比 step1 baseline 与当前 `git status`,拦「危险文件 / 新增文件 / 覆盖 baseline」。要点:
|
|
73
|
+
|
|
74
|
+
- **并发其他会话的 `.sillyspec/changes/<非关联变更>/` 不再被本 quick 拦截**。quick 自己没有 `changes/` 目录,该路径下非关联内容视为并发会话的工作,整体放行(确定性审计无法区分「并发工作」与「本 quick 偷建变更」,后者这类意图软判定留给 sillyhub)。
|
|
75
|
+
- **其他 active 会话已声明的文件自动豁免(`--files` 的他向语义)**。多 agent 并发时,并行 quick 会话 `--files` 显式声明的文件(如它的 `daemon/router.py`)出现在你的窗口内(它在你启动后改的、不在你的 baseline)——`--done` 审计自动剔除归该会话边界管,不再判危险 BLOCK、无需 `--force-baseline`。放行可见:输出 🔗 软警告逐文件列归属会话;若确系你的改动,恢复会话时 `--files` 追加声明;超 7 天未收尾的僵尸会话声明自动失效(`sillyspec run quick --cancel --change <他者会话ID>` 清理)。豁免只认他者**显式声明**——未声明的他者文件照旧走各门判定(漏声明的补 `--files`,危险文件仍需 `--force-baseline`)。
|
|
76
|
+
- **关联变更的文件仍走审计**:reverse-sync 改自己关联变更的 `design.md` 会被拦,需 `--force-baseline` 显式确认。
|
|
77
|
+
- **baseline 折叠目录前缀匹配**:step1 启动时整片 `changes/` 未跟踪会被 git 折叠成 `?? .sillyspec/changes/`(带尾斜杠 token);审计时若该目录下文件被并发会话跟踪而展开成文件级路径,按尾斜杠 token 前缀放行其下所有文件,不误判。
|
|
78
|
+
- **同文件并发 warn(advisory,不阻断)**:step1 启动时若你的某个 `allowedFile` 已在他者脏文件列表里(他者也改了这文件),--done 时 CLI 会比对当前内容 hash 与启动时录入的 hash——不一致(你也改了它)即判「同文件并发」并 warn:整文件 pathspec 提交会夹带他者 hunk。warn 给出分离指引(`git add -p <file>` 交互选你自己的 hunk,或 `git diff <file> > mine.patch` 编辑后 `git apply --cached mine.patch` 再 commit)。这**不阻断** --done(你可能有意整文件提交),看 warn 后自行决定是否分离。
|
|
79
|
+
- **`--confirm` ≠ 解锁**:它只打印概览。blocked 时真正解锁的是 `--force-baseline`(压制 `.sillyspec/` 危险判定 + baseline 覆盖)/`--allow-new`(放行新增)/`--allow-delete`(放行删除,删除是破坏性操作默认 fail-closed,须显式 opt-in),三者可在 step1 或 `--done` 传,都生效。
|
|
80
|
+
|
|
81
|
+
## 典型用法
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 推荐启动:带一句话语义标题(QUICKLOG/平台快速修复列表进行中即可见可读)
|
|
85
|
+
sillyspec run quick --input "修复登录限流 INCR 计数误清" --linked-changes none --files src/auth.ts
|
|
86
|
+
|
|
87
|
+
# 单变更项目,直接开始
|
|
88
|
+
sillyspec run quick
|
|
89
|
+
|
|
90
|
+
# 多变更项目,显式不关联
|
|
91
|
+
sillyspec run quick --linked-changes none
|
|
92
|
+
|
|
93
|
+
# 多变更项目,关联到指定变更
|
|
94
|
+
sillyspec run quick --linked-changes 2026-07-03-add-login
|
|
95
|
+
|
|
96
|
+
# CI/脚本(非交互,避免 prompt 崩溃)
|
|
97
|
+
sillyspec run quick --non-interactive
|
|
98
|
+
sillyspec run quick --done --linked-changes none --output "修复手机号校验"
|
|
99
|
+
|
|
100
|
+
# 限定修改文件范围
|
|
101
|
+
sillyspec run quick --files src/phone.ts,src/phone.test.ts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 铁律
|
|
105
|
+
|
|
106
|
+
- **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
|
|
107
|
+
- quick 直接在主工作区改代码(不创建 worktree),范围必须小且明确
|
|
108
|
+
- 完成后立即 `--done`,不跳过
|
|
109
|
+
- QUICKLOG 记录的**骨架由 CLI 接管**:启动时 CLI 自动分配 ql-ID 并在 `.sillyspec/quicklog/QUICKLOG-<user>.md` 写「进行中」条目(含关联变更 tasks.md),`--done` 时 CLI 自动翻「已完成」+ 勾选 task + 回填文件路径。ql-ID 分配/状态/task 你无需手写,只需用注入的 `<quicklog-id>` 在模块文档变更索引引用
|
|
110
|
+
- **QUICKLOG 落盘已结构化(`--done` 后按需核对,多数无需手改)**:CLI 已落盘结构化条目——标题从 `--output` 的「需求:」自动提取、正文四字段自动分行、文件行用 `--file-notes` 时为多行带括注。`--done` 后只需核对:标题弱才改(禁留 `(quick 任务)` 占位);没用 `--file-notes` 时文件行是单行、可事后补括注(参照同文件早期丰富条目);正文 `需求:`/`根因:`/`方案:`/`结果:` 四段按需充实(禁只留一段「结果:」)。一条 quick = 一条独立 ql,不追加到旧条目
|
|
111
|
+
- **收尾顺序(模块文档在 `--done` 前,QUICKLOG 在 `--done` 后,别记混)**:① 命中模块→改模块文档→`git add`;② `sillyspec run quick --done --change <id> --output "四字段" [--file-notes "..."]`(CLI 自动翻完成 + 勾 task + 落盘 QUICKLOG 标题/文件/正文;若 `--linked-changes` 关联的真实变更其 tasks.md 已全部勾选**且该变更未进入完整流程**(进度库阶段停在 brainstorm 及之前),CLI 会自动将其归档到 `changes/archive/`;已走到 plan/execute/verify/archive 的变更不自动归档(tasks.md 全勾不等于流程收尾),须走原流程收尾);③ 核对 QUICKLOG 标题(CLI 已从「需求:」提取并截断,写成短标题则无需改)→若改了再 `git add`
|
|
112
|
+
- **最后一步 `--done` 必须给全四字段**(逐项一句话):🔧 **推荐四参数形式**(CLI 自动合成结构化模板,无格式事故面):`--done --change <id> --req "一句语义化短标题" --cause "为什么改" --solution "怎么改的" --result "验证结果"`。兼容旧形式 `--output "需求:… 根因:… 方案:… 结果:…"`(手拼须懂截断规则、禁嵌套全角冒号,缺字段被拒 exit 1)。这是 QUICKLOG「结果:」归档的唯一来源。**`--req` 写一句语义化短标题**(写「改了什么」,如「登录限流修复——INCR 计数误清」)——CLI 把它提取为 QUICKLOG 条目标题,截到首个标点、超 80 字截断;写完整需求长句会被截成语义不完整的状语前半段,需求背景放 `--cause`/`--solution`。前两个 step 的 `--output` 是中间摘要,不用此模板
|
|
113
|
+
- **禁止**在没有运行 CLI 的情况下自行决定流程
|
|
114
|
+
|
|
115
|
+
## 用户指令
|
|
116
|
+
$ARGUMENTS
|
package/package.json
CHANGED
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
|
@@ -308,10 +308,16 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
|
|
|
308
308
|
* stats(collectNumstatByPath 产物 Map)缺档落 null 行数(不出伪数据);planEntries 空/缺省
|
|
309
309
|
* → 实际侧 only 行(无 verdict,与主链路计划侧降级同语义)。清单条目未命中冻结面 →
|
|
310
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 退路)。
|
|
311
316
|
* @returns {{ rows: Array<object>, totals: { files: number, additions: number, deletions: number } }}
|
|
312
317
|
*/
|
|
313
|
-
export function buildThinSnapshotRows({ ownFiles, stats, planEntries }) {
|
|
318
|
+
export function buildThinSnapshotRows({ ownFiles, stats, planEntries, crossRepoRows }) {
|
|
314
319
|
const plan = Array.isArray(planEntries) ? planEntries : []
|
|
320
|
+
const crossRows = (Array.isArray(crossRepoRows) ? crossRepoRows : []).filter((r) => r && typeof r === 'object')
|
|
315
321
|
const statMap = stats instanceof Map ? stats : new Map()
|
|
316
322
|
const rowFiles = filterDeliverableFiles([...new Set((Array.isArray(ownFiles) ? ownFiles : [])
|
|
317
323
|
.map((f) => String(f).replace(/\\/g, '/')).filter(Boolean))]).sort()
|
|
@@ -342,12 +348,19 @@ export function buildThinSnapshotRows({ ownFiles, stats, planEntries }) {
|
|
|
342
348
|
}
|
|
343
349
|
for (const e of plan) {
|
|
344
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
|
|
345
355
|
rows.push({
|
|
346
356
|
path: e.path, planned: e.operation || null, additions: 0, deletions: 0,
|
|
347
357
|
kind: 'modified', verdict: 'untouched',
|
|
348
358
|
...(e.repo ? { crossRepo: e.repo } : {}),
|
|
349
359
|
})
|
|
350
360
|
}
|
|
361
|
+
// 跨仓对账行并入总表(D-002@v1,heavy 通道同款语义——「跨仓行并入后自然计入 totals」):
|
|
362
|
+
// 主仓行在前、跨仓行按仓追加;真实三态行(实 +/- 行数)替换恒 ⊘ 谎报
|
|
363
|
+
rows.push(...crossRows)
|
|
351
364
|
let additions = 0
|
|
352
365
|
let deletions = 0
|
|
353
366
|
for (const r of rows) {
|
package/src/flow-review.js
CHANGED
|
@@ -27,6 +27,15 @@ const PROMISE_RE = /at[- ]least[- ]once|exactly[- ]once|不丢失|不重复|不
|
|
|
27
27
|
/** adopted(头脑风暴预段)design 无槽时的机制词全文扫描(窄表防误报)。 */
|
|
28
28
|
const MECHANISM_RE = /乱序|并发写|竞态|死锁|事务隔离|AbortSignal|多实例串|游标推进/
|
|
29
29
|
|
|
30
|
+
/** 盲维四问「自述不适用」整声明变体(2026-10-12-review-na-variant):两仓实测 12/228 实质
|
|
31
|
+
* 作答误升级——整体不适用声明写成「四问全 n/a:」「均不适用」「边界并发四问:不适用:」等
|
|
32
|
+
* 前缀而非「不适用」开头,被 /^不适用/ 机械判成实质作答(误伤样例:纯 CSS 变更 hover-polish
|
|
33
|
+
* 起子代理评审)。窄口径只认整声明开头:可选前缀 + 可选冒号 + 可选限定词 + 不适用|n/a;
|
|
34
|
+
* 逐问编号形态(「1. 乱序:n/a。…」)不聚合——该形态常含逐维实质推理,升级是设计内行为。
|
|
35
|
+
* 前缀声明语义与旧 /^不适用/ 同型(「不适用,但…」旧版本就按开头豁免),加宽词表不加新
|
|
36
|
+
* 语义维度;漏报兜底:豁免面 1/4 抽查采样 + 承诺词/diff 原语/edit_ratio 三路独立信号。 */
|
|
37
|
+
const NA_ANSWER_RE = /^(?:盲维四问|边界并发四问|四问)?\s*[::]?\s*(?:全部|均|皆|都|全)?\s*(?:不适用|n\/a\b)/i
|
|
38
|
+
|
|
30
39
|
/** design 作答面否定语境消解(2026-10-05-review-promise-negation):盲维第 4 问作答讨论
|
|
31
40
|
* 作用域风险时,「无串台面」「不会串台」「杜绝串台」等否定短语按字面命中 PROMISE_RE 一票
|
|
32
41
|
* 升级(实测:两行 console.log 的变更被定档高危起子代理评审)。「串台」是名词性风险词——
|
|
@@ -151,13 +160,13 @@ export function classifyReviewNeed({ changeDir, input = '', patchText = null, ed
|
|
|
151
160
|
if (/<!--\s*AGENT:槽3/.test(dText)) {
|
|
152
161
|
const answer = readSlotAnswer(dText, /^<!--\s*AGENT:槽3/)
|
|
153
162
|
if (!answer) exemptEvidence.push('盲维四问未作答(将由槽位门拦收)')
|
|
154
|
-
else if (
|
|
163
|
+
else if (NA_ANSWER_RE.test(answer)) exemptEvidence.push('盲维四问自述不适用')
|
|
155
164
|
else reasons.push('盲维四问有实质作答(存在时序/并发/切换/作用域风险面)')
|
|
156
165
|
} else if (/^##\s*边界与并发/m.test(dText)) {
|
|
157
166
|
// v2 纯 markdown(2026-10-04-thin-docs-v2):边界节正文作答判定(同 v1 三态语义)
|
|
158
167
|
const answer = readV2SectionAnswer(dText, DESIGN_QUESTIONS_FALLBACK.sections[2].heading, DESIGN_QUESTIONS_FALLBACK)
|
|
159
168
|
if (!answer) exemptEvidence.push('盲维四问未作答(将由 v2 工件校验拦收)')
|
|
160
|
-
else if (
|
|
169
|
+
else if (NA_ANSWER_RE.test(answer)) exemptEvidence.push('盲维四问自述不适用')
|
|
161
170
|
else reasons.push('盲维四问有实质作答(存在时序/并发/切换/作用域风险面)')
|
|
162
171
|
} else if (MECHANISM_RE.test(dText)) {
|
|
163
172
|
reasons.push('头脑风暴设计含并发/时序类机制词(adopted 扫描)')
|