sillyspec 3.28.8 → 3.28.10
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/full.json +1 -0
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +62 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +89 -20
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +206 -18
- package/src/knowledge-classify.js +382 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/probe7-anchor-check.js +69 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +164 -18
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +131 -13
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +123 -23
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +141 -16
- package/src/run/prompt.js +221 -11
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +147 -11
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +456 -56
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +76 -6
- package/src/stages/plan-postcheck.js +32 -5
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +170 -6
- package/src/sync.js +141 -9
- package/src/task-review.js +366 -46
- package/src/verify-postcheck.js +212 -55
- package/src/verify-probes.js +727 -16
- package/src/worktree-apply.js +685 -48
- package/src/worktree-deps.js +124 -3
- package/src/worktree.js +391 -15
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">SillySpec</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center">规范驱动开发工具包 ·
|
|
7
|
+
<p align="center">规范驱动开发工具包 · 多 agent 时代的变更账本与确定性验收层</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
融合 Superpowers + OpenSpec + GSD,从「你说要啥」到「代码能跑」的完整流程<br>
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
|
|
26
26
|
AI agent(Claude Code / Cursor 等)直接上手编码时,容易跳过需求澄清、方案设计、任务拆解这些关键步骤,产出偏离预期、且无法审计。
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
更要命的是**多 agent 并行**:多个会话同时改一个仓库时,谁改了哪个文件分不清、任务编号会被抢注、测试结果靠自报、中断即失忆、跨仓改动误伤用户在途文件。
|
|
29
|
+
|
|
30
|
+
SillySpec 把一个变更的完整生命周期固化为一组**强制阶段**——每个阶段都有明确的入口契约、产物文件名和门禁校验,AI 必须按状态机推进,不能跳步、不能偷工。进度、决策、产物全部持久化到 SQLite,可审计、可回溯、可断点恢复。**纪律不靠模型自觉,靠进程、磁盘和门禁:自报的不算,CLI 亲自跑过的才算。**
|
|
29
31
|
|
|
30
32
|
## 快速开始
|
|
31
33
|
|
|
@@ -133,16 +135,38 @@ sillyspec change-delete <变更名> 删除变更(DB status=deleted 与归
|
|
|
133
135
|
|
|
134
136
|
## 核心特性
|
|
135
137
|
|
|
136
|
-
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
> 完整能力地图与对外叙事基准:[docs/sillyspec/capability-highlights-2026-09-14.md](docs/sillyspec/capability-highlights-2026-09-14.md)
|
|
139
|
+
|
|
140
|
+
**第一层:护城河(外部状态,模型能力不可替代)**
|
|
141
|
+
|
|
142
|
+
- **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ID 分配 + 双占用 fail-closed 硬拦;`--files` 边界声明切分"这是谁改的";`--done` 前并发写预检告警他者未提交改动;平台同步乐观锁(base_ts + 身份 + 服务器权威钟)+ 冲突血统归属(自回声不误报)
|
|
143
|
+
- **实测验收门禁** — verify / quick 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
|
|
144
|
+
- **持久账本 + 断点恢复** — SQLite(node:sqlite)单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账可检索
|
|
145
|
+
- **跨仓变更管理** — task 卡声明 `repo:` 字段即可管理跨仓任务:同构 worktree 隔离、base 锚快照、apply 永不覆盖用户在途改动
|
|
146
|
+
|
|
147
|
+
**第二层:确定性对账(机械核验,零模型轮次)**
|
|
148
|
+
|
|
149
|
+
- **范围对账** — 计划改动 × 实际改动三态对账(planned / unplanned / untouched),"偷偷改了计划外的东西"会被告警
|
|
150
|
+
- **API 契约矩阵** — 自动识别 task 间 provider / consumer 关系,后端完成提取 endpoint 契约注入前端 task,verify 阶段前后端 API parity 对账
|
|
151
|
+
- **文档一致性棘轮** — 文档里源码引用真实性校验(HEAD 模式),失败数只降不升
|
|
152
|
+
- **noAI 下沉** — 机械轮次(骨架预生成 / 上下文注入 / 归档核对代算)下沉为 CLI 代码,判断层留给 agent,token 成本与模型能力同向优化
|
|
153
|
+
|
|
154
|
+
**第三层:协作与流程**
|
|
155
|
+
|
|
156
|
+
- **规范驱动 + 阶段状态机** — 入口契约 / 产物文件名 / 门禁校验强制流转,不能跳步
|
|
157
|
+
- **子代理并行 + Wave 拓扑排序** — depends_on 自动排序,同 Wave 并行执行
|
|
140
158
|
- **Worktree 隔离** — execute 在独立 git worktree 中工作,不污染主分支
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
159
|
+
- **分层独立审查** — Design Grill / stage review / task review 带 provenance 盖章
|
|
160
|
+
- **跨机派发** — 子代理任务可派到 SillyHub MCP 远端 worker 池,自动探测回退本机
|
|
161
|
+
- **平台同步(可选)** — 对接 SillyHub:文档同步 + 团队审批 + 本地 agent 会话日志路径主动上报(支持 Claude Code / Codex / ZCode 自动探测,其他 CLI 用 `SILLYSPEC_AGENT_LOG` 显式指定);本地独立使用功能完整
|
|
162
|
+
- **棕地入库** — scan 扫描已有代码库生成 7 份架构文档 + 模块映射
|
|
163
|
+
- **知识飞轮** — 模块知识按需注入、成功变更 export 为模板、决策 D-xxx@vN 版本化
|
|
164
|
+
|
|
165
|
+
**第四层:自我运维**
|
|
166
|
+
|
|
167
|
+
- **doctor 自诊自愈** — 13 维诊断,幽灵 worktree / 僵尸会话自动归档
|
|
168
|
+
- **摩擦计数** — 门禁回滚 / 实测失败 / 审查打回结构化计数,喂改进循环
|
|
169
|
+
- **dashboard 可视化** — 本地 Web 进度面板(`packages/dashboard/`)
|
|
146
170
|
|
|
147
171
|
## 可靠性保障
|
|
148
172
|
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sillyspec
|
|
3
|
-
description: "
|
|
3
|
+
description: "多 agent 仓库的变更账本 + 确定性验收层,规范驱动开发工具包。绿地用 /sillyspec:init,棕地用 /sillyspec:scan,全自动用 /sillyspec:auto。完整流程:scan → brainstorm → plan → execute → verify → archive。核心能力:CLI 实测验收门禁(自报不算、亲测才算)、多 agent 并发控制(会话/worktree/跨仓隔离 + 竞态硬门)、SQLite 持久账本断点恢复、范围对账与 API 契约矩阵。兼容 Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SillySpec
|
|
@@ -73,15 +73,29 @@ sillyspec init 初始化(零交互,自动检测工具)
|
|
|
73
73
|
|
|
74
74
|
## 核心特性
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
- **
|
|
76
|
+
**护城河层(外部状态,模型能力不可替代)**
|
|
77
|
+
- **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ql-ID 分配 + 双占用 fail-closed 硬拦;边界声明归属切分(窗口∩声明);并发写预检告警;平台乐观锁(base_ts + 身份头 + 服务器权威钟)
|
|
78
|
+
- **实测验收门禁** — verify/quick 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
|
|
79
|
+
- **持久账本 + 断点恢复** — SQLite 单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账
|
|
80
|
+
- **跨仓变更管理** — task 卡声明 `repo:` 字段,跨仓同构 worktree 隔离,base 锚快照,apply 不覆盖用户在途改动
|
|
81
|
+
|
|
82
|
+
**确定性对账层(机械核验,零模型轮次)**
|
|
83
|
+
- **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive/quick 四处收尾注入
|
|
84
|
+
- **API 契约矩阵** — provider/consumer 识别、endpoint 工件提取、前后端 API parity 对账
|
|
85
|
+
- **文档一致性棘轮** — 源码引用真实性校验(HEAD 模式),失败数只降不升
|
|
86
|
+
- **noAI 下沉 / IR 事实层** — 机械轮次下沉 CLI(骨架预生成、注入、代算对账),判断层留给 agent;同一契约清单事前渲染预览、事后引擎核验
|
|
87
|
+
|
|
88
|
+
**协作与流程层**
|
|
89
|
+
- **规范驱动 + 阶段状态机** — 每阶段入口契约、产物文件名、门禁校验强制流转,不能跳步
|
|
90
|
+
- **子代理并行 + Wave 拓扑** — depends_on 自动拓扑排序,同 Wave 并行
|
|
91
|
+
- **独立审查 + 人类审批** — Design Grill / stage review / task review 分层审查带 provenance;wait 等待态与审批答复持久化
|
|
92
|
+
- **跨机派发 / 棕地入库 / 知识飞轮** — SillyHub MCP worker 池 + 本机逐级回退;scan 生成七文档;export 模板蒸馏 + 决策版本化
|
|
93
|
+
|
|
94
|
+
**自我运维层**
|
|
95
|
+
- **doctor 自诊自愈** — 13 维诊断 + 幽灵 worktree/僵尸会话自动归档
|
|
96
|
+
- **摩擦计数** — 门禁回滚/实测失败/审查打回结构化计数喂复盘循环
|
|
97
|
+
|
|
98
|
+
> 📖 完整能力地图与对外叙事基准:`docs/sillyspec/capability-highlights-2026-09-14.md`
|
|
85
99
|
|
|
86
100
|
## MCP 工具
|
|
87
101
|
|
|
Binary file
|
package/package.json
CHANGED
package/src/change-list.js
CHANGED
|
@@ -168,6 +168,37 @@ function looksLikePath(p) {
|
|
|
168
168
|
*/
|
|
169
169
|
const INCIDENTAL_RE = /顺带修复|附带修复|顺带|drive-?by|incidental/i
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* 从清单子段标题 / 路径前缀解析跨仓 repoKey(scope-audit-cross-repo-blindness 改进点 2,
|
|
173
|
+
* 2026-09-15):多仓变更 design 清单按「main 仓 / sub-xxx / spdemo」分段书写,主仓跑
|
|
174
|
+
* scope-audit 时这些条目不是主仓路径,须识别出来标注「跨仓(本表不含)」而非恒「计划未动」。
|
|
175
|
+
*
|
|
176
|
+
* 两种书写形态:
|
|
177
|
+
* ① 子段标题含注册 repoKey(`### sub-grid-security`、`### sub-grid-security 新增文件`、
|
|
178
|
+
* `### spdemo 仓`)——标题 token 化后含 key,或以 key 开头且带「仓/仓库」后缀;
|
|
179
|
+
* ② 路径 cell 显式前缀 `cross-repo:<repo-key>:`(_module-map.yaml 同款约定)。
|
|
180
|
+
* 只认调用方传入的注册表 repoKeys(local.yaml repos 段)——未注册的 key 不剥前缀不标仓,
|
|
181
|
+
* fail-closed 防把普通路径误判跨仓。repoKeys 缺省 null → 全部按主仓解析(零回归)。
|
|
182
|
+
*/
|
|
183
|
+
function detectRepoFromSubsection(title, repoKeys) {
|
|
184
|
+
if (!Array.isArray(repoKeys) || repoKeys.length === 0) return null
|
|
185
|
+
const tokens = String(title || '').split(/[\s()()::,,、/\\]+/)
|
|
186
|
+
for (const k of repoKeys) {
|
|
187
|
+
if (tokens.includes(k)) return k
|
|
188
|
+
if (new RegExp(`^${k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s*仓|\\s*仓库)$`).test(String(title || '').trim())) return k
|
|
189
|
+
}
|
|
190
|
+
return null
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** `cross-repo:<key>:` 前缀拆分(key 须在注册表内);无前缀/未注册 → null */
|
|
194
|
+
function splitCrossRepoPrefix(rawPath, repoKeys) {
|
|
195
|
+
if (!Array.isArray(repoKeys) || repoKeys.length === 0) return null
|
|
196
|
+
const m = String(rawPath || '').match(/^cross-repo:([A-Za-z0-9_.\-]+):(.*)$/)
|
|
197
|
+
if (!m) return null
|
|
198
|
+
if (!repoKeys.includes(m[1])) return null
|
|
199
|
+
return { repo: m[1], path: m[2].trim() }
|
|
200
|
+
}
|
|
201
|
+
|
|
171
202
|
/**
|
|
172
203
|
* 从 design.md 解析文件变更清单(含 incidental 标记)。兼容两种真实写法:
|
|
173
204
|
* ① 表格:`| 操作 | 文件路径 | 说明 |`(brainstorm 模板默认)
|
|
@@ -175,13 +206,15 @@ const INCIDENTAL_RE = /顺带修复|附带修复|顺带|drive-?by|incidental/i
|
|
|
175
206
|
* 表头列顺序自适应(定位「文件/路径/file/path」列,列顺序写反时不会把操作名当路径);
|
|
176
207
|
* 忽略 `.sillyspec/`(keepSillyspecDocs=true 时保留 `.sillyspec/docs/`)与占位符(`—`/`-`/`N/A`/`无`);「不修改/暂缓」子段下的路径会被排除;
|
|
177
208
|
* CRLF 容错。incidental 嗅探:表格非路径列(说明列)+ 路径 cell 原始值(剥注释前的括号内容)+ 列表项原文。
|
|
209
|
+
* repoKeys 传入时识别跨仓子段 / `cross-repo:` 前缀(detectRepoFromSubsection),命中的条目
|
|
210
|
+
* repo 字段携带 repoKey(默认 null = 主仓)——scope-audit 计划侧跨仓标注的数据源。
|
|
178
211
|
*
|
|
179
212
|
* 内核函数:parseFileChangeList(Set 包装,向后兼容)与 parseFileChangeListDetailed 共用,
|
|
180
213
|
* 单一真相源,避免两处各自重写清单解析漂移。
|
|
181
214
|
* @param {string} designMdPath - design.md 文件路径
|
|
182
|
-
* @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}(顺序按首次出现,exclude 子段移除)
|
|
215
|
+
* @returns {Array<{ path: string, operation: string|null, incidental: boolean, repo: string|null }>}(顺序按首次出现,exclude 子段移除)
|
|
183
216
|
*/
|
|
184
|
-
function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}) {
|
|
217
|
+
function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false, repoKeys = null } = {}) {
|
|
185
218
|
if (!designMdPath || !existsSync(designMdPath)) return []
|
|
186
219
|
|
|
187
220
|
const content = readFileSync(designMdPath, 'utf8').replace(/\r\n/g, '\n')
|
|
@@ -202,10 +235,11 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
202
235
|
let opColIdx = -1 // 操作列下标(表头扫描后定位;-1 = 无操作列 → operation=null)
|
|
203
236
|
let listMode = 'include' // include | exclude(分类列表子段)
|
|
204
237
|
let currentOp = null // 分类列表子标题推导的 operation(表格模式不用)
|
|
205
|
-
|
|
238
|
+
let currentRepo = null // 分类列表子标题推导的跨仓 repoKey(detectRepoFromSubsection)
|
|
239
|
+
const entries = new Map() // path -> { path, operation, incidental, repo }(exclude 子段 delete)
|
|
206
240
|
|
|
207
241
|
for (const line of lines) {
|
|
208
|
-
// 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件
|
|
242
|
+
// 分类列表子标题:### 新增文件 / ### 修改文件 / ### 不修改文件 / ### sub-grid-security(跨仓段)
|
|
209
243
|
const subHeader = line.match(/^###\s+(.+?)\s*$/)
|
|
210
244
|
if (subHeader) {
|
|
211
245
|
// ⚠️ EXCLUDE 优先:「### 不修改文件」含「修改」二字,会被 classifySubsectionOp 误匹配。
|
|
@@ -213,9 +247,13 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
213
247
|
if (EXCLUDE_SUBSECTION_RE.test(subHeader[1])) {
|
|
214
248
|
listMode = 'exclude'
|
|
215
249
|
currentOp = null
|
|
250
|
+
currentRepo = null
|
|
216
251
|
} else {
|
|
217
252
|
listMode = 'include'
|
|
218
253
|
currentOp = classifySubsectionOp(subHeader[1])
|
|
254
|
+
// 跨仓段识别(scope-audit-cross-repo-blindness 改进点 2):子段标题含注册 repoKey
|
|
255
|
+
// 或以「<key> 仓/仓库」收尾 → 其下条目标 repo;不含 key 的普通操作子段 → 回主仓。
|
|
256
|
+
currentRepo = detectRepoFromSubsection(subHeader[1], repoKeys)
|
|
219
257
|
}
|
|
220
258
|
continue
|
|
221
259
|
}
|
|
@@ -237,7 +275,12 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
237
275
|
continue
|
|
238
276
|
}
|
|
239
277
|
|
|
240
|
-
const
|
|
278
|
+
const filePathRaw = normalizePath(cells[pathColIdx] || '')
|
|
279
|
+
// 跨仓显式前缀(scope-audit-cross-repo-blindness 改进点 2):`cross-repo:<key>:` 剥前缀
|
|
280
|
+
// 标 repo;无前缀/未注册 key → 按当前子段 repo(缺省主仓 null)
|
|
281
|
+
const crossRepo = splitCrossRepoPrefix(filePathRaw, repoKeys)
|
|
282
|
+
const filePath = crossRepo ? crossRepo.path : filePathRaw
|
|
283
|
+
const entryRepo = crossRepo ? crossRepo.repo : currentRepo
|
|
241
284
|
// 列定位兜底:取到纯操作词(表头未命中且列顺序异常)→ 跳过,避免把「修改」当路径
|
|
242
285
|
if (/^(新增|修改|删除|重命名|new|modify|update|delete|create|rename)$/i.test(filePath)) continue
|
|
243
286
|
if (isPlaceholder(filePath) || (filePath.startsWith('.sillyspec/') && !(keepSillyspecDocs && filePath.startsWith('.sillyspec/docs/')))) continue
|
|
@@ -252,29 +295,37 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
252
295
|
// 校验器)。拆分符:+ / / / | / 、 / ; / ;——每个 token 独立过校验入表;单路径零变化。
|
|
253
296
|
const pathTokens = splitCombinedPaths(filePath)
|
|
254
297
|
if (listMode === 'exclude') { for (const t of pathTokens) entries.delete(t); continue }
|
|
255
|
-
for (const t of pathTokens) entries.set(t, { path: t, operation, incidental })
|
|
298
|
+
for (const t of pathTokens) entries.set(t, { path: t, operation, incidental, repo: entryRepo })
|
|
256
299
|
continue
|
|
257
300
|
}
|
|
258
301
|
|
|
259
302
|
// 分类列表项:`- path` / `- \`path\``
|
|
260
303
|
const listItem = line.match(/^\s*-\s+(.+)/)
|
|
261
304
|
if (listItem) {
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
// (C:\...),列表项是仓根相对路径无盘符,安全。
|
|
305
|
+
// 跨仓显式前缀(改进点 2,同表格分支口径)须先于「路径:描述」剥离判——
|
|
306
|
+
// `cross-repo:<key>:<path>` 的首个冒号在 key 段内,先剥描述会把整条砍成 `cross-repo`。
|
|
307
|
+
const preCross = splitCrossRepoPrefix(normalizePath(listItem[1]), repoKeys)
|
|
266
308
|
let rawItem = listItem[1]
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
309
|
+
if (!preCross) {
|
|
310
|
+
// 坑 brainstorm-gate-agent-unavailable-and-list-path-parse 坑2:列表项「路径:描述」
|
|
311
|
+
// 整行当路径 existsSync 假阴性(文件存在却报幻觉路径)——剥首个中/英冒号及之后描述。
|
|
312
|
+
// 仅列表分支剥、且冒号前段须 looksLikePath:normalizePath 全局剥会毁 Windows 绝对路径
|
|
313
|
+
// (C:\...),列表项是仓根相对路径无盘符,安全。
|
|
314
|
+
const colonIdx = rawItem.search(/[::]/)
|
|
315
|
+
if (colonIdx > 0) {
|
|
316
|
+
const head = rawItem.slice(0, colonIdx).trim()
|
|
317
|
+
if (looksLikePath(head)) rawItem = head
|
|
318
|
+
}
|
|
271
319
|
}
|
|
272
|
-
const
|
|
320
|
+
const itemNorm = normalizePath(rawItem)
|
|
321
|
+
const itemCross = preCross || splitCrossRepoPrefix(itemNorm, repoKeys)
|
|
322
|
+
const filePath = itemCross ? itemCross.path : itemNorm
|
|
323
|
+
const itemRepo = itemCross ? itemCross.repo : currentRepo
|
|
273
324
|
if (isPlaceholder(filePath) || (filePath.startsWith('.sillyspec/') && !(keepSillyspecDocs && filePath.startsWith('.sillyspec/docs/')))) continue
|
|
274
325
|
if (!looksLikePath(filePath)) continue // 脏描述兜底
|
|
275
326
|
const incidental = INCIDENTAL_RE.test(listItem[1])
|
|
276
327
|
if (listMode === 'exclude') { entries.delete(filePath); continue }
|
|
277
|
-
entries.set(filePath, { path: filePath, operation: currentOp, incidental })
|
|
328
|
+
entries.set(filePath, { path: filePath, operation: currentOp, incidental, repo: itemRepo })
|
|
278
329
|
}
|
|
279
330
|
}
|
|
280
331
|
|
|
@@ -285,8 +336,8 @@ function _parseFileListDetailed(designMdPath, { keepSillyspecDocs = false } = {}
|
|
|
285
336
|
* 从 design.md 解析文件变更清单(路径集合,向后兼容)。
|
|
286
337
|
* @param {string} designMdPath - design.md 文件路径
|
|
287
338
|
* @param {string} designMdPath - design.md 文件路径
|
|
288
|
-
* @param {{ keepSillyspecDocs?: boolean }} [opts] - keepSillyspecDocs=true 时保留 `.sillyspec/docs/`(dogfood 模块文档=交付物,apply gate 需识别;默认 false 跳过全部 .sillyspec/ 保持 fileCount
|
|
289
|
-
* @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js"
|
|
339
|
+
* @param {{ keepSillyspecDocs?: boolean, repoKeys?: string[]|null }} [opts] - keepSillyspecDocs=true 时保留 `.sillyspec/docs/`(dogfood 模块文档=交付物,apply gate 需识别;默认 false 跳过全部 .sillyspec/ 保持 fileCount 判档不变);repoKeys=注册仓 key 清单(local.yaml repos 段)时识别跨仓子段 / `cross-repo:` 前缀
|
|
340
|
+
* @returns {Set<string>} 文件路径集合(相对路径,如 "src/worktree.js";跨仓前缀已剥)
|
|
290
341
|
*/
|
|
291
342
|
export function parseFileChangeList(designMdPath, opts) {
|
|
292
343
|
return new Set(_parseFileListDetailed(designMdPath, opts).map(e => e.path))
|
|
@@ -296,9 +347,11 @@ export function parseFileChangeList(designMdPath, opts) {
|
|
|
296
347
|
* 从 design.md 解析文件变更清单(含 operation + incidental 标记)。
|
|
297
348
|
* operation 供 verify 删除探针对账(声明「新增/修改」却整文件删除 = 高风险);
|
|
298
349
|
* incidental 供 assess allowed_paths 豁免。
|
|
350
|
+
* repo(跨仓标注,scope-audit-cross-repo-blindness 改进点 2):命中注册跨仓子段标题或
|
|
351
|
+
* `cross-repo:<key>:` 前缀的条目标 repoKey,否则 null(= 主仓)。
|
|
299
352
|
* @param {string} designMdPath - design.md 文件路径
|
|
300
|
-
* @param {{ keepSillyspecDocs?: boolean }} [opts] - 同 parseFileChangeList
|
|
301
|
-
* @returns {Array<{ path: string, operation: string|null, incidental: boolean }>}
|
|
353
|
+
* @param {{ keepSillyspecDocs?: boolean, repoKeys?: string[]|null }} [opts] - 同 parseFileChangeList
|
|
354
|
+
* @returns {Array<{ path: string, operation: string|null, incidental: boolean, repo: string|null }>}
|
|
302
355
|
*/
|
|
303
356
|
export function parseFileChangeListDetailed(designMdPath, opts) {
|
|
304
357
|
return _parseFileListDetailed(designMdPath, opts)
|
|
@@ -51,6 +51,26 @@ const INTEGRATION_FILE_PATTERNS = [
|
|
|
51
51
|
/startup/i,
|
|
52
52
|
]
|
|
53
53
|
|
|
54
|
+
// ============ quick 出口门禁:路径模式风险表(2026-09-14-quick-exit-tiered-gates task-01) ============
|
|
55
|
+
//
|
|
56
|
+
// quick 侧「风险特征命中」的单一数据源(design D-004@v2:v1 仅路径模式——不做 diff 关键词
|
|
57
|
+
// 扫描、不加 git 子进程,R-04 零子进程承诺;diff 维度待真实需求另立变更)。与上方 verify 侧
|
|
58
|
+
// INTEGRATION_FILE_PATTERNS 语义不同(那张表收跨进程/集成域、供 detectChangeRisk 判级),本表
|
|
59
|
+
// 收 quick 实证踩坑域(R-03 从窄收录:auth/permission/billing/migration/锁/调度),供
|
|
60
|
+
// quick-gate-profile.js 的 computeGateProfile 经 opts.riskTable 默认引用——命中即画像 L2 +
|
|
61
|
+
// checks.runtimeEvidence='required'。detectChangeRisk 判级逻辑不消费本表(verify 侧行为零变化)。
|
|
62
|
+
//
|
|
63
|
+
// 匹配口径:POSIX 路径、大小写不敏感、段边界锚定——前界 (^|[/_-]) + 后界 (?=[/._-]|$),
|
|
64
|
+
// 防 author/booking/lockfile 类子串假阳(R-03);pattern 为稳定域标识(riskHits 审计输出用)。
|
|
65
|
+
export const QUICK_RISK_PATH_PATTERNS = [
|
|
66
|
+
{ pattern: 'auth', re: /(?:^|[/_-])(?:oauth2?|auth(?:orization|entication|enticator)?)(?=[/._-]|$)/i },
|
|
67
|
+
{ pattern: 'permission', re: /(?:^|[/_-])permissions?(?=[/._-]|$)/i },
|
|
68
|
+
{ pattern: 'billing', re: /(?:^|[/_-])billing(?=[/._-]|$)/i },
|
|
69
|
+
{ pattern: 'migration', re: /(?:^|[/_-])(?:migrations?|migrate)(?=[/._-]|$)/i },
|
|
70
|
+
{ pattern: 'lock', re: /(?:^|[/_-])(?:locks?|mutex(?:es)?)(?=[/._-]|$)/i },
|
|
71
|
+
{ pattern: 'scheduling', re: /(?:^|[/_-])(?:schedul(?:er|ing)|cron|jobs?)(?=[/._-]|$)/i },
|
|
72
|
+
]
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* task 级「端到端/deployment-critical」文本判定(execute 批量完成 autoCheckPlanFromReviews 用)。
|
|
56
76
|
*
|
package/src/config-schema.js
CHANGED
|
@@ -128,6 +128,14 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
128
128
|
{ path: 'known_failures', type: 'array', optional: true, status: 'live', readers: ['extractKnownFailures (src/verify-postcheck.js)'], desc: '失败行模式列表(匹配测试输出失败行)。', example: 'tests/some-old-test' },
|
|
129
129
|
],
|
|
130
130
|
},
|
|
131
|
+
{
|
|
132
|
+
id: 'verify_precedents',
|
|
133
|
+
title: '等效验证先例库',
|
|
134
|
+
note: '标准测试/构建命令在本仓不可用(框架硬编码 skip、依赖外部基础设施等)时,登记已验证的等效验证口径,verify prompt 自动注入提示复用(2026-09-15 EHS 实证:mvn test 被框架 parent pom 硬编码 surefire skip,等效口径第二次靠 agent 翻旧 verify-result 正文续命——先例只活在散文里换 agent 即断档)。',
|
|
135
|
+
keys: [
|
|
136
|
+
{ path: 'verify_precedents', type: 'array', optional: true, status: 'live', readers: ['parseVerifyPrecedents (src/run/prompt.js)'], desc: '等效验证先例对象数组(id/standard_command/reason/equivalent/established_by/notes)。verify 阶段 prompt 与 test_strategy 预检提示同挂点注入;行动指引=把等效命令配进 commands.test 照常全量实测对账,而非只靠 test_strategy: skip 放行。', example: '- id: mvn-test-surefire-skip\n standard_command: mvn test\n reason: parent pom 硬编码 surefire skip=true\n equivalent: dependency:build-classpath + javac + JUnitCore 直跑\n established_by: 2026-08-26-building-area-ledger' },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
131
139
|
{
|
|
132
140
|
id: 'worktree-hook',
|
|
133
141
|
title: 'worktree-guard 扩展',
|
|
@@ -136,6 +144,14 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
136
144
|
{ path: 'worktree-hook.readonlyCommands', type: 'array', optional: true, status: 'live', readers: ['loadLocalConfig (src/hooks/worktree-guard.js)'], desc: '额外放行的只读命令名列表(如 rg/fd),绕过 worktree-guard 写操作拦截。', example: 'rg' },
|
|
137
145
|
],
|
|
138
146
|
},
|
|
147
|
+
{
|
|
148
|
+
id: 'worktree',
|
|
149
|
+
title: 'worktree 生成物供给',
|
|
150
|
+
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 数组均可解析。',
|
|
151
|
+
keys: [
|
|
152
|
+
{ 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' },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
139
155
|
{
|
|
140
156
|
id: 'auto_mode',
|
|
141
157
|
title: '变更规模自动分类',
|
|
@@ -181,6 +197,25 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
181
197
|
{ path: 'semantic_guard.enabled', type: 'boolean', optional: true, status: 'live', readers: ['readSemanticGuardEnabled (src/semantic-guard.js)'], desc: '语义守卫总开关,默认 true;设 false 时 quick 进场注入与 --done 断言 WARNING 全停零开销。', example: 'true' },
|
|
182
198
|
],
|
|
183
199
|
},
|
|
200
|
+
{
|
|
201
|
+
id: 'quick-gate',
|
|
202
|
+
title: 'quick 出口门禁阈值',
|
|
203
|
+
note: 'quick --done 分级门禁(L0/L1/L2 advisory)的四个数值阈值覆写(2026-09-14-quick-exit-tiered-gates,D-009)。缺省=src/quick-gate-profile.js 的 THRESHOLDS 代码内校准值,仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。',
|
|
204
|
+
keys: [
|
|
205
|
+
{ path: 'quick-gate.l1_span', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L1 模块跨度阈值:非文档文件命中 ≥N 个模块 → L1(缺省 2)。', example: '2' },
|
|
206
|
+
{ path: 'quick-gate.l1_files', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L1 文件数阈值:变更文件 ≥N → L1(缺省 4)。', example: '4' },
|
|
207
|
+
{ path: 'quick-gate.l2_span', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'L2 模块跨度阈值:命中 ≥N 个模块 → L2(缺省 4)。', example: '4' },
|
|
208
|
+
{ path: 'quick-gate.l2_files_degraded', type: 'integer', optional: true, status: 'live', readers: ['resolveGateThresholds (src/quick-gate-profile.js)'], desc: 'module-map 缺失降级档的 L2 文件数阈值:变更文件 ≥N → L2(缺省 8)。', example: '8' },
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'change-ownership',
|
|
213
|
+
title: '变更所有权守卫',
|
|
214
|
+
note: '接管类操作(apply/cleanup/archive)对他人活跃 change 的所有权校验调参(2026-09-14-change-ownership-guards,D-001/D-005@v1)。缺省 15 分钟,仅确知调参时配置。',
|
|
215
|
+
keys: [
|
|
216
|
+
{ path: 'change-ownership.heartbeat_minutes', type: 'integer', optional: true, status: 'live', readers: ['assertChangeOwnership (src/progress/change-registry.js — 本变更 task-02 接线消费)'], desc: '所有者活跃心跳窗口(分钟):owner 非本会话且 last_active 距今在窗口内 → 拒绝接管类操作(--takeover 显式接管);窗口外 → 放行并自动接管。缺省 15。', example: '15' },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
184
219
|
],
|
|
185
220
|
};
|
|
186
221
|
|
|
@@ -324,12 +359,26 @@ semantic_guard:
|
|
|
324
359
|
known_failures:
|
|
325
360
|
- tests/some-old-stable-failure
|
|
326
361
|
|
|
362
|
+
# ── 等效验证先例库(标准测试/构建命令在本仓不可用时的既有替代口径;verify prompt 自动注入)──
|
|
363
|
+
verify_precedents:
|
|
364
|
+
- id: standard-test-command-blocked
|
|
365
|
+
standard_command: mvn test
|
|
366
|
+
reason: 框架 parent pom 硬编码 surefire skip=true,-D 覆盖无效
|
|
367
|
+
equivalent: mvn dependency:build-classpath + javac + JUnitCore 直跑
|
|
368
|
+
established_by: <确立该等效口径的变更名>
|
|
369
|
+
|
|
327
370
|
# ── worktree-guard 只读命令扩展(execute worktree 隔离期内放行额外只读命令)──
|
|
328
371
|
worktree-hook:
|
|
329
372
|
readonlyCommands:
|
|
330
373
|
- rg
|
|
331
374
|
- fd
|
|
332
375
|
|
|
376
|
+
# ── worktree 生成物供给(gitignore 生成物不随 git 树/untracked overlay 进 worktree,create 期从主仓复制)──
|
|
377
|
+
# 精确路径或 glob(* 单层 / ** 多层),相对仓根;展开上限 200 文件超出截断警告;缺省不供给零行为变化。
|
|
378
|
+
# worktree:
|
|
379
|
+
# supplyFiles:
|
|
380
|
+
# - src/build-id.ts
|
|
381
|
+
|
|
333
382
|
# ── 变更规模自动分类(sillyspec run auto 时按需求描述强制 quick/full;正则数组,i 大小写无关)──
|
|
334
383
|
auto_mode:
|
|
335
384
|
force_full_patterns:
|
|
@@ -350,5 +399,18 @@ docs-check:
|
|
|
350
399
|
# - docs/sillyspec/architecture.md
|
|
351
400
|
# cross_repo_roots: # repo://<仓库名> 跨仓引用 → 本机仓库根(每台设备各自配,绝对路径勿提交)
|
|
352
401
|
# sillyspec: /c/Users/you/IdeaProjects/sillyspec
|
|
402
|
+
|
|
403
|
+
# ── quick 出口门禁阈值(quick --done 分级门禁 L0/L1/L2 的 advisory 阈值覆写;缺省=代码内校准值)──
|
|
404
|
+
# 仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。
|
|
405
|
+
# quick-gate:
|
|
406
|
+
# l1_span: 2 # 跨 ≥2 模块 → L1
|
|
407
|
+
# l1_files: 4 # 或 ≥4 文件 → L1
|
|
408
|
+
# l2_span: 4 # 跨 ≥4 模块 → L2
|
|
409
|
+
# l2_files_degraded: 8 # module-map 缺失降级档:≥8 文件 → L2
|
|
410
|
+
|
|
411
|
+
# ── 变更所有权守卫(接管类操作 apply/cleanup/archive 对他人活跃 change 校验;缺省 15 分钟)──
|
|
412
|
+
# owner 非本会话且 last_active 在窗口内 → 拒绝(--takeover 显式接管);窗口外 → 自动接管。
|
|
413
|
+
# change-ownership:
|
|
414
|
+
# heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
|
|
353
415
|
`;
|
|
354
416
|
}
|
package/src/contract-matrix.js
CHANGED
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs'
|
|
12
|
-
import { join, resolve, basename, dirname, relative } from 'path'
|
|
12
|
+
import { join, resolve, basename, dirname, relative, isAbsolute } from 'path'
|
|
13
13
|
import { gitQuiet } from './git-helper.js'
|
|
14
14
|
import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
|
|
15
|
+
import { parseFileChangeListDetailed } from './change-list.js'
|
|
15
16
|
import {
|
|
16
17
|
scanBackendEndpoints,
|
|
17
18
|
scanFrontendApiCalls,
|
|
@@ -19,7 +20,7 @@ import {
|
|
|
19
20
|
normalizePath,
|
|
20
21
|
diffApiParity,
|
|
21
22
|
} from './endpoint-extractor.js'
|
|
22
|
-
import { parseTaskContracts } from './stages/plan-postcheck.js'
|
|
23
|
+
import { parseTaskContracts, parseRepoRegistry, parseAllowedPaths } from './stages/plan-postcheck.js'
|
|
23
24
|
|
|
24
25
|
// ─── 关键词检测 ─────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -382,7 +383,9 @@ export function _readWorktreeMeta(specBase, scanRoot, changeName) {
|
|
|
382
383
|
for (const metaPath of candidates) {
|
|
383
384
|
try {
|
|
384
385
|
if (!existsSync(metaPath)) continue
|
|
385
|
-
|
|
386
|
+
// BOM 容错(worktree.js:283 同坑):Windows 编辑器写入的 UTF-8 BOM 会让 JSON.parse
|
|
387
|
+
// 失败——双候选下会静默跳根,先剥 BOM 再解析
|
|
388
|
+
const meta = JSON.parse(String(readFileSync(metaPath, 'utf8')).replace(/^\uFEFF/, ''))
|
|
386
389
|
const gitDir = (meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath))
|
|
387
390
|
? meta.worktreePath : scanRoot
|
|
388
391
|
return { meta, gitDir }
|
|
@@ -559,6 +562,74 @@ export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = nu
|
|
|
559
562
|
frontendCalls = scanFrontendApiCalls(frontendRoot)
|
|
560
563
|
}
|
|
561
564
|
|
|
565
|
+
// ── 跨仓前端调用并集(坑 probe5-cross-repo-frontend-blind,2026-09-15 EHS 生产实证:三端
|
|
566
|
+
// 全栈变更的前端在兄弟仓(cross-repo: 前缀声明),主仓 change-diff 永远不含它们 → 报告
|
|
567
|
+
// 「0 frontend calls」假象、本变更端点全落 unused、契约对账只能靠人工逐条对齐 11 端点)。
|
|
568
|
+
// 声明源双取:task 卡 allowed_paths 的 cross-repo:<key>: 前缀 + design 清单跨仓条目
|
|
569
|
+
// (change-list repoKeys 解析,与探针 1 清单同源);仓根经 local.yaml repos 注册表解析
|
|
570
|
+
// (resolveCrossRepoDeclarations 同款)。各仓根扫前端调用并按「该仓声明文件集」收窄后并入
|
|
571
|
+
// frontendCalls——「探针已知口径限制」注记从人肉兜底变机器对账。fail-soft:仓未注册/
|
|
572
|
+
// 不可达 → crossRepoNotes 一行注记不炸整体;无跨仓声明/无注册表零行为(存量单仓流不受影响)。
|
|
573
|
+
const crossRepoNotes = []
|
|
574
|
+
if (changeName && specBase) {
|
|
575
|
+
try {
|
|
576
|
+
const declaredByRepo = {}
|
|
577
|
+
const addDecl = (repo, p) => {
|
|
578
|
+
const n = String(p || '').trim().replace(/^\.\//, '').replace(/\\/g, '/')
|
|
579
|
+
if (repo && n) (declaredByRepo[repo] = declaredByRepo[repo] || new Set()).add(n)
|
|
580
|
+
}
|
|
581
|
+
// 源1:task 卡 allowed_paths 的 cross-repo:<key>: 前缀(与 _module-map.yaml 约定一致)——
|
|
582
|
+
// 不按注册表过滤:未注册 key 也要现身「未注册」注记(漏登记可见,而非静默消失)
|
|
583
|
+
const tasksDir = join(specBase, 'changes', changeName, 'tasks')
|
|
584
|
+
if (existsSync(tasksDir)) {
|
|
585
|
+
for (const f of readdirSync(tasksDir)) {
|
|
586
|
+
if (!/^task-\d+\.md$/.test(f)) continue
|
|
587
|
+
try {
|
|
588
|
+
for (const p of parseAllowedPaths(readFileSync(join(tasksDir, f), 'utf8'))) {
|
|
589
|
+
const m = String(p).match(/^cross-repo:([A-Za-z0-9_.\-]+):(.*)$/)
|
|
590
|
+
if (m) addDecl(m[1], m[2])
|
|
591
|
+
}
|
|
592
|
+
} catch { /* 单卡读取失败不拖垮 */ }
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const registry = parseRepoRegistry(existsSync(join(specBase, 'local.yaml')) ? readFileSync(join(specBase, 'local.yaml'), 'utf8') : '')
|
|
596
|
+
// 源2:design.md 文件清单跨仓子段/前缀(change-list repoKeys 解析需注册表在——
|
|
597
|
+
// 注册表空时源2 不可判跨仓,跳过;源1 已保证未注册声明仍可见)。注意
|
|
598
|
+
// parseFileChangeListDetailed 吃文件路径非文本(内部 existsSync+readFileSync)。
|
|
599
|
+
if (registry.size > 0) {
|
|
600
|
+
const designPath = join(specBase, 'changes', changeName, 'design.md')
|
|
601
|
+
if (existsSync(designPath)) {
|
|
602
|
+
for (const e of parseFileChangeListDetailed(designPath, { repoKeys: [...registry.keys()] })) {
|
|
603
|
+
if (e.repo) addDecl(e.repo, e.path)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
for (const [key, declared] of Object.entries(declaredByRepo)) {
|
|
608
|
+
const raw = registry.get(key)
|
|
609
|
+
if (!raw) {
|
|
610
|
+
crossRepoNotes.push(`repo「${key}」未在 local.yaml repos 注册——该仓前端调用未进对账(register-repo 登记后自动并入)`)
|
|
611
|
+
continue
|
|
612
|
+
}
|
|
613
|
+
const repoRoot = isAbsolute(raw) ? raw : resolve(scanRoot, raw)
|
|
614
|
+
if (!existsSync(repoRoot)) {
|
|
615
|
+
crossRepoNotes.push(`repo「${key}」注册路径不可达:${repoRoot}——该仓前端调用未进对账`)
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
const calls = scanFrontendApiCalls(repoRoot).filter(c => {
|
|
619
|
+
// source 是绝对路径且 Windows 反斜杠形态——与主仓 change-diff 过滤同款手工归一
|
|
620
|
+
// (normalizePath 是 URL 路径归一器,不碰反斜杠,此处不能用)
|
|
621
|
+
const src = String(c.source || '').replace(/\\/g, '/')
|
|
622
|
+
return [...declared].some(dp => src === dp || src.endsWith('/' + dp) || dp.endsWith('/' + src))
|
|
623
|
+
})
|
|
624
|
+
if (calls.length > 0) frontendCalls.push(...calls)
|
|
625
|
+
crossRepoNotes.push(`repo「${key}」@ ${repoRoot}:${calls.length} 前端调用(跨仓声明 ${declared.size} 文件收窄)`)
|
|
626
|
+
frontendScope += ` + cross-repo:${key}(${calls.length})`
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
crossRepoNotes.push(`跨仓前端扫描失败(fail-soft 跳过):${e && e.message ? e.message : e}`)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
562
633
|
// ── 挂载前缀收集(坑 endpoints-mount-prefix-gap,2026-08-31 用户实证):endpoints 提取的是
|
|
563
634
|
// 「router 自身前缀 + 装饰器路径」,不含挂载点前缀(main.py 的 include_router(prefix=)/app.use
|
|
564
635
|
// 与 router 文件分离,静态扫描不做导入图关联)→ 前端全路径调用(/api/xxx)对不上欠前缀端点
|
|
@@ -575,6 +646,14 @@ export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = nu
|
|
|
575
646
|
const key = `${u.method} ${u.path}`
|
|
576
647
|
return liveKeys.has(key) || liveEndpoints.length === 0
|
|
577
648
|
})
|
|
649
|
+
// unused 分层(坑 probe5-unused-stock-noise,2026-09-15 EHS 生产实证:490 个 unused 全是
|
|
650
|
+
// urgent 等他模块存量端点——全仓 live 扫描 × 本变更对账的口径噪音刷屏)。本变更相关 =
|
|
651
|
+
// contract artifact 端点集内(本变更换的端点漏配才是真信号);存量其余折叠计数。无
|
|
652
|
+
// artifact(非契约流)回退全列零回归。
|
|
653
|
+
const artifactKeys = new Set(allProviderEndpoints.map(e => `${e.method} ${e.path}`))
|
|
654
|
+
const hasArtifacts = allProviderEndpoints.length > 0
|
|
655
|
+
const unusedChangeRelevant = hasArtifacts ? narrowedUnused.filter(u => artifactKeys.has(`${u.method} ${u.path}`)) : narrowedUnused
|
|
656
|
+
const unusedStockCount = narrowedUnused.length - unusedChangeRelevant.length
|
|
578
657
|
|
|
579
658
|
const ok = missingBackend.length === 0
|
|
580
659
|
const liveRootSummary = liveByRoot.map(r => `${r.label} ${r.endpoints.length}`).join(' + ')
|
|
@@ -583,11 +662,11 @@ export function verifyApiParity(specBase, scanRoot, runtimeRoot, changeName = nu
|
|
|
583
662
|
: `❌ API parity check failed: ${missingBackend.length} frontend calls have no matching backend endpoint [scope: ${frontendScope}]`
|
|
584
663
|
|
|
585
664
|
if (narrowedUnused.length > 0) {
|
|
586
|
-
summary += ` | ${
|
|
665
|
+
summary += ` | ${unusedChangeRelevant.length} backend endpoints unused by frontend${unusedStockCount > 0 ? ` (+${unusedStockCount} stock noise collapsed)` : ''}`
|
|
587
666
|
}
|
|
588
667
|
if (prefixAlignedCount > 0) {
|
|
589
668
|
summary += ` | ${prefixAlignedCount} calls matched after mount-prefix alignment (artifact paths exclude include_router/app.use prefixes)`
|
|
590
669
|
}
|
|
591
670
|
|
|
592
|
-
return { ok, missingBackend, unusedBackend: narrowedUnused, summary, backendCount: mergedProviderEndpoints.length, frontendCount: frontendCalls.length, prefixAlignedCount, scanRoots: liveByRoot.map(r => r.root) }
|
|
671
|
+
return { ok, missingBackend, unusedBackend: narrowedUnused, unusedChangeRelevant, unusedStockCount, crossRepoNotes, summary, backendCount: mergedProviderEndpoints.length, frontendCount: frontendCalls.length, prefixAlignedCount, scanRoots: liveByRoot.map(r => r.root) }
|
|
593
672
|
}
|