sillyspec 3.33.1 → 3.33.3
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-flow/SKILL.md +17 -9
- package/AGENTS.md +3 -3
- package/README.md +2 -2
- package/assets/command-cards/flow.md +8 -4
- package/package.json +1 -1
- package/src/cross-repo-reconcile.js +4 -1
- package/src/decision-distill.js +22 -8
- package/src/decision-values.js +34 -0
- package/src/decisions-io.js +5 -2
- package/src/design-facts.js +1 -1
- package/src/flow-draft.js +15 -9
- package/src/flow-parity.js +93 -5
- package/src/flow-review-pack.js +215 -0
- package/src/flow-review.js +76 -11
- package/src/flow.js +258 -47
- package/src/index.js +21 -14
- package/src/init.js +7 -30
- package/src/run/command.js +1 -1
- package/src/run/gate-snapshot.js +47 -14
- package/src/run/gates.js +2 -2
- package/src/scope-audit.js +6 -1
- package/src/stage-templates.js +3 -2
- package/src/stages/brainstorm.js +6 -4
- package/src/task-tick.js +22 -0
- package/src/ui-visual.js +3 -0
- package/templates/agents-instruction.md +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sillyspec:flow
|
|
3
|
-
description:
|
|
3
|
+
description: 轻量变更(默认快道)——需求明确的小/中改动。适合用户说"直接改、快速修、顺手调整、改个文案、修个小 bug、更新一个文件、需求很明确、开干吧、把这个改了"。协议链:flow start → 补 spec 面(FR 绑定+design 四节)→ 批准(flow approve / 用户显式 --autopilot)→ 干活(改代码写测试)→ flow done(实测门+测试绑定+patch 留档)。顺序纪律:先答 spec 面、批准后才动码(读代码探究不算动码)。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## 何时使用
|
|
@@ -11,7 +11,9 @@ description: 轻量变更(默认快道)——需求明确的小/中改动两
|
|
|
11
11
|
- 需求不清晰 → CLI 会在 flow start 处拦下给两选一(头脑风暴预段 / 确认输入已含决策);或直接先走 `/sillyspec:brainstorm`,完成后 `flow start` 收编续跑(design 以头脑风暴版为准)
|
|
12
12
|
- 大改动(跨模块取舍 / 需要 Wave 计划编排 / 设计期人机对抗)→ 不走本 skill,走完整五阶段流程
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## 协议链与顺序纪律
|
|
15
|
+
|
|
16
|
+
顺序正典:flow start 后**先答 spec 面 → 断点① 批准 → 才动码**(读代码探究不算动码)。批准是动码前置义务,不是收口补救动作。
|
|
15
17
|
|
|
16
18
|
### ① flow start(立项 + 锁基线)
|
|
17
19
|
|
|
@@ -35,16 +37,22 @@ sillyspec flow start --change <变更名> --input "<一句 10-30 字中文概括
|
|
|
35
37
|
- 新增单测覆盖窗口翻转边界,npm test 全绿
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
### ②
|
|
39
|
-
|
|
40
|
-
改代码、写测试。治理工件(proposal/requirements/design)CLI 机器起草,你只需要:
|
|
40
|
+
### ② 补 spec 面(动码前)
|
|
41
41
|
|
|
42
|
-
-
|
|
42
|
+
- requirements:每条 FR 行为句(带强度词 必须/禁止/SHOULD)+ 测试绑定行(无测试面写「不适用:理由」)
|
|
43
|
+
- design.md 四节 AGENT 槽作答(做法概述/接口契约/边界并发四问/风险与死路)——**动码前完成**,每节至少一行,写「不适用:<理由>」也算答,空节 flow done 拒收;列改动文件用独立「## 文件变更清单」节
|
|
43
44
|
- 变更目录若已有收割生成的 decisions.md(frontmatter `author: flow-machine-draft`),它是底稿不是终稿:槽4 作答含「放弃方案」的,收口前把每个放弃方案拆成独立 rejected 条目(问题=决策专属问句、否决理由=放弃理由、复潮条件=重提前提),风险与对策留在主条目——rejected 条目才走蒸馏防复潮通道,埋在散文里的否决理由进不了通道;无实质放弃方案则不动
|
|
44
|
-
-
|
|
45
|
-
|
|
45
|
+
- tasks.md 改写为真实工作分解(机器种子按实现路径改写,保持 `- [ ] task-NN:` 行形态)
|
|
46
|
+
|
|
47
|
+
### ③ 断点①:批准后才动码
|
|
48
|
+
|
|
49
|
+
把 FR 条目+design 四节摘要给用户看 → 用户运行 `sillyspec flow approve --change <名>`(agent 代跑如实声明通道 `--user-confirmed` / `--self-approved`,无 flag 按 unattested 留痕);用户明确说「直接跑完/不用问我」→ 重跑 `flow start --autopilot` 声明豁免(用户显式授权,非 agent 自选补救)。未批准且未声明 --autopilot 时 flow done 拒收。
|
|
50
|
+
|
|
51
|
+
### ④ 干活(已批准)
|
|
52
|
+
|
|
53
|
+
写测试、改代码(按层适用——纯逻辑先写测试、行为契约断言用户可见行为、视觉样式与纯配置不写测试)。以 tasks.md 为进度源:做一件 → 勾一格(`- [ ]` → `- [x]`)→ 继续下一条,勿攒一把勾(勾选是收口哨兵的证据面——逐 task 核提交 token/review.json);`flow status --change <名>` 为自愿查看/恢复面(②阶段给下一任务指针与进度,非协议必需——D-007);交付代码用显式 pathspec 提交(`git commit -m "..." -- 文件1 文件2`;patch 冻结面=baseline..HEAD 提交面,未提交的代码不进审计件)
|
|
46
54
|
|
|
47
|
-
###
|
|
55
|
+
### ⑤ flow done(收口)
|
|
48
56
|
|
|
49
57
|
```bash
|
|
50
58
|
sillyspec flow done --change <变更名>
|
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
## 选道(动手前先对号入座)
|
|
8
8
|
| 需求形态 | 走法 |
|
|
9
9
|
|---|---|
|
|
10
|
-
| 需求已含决策(改什么、成功标准说得清——成功标准须来自**用户原话或既有文档转写**;要靠你推断拟出的标准=需求不清晰) | **轻量变更(默认快道)**:`flow start` →
|
|
10
|
+
| 需求已含决策(改什么、成功标准说得清——成功标准须来自**用户原话或既有文档转写**;要靠你推断拟出的标准=需求不清晰) | **轻量变更(默认快道)**:`flow start` → 补 spec 面(FR 绑定+design 四节)→ 批准(`flow approve` / 用户显式 `--autopilot`)→ 干活 → `flow done`(协议调用仍 2 次,批准由用户运行;操作细节见 `/sillyspec:flow` skill 与 flow start 输出) |
|
|
11
11
|
| 用户显式要求极速(「极速/别验证/只要 spec 和代码」类原话)且需求已含决策 | **轻量变更极速版**:同上行加 `--turbo`(`flow start --change <名> --input "<...>" --turbo`)——**仅用户显式触发,禁止你自行启用**;收口跳过实测与重仪式门(hunk/dirty/ui 探针/评审),spec 结构校验/tasks 全勾/ledger/patch 冻结/distill/归档照常(跳的是验证不是证据);升厚即失效恢复全量仪式;收口输出显式标注「不含测试客观核验」。预段组合极速:头脑风暴预段走轻量跑判定+单轮合并问询+burst 一次 --done,收编时按用户要求带 --turbo |
|
|
12
12
|
| 需求不清晰 / 需要方案探索(含「成功标准要你替用户拟」的形态——自己拟标准再自答是把默认共识伪装成共识) | 头脑风暴预段:`run brainstorm --change <名>` → 完成后 `flow start --change <名>` 收编续跑(design 以头脑风暴版为准)。上个轻量变更被 route-hindsight 标记时全新 start 会被同意门拦下要求显式选道:确认已含决策带 `--no-prestage` 声明留痕,否则先走本行预段 |
|
|
13
13
|
| 大改动:跨模块取舍 / Wave 计划编排 / 多阶段治理 / 设计期人机对抗 | 完整流程五阶段:`run brainstorm → plan → execute → verify → archive`(每阶段一次渲染 + 一次 --done 收口;细节见各阶段 skill 与 CLI 输出) |
|
|
@@ -32,8 +32,8 @@ sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文
|
|
|
32
32
|
## 核心规则
|
|
33
33
|
1. **禁止绕过本文件规则和 SillySpec 流程**。所有变更走 sillyspec 流程,不裸改裸提交。
|
|
34
34
|
2. **改代码前必须先说明依据**——依据的文档路径(design.md / 模块文档 / file-lifecycle.md(如有))或现有代码依据,无依据不改。
|
|
35
|
-
3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 →
|
|
36
|
-
4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`。以落盘文件与测试结果为准,不信口头"已完成"
|
|
35
|
+
3. **执行顺序**:文档 → 读代码 → 写测试 → 写实现 → 跑测试 → 验收 → 更新文档(轻量道即:FR 绑定+design 四节+用户批准先于动码,读代码探究不算动码)。测试按层适用:纯逻辑先写测试,行为契约断言用户可见行为,视觉样式与纯配置/文档不写测试(记「不适用:理由」,验收靠规范与人工)。
|
|
36
|
+
4. **实证核验再收口**:触及 `src`/`test` 的改动,CLI 会在收口时亲自实测(实测失败阻断收口;纯 doc/配置自动跳过)。测试面按变更动态推断(本变更测试 ∪ FR 关联回归 ∪ import 依赖,runner 自项目结构推断——无需配置);lint 走 `.sillyspec/local.yaml` 的 `commands.lint`。以落盘文件与测试结果为准,不信口头"已完成"。自验口径=变更测试面(新增测试+受影响既有面)+ lint 即收口证据——全量套件归 pre-push 门,勿每变更重复手跑(共享仓全量红先做归属判别,非本变更引入如实留痕不阻断);跑批输出落 `.sillyspec/.runtime/logs/` 日志回看尾部,勿整段吞掉。
|
|
37
37
|
5. **非测试逻辑本身有误时,禁止改测试来"通过"**——修逻辑,不修测试。
|
|
38
38
|
6. **git hook 拦截提交时禁止跳过**(如 `.husky/pre-push`),修复问题后再提交。
|
|
39
39
|
7. **代码必须兼容 Windows / Linux / macOS**(路径 / 换行 / 并发都要顾)。
|
package/README.md
CHANGED
|
@@ -46,9 +46,9 @@ npx sillyspec init
|
|
|
46
46
|
| Claude Code(推荐) | `claude` | `AGENTS.md`(完整指引)+ `CLAUDE.md`(`@AGENTS.md` 指针)+ `.claude/skills/sillyspec-*/` |
|
|
47
47
|
| Cursor | `cursor` | `.cursor/skills/sillyspec-*/` |
|
|
48
48
|
| OpenAI Codex | `codex` | `AGENTS.md`(完整指引) |
|
|
49
|
-
| OpenCode | `opencode` | `INSTRUCTIONS.md` |
|
|
50
49
|
| OpenClaw | `openclaw` | `.openclaw/skills/sillyspec-*/` |
|
|
51
|
-
|
|
|
50
|
+
| OpenCode | `opencode` | `AGENTS.md`(完整指引)+ `.opencode/skills/sillyspec-*/` |
|
|
51
|
+
| Gemini | `gemini` | `AGENTS.md`(完整指引) |
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
npx sillyspec init --tool claude # 指定工具
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sillyspec:flow
|
|
3
|
-
description:
|
|
3
|
+
description: 轻量变更(默认快道):需求明确的小/中改动——flow start → 补 spec 面 → 批准 → 干活 → flow done
|
|
4
4
|
---
|
|
5
5
|
# 何时用
|
|
6
6
|
|
|
@@ -18,12 +18,16 @@ sillyspec flow start --change <变更名> --input "<一句 10-30 字中文概括
|
|
|
18
18
|
# 极速变体:用户显式说「极速/别验证/只要 spec 和代码」类原话时才加 --turbo(仅用户显式触发,
|
|
19
19
|
# 禁止你自行启用)——flow-state 留痕,收口跳过实测与重仪式门(hunk/dirty/ui 探针/评审),
|
|
20
20
|
# spec 结构校验、tasks 全勾、ledger、patch 冻结、distill、归档照常;升厚即失效恢复全量仪式。
|
|
21
|
-
# ②
|
|
22
|
-
#
|
|
21
|
+
# ② 补 spec 面(动码前):requirements FR 行为句(强度词)+ 测试绑定行、design.md 四节作答
|
|
22
|
+
# (空节 flow done 拒收;列改动文件用独立「## 文件变更清单」节);tasks.md 改写为工作队列
|
|
23
|
+
# ——机器种子改写为实现步骤(每行做什么+怎么验证,保持 `- [ ] task-NN:` 行形态)。
|
|
24
|
+
# 然后过断点①:摘要给用户看 → 用户运行 sillyspec flow approve --change <名>(批准留痕;
|
|
25
|
+
# 用户显式说「直接跑完」→ 重跑 start --autopilot 豁免)。批准是动码前置义务,不是收口补救。
|
|
26
|
+
# ③ 干活(已批准):写测试、改代码(按层适用——纯逻辑先写、行为契约断用户所见、样式/纯配置不写);
|
|
23
27
|
# 执行循环:Working on task N/M → 做一件 → 测试绿后当场勾一格(sillyspec task tick --change <名> --task task-NN
|
|
24
28
|
# 即时回显进度与下一任务)→ 下一行;每格勾选要有对应提交(消息带 task-NN),一把勾收口被拒。
|
|
25
29
|
# 交付代码显式 pathspec 提交(patch 冻结面=baseline..HEAD 提交面,未提交不进审计件)
|
|
26
|
-
#
|
|
30
|
+
# ④ 收口(CLI 亲自实测 test/lint + 独立评审定档 + patch 留档;--turbo 变更自动跳过实测与重仪式门):
|
|
27
31
|
sillyspec flow done --change <变更名>
|
|
28
32
|
# 断点恢复 / 随时查进度:
|
|
29
33
|
sillyspec flow status --change <变更名>
|
package/package.json
CHANGED
|
@@ -215,8 +215,11 @@ export function collectRepoActual({ repoKey, specBase, cwd, runtimeRoot = null,
|
|
|
215
215
|
// 2) 仓根(isAbsolute/resolve 兼容 Windows 盘符绝对注册与相对 cwd 注册)
|
|
216
216
|
const repoPath = isAbsolute(rawPath) ? rawPath : resolve(cwd, rawPath)
|
|
217
217
|
if (!existsSync(repoPath)) {
|
|
218
|
+
// degradedReason 不嵌绝对路径(2026-10-19-cross-repo-face-tool-filter,平台评审 P2):
|
|
219
|
+
// 该文案会随 change-patch.json 冻结并透出到平台资产卡,本机布局属环境信息不外泄;
|
|
220
|
+
// repoPath 仍留在返回值供本机诊断使用。
|
|
218
221
|
return { repo: repoKey, repoPath, anchor: degradedAnchor(), files: [],
|
|
219
|
-
degradedReason:
|
|
222
|
+
degradedReason: `注册路径不可达(local.yaml repos.${repoKey} 的注册值在本机不存在)——请人工到对应仓核对` }
|
|
220
223
|
}
|
|
221
224
|
|
|
222
225
|
// 3) A 档 reviews-range:runtimeRoot 与 changeName 双提供才尝试(缺省跳过 = 向后兼容)
|
package/src/decision-distill.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏异态判定(2026-10-20-distill-abnormal-upgrade-fix):needsWait 非空是唯一异态信号——
|
|
3
|
+
* 与 needsWait 产出(distillIntoKnowledge)单一源同处,返回值契约变化时两处同文件可见不漂移。
|
|
4
|
+
* 历史:flow done 曾内联 `res.rejected > 0 || res.needsWait > 0`——前半引用返回值不存在的字段
|
|
5
|
+
* (幽灵引用恒 undefined),后半字符串与 >0 比较恒 false,「异态升厚留人工裁决」从未生效。
|
|
6
|
+
* @param {{ written?: unknown[], skipped?: string|null, needsWait?: string|null }} res distillIntoKnowledge 返回值(容错 null/undefined)
|
|
7
|
+
* @returns {boolean} needsWait 为非空字符串时 true
|
|
8
|
+
*/
|
|
9
|
+
export function isDistillAbnormal(res) {
|
|
10
|
+
return Boolean(res && typeof res.needsWait === 'string' && res.needsWait.length > 0)
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* decision-distill — 决策提炼纯函数(change: 2026-08-23-adopt-harness-practices,FR-02/03/04,D-007@v1)
|
|
3
15
|
*
|
|
@@ -24,14 +36,16 @@
|
|
|
24
36
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
25
37
|
import { modulePathMatches } from './modules.js'
|
|
26
38
|
import { basename, dirname, join } from 'node:path'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
39
|
+
import { KNOWLEDGE_STATUSES, KNOWLEDGE_TYPES } from './decision-values.js'
|
|
40
|
+
|
|
41
|
+
// 入选集=正典单一源派生(2026-10-20-decision-caliber-unify)。历史:五类集漏选 8 条
|
|
42
|
+
// feasibility+accepted 决策(2026-09-11 驾驭第十二批,2026-08-08-concurrent-write-preflight
|
|
43
|
+
// 实证)后扩为 7 值字面集;scope 排除是刻意设计(范围裁决不入库)。现改 KNOWLEDGE_TYPES 派生
|
|
44
|
+
// (恰等于历史 7 值零漂移)。状态集纳入 implemented:知识库入选输出值本就是「状态:implemented」,
|
|
45
|
+
// 输入侧声明 implemented 却永不入选是白名单后加未同步蒸馏的遗留缺陷。
|
|
46
|
+
const IMPLEMENTED_TYPES = new Set(KNOWLEDGE_TYPES)
|
|
47
|
+
/** FR-02:implemented 入选的 status(proposed/draft/superseded 等其余状态不入选) */
|
|
48
|
+
const IMPLEMENTED_STATUSES = new Set(KNOWLEDGE_STATUSES)
|
|
35
49
|
/** 三级兜底终点域(仍不中归此;域文件由本模块自管) */
|
|
36
50
|
const UNMAPPED_DOMAIN = 'unmapped'
|
|
37
51
|
/** 旧格式容错占位值(FR-01:缺锚点/缺 headHash 不阻断) */
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* decision-values.js — 决策 type/status 正典值集单一源(2026-10-20-decision-caliber-unify)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:值集此前三套口径并存——decisions-io 写入白名单(type 6 值缺 feasibility/consistency)、
|
|
5
|
+
* decision-distill 蒸馏入选集(类型 7 值含 scope 排除;状态仅 confirmed/accepted,implemented
|
|
6
|
+
* 写入合法却永不入选)、brainstorm 提示面两处清单(term/premise/risk/unresolved 等机器永不
|
|
7
|
+
* 收录的值)。写入者拒绝蒸馏想收的值、提示面教 agent 写机器不认的值——口径漂移是事故温床
|
|
8
|
+
* (八条 feasibility 决策因五类集漏选入库事故,见 decision-distill.js 头注)。
|
|
9
|
+
*
|
|
10
|
+
* 单一消费契约:
|
|
11
|
+
* - decisions-io 写入白名单 = 正典全集(fail-fast 拦正典外值)
|
|
12
|
+
* - decision-distill 入选裁决 = KNOWLEDGE_* 派生集(scope 刻意不入知识库——范围裁决,
|
|
13
|
+
* 文档明载;KNOWLEDGE_TYPES 恒等于正典去 scope)
|
|
14
|
+
* - brainstorm 决策记录提示清单 = 正典全集(渲染产物 docs/prompt/ 经 _extract 链同步)
|
|
15
|
+
*
|
|
16
|
+
* 开放世界边界:手写 decisions.md 的历史散值(40+ 种 type)不在正典集,由 parseDecisions
|
|
17
|
+
* 宽容解析保留、蒸馏不入选——存量行为零变化,本集只约束机器面(写入校验/入选裁决/提示文案)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** 正典 type:写入白名单全集(8 值)。 */
|
|
21
|
+
export const DECISION_TYPES = Object.freeze([
|
|
22
|
+
'architecture', 'compatibility', 'boundary', 'definition', 'process', 'scope', 'feasibility', 'consistency',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/** 正典 status:写入白名单全集(7 值)。 */
|
|
26
|
+
export const DECISION_STATUSES = Object.freeze([
|
|
27
|
+
'confirmed', 'accepted', 'implemented', 'rejected', 'proposed', 'superseded', 'draft',
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
/** 蒸馏入选 type:正典去 scope(范围裁决刻意不入知识库,保持既有 7 值口径)。 */
|
|
31
|
+
export const KNOWLEDGE_TYPES = Object.freeze(DECISION_TYPES.filter((t) => t !== 'scope'))
|
|
32
|
+
|
|
33
|
+
/** 蒸馏入选 status:定案/接受/已实现三值(rejected 走独立防复潮通道,其余状态不入选)。 */
|
|
34
|
+
export const KNOWLEDGE_STATUSES = Object.freeze(['confirmed', 'accepted', 'implemented'])
|
package/src/decisions-io.js
CHANGED
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
import { existsSync, readFileSync } from 'node:fs'
|
|
18
18
|
import { join } from 'node:path'
|
|
19
19
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
20
|
+
import { DECISION_STATUSES, DECISION_TYPES } from './decision-values.js'
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// 白名单=正典全集单一源派生(2026-10-20-decision-caliber-unify):此前字面集与蒸馏入选集、
|
|
23
|
+
// brainstorm 提示清单三套口径漂移——feasibility/consistency 被写入者拒绝而蒸馏想收。
|
|
24
|
+
const STATUS_VALUES = new Set(DECISION_STATUSES)
|
|
25
|
+
const TYPE_VALUES = new Set(DECISION_TYPES)
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* canonical 条目渲染(正文散文行原样保留在 body 数组)。
|
package/src/design-facts.js
CHANGED
|
@@ -387,7 +387,7 @@ export function generateDesignSkeleton({ changeName, decisionsText, author, now
|
|
|
387
387
|
lines.push('- [ ] frontmatter 字段齐全(author/created_at/scale)')
|
|
388
388
|
lines.push('- [ ] 引用所有当前版本 D-xxx@vN')
|
|
389
389
|
lines.push('- [ ] 涉及生命周期关键词时含「生命周期契约表」或紧邻豁免短语')
|
|
390
|
-
lines.push('- [ ] UI 原型分级核对(涉前端文件时变更目录应有 prototype-*.html——新生成(定稿=与实现同方言的真码产物,手绘仅限粗选,输出须带素材来源声明:token
|
|
390
|
+
lines.push('- [ ] UI 原型分级核对(涉前端文件时变更目录应有 prototype-*.html——新生成(定稿=与实现同方言的真码产物,手绘仅限粗选,输出须带素材来源声明:token 源/组件来源/入口位置;产物必须可溯源到源码(管线编译/真实渲染捕获/登记复用三通道),手写 HTML 不得交付,对比稿供用户决策即定稿)或登记复用(复用须按 prototype-<名称>.html 命名,execute 才注入)或跳过原因记入风险登记;本仓形态未在 local.yaml ui_prototype 登记的,收口前补登记)')
|
|
391
391
|
lines.push('- [ ] 不确定的问题标注「⚠️ 自审存疑」')
|
|
392
392
|
lines.push('')
|
|
393
393
|
|
package/src/flow-draft.js
CHANGED
|
@@ -327,7 +327,7 @@ function draftTasksV2({ change, criteria, withTasks }) {
|
|
|
327
327
|
* 零 advisory——工作分解归 agent,2026-10-07-thin-tasks-v3)。
|
|
328
328
|
* adopted brainstorm 变更(opts.skipDesign)豁免 design 四节门(设计承诺以 brainstorm design 为准)。
|
|
329
329
|
*/
|
|
330
|
-
export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
330
|
+
export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false, repair = true }) {
|
|
331
331
|
const violations = []
|
|
332
332
|
const advisories = []
|
|
333
333
|
const repairs = []
|
|
@@ -392,12 +392,18 @@ export function verifyThinDocsV2({ changeDir, ledger, skipDesign = false }) {
|
|
|
392
392
|
}
|
|
393
393
|
}
|
|
394
394
|
if (repairEntries.length > 0) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
for (const e of repairEntries)
|
|
399
|
-
}
|
|
400
|
-
|
|
395
|
+
// repair:false 只读轨(2026-10-19-approve-gate-tick-nudge):approve 批准面不做自动
|
|
396
|
+
// 回填(写盘修复属 flow done 面)——违规照报,design.md 字节不动。
|
|
397
|
+
if (repair === false) {
|
|
398
|
+
for (const e of repairEntries) violations.push(`design「${e.heading}」问题文本被改写——原文:${e.q}(批准校验只读不自动回填,手工恢复此行后重试)`)
|
|
399
|
+
} else {
|
|
400
|
+
try {
|
|
401
|
+
writeAtomicSync(join(changeDir, 'design.md'), repairedLines.join('\n'))
|
|
402
|
+
dLines = repairedLines
|
|
403
|
+
for (const e of repairEntries) repairs.push(`design「${e.heading}」问题行曾被改写——已按锚回填原文(agent 作答保留):${e.q}`)
|
|
404
|
+
} catch {
|
|
405
|
+
for (const e of repairEntries) violations.push(`design「${e.heading}」问题文本被改写——原文:${e.q}(机器回填写盘失败,请手工恢复此行,答案写在下方)`)
|
|
406
|
+
}
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
409
|
for (const sec of anchorSections) {
|
|
@@ -1057,13 +1063,13 @@ export function amendFlowDraft({ changeDir, change, runtimeRoot }) {
|
|
|
1057
1063
|
* schemaVersion===2 → verifyThinDocsV2(文档↔锚对比:问题/节/FR/绑定拒收 + 漂移 advisory);
|
|
1058
1064
|
* v1(缺省)→ verifyMarkers 指纹三态(存量在途变更零影响)。
|
|
1059
1065
|
* opts.skipDesign:adopted brainstorm 变更豁免 design 四节门(flow.js 判定后透传)。 */
|
|
1060
|
-
export function verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign = false }) {
|
|
1066
|
+
export function verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign = false, repair = true }) {
|
|
1061
1067
|
const ledgerPath = draftLedgerPath(runtimeRoot, change)
|
|
1062
1068
|
if (!existsSync(ledgerPath)) return { applicable: false, violations: [] }
|
|
1063
1069
|
let ledger
|
|
1064
1070
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { return { applicable: false, violations: [] } }
|
|
1065
1071
|
if (ledger.schemaVersion === DRAFT_SCHEMA_V2) {
|
|
1066
|
-
const r = verifyThinDocsV2({ changeDir, ledger, skipDesign })
|
|
1072
|
+
const r = verifyThinDocsV2({ changeDir, ledger, skipDesign, repair })
|
|
1067
1073
|
return { applicable: true, schema: DRAFT_SCHEMA_V2, ...r }
|
|
1068
1074
|
}
|
|
1069
1075
|
const violations = []
|
package/src/flow-parity.js
CHANGED
|
@@ -267,13 +267,24 @@ export function renderThinDocsDebt({ specBase, projectRoot, changedFiles } = {})
|
|
|
267
267
|
*/
|
|
268
268
|
export function renderVerifyReceipt({ change, baseline, head, gateSummary, review, traceCount, patchMeta, generatedAt }) {
|
|
269
269
|
const sha = patchMeta && patchMeta.patchSha256 ? patchMeta.patchSha256.slice(0, 12) : null
|
|
270
|
+
// 空面跨仓冻结(2026-10-19-thin-empty-face-cross-repo-freeze):JSON-only 留档没有顶级
|
|
271
|
+
// patchSha256,但 scopeAudit.repos[] 内有跨仓 patch 正文——回执须如实报「跨仓 N 仓」,
|
|
272
|
+
// 不能写「无冻结件」(否则平台/归档读者把成功冻结误判成缺件)。
|
|
273
|
+
const repoFrozen = patchMeta && patchMeta.scopeAudit && Array.isArray(patchMeta.scopeAudit.repos)
|
|
274
|
+
? patchMeta.scopeAudit.repos.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0)
|
|
275
|
+
: []
|
|
276
|
+
const freezeLine = sha
|
|
277
|
+
? `change.patch(sha256 ${sha}…)`
|
|
278
|
+
: repoFrozen.length > 0
|
|
279
|
+
? `跨仓 ${repoFrozen.length} 仓(diff 见 change-patch.json scopeAudit.repos[];主仓面为空)`
|
|
280
|
+
: '(无冻结件)'
|
|
270
281
|
const reviewLine = review
|
|
271
282
|
? review.verdict === 'exempt'
|
|
272
283
|
? '豁免(低风险证据齐全' + (review.sampled ? '' : '') + ')'
|
|
273
284
|
: `${review.verdict}(reviewer 见 review.json${Number.isFinite(review.findingsP1) ? `,P1 ${review.findingsP1}` : ''})`
|
|
274
285
|
: '—'
|
|
275
286
|
const headNote = head && baseline && head === baseline
|
|
276
|
-
? '
|
|
287
|
+
? '(等于基线——交付代码尚未提交,冻结面以交付留档实际内容为准:change.patch 或 change-patch.json repos[])'
|
|
277
288
|
: ''
|
|
278
289
|
return [
|
|
279
290
|
`---`,
|
|
@@ -287,7 +298,7 @@ export function renderVerifyReceipt({ change, baseline, head, gateSummary, revie
|
|
|
287
298
|
`- **实测面**:${gateSummary || '—'}`,
|
|
288
299
|
`- **独立评审**:${reviewLine}`,
|
|
289
300
|
`- **测试绑定**:${traceCount > 0 ? `${traceCount} 行(test-trace.json,已随发号提升)` : '0(无锚行)'}`,
|
|
290
|
-
`- **交付冻结**:${
|
|
301
|
+
`- **交付冻结**:${freezeLine}`,
|
|
291
302
|
`- **生成**:${generatedAt}(机器合成,勿手改;明细见 flow-telemetry.jsonl / change-patch.json)`,
|
|
292
303
|
``,
|
|
293
304
|
].join('\n')
|
|
@@ -368,9 +379,23 @@ export default { reconcileModuleDocs, renderVerifyReceipt, harvestSlot4Decision,
|
|
|
368
379
|
* 反解,baseline 自 flow-state.yaml 读取;不可得(非 git 仓/无 flow-state/git 失败/归档后
|
|
369
380
|
* 目录已移走)= 行为与旧版完全一致。保守:被无后缀裸提交触碰过的文件不剔(fail-closed)。
|
|
370
381
|
*/
|
|
382
|
+
// 工具治理模板(2026-10-19-freeze-filter-tool-scaffold):各 agent 工具的
|
|
383
|
+
// skills/commands/settings 注入面——不是本项目的交付语义,冻结面出现只会刷屏
|
|
384
|
+
// 真实交付(消防工作区 4e9eb05b 实证:61 文件里 50 个是 .claude/.codex 模板)。
|
|
385
|
+
// AGENTS.md / INSTRUCTIONS.md 是项目级指令(可能是变更的真实交付),保留。
|
|
386
|
+
const TOOL_GOVERNANCE_DIRS = ['.claude/', '.codex/', '.gemini/', '.opencode/', '.zcode/', '.cursor/']
|
|
387
|
+
const TOOL_GOVERNANCE_FILES = new Set(['CLAUDE.md'])
|
|
388
|
+
|
|
389
|
+
export function isToolGovernancePath(f) {
|
|
390
|
+
const p = String(f || '').replace(/\\/g, '/')
|
|
391
|
+
if (!p) return false
|
|
392
|
+
return TOOL_GOVERNANCE_FILES.has(p) || TOOL_GOVERNANCE_DIRS.some((d) => p.startsWith(d))
|
|
393
|
+
}
|
|
394
|
+
|
|
371
395
|
export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
|
|
372
396
|
const base = committedRaw
|
|
373
397
|
.map((f) => String(f).replace(/\\/g, '/'))
|
|
398
|
+
.filter((p) => !isToolGovernancePath(p))
|
|
374
399
|
.filter((p) => !p.startsWith('.sillyspec/') || p.startsWith('.sillyspec/docs/'))
|
|
375
400
|
try {
|
|
376
401
|
const cwd = opts.cwd || process.cwd()
|
|
@@ -409,6 +434,63 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
|
|
|
409
434
|
* @param {{ cwd: string, change: string, freezeHead: string|null }} opts
|
|
410
435
|
* @returns {{ drifted: boolean, ownCommits: string[], head: string|null, ownFiles: string[], governanceOnly: boolean, touchedPromiseFace: boolean }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
|
|
411
436
|
*/
|
|
437
|
+
/**
|
|
438
|
+
* 冻结件完整性探针(2026-10-19-thin-empty-face-cross-repo-freeze):空面跨仓冻结走
|
|
439
|
+
* JSON-only 留档(主仓面为空,`scopeAudit.repos[]` 承载跨仓 patch 正文,无 change.patch)。
|
|
440
|
+
* 存在性复查不能把「无 change.patch」一律判缺件——JSON 在场且 repos 内有 patch 正文即完整。
|
|
441
|
+
* 坏 JSON/读失败按缺件处理(重冻结兜底,fail-safe 不新造静默跳过面)。
|
|
442
|
+
* @returns {{ hasJson: boolean, hasText: boolean, repoPatchCount: number, complete: boolean }}
|
|
443
|
+
*/
|
|
444
|
+
export function inspectFreezeArtifacts(changeDir) {
|
|
445
|
+
const patchJsonPath = join(changeDir, 'change-patch.json')
|
|
446
|
+
const patchTextPath = join(changeDir, 'change.patch')
|
|
447
|
+
const hasJson = existsSync(patchJsonPath)
|
|
448
|
+
const hasText = existsSync(patchTextPath)
|
|
449
|
+
let repoPatchCount = 0
|
|
450
|
+
let jsonOk = false
|
|
451
|
+
if (hasJson) {
|
|
452
|
+
try {
|
|
453
|
+
const meta = JSON.parse(readFileSync(patchJsonPath, 'utf8'))
|
|
454
|
+
jsonOk = true
|
|
455
|
+
const repos = meta && meta.scopeAudit && meta.scopeAudit.repos
|
|
456
|
+
if (Array.isArray(repos)) {
|
|
457
|
+
repoPatchCount = repos.filter((r) => r && typeof r.patch === 'string' && r.patch.length > 0).length
|
|
458
|
+
}
|
|
459
|
+
} catch { /* 坏 JSON → 按缺件处理(重冻结兜底) */ }
|
|
460
|
+
}
|
|
461
|
+
// jsonOk 与 hasText 必须同时成立:change.patch 在场但 change-patch.json 损坏时,
|
|
462
|
+
// 冻结元数据已不可信(审计面缺锚),不能因正文文件在就判完整。
|
|
463
|
+
return { hasJson, hasText, jsonOk, repoPatchCount, complete: jsonOk && (hasText || repoPatchCount > 0) }
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 冻结缺口标签(存在性复查的调用点共用逻辑,纯函数便于直测):
|
|
468
|
+
* 完整 → [];否则给出可读缺件清单(坏 JSON 与真缺件区分,防止「正文在就判完整」盲区)。
|
|
469
|
+
* @param {{ hasJson?: boolean, hasText?: boolean, jsonOk?: boolean, repoPatchCount?: number, complete?: boolean }} state
|
|
470
|
+
* @returns {string[]}
|
|
471
|
+
*/
|
|
472
|
+
export function freezeGapLabels(state) {
|
|
473
|
+
const s = state && typeof state === 'object' ? state : {}
|
|
474
|
+
if (s.complete === true) return []
|
|
475
|
+
const gaps = []
|
|
476
|
+
if (!s.hasJson) gaps.push('change-patch.json')
|
|
477
|
+
else if (!s.jsonOk) gaps.push('change-patch.json(损坏)')
|
|
478
|
+
if (!s.hasText && !(Number(s.repoPatchCount) > 0)) gaps.push('change.patch')
|
|
479
|
+
if (gaps.length === 0) gaps.push('冻结件不完整')
|
|
480
|
+
return gaps
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 冻结缺口标签(目录级入口,flow done 存在性复查的唯一调用面):
|
|
485
|
+
* 检查(inspect)与标签(freezeGapLabels)一次封装——调用点直接用本函数,
|
|
486
|
+
* 直测本函数即覆盖调用点接线(防「探针修了调用点没接」的回归)。
|
|
487
|
+
* @param {string} changeDir
|
|
488
|
+
* @returns {string[]}
|
|
489
|
+
*/
|
|
490
|
+
export function missingFreezeLabels(changeDir) {
|
|
491
|
+
return freezeGapLabels(inspectFreezeArtifacts(changeDir))
|
|
492
|
+
}
|
|
493
|
+
|
|
412
494
|
export function detectPatchDrift({ cwd, change, freezeHead }) {
|
|
413
495
|
try {
|
|
414
496
|
if (!freezeHead) return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
|
|
@@ -442,6 +524,7 @@ export function detectPatchDrift({ cwd, change, freezeHead }) {
|
|
|
442
524
|
*/
|
|
443
525
|
export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive }) {
|
|
444
526
|
const deliverable = (f) => !String(f).replace(/\\/g, '/').startsWith('.sillyspec/')
|
|
527
|
+
&& !isToolGovernancePath(f)
|
|
445
528
|
let dirty = []
|
|
446
529
|
try {
|
|
447
530
|
// -uall(2026-10-08-thin-done-dirty-gate-and-paren-attribution):全新目录会折叠成
|
|
@@ -519,6 +602,7 @@ export function scanOrphanFace(cwd, { baseline } = {}) {
|
|
|
519
602
|
.split('\n').map((s) => s.trim()).filter(Boolean)
|
|
520
603
|
const isAncestor = (sha, of) => git(['merge-base', '--is-ancestor', sha, of], {}) !== null
|
|
521
604
|
const deliverable = (f) => !String(f).replace(/\\/g, '/').startsWith('.sillyspec/')
|
|
605
|
+
&& !isToolGovernancePath(f)
|
|
522
606
|
const orphanCommits = []
|
|
523
607
|
const orphanFiles = new Set()
|
|
524
608
|
for (const h of [...new Set(reflogHashes)]) {
|
|
@@ -656,13 +740,17 @@ export function projectTraceFaceRows(rows) {
|
|
|
656
740
|
* @param {object} meta change-patch.json 顶级增量键(note/moduleScope 等,展开在 totals 后)
|
|
657
741
|
* @returns {{ patchStatus: 'ok'|'failed', patchSha256: string|null }}
|
|
658
742
|
*/
|
|
659
|
-
export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, files, metaTotals, patchText, savedAt, snapObj, meta = {} }) {
|
|
743
|
+
export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, files, metaTotals, patchText, savedAt, snapObj, meta = {}, patchStatus = null }) {
|
|
660
744
|
const fileList = Array.isArray(files) ? files : []
|
|
661
745
|
const patchOk = typeof patchText === 'string' && patchText.length > 0
|
|
662
746
|
const patchBody = patchOk ? (patchText.endsWith('\n') ? patchText : patchText + '\n') : null
|
|
663
747
|
const patchSha256 = patchBody
|
|
664
748
|
? createHash('sha256').update(patchBody.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
665
749
|
: null
|
|
750
|
+
// 显式 patchStatus(2026-10-19-thin-empty-face-cross-repo-freeze):空面跨仓冻结
|
|
751
|
+
// 没有主仓 patchText,但冻结本身成功——调用方传 'ok' 表达「冻结成功,无主仓 patch」,
|
|
752
|
+
// 不能复用 patchText 判空落 'failed'(平台/归档读者会把成功冻结误判为采集失败)。
|
|
753
|
+
const status = patchStatus === 'ok' || patchStatus === 'failed' ? patchStatus : (patchOk ? 'ok' : 'failed')
|
|
666
754
|
if (patchOk) writeFileSync(join(changeDir, 'change.patch'), patchBody)
|
|
667
755
|
writeFileSync(join(changeDir, 'change-patch.json'), JSON.stringify({
|
|
668
756
|
change,
|
|
@@ -676,9 +764,9 @@ export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, fi
|
|
|
676
764
|
...meta,
|
|
677
765
|
scopeAudit: { ...snapObj },
|
|
678
766
|
...(patchSha256 ? { patchSha256 } : {}),
|
|
679
|
-
patchStatus:
|
|
767
|
+
patchStatus: status,
|
|
680
768
|
}, null, 2) + '\n')
|
|
681
|
-
return { patchStatus:
|
|
769
|
+
return { patchStatus: status, patchSha256 }
|
|
682
770
|
}
|
|
683
771
|
|
|
684
772
|
/**
|