Skip to content

RFC: 统一机器可读输出协议 —— envelope + destructive + --format 归一 + 老命令永久兼容 #379

Description

@speak-agent

背景

mcpp 的机器可读输出目前是分散演化的:xpkg parse --jsoncache list --json 各自决定输出结构,pack --format 是产物形态不是输出格式,self env 只有人类文本,#372 又引入了第三套(ide snapshot --format json / ide configure --format ndjson + 独立的 envelope 和 ID 体系)。

同时已经有两个真实客户端在等接口:

本 RFC 提议先把契约层统一下来,再谈新增命令。

一、现状盘点(均为实测/源码核对)

1.1 输出格式选项已经分裂,且 --format 有语义冲突

命令 选项 语义 状态
mcpp pack --format tar|dir --format 产物形态 已发布
mcpp xpkg parse --json --json 输出格式 已发布,mcpp-community/mcpp-vscode#8 在用
mcpp cache list --json --json 输出格式 已发布
mcpp ide snapshot --format json --format 输出格式 仅在 #372
mcpp ide configure --format ndjson --format 输出格式 仅在 #372

已发布的 mcpp 里,"输出格式"的事实拼写是 --json--formatpack 占用为另一个语义。

1.2 未知格式的响应,#372 内部就不一致

同一个 PR、同一天写的两个命令:

输出通道 输出格式 退出码
ide snapshot --format yaml stdout JSON 文档 + MCPP_IDE_UNSUPPORTED_FORMAT 诊断 3
ide configure --format json stderr 人类文本 2

三个维度全不同。在把 --format 推广到更多命令之前,必须先定死这一条,否则是把已知缺陷标准化。

1.3 客户端拿不到的东西,mcpp 其实已经在算

mcpp self env 已经打印 MCPP_HOME / xlings home / index repos / default toolchain,但没有 --format json。结果 mcpp-community/mcpp-vscode#8 §2.4 被迫自己重实现整条定位逻辑:

mcpp home 定位按 src/home.cppm 的顺序:$MCPP_HOME > 二进制自包含布局 > ~/.mcpp……判定:二进制位于 <dir>/bin/mcpp,且祖先路径不含 target/data/xpkgs/……扩展侧补充一条检查:PATH 上的 mcpp 可能是 xlings shim(符号链接追到调度器而非真实二进制),因此自包含分支额外要求 <dir>/registry 实际存在

这段逻辑 100% 依赖 mcpp 内部实现、跨平台、易碎,而且是 mcpp-community/mcpp-vscode#8 全篇里唯一没有契约测试兜底的部分。self env 加 JSON 输出约 15 行,可以直接删掉它。

1.4 xpkg parse --json 确实没有 schema 版本字段

实测确认,mcpp-community/mcpp-vscode#8 §7.1 的请求成立且未被满足。成本是一个字段。

1.5 我们自己就是"汇合式协议"的消费者,而且绕开了它

xlings interface 是完整的汇合式设计:interface <capability> --args '<JSON>' + --list(20 个 capability,含 destructive 标记和 inputSchema)+ --version

而 mcpp 对它的实际用法(src/xlings.cppm:1064-1076,main 分支):

// All platforms: try direct `xlings install ... -y` first.
// The direct command is more reliable for large packages (e.g. LLVM ~800MB) because:
//   - it doesn't pipe through NDJSON interface (simpler subprocess chain)
//   - xlings manages its own stdin/stdout/stderr
//   - extraction subprocess coordination works normally
// The NDJSON interface path is kept as a fallback for progress reporting.

主路径走裸 xlings install -y,把输出全部重定向到 null,然后 mcpp 自己在 stderr 画一个只有秒数的 spinner。也就是说:我们宁愿丢掉 xlings 的全部结构化进度事件,也不走那条 NDJSON 管道。

事件流的唯一独占价值就是进度事件,它在真实压力下不可靠 → 消费者放弃它。这是"汇合传输层"最直接的反证。

