sillyspec 3.28.8 → 3.28.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -224
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +118 -117
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +20 -0
- package/src/config-schema.js +32 -0
- package/src/db.js +11 -3
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-gate.js +105 -6
- package/src/doctor-diagnostics.js +224 -2
- package/src/friction-ledger.js +171 -0
- package/src/hooks/worktree-guard.js +13 -1
- package/src/index.js +188 -17
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- package/src/progress/change-registry.js +183 -8
- package/src/progress/shared.js +3 -2
- package/src/progress.js +100 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +155 -14
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +107 -12
- package/src/run/complete-handlers.js +511 -21
- package/src/run/complete.js +54 -16
- package/src/run/concurrent-detect.js +98 -0
- package/src/run/gates.js +50 -9
- package/src/run/prompt.js +134 -5
- package/src/run/quick-audit.js +111 -13
- package/src/run/shared.js +76 -10
- package/src/run/stage.js +6 -0
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +23 -7
- package/src/stages/brainstorm.js +6 -0
- package/src/stages/execute.js +85 -3
- package/src/stages/knowledge.js +52 -3
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +135 -7
- package/src/task-review.js +292 -21
- package/src/verify-postcheck.js +20 -0
- package/src/verify-probes.js +340 -4
- package/src/worktree-apply.js +538 -44
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +117 -5
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/.patch16-tmp.mjs +0 -169
- package/sa-rename-probe/src/new.js +0 -4
package/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
|
@@ -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
|
@@ -181,6 +181,25 @@ export const LOCAL_YAML_SCHEMA = {
|
|
|
181
181
|
{ 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
182
|
],
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
id: 'quick-gate',
|
|
186
|
+
title: 'quick 出口门禁阈值',
|
|
187
|
+
note: 'quick --done 分级门禁(L0/L1/L2 advisory)的四个数值阈值覆写(2026-09-14-quick-exit-tiered-gates,D-009)。缺省=src/quick-gate-profile.js 的 THRESHOLDS 代码内校准值,仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。',
|
|
188
|
+
keys: [
|
|
189
|
+
{ 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' },
|
|
190
|
+
{ 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' },
|
|
191
|
+
{ 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' },
|
|
192
|
+
{ 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' },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: 'change-ownership',
|
|
197
|
+
title: '变更所有权守卫',
|
|
198
|
+
note: '接管类操作(apply/cleanup/archive)对他人活跃 change 的所有权校验调参(2026-09-14-change-ownership-guards,D-001/D-005@v1)。缺省 15 分钟,仅确知调参时配置。',
|
|
199
|
+
keys: [
|
|
200
|
+
{ 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' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
184
203
|
],
|
|
185
204
|
};
|
|
186
205
|
|
|
@@ -350,5 +369,18 @@ docs-check:
|
|
|
350
369
|
# - docs/sillyspec/architecture.md
|
|
351
370
|
# cross_repo_roots: # repo://<仓库名> 跨仓引用 → 本机仓库根(每台设备各自配,绝对路径勿提交)
|
|
352
371
|
# sillyspec: /c/Users/you/IdeaProjects/sillyspec
|
|
372
|
+
|
|
373
|
+
# ── quick 出口门禁阈值(quick --done 分级门禁 L0/L1/L2 的 advisory 阈值覆写;缺省=代码内校准值)──
|
|
374
|
+
# 仅确知调参时配置;键非法(非 ≥1 整数)回退默认并 warn。
|
|
375
|
+
# quick-gate:
|
|
376
|
+
# l1_span: 2 # 跨 ≥2 模块 → L1
|
|
377
|
+
# l1_files: 4 # 或 ≥4 文件 → L1
|
|
378
|
+
# l2_span: 4 # 跨 ≥4 模块 → L2
|
|
379
|
+
# l2_files_degraded: 8 # module-map 缺失降级档:≥8 文件 → L2
|
|
380
|
+
|
|
381
|
+
# ── 变更所有权守卫(接管类操作 apply/cleanup/archive 对他人活跃 change 校验;缺省 15 分钟)──
|
|
382
|
+
# owner 非本会话且 last_active 在窗口内 → 拒绝(--takeover 显式接管);窗口外 → 自动接管。
|
|
383
|
+
# change-ownership:
|
|
384
|
+
# heartbeat_minutes: 15 # 所有者活跃心跳窗口(分钟)
|
|
353
385
|
`;
|
|
354
386
|
}
|
package/src/db.js
CHANGED
|
@@ -7,7 +7,7 @@ import { dirname } from 'path';
|
|
|
7
7
|
// 每次 new ProgressManager 都过 init,靠版本戳跳过建表省开销)。
|
|
8
8
|
// node:sqlite(DatabaseSync)是原生 SQLite 引擎,打开即持久化(不像 sql.js 纯内存需整库 export 落盘),
|
|
9
9
|
// _createSchema 内 DDL 直接落盘,无需额外 _save。
|
|
10
|
-
const DB_SCHEMA_VERSION =
|
|
10
|
+
const DB_SCHEMA_VERSION = 6;
|
|
11
11
|
|
|
12
12
|
// SQLITE_BUSY 应用层有限重试(R-08 / NFR-03):WAL 单写者模型,并发写第二者在
|
|
13
13
|
// busy_timeout=5000(init PRAGMA)后抛 SQLITE_BUSY。busy_timeout 已在引擎层处理大部分等待;
|
|
@@ -252,7 +252,7 @@ export class DB {
|
|
|
252
252
|
CREATE TABLE IF NOT EXISTS project (
|
|
253
253
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
254
254
|
name TEXT NOT NULL,
|
|
255
|
-
schema_version INTEGER DEFAULT
|
|
255
|
+
schema_version INTEGER DEFAULT 6,
|
|
256
256
|
created_at TEXT NOT NULL,
|
|
257
257
|
updated_at TEXT NOT NULL
|
|
258
258
|
)
|
|
@@ -271,7 +271,8 @@ export class DB {
|
|
|
271
271
|
platform_change_id INTEGER,
|
|
272
272
|
platform_workspace_id INTEGER,
|
|
273
273
|
platform_last_sync TEXT,
|
|
274
|
-
platform_sync_enabled INTEGER DEFAULT 0
|
|
274
|
+
platform_sync_enabled INTEGER DEFAULT 0,
|
|
275
|
+
owner_session TEXT
|
|
275
276
|
)
|
|
276
277
|
`);
|
|
277
278
|
|
|
@@ -370,6 +371,13 @@ export class DB {
|
|
|
370
371
|
// 已语义化,title/quicklog_id 留空。不纳入平台同步脏度(本地展示用元信息)。
|
|
371
372
|
this._migrateAddColumn('changes', 'title', 'TEXT');
|
|
372
373
|
this._migrateAddColumn('changes', 'quicklog_id', 'TEXT');
|
|
374
|
+
|
|
375
|
+
// v6(2026-09-14-change-ownership-guards task-01,D-005@v1):changes 表加 owner_session——
|
|
376
|
+
// change 级所有权载体(接管类操作对他人活跃 change 拒绝执行的心跳窗口判定数据源)。
|
|
377
|
+
// NULL=无主(存量行迁移后即此态,任何会话可接管——向后兼容零行为变化);写入方=
|
|
378
|
+
// change-registry 首建/claimChangeOwner(task-02 接线会话标识);消费方=assertChangeOwnership
|
|
379
|
+
// (task-02)+ serializeForSync 平台同步投影。幂等 ALTER(列存在跳过,v5 先例同款)。
|
|
380
|
+
this._migrateAddColumn('changes', 'owner_session', 'TEXT');
|
|
373
381
|
}
|
|
374
382
|
|
|
375
383
|
/**
|
package/src/decision-distill.js
CHANGED
|
@@ -80,6 +80,10 @@ function applyField(entry, label, value) {
|
|
|
80
80
|
case '模块域': case '模块': case 'domains': case 'domain': entry.domains = parseListValue(value); break
|
|
81
81
|
// 文件字段(FR-01/D-001@v1,2026-09-11-cross-change-decision-guard):列表语义同模块域 + 逐项反斜杠归一 POSIX
|
|
82
82
|
case '文件': case 'files': entry.files = parseListValue(value).map(s => s.replace(/\\/g, '/')); break
|
|
83
|
+
// 故障面/退役判据(FR-01/D-001@v1,2026-09-15-tax-governance):自维护税治理字段——本决策
|
|
84
|
+
// 引入的新失败模式 / 出现什么信号时简化或删除本机制;单值语义,仅中文标签(模板同源)
|
|
85
|
+
case '故障面': entry.failureMode = value; break
|
|
86
|
+
case '退役判据': entry.retireWhen = value; break
|
|
83
87
|
case '否决理由': case 'reject_reason': case 'rejectreason': entry.rejectReason = value; break
|
|
84
88
|
case '复潮条件': case 'revisit_when': case 'revisitwhen': entry.revisitWhen = value; break
|
|
85
89
|
case 'supersedes': entry.supersedes = value; break
|
|
@@ -95,7 +99,8 @@ function applyField(entry, label, value) {
|
|
|
95
99
|
* @returns {{ entries: Array<{
|
|
96
100
|
* id: string, number: string, version: number, title: string,
|
|
97
101
|
* type?: string, status?: string, question?: string, answer?: string,
|
|
98
|
-
* anchor?: string, domains?: string[], files?: string[],
|
|
102
|
+
* anchor?: string, domains?: string[], files?: string[], failureMode?: string, retireWhen?: string,
|
|
103
|
+
* rejectReason?: string, revisitWhen?: string,
|
|
99
104
|
* supersedes?: string, impacts?: string, normalizedRequirement?: string,
|
|
100
105
|
* selected: 'implemented'|'rejected'|null, raw: string }>, missing: boolean }}
|
|
101
106
|
* missing=true = decisions.md 不存在;selected = FR-02 入选裁决,null 表示解析保留但不入选。
|
|
@@ -166,8 +171,10 @@ export function parseDecisions(changeDir) {
|
|
|
166
171
|
return { entries, missing: false, zeroWithContent: hasDecisionLike && entries.length === 0 }
|
|
167
172
|
}
|
|
168
173
|
|
|
169
|
-
/** applyField 认识的字段标签白名单(行内 |
|
|
170
|
-
|
|
174
|
+
/** applyField 认识的字段标签白名单(行内 | 分段与缩进子项只按白名单进字段,散文误伤为零)。
|
|
175
|
+
* 故障面/退役判据(2026-09-15-tax-governance FR-01):白名单双触点之二——缺收录则两标签
|
|
176
|
+
* 永远留 raw 静默丢弃(applyField case 缺一不可)。 */
|
|
177
|
+
const FIELD_LABEL_RE = /^(type|types|status|question|问题|answer|答案|锚点|anchor|模块域|模块|domains|domain|否决理由|reject_reason|rejectreason|复潮条件|revisit_when|revisitwhen|supersedes|impacts|影响|文件|files|normalized_requirement|状态|类型|故障面|退役判据)$/i
|
|
171
178
|
|
|
172
179
|
// ---------------------------------------------------------------------------
|
|
173
180
|
// 域三级兜底(FR-03)
|
|
@@ -300,6 +307,11 @@ function renderBlockLines(entry, headHash, supersedesNote, changeName) {
|
|
|
300
307
|
lines.push(`最近确认:${oneLine(headHash) || NOT_RECORDED}`)
|
|
301
308
|
lines.push(`理由:${oneLine(entry.answer || entry.normalizedRequirement || entry.question)}`)
|
|
302
309
|
if (supersedesNote) lines.push(`supersedes:${oneLine(supersedesNote)}`)
|
|
310
|
+
// 故障面/退役判据行(FR-01/D-001@v1,2026-09-15-tax-governance):仅非空渲染——存量条目零迁移、
|
|
311
|
+
// 幂等重归档不添空行(先例 :298-299 文件行);位置在「理由:/supersedes:」之后、rejected 专属行
|
|
312
|
+
// 之前——绝不插在「锚点:」与「文件:」行之间(decision-file-field.test 行序断言红线)
|
|
313
|
+
if (entry.failureMode) lines.push(`故障面:${oneLine(entry.failureMode)}`)
|
|
314
|
+
if (entry.retireWhen) lines.push(`退役判据:${oneLine(entry.retireWhen)}`)
|
|
303
315
|
if (entry.selected === 'rejected') {
|
|
304
316
|
lines.push(`否决理由:${oneLine(entry.rejectReason)}`)
|
|
305
317
|
lines.push(`复潮条件:${oneLine(entry.revisitWhen)}`)
|
package/src/design-facts.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* decision_module_ref_invalid(ERROR)/ decision_module_domain_gap(WARNING)/
|
|
22
22
|
* decision_module_check_skipped(skipped 原因字符串,接线层包 code)。
|
|
23
23
|
*/
|
|
24
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
24
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
25
25
|
import { join } from 'node:path'
|
|
26
26
|
import { parseDecisions } from './decision-distill.js'
|
|
27
27
|
import { parseFileChangeListDetailed } from './change-list.js'
|
|
@@ -134,6 +134,26 @@ export function loadModuleMap(specRoot, project) {
|
|
|
134
134
|
return { ids, prefixPairs }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* 在兄弟项目的 map 里定位模块 id 归属(多项目仓 docs/<其他项目>/modules/_module-map.yaml)。
|
|
139
|
+
* 幻觉 ERROR 的消歧源(2026-09-14 quick,用户反馈①):id 真实存在但属另一子项目时报
|
|
140
|
+
* 「归属 + 本次核验只认当前项目 map」,堵 agent 按占位提示误读子项目 map 的死胡同。
|
|
141
|
+
* @param {string} specRoot 规范根目录
|
|
142
|
+
* @param {string} currentProject 当前核验项目(跳过自身)
|
|
143
|
+
* @param {string} id 模块 id
|
|
144
|
+
* @returns {string | null} 命中的项目名(docs/ 下第一段目录名),无命中/不可读 → null
|
|
145
|
+
*/
|
|
146
|
+
function locateIdInSiblingProjects(specRoot, currentProject, id) {
|
|
147
|
+
let entries
|
|
148
|
+
try { entries = readdirSync(join(specRoot, 'docs'), { withFileTypes: true }) } catch { return null }
|
|
149
|
+
for (const e of entries) {
|
|
150
|
+
if (!e.isDirectory() || e.name.startsWith('.') || e.name === currentProject) continue
|
|
151
|
+
const m = loadModuleMap(specRoot, e.name)
|
|
152
|
+
if (m && m.ids.has(id)) return e.name
|
|
153
|
+
}
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
137
157
|
// ---------------------------------------------------------------------------
|
|
138
158
|
// 核验(FR-02 分级:ERROR / WARNING / skipped)
|
|
139
159
|
// ---------------------------------------------------------------------------
|
|
@@ -162,7 +182,8 @@ export function deriveActualModules(filePaths, prefixPairs) {
|
|
|
162
182
|
* 核验变更决策的模块域引用(FR-02/D-002/D-004;brainstorm「生成规范文件」步 --done gate 本体,
|
|
163
183
|
* 接线在 task-02)。分级:
|
|
164
184
|
* - ERROR(decision_module_ref_invalid):
|
|
165
|
-
* · 模块域项 ∉ _module-map.yaml 且无 NEW:
|
|
185
|
+
* · 模块域项 ∉ _module-map.yaml 且无 NEW: 前缀(模块幻觉)——出路提示二选一;消息内嵌
|
|
186
|
+
* 当前项目 map 路径 + 合法 id 示例,id 命中兄弟项目 map 时改报归属消歧(仍拦);
|
|
166
187
|
* · 裸项 'NEW:'(`NEW: <名>` 冒号后带空格被列表切分)——书写错误,附正确写法。
|
|
167
188
|
* - WARNING(decision_module_domain_gap):
|
|
168
189
|
* · design.md 文件清单推导的实改模块集 × 声明域并集差异(双向,措辞留人工裁量);
|
|
@@ -202,7 +223,16 @@ export function validateDecisionModuleRefs({ changeDir, specRoot, project } = {}
|
|
|
202
223
|
} else if (item.startsWith('NEW:')) {
|
|
203
224
|
// 新模块声明:豁免存在性核验(D-002/D-004),原样保留
|
|
204
225
|
} else if (!moduleMap.ids.has(item)) {
|
|
205
|
-
|
|
226
|
+
// 消息自足(2026-09-14 quick,用户反馈①):内嵌解析出的 map 路径 + 合法 id 示例,
|
|
227
|
+
// agent 不必再猜「合法 id 从哪张 map 取」;多项目仓先查兄弟项目 map 消歧——id 真实
|
|
228
|
+
// 存在但属其他子项目时报归属,而非笼统「模块幻觉」(语义不变:仍 ERROR 拦)。
|
|
229
|
+
const mapRel = `docs/${project}/modules/_module-map.yaml`
|
|
230
|
+
const ids = [...moduleMap.ids]
|
|
231
|
+
const sample = `${ids.slice(0, 5).join('、')}${ids.length > 5 ? ' 等' : ''}`
|
|
232
|
+
const sibling = locateIdInSiblingProjects(specRoot, project, item)
|
|
233
|
+
errors.push(sibling
|
|
234
|
+
? `[decision_module_ref_invalid] ${id} 模块域含未注册模块 "${item}"——该 id 属于子项目 ${sibling}(docs/${sibling}/modules/_module-map.yaml),本次核验只认当前项目 ${mapRel} 的 id(合法示例:${sample},共 ${ids.length} 个);跨项目引用请补录当前项目 map 或改用 NEW:${item} 前缀`
|
|
235
|
+
: `[decision_module_ref_invalid] ${id} 模块域含未注册模块 "${item}"——合法 id 来源:${mapRel}(示例:${sample},共 ${ids.length} 个);补录该 map 或改用 NEW:${item} 前缀声明新模块`)
|
|
206
236
|
}
|
|
207
237
|
}
|
|
208
238
|
}
|
package/src/docs-gate.js
CHANGED
|
@@ -11,10 +11,19 @@
|
|
|
11
11
|
*
|
|
12
12
|
* exit code:0 过(≤基线)/ 1 拦(>基线)/ 2 配置或 IO 错误(含无基线)。
|
|
13
13
|
* 纯判定逻辑(evaluateRatchet)与 IO 面(runDocsGate)分离,前者可单测。
|
|
14
|
+
*
|
|
15
|
+
* 坑 docs-gate-stale-baseline(ql-20260915-004,2026-09-15 用户实证):基线文件是静态快照,
|
|
16
|
+
* 远端 origin/main 已合入的失效消化/新增不会回流本地基线——出现「基线 371 < origin/main
|
|
17
|
+
* 实测 379、本地 current 379」时 ratchet 拦 379>371,但本次推送零增量(不劣于远端)——
|
|
18
|
+
* ratchet 本质=拦增量,被陈旧基线破坏成拦存量。修复:current > baseline 分支先实测
|
|
19
|
+
* origin/main 树(临时 detach worktree 跑 runDocsCheck),current ≤ 实测值即放行 + 提示
|
|
20
|
+
* 重锚;实测失败/无远端 ref fail-open 回原拦。快路径(current ≤ baseline)零成本零变化。
|
|
14
21
|
*/
|
|
15
|
-
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
16
23
|
import { join } from 'node:path';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
17
25
|
import { runDocsCheck, readDocsCheckConfig, DocsCheckConfigError } from './docs-check.js';
|
|
26
|
+
import { safeGit } from './git-helper.js';
|
|
18
27
|
|
|
19
28
|
/** 基线文件名(相对 specBase/.sillyspec 根;specBase 由调用方传入完整路径锚) */
|
|
20
29
|
export const BASELINE_FILENAME = 'docs-check-baseline';
|
|
@@ -58,6 +67,73 @@ export function writeBaseline(specBase, value) {
|
|
|
58
67
|
writeFileSync(join(specBase, BASELINE_FILENAME), `${value}\n`, 'utf8');
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 实测远端基准树的 docs check 失效数(坑 docs-gate-stale-baseline,ql-20260915-004)。
|
|
72
|
+
*
|
|
73
|
+
* 为什么临时 worktree 而非 runDocsCheck 的 --against reader:against reader 的干净文件
|
|
74
|
+
* 走磁盘直读(磁盘 == HEAD 才成立,docs-check.js createHeadReader :88)——ref=origin/main
|
|
75
|
+
* 时本地领先 origin 的已提交内容会被误当 origin 版,实测失真。临时 detach worktree
|
|
76
|
+
* (`git worktree add --detach <tmp> origin/main`)检出的是真远端树,磁盘即实测基。
|
|
77
|
+
*
|
|
78
|
+
* 配置口径:读树内 local.yaml(gitignored → 通常缺省 → DEFAULT_DOC_PATHS,与远端真实
|
|
79
|
+
* CI 口径一致——设备本地 skip 定制不污染远端实测)。清理含 .git 锁容错:worktree remove
|
|
80
|
+
* 失败 → prune + rmSync 兜底;仍失败 stderr 一行不抛(临时目录残留 OS 清理,不阻断 gate)。
|
|
81
|
+
*
|
|
82
|
+
* @param {string} projectRoot 源码仓根
|
|
83
|
+
* @param {string[]} [remoteRefs] 候选远端 ref(origin/main 优先;master 缺省仓兜底)
|
|
84
|
+
* @returns {Promise<{ originCount: number|null, ref: string|null, error: string|null }>}
|
|
85
|
+
* originCount null = 未实测(无 ref / worktree 建/跑失败——调用方 fail-open 回原拦)
|
|
86
|
+
*/
|
|
87
|
+
export async function measureRemoteBaselineCount(projectRoot, remoteRefs = ['origin/main', 'origin/master']) {
|
|
88
|
+
let ref = null;
|
|
89
|
+
for (const r of remoteRefs) {
|
|
90
|
+
if ((safeGit(projectRoot, ['rev-parse', '--verify', '--quiet', r]) || {}).value) { ref = r; break; }
|
|
91
|
+
}
|
|
92
|
+
if (!ref) return { originCount: null, ref: null, error: 'no-remote-ref' };
|
|
93
|
+
|
|
94
|
+
const tmp = mkdtempSync(join(tmpdir(), 'sillyspec-docsgate-remote-'));
|
|
95
|
+
let originCount = null;
|
|
96
|
+
let error = null;
|
|
97
|
+
try {
|
|
98
|
+
// git worktree add 接受已存在的空目录(mkdtempSync 产物);失败 fail-open 由调用方回原拦
|
|
99
|
+
const added = safeGit(projectRoot, ['worktree', 'add', '--detach', tmp, ref], { timeout: 120000 });
|
|
100
|
+
if (added.error) {
|
|
101
|
+
error = added.error;
|
|
102
|
+
return { originCount: null, ref, error };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const cfg = readDocsCheckConfig(tmp);
|
|
106
|
+
const res = runDocsCheck({
|
|
107
|
+
projectRoot: tmp,
|
|
108
|
+
paths: cfg.paths,
|
|
109
|
+
skip: cfg.skip,
|
|
110
|
+
keywordAssert: cfg.keywordAssert,
|
|
111
|
+
crossRepoRoots: cfg.crossRepoRoots,
|
|
112
|
+
});
|
|
113
|
+
originCount = res.invalid.length;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (e instanceof DocsCheckConfigError) {
|
|
116
|
+
error = e.message;
|
|
117
|
+
originCount = null;
|
|
118
|
+
} else {
|
|
119
|
+
throw e;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
try {
|
|
124
|
+
const removed = safeGit(projectRoot, ['worktree', 'remove', '--force', tmp]);
|
|
125
|
+
if (removed.error) {
|
|
126
|
+
// .git 锁/并发竞争容错:prune 清注册残留 + 直删目录兜底
|
|
127
|
+
safeGit(projectRoot, ['worktree', 'prune']);
|
|
128
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* 目录已不在 */ }
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* 同上 */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { originCount, ref, error };
|
|
135
|
+
}
|
|
136
|
+
|
|
61
137
|
/**
|
|
62
138
|
* IO 入口:跑一次 gate。
|
|
63
139
|
* @param {{ projectRoot: string, specBase: string, initBaseline?: boolean }} opts
|
|
@@ -83,7 +159,7 @@ export async function runDocsGate(opts = {}, checkOpts = {}) {
|
|
|
83
159
|
});
|
|
84
160
|
} catch (e) {
|
|
85
161
|
if (e instanceof DocsCheckConfigError) {
|
|
86
|
-
return { exitCode: 2, ok: false, current: null, baseline: null, delta: null, message: `docs gate 配置错误:${e.message}`, inited: false };
|
|
162
|
+
return { exitCode: 2, ok: false, current: null, baseline: null, delta: null, originCount: null, message: `docs gate 配置错误:${e.message}`, inited: false };
|
|
87
163
|
}
|
|
88
164
|
throw e;
|
|
89
165
|
}
|
|
@@ -91,20 +167,43 @@ export async function runDocsGate(opts = {}, checkOpts = {}) {
|
|
|
91
167
|
|
|
92
168
|
if (initBaseline) {
|
|
93
169
|
writeBaseline(specBase, current);
|
|
94
|
-
return { exitCode: 0, ok: true, current, baseline: current, delta: 0, message: `📌 docs gate: 基线已初始化为当前实测 ${current} 处失效(${baselineDisplay(specBase)})`, inited: true };
|
|
170
|
+
return { exitCode: 0, ok: true, current, baseline: current, delta: 0, originCount: null, message: `📌 docs gate: 基线已初始化为当前实测 ${current} 处失效(${baselineDisplay(specBase)})`, inited: true };
|
|
95
171
|
}
|
|
96
172
|
|
|
97
173
|
const baseline = readBaseline(specBase);
|
|
98
174
|
if (baseline === null) {
|
|
99
175
|
return {
|
|
100
|
-
exitCode: 2, ok: false, current, baseline: null, delta: null,
|
|
176
|
+
exitCode: 2, ok: false, current, baseline: null, delta: null, originCount: null,
|
|
101
177
|
message: `❌ docs gate: 无基线文件(${baselineDisplay(specBase)})。首次使用先跑 sillyspec docs gate --init-baseline(以当前实测数立基线,存量既往不咎只拦增量)`,
|
|
102
178
|
inited: false,
|
|
103
179
|
};
|
|
104
180
|
}
|
|
105
181
|
if (Number.isNaN(baseline)) {
|
|
106
|
-
return { exitCode: 2, ok: false, current, baseline: null, delta: null, message: `❌ docs gate: 基线文件损坏(非非负整数),手工修正或 --init-baseline 重置`, inited: false };
|
|
182
|
+
return { exitCode: 2, ok: false, current, baseline: null, delta: null, originCount: null, message: `❌ docs gate: 基线文件损坏(非非负整数),手工修正或 --init-baseline 重置`, inited: false };
|
|
107
183
|
}
|
|
108
184
|
const v = evaluateRatchet({ current, baseline });
|
|
109
|
-
|
|
185
|
+
if (v.ok) {
|
|
186
|
+
// 快路径(current ≤ baseline):原路零变化——不触远端实测(零 git 成本零行为漂移)
|
|
187
|
+
return { exitCode: 0, ok: true, current, baseline, delta: v.delta, originCount: null, message: v.message, inited: false };
|
|
188
|
+
}
|
|
189
|
+
// 坑 docs-gate-stale-baseline(ql-20260915-004):current > baseline 时 origin 实测兜底——
|
|
190
|
+
// ratchet 本质=拦增量;基线是静态快照,远端已合入的失效增长不回流基线会造成「未劣于远端
|
|
191
|
+
// 也被拦」的假拦。实测 origin/main 树:current ≤ 实测值 = 本次不劣于远端 → 放行 + 提示
|
|
192
|
+
// 重锚;无远端 ref / 实测失败 fail-open 回原拦;current > 实测值 = 真增量劣于远端 → 拦。
|
|
193
|
+
const measured = await measureRemoteBaselineCount(projectRoot);
|
|
194
|
+
if (measured.originCount !== null && current <= measured.originCount) {
|
|
195
|
+
return {
|
|
196
|
+
exitCode: 0, ok: true, current, baseline, delta: v.delta, originCount: measured.originCount,
|
|
197
|
+
message: `⚠️ docs gate: 基线陈旧:基线 ${baseline} < ${measured.ref} 实测 ${measured.originCount},本次 ${current} 处失效未劣于远端不拦——建议 sillyspec docs gate --init-baseline 重锚锁定(以当前实测 ${current} 立线,存量既往不咎只拦增量)`,
|
|
198
|
+
inited: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const remoteNote = measured.originCount !== null
|
|
202
|
+
? `,且劣于 ${measured.ref} 实测 ${measured.originCount} 处(真增量)`
|
|
203
|
+
: '';
|
|
204
|
+
return {
|
|
205
|
+
exitCode: 1, ok: false, current, baseline, delta: v.delta, originCount: measured.originCount,
|
|
206
|
+
message: `❌ docs gate: ${current} 处失效 > 基线 ${baseline}(新增 ${v.delta} 处)${remoteNote},拦截。修掉新增引用或显式 --init-baseline 重置基线(需你确认存量合法)`,
|
|
207
|
+
inited: false,
|
|
208
|
+
};
|
|
110
209
|
}
|