Skip to content

Commit e0abcff

Browse files
committed
docs: 机器可读输出协议 —— 对 RFC #379 的核对与修正
逐条实跑核对 #379 的事实主张(不看代码推断)。基本成立,但有一个结构性遗漏会让它 的阶段 0–2 全部落空。 **未知选项的报错走 stdout,rc=1,stderr 为空** —— 而且那段代码在依赖包 `mcpplibs.cmdline` 里,不在 mcpp。RFC 的阶段 0 只规定未知**值**,但客户端探测能力 时在旧版本上先撞到的是未知**选项**;只规定值,等于把最常走的那条路留在污染状态, 而修它是跨包工作,不在 RFC 的行数估算里。 由此推出更关键的一条:RFC 提议的 `mcpp --protocol-version` **解决不了它被设计来解决 的问题**。在任何尚未实现它的 mcpp 上,它自己就是那个「可能失败的命令」,且失败与 成功同通道。换 `--json` 拼写也一样 —— 拼写之争对发现机制毫无影响。唯一稳健的客户端 规则是正向识别:读 stdout 尝试 JSON 解析,含 schemaVersion 与 kind 才算数;不得以 退出码或「没报错」为判据。 第三条:`destructive` 只放 envelope 不够。VSCode 的 untrusted 门要在**执行之前**知道, 而拿到 envelope 时副作用已经发生。建议同时进 `--protocol-version` 的静态表。 核对过程本身有一处自纠:我先用 grep 断定 `cache list --json` 不存在,实跑后推翻 —— flag 注册不带横线。文档里每条结论都以实跑为准。 另补一条可执行纪律:golden fixture 必须反向验证过(改字段名要变红)。只「跑通了」 而没有「改坏了会红」的 fixture,和没有 fixture 等价 —— 本轮我自己写过三个因为错误 的理由而通过的测试,都是主动把实现改坏才发现的。
1 parent c1e9360 commit e0abcff

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
# 机器可读输出协议 —— 对 RFC #379 的核对与修正
2+
3+
> 状态:设计待 review。核对基准 mcpp 2026.8.8.3(本机构建),xlings 2026.8.8.1。
4+
> 本文不复述 #379,只写**核对结果****它需要改的地方**
5+
6+
## 0. 结论先行
7+
8+
RFC #379 的分析基本成立,逐条实测核对见 §1。但它有一个**结构性遗漏**,会让阶段 0
9+
到阶段 2 全部落空:
10+
11+
> **未知选项的报错走 stdout,退出码 1,stderr 为空 —— 而且这段代码在依赖包
12+
> `mcpplibs.cmdline` 里,不在 mcpp。**
13+
14+
RFC 花了整个阶段 0 去规定「未知**格式值**怎么办」,但客户端在真实使用中先撞到的是
15+
「未知**选项**」——因为它要探测的正是「这个版本认不认 `--format`」。而那条路径今天
16+
把人类文本打进了协议要独占的通道。
17+
18+
由此推出的第二件事更关键:**RFC 提议的协商入口 `mcpp --protocol-version`,解决不了
19+
它被设计来解决的那个问题**(§2.2)。
20+
21+
---
22+
23+
## 1. 逐条核对
24+
25+
方法:全部在本机对已构建的 mcpp 实跑,不看代码推断。一处我用 grep 得出的结论是错的
26+
(以为 `cache list --json` 不存在),实跑后推翻 —— flag 注册不带横线,grep `"--json"`
27+
搜不到。**下面每一条都以实跑为准。**
28+
29+
| RFC 主张 | 核对 | 结果 |
30+
|---|---|---|
31+
| §1.1 `--json` 是已发布的事实拼写 | `xpkg parse --json` ✓、`cache list --json` ✓ 均有输出 | **成立** |
32+
| §1.3 `self env` 无 JSON | `--format json` / `--json` 均报 unknown option | **成立** |
33+
| §1.4 `xpkg parse --json` 无 schemaVersion | 输出 `{"namespace":"","name":"7zip","form":"A"}` | **成立** |
34+
| §1.5 `xlings interface` 的 outputSchema 全空 | `--list` 20 个 capability,**20/20** 只有 `exitCode` | **成立** |
35+
| §1.6 CDB 早于 ninja 写出 | `ninja_backend.cppm:1549` `write_compile_commands`,其后才起 ninja | **成立** |
36+
| §1.1 `pack --format` 语义冲突 | `pack --format bogus` → rc=2,人类文本走 **stderr** | **成立** |
37+
38+
### 1.1 新发现 F1:未知选项污染 stdout
39+
40+
```
41+
$ mcpp cache list --format json
42+
stdout: Error: unknown option: --format
43+
stderr: (空)
44+
rc: 1
45+
```
46+
47+
对照 mcpp 自己的错误路径:
48+
49+
```
50+
$ mcpp xpkg parse /nonexistent.lua
51+
stdout: (空)
52+
stderr: error: cannot open '/nonexistent.lua'
53+
```
54+
55+
**mcpp 自己的 `ui::error` 是对的**(`src/ui.cppm:327`,写 stderr)。洞恰好开在参数解析
56+
的边界上,而且代码在依赖里:
57+
58+
```
59+
mcpplibs-x-cmdline/0.0.2/cmdline-0.0.2/src/cmdline.cppm:127
60+
std::println("Error: {}", result.error().message); // ← stdout
61+
```
62+
63+
于是当前有**四种**未知输入行为,而 RFC 只识别出后两种(且它们都还只在 #372 里):
64+
65+
| 场景 | 通道 | rc |
66+
|---|---|---|
67+
| 未知**选项**(`cache list --format …`) | **stdout** | 1 |
68+
| 未知****(`pack --format bogus`) | stderr | 2 |
69+
| #372 `ide snapshot --format yaml` | stdout | 3 |
70+
| #372 `ide configure --format json` | stderr | 2 |
71+
72+
---
73+
74+
## 2. 这改变了什么
75+
76+
### 2.1 阶段 0 的范围划错了
77+
78+
RFC 的阶段 0 只规定未知****。但客户端探测能力时发的第一个请求,在旧版本上命中的
79+
是未知**选项**。只规定值,等于把最常走的那条路留在污染状态。
80+
81+
**修正:阶段 0 必须覆盖选项层,而这意味着要动 `mcpplibs.cmdline`**(改成写 stderr,
82+
或让 mcpp 不用它内建的错误打印)。这是 RFC 的行数估算里没有的一项跨包工作。
83+
84+
### 2.2 协商入口解决不了它被设计来解决的问题
85+
86+
RFC 给 `--protocol-version` 的理由是:
87+
88+
> 客户端启动时判断该走哪条路,不用先 spawn 一个可能失败的命令再解析错误消息
89+
90+
实测,在**任何尚未实现它的 mcpp** 上:
91+
92+
```
93+
$ mcpp --protocol-version
94+
stdout: Error: unknown option: --protocol-version
95+
stderr: (空)
96+
rc: 1
97+
```
98+
99+
它自己就是「那个可能失败的命令」,而且失败与成功**同通道**。客户端仍然只能靠解析
100+
stdout 内容来判断 —— 正是要避免的事。
101+
102+
`--json` 拼写也一样:两种拼写在旧版本上都走同一条未知选项路径。**所以拼写之争
103+
(`--format` vs `--json`)对发现机制毫无影响**,RFC 用「布尔无法表达未知值分支」来论证
104+
`--format` 是对的,但不要把它当成解决了发现问题。
105+
106+
**修正:唯一对旧版本稳健的客户端规则是「正向识别」**——
107+
108+
> 读 stdout,尝试 JSON 解析;解析成功****`schemaVersion``kind` 才认为拿到了
109+
> 协议输出。**不得**以退出码 0、或「命令没报错」作为判据。
110+
111+
这条规则要写进 `docs/spec/`,并且是 envelope 必须**自识别**的理由:客户端没有别的
112+
可靠信号。`--protocol-version` 仍值得有(新版本上省一次 spawn),但它是优化,不是
113+
契约地基。
114+
115+
### 2.3 `destructive` 字段:同意,但它落在错误的一侧
116+
117+
RFC 提议在 envelope 里放 `destructive`。这对**已经拿到输出**的客户端有用,但
118+
VSCode 的 untrusted-workspace 门需要在**执行之前**知道 —— 而拿到 envelope 的时候,
119+
副作用已经发生了。
120+
121+
**修正:`destructive` 必须同时出现在两个地方**——
122+
123+
- envelope 里(事后自述,便于日志与审计)
124+
- `mcpp --protocol-version` 的输出里,以「命令 → destructive」的静态表形式给出
125+
126+
后者才是 untrusted 门能用的东西。这也顺带回答 RFC §七 给 @sunrisepeak 的第 4 问:
127+
接受这个字段,但只放 envelope 不够。
128+
129+
---
130+
131+
## 3. 修正后的阶段划分
132+
133+
改动集中在阶段 0 与阶段 1,阶段 2 之后与 RFC 一致。
134+
135+
### 阶段 0(前置)—— stdout 归属,而不只是「未知格式」
136+
137+
1. **`mcpplibs.cmdline` 的解析错误改写 stderr**,并给出非 0 退出码。跨包改动,需要
138+
发一个 cmdline 版本;mcpp 侧同步 bump。
139+
2. mcpp 侧统一:未知****与未知**选项**都走 stderr + rc=2(用法错误)。
140+
—— RFC 原文提议未知格式「走 stdout + envelope」。**建议改为 stderr + rc=2**:
141+
一个还不知道自己该输出什么格式的请求,不该往协议通道里写东西;客户端按 §2.2 的
142+
正向识别规则,stdout 无 JSON 即判定为「不支持」,语义完整且不需要额外约定。
143+
3. 加一条 e2e:对**每个**声明支持 `--format` 的命令,断言未知值与未知选项在通道、
144+
退出码上一致。这是防止 §1.1 那张表重新长出来的唯一办法。
145+
146+
### 阶段 1 —— `mcpp.wire` envelope
147+
148+
与 RFC 一致(从 #372`src/ide/model.cppm` / `snapshot.cppm` 提升),补两点:
149+
150+
- envelope **必须自识别**:`schemaVersion` + `kind` 是客户端唯一的正向判据(§2.2)
151+
- `--protocol-version` 的输出**包含命令 → destructive 的静态表**(§2.3)
152+
153+
### 阶段 2–4
154+
155+
与 RFC 一致。`--json` 永久别名、入口一处归一、不打 deprecation 警告 —— 这套做法在
156+
本仓库有先例(`src/toolchain/compat.cppm`),照搬即可。
157+
158+
`pack --format tar|dir`:建议走 (a)**声明为例外**。它没有 stdout 机器输出,不参与本
159+
协议;加 `--layout` 别名是为一个不存在的一致性付迁移成本。
160+
161+
---
162+
163+
## 4. 必须守住的纪律(RFC §四)—— 同意,补一条
164+
165+
RFC 说「envelope 的价值全在 `data` 被真正版本化和文档化上」,并以 xlings 的
166+
20/20 空 outputSchema 为反例。核对属实。
167+
168+
补一条**可执行**的约束,否则「只增不改」会像那 20 个 schema 一样退化成口号:
169+
170+
> 每个 `kind` 的 golden fixture 必须**在测试里被反向验证**:改一个字段名要让测试变红。
171+
> 只有「跑通了」而没有「改坏了会红」的 fixture,和没有 fixture 等价。
172+
173+
这一条不是文风要求。本轮工作里我自己就写过三个「因为错误的理由而通过」的测试:
174+
一个 e2e 分支够不到它要测的模式,一个判据选成「跑不跑得起来」(在任何有宿主工具链
175+
的机器上恒真),一个 `ldd` 解释器行造成的误报。三个都是靠**主动把实现改坏、看测试
176+
会不会红**才发现的。
177+
178+
---
179+
180+
## 5. 与 #372 的关系
181+
182+
同意 RFC §五 的 A/B/C 拆分。补充一点:A 部分里的
183+
`write_fresh_compile_commands` + 原子替换,**独立于本协议就应该合入** —— 现状是
184+
`ofstream` 截断写,clangd 可能读到半截 JSON。那是既有缺陷,不该等协议设计定稿。
185+
186+
---
187+
188+
## 6. 待确认(在 RFC 七问之外)
189+
190+
1. **`mcpplibs.cmdline` 的解析错误改走 stderr** —— 这是跨包改动且影响所有使用者,
191+
是否接受?若不动依赖,替代方案是 mcpp 不用 `App::run()` 的内建错误打印、自己接管
192+
`ParseResult`(mcpp 侧约 20 行,但要覆盖每个命令入口)。
193+
2. 未知格式**走 stderr + rc=2**(本文 §3 阶段 0.2)还是 RFC 原案的 **stdout +
194+
envelope**?我倾向前者,理由见 §3;但若客户端强烈希望「任何情况下 stdout 都是
195+
JSON」,后者也自洽 —— 需要先定,因为它决定 §2.2 的客户端规则怎么写。
196+
3. `destructive` 静态表放进 `--protocol-version` 输出 —— 是否接受(§2.3)?

0 commit comments

Comments
 (0)