另一个观察:xlings interface --list 里 20 个 capability 的 outputSchema 全部是 {"properties":{"exitCode":{"type":"integer"}}}。声明了 schema 但没填 —— 这比不声明更危险,因为客户端会以为有契约。(我们踩过:#238 的 multi-repo 失败模式发出 {"exitCode":1} 且没有 error 事件,所以 InstallProgressHandler 才要用 capturedDiagnostics_ 手工兜住 error/warn 文本。)

1.6 CDB 的 bootstrap 问题比想象中小

实测(mcpp 2026.8.6.3):

src/main.cpp:  int main( {  return missing_symbol; }
$ mcpp build             → rc=1, "3 errors generated"
$ ls compile_commands.json → 1624 bytes,entry 完整
   -std=c++23 -fprebuilt-module-path=... --no-default-config -nostdinc++ -isystem .../c++/v1

源码上的原因:src/build/ninja_backend.cppm:1546write_compile_commands() 在 spawn ninja 之前执行。真正的门槛不是"编译成功",而是 prepare_build() 成功。

因此 IDE 侧真正缺的不是"能在编不过时拿到 CDB"(已经有了),而是:

  1. 不为拿 CDB 付一次完整编译 + 链接(这才是核心)
  2. CDB 覆盖 tests/** 及其 dev-dependency 上下文
  3. 退出码能区分"CDB 没写"和"CDB 写了但编译失败"
  4. 哪些输入变化会让这份 CDB 失效 —— 目前只有文档散文,没有机器可读输出

二、判据

沿用三档划分:

  1. 只有 mcpp 能做、且适合 mcpp 做的 → mcpp 做
  2. mcpp 可以很简单实现、且可复用的 → 可以 mcpp 做
  3. 外部插件实现与 mcpp 实现复杂度相当的 → mcpp 不做

第 3 档要注意一个反直觉的事实:有些能力 mcpp 做反而更贵,因为 mcpp 是多进程、公开契约、要向后兼容的,必须额外承担只读性保证、路径 containment、选择器语义、诊断降级 —— 而插件在自己进程里读文件,这些负担都不存在。

三、提案

阶段 0 — 先定死"未知 format 怎么办"(前置条件)

客户端在拿到输出之前无法知道 mcpp 支不支持它请求的格式,所以拒绝也必须用客户端能解析的形式说出来。

统一契约:
  - 走 stdout(不是 stderr)
  - 输出标准 envelope + diagnostics[0].code = MCPP_UNSUPPORTED_FORMAT
  - 退出码 2(用法错误,符合 CLI 惯例)
  - 不执行任何有副作用的工作

这一条不定,后面所有 --format 推广都是在复制缺陷。

阶段 1 — 通用输出信封(新模块 mcpp.wire

{
  "schemaVersion": 1,
  "kind": "mcpp.metadata",              // mcpp.env / mcpp.xpkg / mcpp.cdb / ...
  "destructive": false,                 // ← 借鉴 xlings interface 的设计
  "mcpp": {
    "version": "2026.8.8.1",
    "protocol": { "min": 1, "max": 1 }
  },
  "data": { /* 命令特有 */ },
  "diagnostics": [
    { "code": "...", "severity": "error|warning", "source": "mcpp",
      "message": "...", "path": "...", "range": {"start":{"line":1,"column":1},"end":{...}} }
  ]
}

destructive 字段的价值mcpp-community/mcpp-vscode#8 §7.2 花了一整段请求"请确认 xpkg parse --json 无写副作用并可作为契约依赖",本机实测过还要请上游固定。有了这个字段,请求当场变成机器可判定的契约。它同时替代 #372 文档里那段"configure 不是 read-only,IDE 必须先拿 workspace trust"的散文 —— VSCode 的 untrusted workspace 门可以直接读字段决定跑不跑。

代码来源Diagnostic/Position/Range/Severity 模型和 envelope 构造 + 内容寻址 ID 在 #372 里已经写好且质量很高(src/ide/model.cppmsrc/ide/snapshot.cppm,约 150 行)。提议把它们从 mcpp.ide.* 提升为 mcpp.wire,服务所有命令而不只是 IDE。

新增协商入口(唯一"汇合"的地方):

mcpp --protocol-version   →   {"protocol":{"min":1,"max":1}}

一次调用、不需要项目、不触网。客户端启动时判断该走哪条路,不用先 spawn 一个可能失败的命令再解析错误消息(mcpp-community/mcpp-vscode#5 验收标准第 2 条)。

阶段 2 — --format 归一 + 老命令永久兼容

规范拼写--format jsonndjson 保留给未来真正需要流式的场景)。

--format 而非 --json 的理由不是"跟 #372 一致",而是:布尔开关无法表达第二种格式,也无法承载"未知值"这个错误分支——而阶段 0 恰恰需要它。

兼容策略 —— 直接套用本仓库已有的范式src/toolchain/compat.cppm):

THE ONLY FILE that knows pre-0.0.93 spellings. Core code sees canonical forms exclusively; the public parse entry points call normalize_ first.* Deleting this module would break exactly one thing: old inputs — never a canonical path.

cmdline 入口一处归一化:  --json  →  --format json
核心代码只见 --format

三条硬约束:

  1. --json 永久保留,不删
  2. 不打 deprecation 警告 —— design: mcpp.toml 补全重做方案 —— 语义模块 + index 动态数据(#4 重做) mcpp-vscode#8 已经在用它,用户装的 mcpp 版本参差;警告只会污染扩展捕获的输出
  3. 归一化只在入口做一次,核心代码不得再感知旧拼写

pack --format tar|dir 的语义冲突:它是产物形态不是 stdout 格式。两条路,倾向前者:

  • (a) 文档声明 pack 为例外(它没有机器可读输出,不参与本协议)
  • (b) 加 --layout tar|dir 为规范拼写,--format 降为别名,走同一套兼容机制

阶段 3 — 补齐机器接口(按判据筛过)

接口 destructive 内容 服务 估算
mcpp self env --format json false MCPP_HOME / xlings home / registry / index repos / default toolchain mcpp-community/mcpp-vscode#8 §2.4 立刻减负 ~15 行
mcpp xpkg parse --format json false 现有输出 + envelope(schemaVersion 随之而来) mcpp-community/mcpp-vscode#8 §7.1 ~10 行
mcpp cache list --format json false 现有输出 + envelope CI / 工具 ~10 行
mcpp metadata --format json false workspace 成员 + targets(含推断的 target)+ manifest 诊断(code/path/line/column) mcpp-community/mcpp-vscode#8 补全校验、mcpp-community/mcpp-vscode#5 发现、CI ~150 行
mcpp metadata --resolved --format json true 上者 + 依赖图(版本/来源 index)+ CDB 失效输入清单 mcpp-community/mcpp-vscode#5 configure 后 ~100 行
mcpp build --configure-only --format json true 不编译不链接,产出 CDB + 失效清单 mcpp-community/mcpp-vscode#5 核心诉求 ~150 行

两个设计约束:

  1. mcpp metadata 默认必须不触网、不解析依赖(对标 cargo metadata --no-deps)。否则 design: mcpp.toml 补全重做方案 —— 语义模块 + index 动态数据(#4 重做) mcpp-vscode#8 的场景(编辑 mcpp.toml 时补全)每次敲键都可能触发网络。需要依赖图和失效清单的,显式加 --resolved
  2. CDB 失效输入清单是当前最大的缺口,它是插件绝对拿不到的信息(哪些输入进了指纹),而且数据已经全在 BuildContext::fpBuildPlan 里:
"invalidatedBy": [
  { "path": "mcpp.toml", "hash": "..." },
  { "path": "mcpp.lock", "hash": "..." },
  { "glob": "src/**/*.{cppm,cpp,cc,c,S,s,asm}" },
  { "toolchainFingerprint": "toolchain-fnv1a64:..." }
]

有了它,客户端的 watcher 就有了机器可读依据,而不是照着文档里一段会漂移的散文硬编码。

阶段 4 — 明确不做

不做 理由
mcpp interface <cap> --args / --list / capability 自描述 mcpp 已有 subcommand 树 + --help 作为发现机制;xlings 的 interface 面向 AI agent 编排,mcpp 的消费者是 IDE 插件和 CI 脚本,形态不同
NDJSON 事件流(seq / operationId / progress 事件) one-shot 进程用「spawn + 退出码 + 读结果文件」就够(插件侧约 10 行)。事件协议是为 daemon 准备的,daemon 不在计划内。而且我们自己已经绕开了 xlings 的同款设计(§1.5)
workspace 成员发现的独立命令(ide snapshot 插件读 [workspace].members 约 60 行;mcpp 做要 400+ 行(额外承担只读保证、路径 containment、选择器语义、诊断降级)。成员发现并入 mcpp metadata 即可
artifact 状态枚举 / phase 状态机 / 快照 ID 体系 stat() + 客户端自己的状态机
CDB 的版本化发布体系(reply 内容寻址 / current.json / 发布锁 / 回滚) 插件要「保留上一份可用 CDB」只需 copy + 失败 copy 回来(约 20 行);mcpp 做要 330+ 行且引入 current.json 单例 vs 多 configurationId 的结构性问题。mcpp 侧只需保证「自己写 CDB 时不写坏」(原子 replace,约 30 行)

四、必须守住的纪律

xlings interfaceoutputSchema 全空(§1.5)教了一课:envelope 的价值全在 data 被真正版本化和文档化上。只做外层信封、内层随手改,客户端会因为"看到 schemaVersion 就以为有契约"而被坑得更惨。

具体到 mcpp:

  • 每个 kinddata 形状进 docs/spec/
  • 改动规则:只增字段,不改语义,不删字段;破坏性变更 bump schemaVersionprotocol.min/max 同时给出重叠窗口
  • 每个 kind 至少一个 golden fixture 测试(feat(ide): add versioned project snapshots and pre-build CDB #372tests/fixtures/ide/snapshot-v1.json 是好范式,值得保留并推广)

同一条纪律也适用于错误码:MCPP_IDE_CONFIGURE_FAILED 这种「一个码覆盖全部失败 + 文档明令不许解析人类消息」的形状,是名义结构化、实际空洞。新增的每个失败分支要么有自己的 code,要么显式声明为「不可细分」。

五、与 #372 的关系

#372 里有三类内容,建议分开处理:

A. 直接可用、建议尽快单独合入

  • src/build/test_targets.cppm(81 行)—— 把 tests/** 发现从 run_tests 提取出来,消除了 IDE CDB 与 mcpp test 在 member 选择 / 命名 / per-glob flags 上漂移的可能。是真正的收敛。
  • write_fresh_compile_commands + platform::fs::replace_file + FileLock(ec) —— 顺带修掉一个既有缺陷:write_compile_commands() 目前是 ofstream 截断写,非原子、不检查失败,clangd 可能读到半截 JSON。
  • prepare.cppm 的 stdout 纪律修复。
  • 对应单测(test_compile_commands / test_platform_fs / test_test_options)。

B. 建议捞进本 RFC 复用

  • src/ide/model.cppmDiagnostic/Position/Range/Severity + wire_name(~90 行)
  • src/ide/snapshot.cppm 的 envelope 构造 + 内容寻址 ID(~60 行)

这两块设计质量高,只是被绑死在一个命令上。提升为 mcpp.wire 后可服务全部 JSON 出口。

C. 建议不合入

inspect.cppm(433) / publish.cppm(333) / events.cppm(106) / cmd_ide.cppm(119) / ide snapshot / ide configure / IdePhase·SnapshotState·ArtifactState(生产代码零使用者)/ describe_std_module(仅单测调用)/ .xlings.json pin bump(与本 PR 无关)/ docs/superpowers/ 目录树(本仓库设计文档惯例是 .agents/docs/)。

另外,若 #372 按现状推进,两个问题需要先修:

  1. NDJSON stdout 仍有未堵住的污染源。 src/pm/package_fetcher.cppm:1036 硬编码 /*quiet=*/false 调用 ensure_official_package_index_fresh,其内部 xlings::print_statusxlings.cppm:1583)和 update_index_unguardedrun_streaming 回调直接 std::println 到 stdout,不看 mcpp::ui::is_quiet()。任一 xim: 包(含工具链)在本地索引缺失且未 debounce 时触发,xlings update 的全量输出会进 NDJSON 流。tests/e2e/198_inherit_toolchain.sh 预置本机工具链,索引永远命中,结构性覆盖不到。
    根因是 stdout 归属分散在多个 bool 参数里;建议收敛为单一开关(让 xlings::print_statusmcpp::ui::status,或加 ui::stdout_is_protocol() 一票否决)。
  2. 文档指定的客户端流程在 rooted workspace 的根成员上必然失败。 inspect.cppm 对 rooted workspace 产出 workspacePath == "."(单测 RootedWorkspaceSelectsRoot 明确断言),而文档 §7.2 要求客户端 configure --package <workspacePath> per member;-p .resolve_member_dir[workspace].members 里找不到 . → 报错退出 3。
    根因是 member selector 语法在三处独立推导:project.cppm::resolve_member_dirprepare.cppm:951ide/inspect.cppm::matches_selector,前两处一致、第三处多了 . 别名。现有测试恰好绕开(e2e 用 virtual workspace;单测里 name/dir/workspacePath 三者相同)。

六、实施顺序

1. 阶段 0:未知 format 的统一契约(前置,不做后面全是复制缺陷)
2. 阶段 1:mcpp.wire envelope + destructive + --protocol-version   (~150 行,从 #372 捞)
3. 阶段 2:--format 归一 + --json 入口别名(compat 模式,无警告)  (~50 行)
4. 阶段 3a:self env / xpkg parse / cache list 接入 envelope        (~35 行)
5. 阶段 3b:mcpp build --configure-only + CDB 失效输入清单          (~170 行)
6. 阶段 3c:mcpp metadata [--resolved]                              (~250 行)

前 5 步合计约 400 行,即可满足 mcpp-community/mcpp-vscode#5 的全部验收标准和 mcpp-community/mcpp-vscode#8 §7 的两条请求。

七、待确认

@Sunrisepeak

  1. --format 作为规范拼写、--json 永久别名且不打 deprecation 警告 —— 是否接受?
  2. pack --format tar|dir 走「声明为例外」还是「加 --layout 别名迁移」?
  3. mcpp --protocol-version 作为唯一的汇合式协商入口 —— 是否接受?(明确不做 mcpp interface
  4. destructive 字段是否接受作为公开契约(等于把 design: mcpp.toml 补全重做方案 —— 语义模块 + index 动态数据(#4 重做) mcpp-vscode#8 §7.2 的人工承诺固化)?

@wellwei

  1. feat(ide): add versioned project snapshots and pre-build CDB #372 按 §5 的 A/B/C 拆分是否可行?A 部分单独 PR 可以很快合入并发版,不阻塞 feat: integrate the mcpp IDE configure protocol into clangd workflow mcpp-vscode#5
  2. feat: integrate the mcpp IDE configure protocol into clangd workflow mcpp-vscode#5 若改为消费 mcpp build --configure-only(spawn + 退出码 + 读 CDB)而非 NDJSON 事件流,扩展侧代码会更少 —— 但已有实现分支需返工,是否值得?
  3. §5 末尾两个问题(NDJSON 污染、-p .)若 feat(ide): add versioned project snapshots and pre-build CDB #372 继续推进,需要先处理。

@Ximiaw

  1. mcpp self env --format json 是否能覆盖 design: mcpp.toml 补全重做方案 —— 语义模块 + index 动态数据(#4 重做) mcpp-vscode#8 §2.4 的全部需求(home / registry / index 路径 + shim 情形)?还缺什么字段请列出。
  2. mcpp metadata --format json(不触网,含 manifest 诊断 code/path/line/column)是否满足 design: mcpp.toml 补全重做方案 —— 语义模块 + index 动态数据(#4 重做) mcpp-vscode#8 §1「不做」栏里那条"等上游版本化 schema"的一部分?还是说静态字段键/枚举值仍需要独立的 schema 出口?

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationenhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions