diff --git a/.agents/docs/2026-11-08-ecosystem-885-supplement-design.md b/.agents/docs/2026-11-08-ecosystem-885-supplement-design.md
new file mode 100644
index 000000000..09f35f186
--- /dev/null
+++ b/.agents/docs/2026-11-08-ecosystem-885-supplement-design.md
@@ -0,0 +1,242 @@
+---
+subject: ecosystem
+status: active
+---
+
+# #885 收尾补充方案:目标角色、平台事实、系统接口与版本书写
+
+**日期:** 2026-11-08
+**状态:** 第三版,待审查(Q1–Q4、Q6 已确认;Q5 改为"并存",见 S3)
+**上游文档:** [2026-11-07-ecosystem-885-closeout-design.md](2026-11-07-ecosystem-885-closeout-design.md)(D1–D20)
+**相关:** mcpp#903、mcpp#904、mcpp-language-server#80
+
+本文记录 #885 收尾过程中出现的七项发现(F1–F7),给出补充决定(S1–S6),以多个视角审查这些决定(第 3 节),并列出归属、版本与待审查的问题。被考虑而未采用的方案见附录 A。
+
+## 0. 决定摘要
+
+| 编号 | 决定 | 归属 |
+|---|---|---|
+| S1 | openkal 与原生目标不分等级,分"发布形态"与"构建目标"两种角色 | mcpp 文档;mcpp tier 评估 |
+| S2 | os 提供"编译期能否 + 运行时是否"的两层平台判断,以及只接受泛型 lambda 的 `on
` | mcpplibs.os |
+| S3 | 平台模块在能回答的地方定义;映像中的平台事实由 os 经可移植通道实现,openkal 不变;`is_<平台>` 与两层判断并存,各有确切含义 | mcpplibs.os;mcppls 过渡日志 |
+| S4 | 生态的源文件与测试不包含系统头文件;系统头文件只在 os 的平台实现中;本轮并入 #52、#5 | mcpp-plugins 0.21.0、mcpp-ide 1.2.0、mcpp |
+| S5 | 依赖写在一张表中:身份键、精确版本、一种拼写;锁文件的 `targets` 只随所规划的配置变化 | 生态各仓库(已完成);mcpp#903、#904 |
+| S6 | 原生引擎一律链接 zlib;darwin 的比值超限先由探针定位,上限不放宽 | mcppls-clangd 0.0.21 |
+| S7 | mcpp 定义测试协议(用例的列出、选择、报告与上下文),测试框架以适配方式实现;`mcpp.core` 提供测试用的上下文注入 | mcpp 下一版本(提案,待审查) |
+
+## 1. 发现
+
+### F1 三层对 openkal 目标的定位不一致
+
+| 层 | 依据 | openkal 的位置 |
+|---|---|---|
+| mcpp 目标表 | docs/21:tier 表示产物经过构建与运行 | `x86_64-openkal`、`aarch64-openkal` 为 `preview`;主要原生行为 `verified` |
+| 生态应用的发布 | D18:mcppls-clangd、mcppls、mcpp-ide 只发布 ok 映像,原生构建只在手动对比运行中出现 | 唯一发布形态 |
+| mcpplibs.os 接口 | README R3:编译期平台常量为首要接口;清单:映像中不定义任何平台模块 | 次要形态 |
+
+实际发布的形态在 mcpp 中为 `preview`,在 os 中缺少平台模块。F2 是这一错位的直接后果。
+
+### F2 映像程序静默失去平台事实
+
+ok 映像的 `PLATFORM` 为 `openkal`,`is_windows`、`is_linux` 恒为 `false`。mcppls 的两处平台判断因此在全部发布产物中被丢弃:
+
+- `src/orchestrator/workspace.cpp:706`:`if constexpr (os::is_windows) … os::windows::on_dev_drive(…)`,即 0.0.14 E1 的 Dev Drive 提示;
+- `modules/toolrun/src/toolrun.cpp:229`:`if constexpr (os::is_linux) … os::linux::translator()`,即 PRoot 识别。
+
+编译器与测试均不报告这一丢弃。第二处不只影响日志:`toolrun::sandbox()` 决定 PRoot 下模块构建在进程内进行(`src/engine/clangd.cpp:1968`),映像在 PRoot 下因此按普通内核的方式使用 worker。原生构建的 mcpp 不受影响,其 10 处平台模块调用(`windows::code_page` ×4、`console_output_code_page`、`long_paths_enabled` ×2、`machine_registry_path`、`writable_fixed_drive`、`posix::set_creation_mask`)依赖编译期分派。
+
+### F3 系统头文件
+
+| 仓库 | 状态 |
+|---|---|
+| mcppls | 0 处(`253aff8` 删除 test_archive 中针对已修复 Windows 映像故障的 `open`/`fstat` 探针;`devtools check os-surface` 报告任何 `#include`) |
+| mcpp-plugins | 全局模块片段中包含 ``(`dist/*.cppm`、`rules/*.cppm` 共 39 处,用于 `popen`)、``、``;#52 的 `docs/plugin-development.md` §4 规定"只包含 ISO C 之外的平台函数头文件" |
+| mcpp-ide | 测试使用本地头文件 `support.hpp` 及 ``、``、``、`` |
+
+mcpplibs.os 0.3.1 已提供对应能力:`process::run`(含输出捕获与有界读取)、`library::open`、`spawn_options` 中的子进程环境。mcpplibs.mwrap.libc 只覆盖 ISO C,不含 POSIX。tests 下的 Qt、Android 头文件属于测试用的 C/C++ 使用方工程,不在此列。
+
+### F4 依赖的书写与精确性
+
+本轮将 mcppls、mcpp-ide、mcpp-plugins、mcppls-clangd 的依赖改为单表、不带引号的身份键与精确版本。mcpp 解析中,不带运算符的版本即精确地址(`try_merge_semver` 将其规范为 `=`),因此 `"0.3.1"` 与 `"=0.3.1"` 等价。mcpp 本体仍使用带引号的旧式键与按命名空间分立的子表,`os` 写作 `"=0.3.1"`(mcpp#904)。mcpplibs.os 自身以范围依赖 openkal 家族(`openkal = "^0.24.0"` 等),其精确性由平台版本组(D12)给出。
+
+### F5 锁文件的 `targets` 随命令变化
+
+`mcpp test --workspace --no-run` 删除未规划配置的目标,`mcpp test -p … --target …` 为原本无 `targets` 的条目写入目标。锁文件因此在不改变解析的命令下改写(mcpp#903)。
+
+### F6 对比运行(mcppls-clangd run `42369457883`)
+
+- **win32-x64 分片交换失败(image → native)。** 原生 Windows 引擎读取映像写出的分片时报告 `Compressed string table, but zlib is unavailable` 并重建全部 12 个单元。`ci/scripts/configure.sh` 在 Windows 上未提供 zlib,LLVM 的 `LLVM_ENABLE_ZLIB` 在找不到 zlib 时关闭。0.0.21 CHANGELOG 中"原生 clangd 同样链接 zlib"一句对 win32 不成立。
+- **darwin-arm64 的比值超限。** 该测量有效(四对,一对在离散界外,政策允许一对)而未达限:冷启动 `ready_seconds` 的映像/原生中位比值为 1.204,上限 1.15。按 `ci/performance-policy.md`,有效而未达限的测量即为结果,不重复;其后进行的一次重跑违反此条,结果(冷启动 1.141,热启动 `ready_seconds` 1.177、`cpu_seconds` 1.244)只作诊断参考。0.0.20 的同一测量为冷启动 0.963、热启动 `cpu_seconds` 1.081。
+
+### F7 复用答案时的编辑范围(mcppls#80,已修复)
+
+clangd 的 include 补全替换光标后编辑器自动补出的右引号。mcppls 复用早先答案的三条路径(同词完整答案、迟到答案、答案层)均把编辑终点改为光标,导致 `"foo.h""`。修复保留越过旧光标的部分(在光标后的行文本不变时),且答案层不回答 include 行。
+
+## 2. 补充决定
+
+### S1 目标的角色
+
+openkal 与原生目标之间不设等级,按角色区分(Q1 已确认):
+
+- **发布形态:** 生态中的应用以 ok 映像作为唯一发布物,一份产物覆盖全部系统,测试在每个真实系统上运行映像。适用于只需文件、进程、网络与终端的程序:mcppls-clangd、mcppls、mcpp-ide 的后端。
+- **构建目标:** 原生目标(`*-linux-gnu`、`*-linux-musl`、`*-windows-msvc`、`aarch64-macos`、Android、iOS、`wasm32-emscripten`、`*-none-elf`)是完整支持的构建目标,mcpp 与 mcpplibs.os 对其提供与映像同等的保证。下列情形只能以原生目标构建:
+ - mcpp 本体:按系统发布原生二进制,以编译期分派调用 `os::windows::*`、`os::posix::*`;
+ - 库:mcpplibs.cmdline、tinyhttps、mcpplibs.os 本身,随使用者选择的目标编译进使用者的程序;
+ - 链接系统或厂商库的程序:Qt、Vulkan/Metal、CUDA/SYCL、Android 的 `native_activity`、iOS 应用(mcpp-plugins 的测试工程即此类);
+ - 映像不覆盖的平台:Android、iOS、wasm、裸机。
+
+按具体系统构建时,os 按 `[target..build]` 只编译该系统的实现文件(`src/linux/*.cpp`、`src/posix/*.cpp`、`src/windows/*.cpp`),平台模块 `mcpplibs.os.<平台>` 只在其平台的构建中有定义;这一机制不因 S2、S3 改变。
+
+mcpp 依 docs/21 的 tier 定义评估两行 openkal 的升级:两行已在四个系统上构建并运行,满足 `verified` 的条件。角色划分写入 mcpp docs/24(openkal)与 docs/35(平台)。
+
+### S2 mcpplibs.os:两层平台判断
+
+R5 依赖一条语言规则:被丢弃的语句(discarded statement)中出现的函数不构成 ODR 使用,因而 Linux 原生构建引用 `os::windows::*` 不要求其定义。普通的 `if` 不具备这一性质,即使条件是常量;映像中又只有运行时判断可用。os 因此提供两层判断:
+
+```cpp
+// 本构建能否运行在 p 上:原生构建为 PLATFORM == p;映像为 p 属于 {linux, macos, windows}
+constexpr bool can_run_on(platform p) noexcept;
+
+if constexpr (os::can_run_on(os::platform::windows)) // 原生的其他平台:整段丢弃,零开销
+ if (os::host() == os::platform::windows) // 原生为常量,映像在加载时判断
+ cacheOnDevDrive = os::windows::on_dev_drive(dir);
+```
+
+映像中,运行时判断是主要机制:`os::host()` 返回加载映像的系统,`os::conventions()`(可执行文件后缀、路径列表分隔符、名称是否大小写无关)与 `os::dirs` 已在运行时作答。两层判断使同一源码在原生构建与映像构建中都正确。
+
+简写为 `os::on(f)`,`f` 必须是泛型 lambda:
+
+```cpp
+os::on([&](auto) { cacheOnDevDrive = os::windows::on_dev_drive(dir); });
+```
+
+泛型 lambda 的函数体是模板,未实例化时不构成 ODR 使用;普通 lambda 的函数体不是被丢弃的语句,引用其他平台的符号属于"不合规、无须诊断",因此 `on` 以约束拒绝非泛型的可调用对象。os 的文档给出常见写法(两层判断、`on
`、`conventions()`、`dirs`)及其适用场景;语法糖只以合乎标准 C++ 的库接口提供,不提供构建插件(Q4 已确认)。
+
+### S3 mcpplibs.os:映像中的平台事实
+
+- **R5 修订:** 平台模块的函数在能回答的地方定义——自身平台的原生构建,以及映像。其他平台的原生构建仍不定义,误用仍为链接错误。现有签名不变,mcpp 的调用不受影响。
+- **映像能做与不能做的:** 映像只经由 openkal 的 `kal_*` 接口与宿主交互。它不能调用 Win32、sysctl 等原生接口,也不能加载宿主的库(openkal SPEC 第 12 条;os 0.3.1 的 `src/openkal/library.cpp` 对 `library::open` 返回 `not_supported`);一个映像服务全部系统,因而没有编译期的平台分支。它能做的是运行时判断(`host()`)与经由可移植通道取得事实:读文件、读环境、启动宿主程序并解析其输出(`process::run`)。
+- **事实的三类与各自的实现:**
+
+ | 类 | 函数 | 映像中的实现 |
+ |---|---|---|
+ | okrun 自身的事务 | `windows::code_page`、`console_output_code_page`、`long_paths_enabled`、`machine_registry_path`、`writable_fixed_drive` | 不提供(误用为链接错误)。代码页与长路径约束原生 Win32 程序;映像的输入输出全部经由 okrun,由 okrun 处理。后两者服务 mcpp 自身的安装与工具链定位,不属于映像程序 |
+ | 经由可移植通道可得 | `linux::translator()` | 读取 `/proc/self/status`、追踪者的 `/proc//comm` 与环境,复用原生实现的 `classify_translator` |
+ | | `macos::translated()` | 恒为 `false`,不启动进程:macOS 上运行映像的 okrun 只有 aarch64 版本(`x86_64-macos` 在目标表中为 `planned`),映像因此总以本机架构运行。日后有 x86-64 的 okrun 时,判断为"映像为 x86-64 且 `hw.optional.arm64` 为 1";不读 `sysctl.proc_translated`,它回答的是被启动的 sysctl 进程自己 |
+ | | `windows::elevated()` | `%SystemRoot%\System32\whoami.exe /groups` 的输出中含 SID `S-1-16-12288`(高完整性级别)。以 SID 而非组名判断,与系统语言无关;子进程继承 okrun 的令牌 |
+ | | `windows::on_dev_drive()` | 经由 `fsutil` 查询所在卷,返回值已是 `std::optional`。是否需要管理员权限、输出是否随语言变化,实施前实测;不能可靠回答时返回空 |
+ | | `posix::effective_user()` | `/usr/bin/id -u`(Linux 也可读 `/proc/self/status` 的 `Uid:`) |
+ | 只有原生接口能回答、无宿主程序可用 | — | 目前未见实例 |
+
+ 这些实现位于 os 的 `src/openkal/` 下,由 os 清单的 `cfg(os = "openkal")` 选择,与原生实现并列。宿主程序以绝对路径启动(不经 PATH 搜索),以有界读取与超时运行;失败时返回空(`optional`)或保守值(`elevated()` 为 `false`)。结果在进程内缓存,每个会话只付一次进程启动的代价,且只在调用时发生。全部工作在 mcpplibs.os 内完成,openkal 不变(Q3)。
+- **`is_<平台>` 与两层判断并存(Q5)。** 二者回答不同的问题,都保留,都不标 deprecated:
+ - `is_windows`、`is_linux`、`is_macos`、`is_posix`:本构建是否为该平台的原生构建。编译期常量,映像中为 `false`。它是"只属于原生构建"的代码的正确写法,例如设置控制台代码页:映像的输入输出由 okrun 处理,这段代码在映像中本就不应执行。
+ - `can_run_on(p)` 与 `host()`:本构建能否运行在、是否正运行在该平台上。一段代码要在映像中也按平台生效时用它们。
+
+ 第一版拟在映像构建中把 `is_<平台>` 标为 deprecated。审查发现这会误报上面那类合法用法,而 deprecated 警告只能以编译器的诊断指令(宏形式)逐处抑制,与项目规则相悖;因此不采用(附录 A.5)。F2 这类错误由两处防止:os 的文档按"原生专属 / 随平台生效"说明两种写法的选择;以映像为发布形态的应用在自身的检查中禁止 `is_<平台>`(mcppls 的 `devtools check os-surface`、mcpp-ide 的等价检查),因为它们的每一段平台代码都应在映像中生效。
+
+mcppls 在 os 发布该版本后改用 S2 的写法,两项功能随之恢复;在此之前,映像在 Windows 与 Linux 上运行时于日志中说明不检测 Dev Drive 与 PRoot(mcppls 0.0.21 `e34cca0`,docs/50-troubleshooting.md),不再静默跳过。
+
+### S4 系统接口的书写规则
+
+生态中的源文件与测试不包含系统头文件:操作系统经由 mcpplibs.os,C 库经由 `std`(宏经由 mcpplibs.mwrap.libc)。系统头文件只出现在 os 自身按平台选择的实现文件中,这是 os 的边界。原始 POSIX 或 Win32 接口不作为 os 的模块提供;确有需要的项目以独立的 mwrap 包(例如 `mcpplibs.mwrap.posix`)承载,os 保持语义接口。
+
+此规则对原生构建同样成立:插件在 Linux 上原生构建时,`os::process::run` 编译为 os 的 `src/posix/exec.cpp`,在 Windows 上编译为 `src/windows/exec.cpp`;插件源码不再随平台变化。
+
+本轮并入 mcpp-plugins #52(0.21.0)与 mcpp-ide #5(1.2.0)(Q2)。
+
+- **mcpp-plugins:**
+ - 七个模块(`dist/apk`、`rules/cuda`、`metal`、`slang`、`spirv`、`swift`、`sycl`)各自以 `popen`/`_popen` 与 `#if defined(_WIN32)` 取一条命令的输出。改为 `mcpp.plugins.tool` 中的一个函数:以 argv 调用 `os::process::run`(不经 shell,因而不再拼接 `2>/dev/null`、`2>nul` 与引号),返回退出状态与输出;七处调用改用它,预处理平台分支随之消失。
+ - `rules/cuda` 以 `dlopen`/`dlsym` 读取驱动版本,改为 `os::library::open` 与 `handle::function<>`。
+ - `src/testing.cppm` 本就为每个用例启动一次自身(`--mcpp-plugins-testing-case=`),此前以 `setenv`/`_putenv_s` 改写本进程的环境、经 `std::system` 与 shell 重定向把上下文传给子进程,并以 `environ`/`_NSGetEnviron` 枚举环境。改为以 `os::process::run` 启动,用例的上下文写在 `spawn_options.env` 中(本进程的环境不再被改写),环境由 `os::env::variables()` 枚举。设计不变,平台分支与头文件随之消失(Q7 因此不再成立)。
+ - os 由 `plugins-progress` 的依赖变为插件的基础依赖,`docs/plugin-development.md` §4 改为"不包含系统头文件",CI 增加与 mcppls `os-surface` 等价的检查(报告 `#include` 与平台预处理分支;`tests/` 下的使用方工程除外)。os 成为基础依赖对构建程序首次构建时间的影响,实施时测量并记录在 PR 中。
+ - 实施中发现 GCC 16 的模块缺陷(mcpp#905):内联函数体经由 mcpplibs.os 时,导入方读不回模块("Bad file data")或链接时缺 `std::optional>` 的成员。规避为"经由 os 的辅助函数不内联",写入 `docs/plugin-development.md` §4(`e77b920`)。`capture` 的标准错误以独立管道读取后追加(`stdio::output` 不是合并到子进程的标准输出管道,`6305d89`)。
+- **mcpp-ide:** `tests/support.hpp` 改为测试模块 `mcppide.testsupport`(`testsupport/`,`mcppide.testing` 已是后端的测试框架模块),各测试改为 `import std` 与该模块;`mcpp-ide-dev check-sources` 在 CI 中报告头文件、预处理分支与 `os::is_<平台>`(`f5686ac`)。
+- **mcpp:** CI 增加等价检查,与 mcpp#904 一并处理。
+
+### S5 依赖书写与锁文件
+
+- 依赖写在一张表中,键为不带引号的身份(命名空间在前),版本为精确版本,精确版本只用一种拼写(不带运算符)。`[workspace.package] mcpp = ">=…"` 声明构建所需的最低 mcpp,不属于依赖,保留范围。
+- mcpp 本体按此改写清单与 `examples/02-with-deps`,并对带引号的点分键给出带改写提示的警告(mcpp#904)。
+- 锁条目的 `targets` 为解析到达该包的配置集合,命令只更新其规划的配置,不删除也不新增其他配置(mcpp#903)。
+
+### S6 mcppls-clangd 0.0.21 的对比运行
+
+- **原生引擎一律链接 zlib**(`80daf27`)。`ci/scripts/configure.sh` 对全部原生平台设 `-DLLVM_ENABLE_ZLIB=FORCE_ON`,找不到 zlib 即停止;Windows 取 vcpkg 的 `x64-windows-static`(与 `/MT` 一致)。原生与映像双向读取对方的分片,CHANGELOG 的 zlib 陈述随之成立。
+- **darwin 的比值超限先定位,上限不放宽。** 0.0.21 的映像相对 0.0.20 新增 zlib;Linux 两行(原生使用发行版的普通 zlib)通过,只有 macOS(原生使用系统优化的 libz)未达限,热启动读取分片时的解压是首要嫌疑。探针分支 `probe/darwin-zlib-off` 以 `LLVM_ENABLE_ZLIB=OFF` 构建映像,只对 darwin-arm64 做对比运行(run `42386660489`)。比值回到 0.0.20 的水平即确认,处理为映像使用带 NEON 与 CRC 指令的 deflate 实现(Q6):aarch64 基线即有 NEON,分片格式与行为不变;实现以 mcpp-index 的包提供,原生构建不受影响。另一可选项"映像写出不压缩的分片"不采用(附录 A.6)。比值不回落则 zlib 被排除,按装载与页错误继续定位(冷启动 `page_faults` 比值 2.74–2.87)。0.0.21 在 darwin 达限之前不发布;mcppls 0.0.21 锁定该引擎,随之等待。
+- 新提交上的完整对比运行(run `42388978321`;Windows 的 zlib 以头文件目录与库文件指定)通过后,CHANGELOG 记录 `Comparison run: `,按 D18 发布。
+
+### S7 mcpp 的测试协议(已实现于 mcpp 2026.11.8.1,见顶层方案 §9.2)
+
+**现状。** 生态中有四种互不相通的测试方式:mcpp 本体用 GoogleTest;mcppls 有自己的模块 `mcppls.testing`(无宏,`"名称"_test = [] {}`);mcpp-plugins 有 `mcpp.plugins.testing`(插件逻辑的用例,经改写进程环境注入上下文);mcpp-ide 有 `mcppide.testsupport`。mcpp 对测试的契约只有"一个 `.cpp` 一个程序,以退出码判定"(docs/08),用例是框架内部的事;mcpp-ide 因此在 `spec/test-frameworks.json` 与 `mcppide.testing` 中逐个框架编码列出用例的参数、报告格式与宏名,才能给出用例树、单个用例的运行与失败位置。每增加一个框架或一个消费方(IDE、CI、agent),这份知识就要再写一遍。
+
+**提案。** mcpp 以规范定义测试程序与运行器之间的协议,mcpp 只实现协议的运行器一侧,框架以适配实现另一侧,开发者自选框架。这与构建插件的关系相同:mcpp 给出契约,具体实现属于包。
+
+- **能力声明。** 提供测试入口的框架包已以 `kind = "entry"` 声明入口、由 `[test] main = { from = "fw/main" }` 选用(docs/08);入口再声明它实现的协议版本(例如 `test-protocol = 1`)。未声明的程序照旧是一个以退出码判定的用例,现有测试无须改动。
+- **列出。** 运行器以协议规定的参数启动程序,程序输出用例的结构化清单:标识、显示名、源位置、标签。
+- **选择。** 运行器以用例标识选择要运行的用例;转义与通配由协议规定,不再是各框架的命令行语法。
+- **报告。** 程序把每个用例的开始、结束、状态(通过、失败、跳过)、耗时、失败位置与输出写到运行器给出的通道(NDJSON),mcpp 汇总到 `mcpp test --message-format json`。mcpp-ide、CI 与 agent 只读这一种格式。
+- **上下文。** 协议允许运行器为单个用例给出上下文(环境与工作目录),并规定用例在同一进程或各自的进程中运行由运行器决定。`mcpp.core` 的访问器经由一个可在测试中按作用域替换的上下文读取引擎的值,替代对 `std::getenv` 的直接读取;插件逻辑的测试因此在进程内设置上下文,S4 中保留的 `setenv` 随之删除。
+
+**框架与头文件。** GoogleTest 成为协议的一种实现:一个适配包以 GoogleTest 的事件监听接口实现列出、选择与报告,使用者仍写 `TEST`、`EXPECT_EQ`,因而仍包含其头文件——宏无法经由模块导出,这是 GoogleTest 自身的形态。消除头文件的途径是一个模块原生的框架:以 `std::source_location` 记录位置、以用户定义字面量或函数注册用例、不定义宏(`mcppls.testing` 已是此形态)。协议使它与 GoogleTest 处于同等地位;生态的四种测试方式收敛为这一个框架包,项目仍可选择 GoogleTest、doctest、Catch2 的适配。
+
+**归属。** 规范与运行器属于 mcpp 的下一版本(新 SPEC 与 docs/08);测试库、引擎与适配为通用测试系统 mtest(`mcpp-community/mtest`,设计见 [mtest .agents/docs/2026-11-08-mtest-design.md](https://github.com/mcpp-community/mtest/blob/main/.agents/docs/2026-11-08-mtest-design.md));mcpp-ide 改为读取协议的报告并以 `spec/test-frameworks.json` 作为未声明协议的程序的后备。不并入 #885 本轮。
+
+## 3. 多视角自我审查
+
+| 视角 | 审查所见 | 处理 |
+|---|---|---|
+| 架构 | 第一版的 S3 以 openkal 的"宿主事实查询"为前提,把一系统的产品特性压到内核抽象层 | 改为 os 经可移植通道实现;openkal 不变,`openkal.host` 只作为热路径需求出现时的扩展(附录 A.3) |
+| 架构 | `can_run_on` 若在映像中恒为 `true`,会把 `emscripten` 也算作映像可运行的平台 | 定义为映像中 `p ∈ {linux, macos, windows}` |
+| 稳定性 | 从映像启动宿主程序:PATH 可被替换、输出随语言变化、程序可能挂起 | 绝对路径启动;以 SID、数值等与语言无关的字段判断;有界读取与超时;失败返回空或保守值 |
+| 稳定性 | `linux::translator()` 在映像中读取 `/proc`,取决于 openkal 在 Linux 上是否能访问 `/proc` | 实施前在 Linux 的映像中实测;不可读时返回空并在日志中说明 |
+| 性能 | 进程启动在 Windows 上约数十毫秒,若在启动路径上会拖慢 mcppls 的就绪 | 结果缓存、只在调用时发生;mcppls 的 Dev Drive 检查移出启动的关键路径 |
+| 性能 | darwin 的比值超限(F6) | 探针定位,上限不放宽(S6) |
+| 用户体验 | 在 os 发布之前,映像用户失去 Dev Drive 提示与 PRoot 识别 | 0.0.21 的日志与故障排查文档明确说明(`e34cca0`);os 发布后恢复 |
+| 兼容性 | 第二版拟在映像构建中把 `is_<平台>` 标为 deprecated:会误报"只属于原生构建"的合法用法,抑制只能用宏形式的诊断指令;已有的映像构建在 `--strict` 下转为错误 | 改为并存:`is_<平台>` 表示原生构建,两层判断表示运行平台;防止 F2 的检查放在以映像为发布形态的应用中(S3);os 0.4.0 只增加接口,不改变已有接口的行为 |
+| 兼容性 | plugins 改用 argv 调用后,原先依赖 shell 的命令(重定向、管道)行为可能不同 | 七处命令逐一核对,只取标准输出、丢弃标准错误由 `process::run` 的选项表达;插件逻辑用例与各 rules 的使用方工程在三个系统上验证 |
+| 跨平台 | 不经 shell 后,Windows 上以 `.bat`/`.cmd` 提供的工具(例如 Android SDK 的若干脚本)不能直接作为程序启动 | 实施时核对七处命令所启动的程序;脚本形式的工具经 `cmd.exe /d /c` 显式启动,并在该函数中注明 |
+| 架构 | 第三、四版对 plugins 测试的分析有误:测试工具本已每个用例一个子进程,`setenv` 只是传递上下文的手段 | 改为在 `spawn_options.env` 中陈述上下文;`mcpp.core` 的上下文注入仍属 S7,用于进程内测试 |
+| 兼容性 | R5 修订只增加定义,不删减;签名不变 | mcpp 的原生调用不受影响 |
+| 跨平台 | `macos::translated()` 读 `sysctl.proc_translated` 会得到 sysctl 进程自己的答案;且 macOS 上只有 aarch64 的 okrun | 映像中恒为 `false`;有 x86-64 的 okrun 时改用"映像架构 + `hw.optional.arm64`" |
+| 一致性 | 精确版本有 `"0.3.1"` 与 `"=0.3.1"` 两种拼写(mcpp 本体与生态) | 统一为不带运算符(mcpp#904) |
+| 一致性 | 生态的头文件规则只在 mcppls 有检查 | plugins、ide、mcpp 各自增加等价检查(S4) |
+| 无感升级 | 依赖精确固定后,os 的新版本不会被静默带入 | 升级随各仓库的版本 PR 显式进行,CI 在升级时报告 deprecated |
+| 测试覆盖 | 映像中的平台事实若只在原生 CI 测试,会重演 F2 | os 的 CI 以映像在四个真实系统上运行这些函数:Windows 运行器为管理员,`elevated()` 断言为真;Linux 运行器在 PRoot 下运行映像,断言 `translator()` 为 `proot`;macOS 断言 `translated()` 为假 |
+| 测试覆盖 | 非管理员的 Windows 路径在 CI 中不易得到 | 以受限令牌启动(`runas /trustlevel` 或等价方式)的测试在实施时评估;不可得时在文档中记录覆盖缺口 |
+| 过程 | 对 darwin 有效测量的重跑违反性能政策 | 在 F6 记录;只作诊断参考,结论以第一次测量为准 |
+
+## 4. 归属与版本
+
+| 项 | 仓库 | 版本 | 状态 |
+|---|---|---|---|
+| F7 修复 | mcppls | 0.0.21(#79) | 已提交 `aaf7174` |
+| F3 mcppls 头文件、`os-surface` 检查 | mcppls | 0.0.21(#79) | 已提交 `253aff8` |
+| S3 末段:mcppls 过渡日志 | mcppls | 0.0.21(#79) | 已提交 `e34cca0` |
+| S5 生态清单改写 | mcppls、mcpp-ide、mcpp-plugins、mcppls-clangd | 各自本轮 PR | 已提交 |
+| S6 zlib | mcppls-clangd | 0.0.21(#18) | 已提交 `80daf27`、`becb991`,对比运行进行中 |
+| S6 darwin | mcppls-clangd | 0.0.21(#18) | 探针进行中 |
+| S4 mcpp-plugins | mcpp-plugins | 0.21.0(#52) | 已提交 `8d0afd7`(52 个插件逻辑用例、all-rules-compile 本地通过) |
+| S4 mcpp-ide | mcpp-ide | 1.2.0(#5) | 已提交 `f5686ac`(`testsupport/` 模块、`check-sources`) |
+| S7 | mcpp、mcpplibs、mcpp-ide | mcpp 下一版本 | 提案,独立设计 |
+| S1 文档与 tier 评估 | mcpp | 下一版本 | 新 issue |
+| S5 mcpp 本体 | mcpp | 下一版本 | mcpp#903、mcpp#904 |
+| S2、S3 | mcpplibs.os | 0.4.0(只增加接口) | 新 issue;mcppls、mcpp-ide 随后迁移 |
+| openkal | — | — | 不变 |
+
+## 5. 待审查的问题
+
+- **Q1、Q3、Q4、Q6** 已确认。
+- **Q2** 已确认:S4 并入 #52、#5。
+- **Q5** 已确认:并存(S3),`is_<平台>` 不标 deprecated;防止 F2 的检查放在以映像为发布形态的应用中。
+- **Q7** 不再成立:plugins 测试工具本已每个用例一个子进程,改为以 `spawn_options.env` 陈述上下文(`8d0afd7`)。
+- **Q8** S7(mcpp 测试协议、模块原生框架、GoogleTest 适配、`mcpp.core` 上下文注入)作为 mcpp 下一版本的独立设计推进,是否同意。
+
+## 附录 A 被考虑而未采用的方案
+
+- **A.1 构建插件提供平台语法糖。** 插件在构建时同样不知道映像的加载系统;改写源码形成 clangd 与 IDE 无法解析的方言;宏形式与项目规则相悖。
+- **A.2 os 提供原始系统接口的映射(`os.unix`、`os.windows` 下的 POSIX/Win32 镜像)。** 生态中唯一的直接 POSIX 调用是已删除的诊断探针,未见用例;原始接口是一部分系统的表面,与 os 的语义接口和 R5 相悖。确有需要时以独立的 mwrap 包承载。
+- **A.3 openkal 收录平台事实。** 逐项进入 ABI 会使 ABI 的版本随各系统的新特性变化。热路径上确需零开销时,合乎现有机制的形式是可选接口 `openkal.host`:okrun 作为原生程序在宿主侧作答,映像以弱导入使用(`examples/weak`),调用代价与其他 `kal_*` 调用相同;未提供该接口的运行器使弱导入为空,os 回退到可移植通道。目前 libokrun 只绑定规范的接口面(`run/src/bindings.cpp` 的静态表),不提供登记额外名字的接口;libokld 不在运行时提供服务。本轮所涉事实均为每会话一次的查询,不引入该接口。
+- **A.4 映像中无法回答的事实定义为返回空值。** 会改变 `bool`、`unsigned` 等现有签名,影响 mcpp 的原生调用;不提供(链接错误)使误用在构建时可见。
+- **A.5 映像构建中把 `is_<平台>` 标为 deprecated。** 误报"只属于原生构建"的合法用法,逐处抑制需要宏形式的诊断指令;以并存与应用侧的检查代替(S3)。
+- **A.6 映像写出不压缩的分片。** 分片体积增大,映像与原生写出的分片在格式上不同;以更快的 deflate 实现代替(S6)。
diff --git a/.agents/docs/2026-11-08-ecosystem-top-level-design.md b/.agents/docs/2026-11-08-ecosystem-top-level-design.md
new file mode 100644
index 000000000..2a907166a
--- /dev/null
+++ b/.agents/docs/2026-11-08-ecosystem-top-level-design.md
@@ -0,0 +1,448 @@
+---
+subject: ecosystem
+status: active
+---
+
+# 生态顶层方案:#885 收尾之后的工作流、原则与依赖
+
+**日期:** 2026-11-08
+**状态:** 第六版,实施中(T1–T5、U1 已确认;第 8 节为实施计划,第 9 节为实施记录)
+**范围:** mcpp、mcpplibs.os、openkal、mcpp-plugins、mcppls、mcppls-clangd、mcpp-ide、mcpp-index、xim-pkgindex、mtest
+
+本文是生态层面的顶层方案。它汇总三份设计与三个新 issue,给出贯穿各仓库的原则、全部工作流及其依赖、尚未收敛的事项,并从全局与生态的视角审查。各项的细节以所引设计为准。
+
+| 文档 | 内容 |
+|---|---|
+| [2026-11-07-ecosystem-885-closeout-design.md](2026-11-07-ecosystem-885-closeout-design.md) | #885 收尾:D1–D20(mcpp 2026.11.7.1/.2、openkal 级联、IDE、mcppls、插件) |
+| [2026-11-08-ecosystem-885-supplement-design.md](2026-11-08-ecosystem-885-supplement-design.md) | 补充:S1 目标角色、S2/S3 平台判断与映像中的平台事实、S4 系统接口、S5 依赖书写、S6 对比运行、S7 测试协议 |
+| [mtest .agents/docs/2026-11-08-mtest-design.md](https://github.com/mcpp-community/mtest/blob/main/.agents/docs/2026-11-08-mtest-design.md) | 通用测试系统 mtest |
+| mcpp#903 | 锁文件的 `targets` 随命令变化 |
+| mcpp#904 | mcpp 本体的依赖书写 |
+| mcpp#905 | GCC 16 模块:内联函数体经由 mcpplibs.os 时导入方失败(upstream-bug,已有规避) |
+| mcpp#874 | 工作区的 `target/` 占用与 `mcpp test --workspace` 的重复编译(开放;主体已在 2026.10.31.3 实现,待在报告者的工作区验证) |
+
+## 1. 贯穿生态的原则
+
+| 编号 | 原则 | 出处 |
+|---|---|---|
+| P1 | **核心只定契约,实现在包中。** 构建插件、平台、测试协议皆如此:mcpp 给出规范与运行器一侧,具体实现属于包 | 既有原则;S7、mtest §2 |
+| P2 | **两种角色,不分等级。** 生态应用以 ok 映像为唯一发布形态;原生目标是完整支持的构建目标 | S1 |
+| P3 | **系统经由模块。** 源文件与测试不包含系统头文件、不含预处理平台分支;操作系统经由 mcpplibs.os,C 库经由 `std` 与 mcpplibs.mwrap.libc;原始 POSIX/Win32 不进入 os | S4 |
+| P4 | **平台判断分两种。** `is_<平台>` 表示原生构建(编译期);`can_run_on` 与 `host()` 表示运行平台(编译期能否、运行时是否);发布映像的应用在自身的检查中禁止 `is_<平台>` | S2、S3 |
+| P5 | **平台事实在 os 中,openkal 不收录产品特性。** 映像中的平台事实由 os 经可移植通道回答;热路径确有需要时以可选接口 `openkal.host` 扩展 | S3、附录 A.3 |
+| P6 | **依赖一张表、身份键、精确版本、一种拼写** | S5 |
+| P7 | **政策由工具执行。** 测量的有效性、能否重复、能否重试,由工具依声明判定,不依赖记忆 | S6;mtest §4.4 |
+| P8 | **面向 Agent 的输出。** 结构化、带 Schema 与版本、简短、给出复现命令 | mtest §6 |
+| P9 | **自身是自身的用户。** mtest 以自身测试自身;生态的应用以映像测试,与发布物一致 | mtest §9;D18 |
+| P10 | **每个仓库每个版本一个 PR;确定的事项不拆到以后;编译器缺陷以 mcpp 的 upstream-bug issue 记录并在实现中规避** | 既有原则 |
+| P11 | **底层与基础设施的变动符合规范,并经严格审查。** openkal 家族、openkal-platform、mcpplibs.os、mwrap 包装、okrun 与 okld 的每一处变动,对应其规范条款(openkal SPEC、mcpp SPEC),说明兼容性,经下游使用者的候选验证;mtest 落地后,规范一致性套件、接口面变化检测与审查记录成为它们的发布门禁 | mtest §3.2 |
+| P12 | **一个仓库多个组件时,版本按系列表达,CI 按组件触发。** `X.Y` 为系列,由服务端(或后端)与全部插件共享;`Z` 为各组件自己的补丁号。改动所属的组件由一张声明的表判定,只运行该组件及依赖它的组件的检查;未声明的路径按全部组件处理 | W6;mcpp-ide 1.2.0 的版本规则 |
+
+## 2. 工作流
+
+### W1 #885 本轮(进行中)
+
+| 仓库 | 版本 / PR | 状态 | 阻塞 |
+|---|---|---|---|
+| mcpp | 2026.11.7.2 | 已发布、镜像、入索引 | — |
+| mcppls-clangd | 0.0.21(#18) | PR CI 通过;对比运行 `42395068307` 验证 win32 原生 zlib(`zs.lib`);映像改用 compat.zlib-ng(本地提交,待索引发布后推送) | W5:compat.zlib-ng 2.3.3 已合入索引(mcpp-index#727),待发布 |
+| mcppls | 0.0.21(#79) | #80 修复、头文件清零、过渡日志、依赖改写、mcpp 2026.11.7.2;CI 除 ux-xlings 外通过(U4-cold 冷阶段仅 2 轮,引擎占比 0.88,按无效测量重跑) | 引擎锁需 clangd 0.0.21 发布;W6 的采用时机(T5) |
+| mcpp-ide | 1.2.0(#5) | 测试模块、`check-sources`、CLion 公开 API;CI 除 CLion 2026.2.4.1 外通过 | U3:窗口在动作后为 `visible=false`、所选内容为 Module Graph;测试已改为记录窗口可见性的时间线与窗口类型(`d5ced7a`) |
+| mcpp-plugins | 0.21.0(#52) | S4 完成(`8d0afd7`、`6305d89`、`e77b920`);本地 52 个用例与各消费者通过;CI 全部通过 | — |
+| mcpp-index | — | #727(compat.zlib-ng 2.3.3)已合入;其余待 plugins 0.21.0 发布后的更新 | plugins 发布 |
+| xim-pkgindex | — | 待 mcppls、mcpp-ide 发布后的 bump | 各自发布 |
+
+收尾:生态级全局审查 → 各 PR squash 合入 → 发布与 GitCode 镜像 → 索引 → xlings 全生态验证 → 以 speak-agent 关闭 issue(#896、#897、#839、#840、#843、#803、mcppls-clangd#17、#887、#885 总账)与综合汇报。
+
+**xlings 全生态验证(T2 已确认)。** 在一个新建的临时目录中进行,与本机的全局环境隔离:
+- 环境:`HOME`、`XLINGS_HOME`、`MCPP_HOME` 与 `XDG_DATA_HOME`、`XDG_CONFIG_HOME`、`XDG_CACHE_HOME`、`XDG_STATE_HOME` 指向该目录之下;`env -u` 去除会话中的 `XLINGS_PROJECT_DIR`、`XLINGS_ACTIVE_SUBOS`、`XLINGS_SUBOS_MODE`、`XLINGS_SUBOS_LIB`、`XLINGS_BIN`、`XLINGS_SHIM_DEPTH`;工作目录为中性目录;不使用 shim。
+- 步骤:`xlings update`;不带版本安装 `mcpp`、`mcppls`、`mcpp-ide`、`mcpp-ide-nvim`(必须解析到新版本,GLOBAL 与 CN 两个镜像各一次);以安装的 mcpp 新建项目并 `build`、`run`、`run --print-launch`(第二次走快路径)、`test`、`--target x86_64-openkal` 的构建与运行;mcppls 的 `--version` 与一次 `diagnostics`;mcpp-ide 后端以 okrun 启动并完成初始化握手;nvim 插件在无界面模式下载入。
+- 记录:每一步的版本、耗时与结果写入一份报告,附在 #885 的总账中。
+
+### W2 mcpp 下一版本
+
+| 项 | 来源 |
+|---|---|
+| 锁文件 `targets` 只随所规划的配置变化 | #903 |
+| mcpp 本体依赖改写;带引号的点分键给出改写提示;`examples/02-with-deps`;源文件检查(头文件、预处理分支) | #904、S4、S5 |
+| 目标角色写入 docs/24 与 docs/35;openkal 两行按 tier 定义评估为 `verified`。评估所见:mcpp 自身的 CI 只在 Linux 上运行映像(942、960、961、964 声明 `llvm`,而只有 Linux 分片提供它),不满足 docs/21 对 `verified` 的定义。为此新增测试 1033(以平台的全部层构建本机架构的映像并运行、测试,另一架构只构建),并由 `tests/e2e/host-requirements.tsv` 与 `check_e2e_coverage.py` 要求它在 Linux、macOS、Windows 上通过 | S1 |
+| 测试协议的运行器一侧;`[test]` 的协议声明;`mcpp.core` 的上下文注入;受影响选择所需的构建图查询;`mcpp new` 默认使用 mtest | S7、mtest §11 |
+| GCC 16 模块缺陷的跟踪与精简复现 | #905 |
+| 构建在项目根目录写下 `.mcpp/compiler-runs//`(编译生成的锁,`src/build/schedule/detach_codegen.cppm`,有意置于 `target/` 之外以免被 clean 删除),而该目录不被版本控制忽略:os 0.4.0 的 PR 曾误提交其中的锁文件。mcpp 创建 `.mcpp/` 时在其中写入内容为 `*` 的 `.gitignore`,使目录自我忽略 | 本轮发现 |
+| 工作区的空间与重用:在报告者的工作区(Sunrisepeak/mui,112 个成员)以当前 mcpp 复测,按 issue 的表格给出各区域大小、一次 `mcpp test --workspace` 中重复构建的边数与时间、宿主工具每个键的变体数;与 issue 的期望逐条对照,未实现的部分在本版本实现:`[workspace] target-budget`(或等价的环境变量)、低空间警告以"上一次构建该配置写入的字节数"为阈值(当前为固定的 2 GiB)、std 模块构建的收集。后者在本机已复现:`~/.mcpp/build-cache/v1/std` 有 159 份构建(4.9 GiB,最早 2026-11-01),`mcpp cache prune --older-than 2d` 与 `cache gc --older-than 2d` 均未收集任何一份(gc 只覆盖包与宿主工具)。复测数据与差距写入 issue 后关闭 | #874 |
+
+### W3 mcpplibs.os 0.4.0
+
+`can_run_on`、`on`(泛型 lambda)、R5 修订(平台模块在能回答的地方定义)、映像中的平台事实实现(`linux::translator`、`windows::elevated`、`windows::on_dev_drive`、`posix::effective_user`;`macos::translated` 恒为 `false`)、文档中的写法选择;os 的 CI 以映像在三个系统上运行这些函数。只增加接口。随后 mcppls 与 mcpp-ide 迁移,Dev Drive 提示与 PRoot 识别在映像中恢复。
+
+**`is_image` 改名为 `is_openkal`(本轮讨论确定)。** `os::is_image` 的含义是"本构建以 openkal 目标构建,产物为 ok 映像",即 `PLATFORM == platform::openkal`,并不是"内核层为 openkal"(os 的全部构建都在 openkal 之上)。原名有两处不清楚:
+- 它是 `is_<平台>` 家族中唯一不以枚举值命名的成员(`is_linux` 对应 `platform::linux`,而 `is_image` 对应 `platform::openkal`),而 mcpp 清单对同一事实写作 `cfg(os = "openkal")`;
+- "image" 单独出现时可指容器或磁盘映像。
+
+处理:
+- os 0.4.0 以 `is_openkal` 为正式名,定义为 `PLATFORM == platform::openkal`,注释写明"以 openkal 目标构建,即 ok 映像(openkal SPEC clause 12),不是'在 openkal 之上'";
+- `is_image` 保留为 `[[deprecated("use is_openkal")]]` 的别名,现有用户(mcppls `workspace.cpp:708`)得到迁移提示而不中断,在 mcppls 迁移到 os 0.4.0 时改写;
+- README、CHANGELOG 与 `checks/discarded` 的两层写法示例同步改名。
+
+### W4 mtest
+
+`mcpp-community/mtest`(包 `mcpplibs.mtest`):规范、引擎(ok 映像)、C++ 模块原生库、适配、C ABI 报告库(`libmtest.okso`)、宿主集成、前端;以自身测试自身;首批迁移五个项目;目标为 PR CI 墙钟中位数减半、发布 CI 关键路径缩短 30%。
+
+### W5 映像中的 deflate(已确认,实施中)
+
+探针确认了原因。darwin-arm64 上映像相对原生(链接 macOS 带 NEON 的 libz)的热启动处理器时间比值,四对逐一为:以 compat.zlib 构建 1.332、1.236、1.214、1.252(运行 `42369457883`);不带 zlib 构建 0.973、1.050、1.107、1.088(运行 `42386660489`)。两组不相交,差距远大于 0.03 的离散界,故差距来自分片的解压。
+
+实现为 mcpp-index 的 `compat.zlib-ng` 2.3.3(#727):zlib 兼容形态,即 CMake 的 `WITH_RUNTIME_CPU_DETECTION=OFF` 构建,各函数在编译期绑定到架构基线保证的实现,aarch64 为 NEON(inflate_fast、adler32、chunkmemset、slide_hash、compare256、longest_match),x86_64 为 SSE2。映像无法询问处理器(openkal 不提供处理器特性),所以只构建基线之内的路径。Armv8 的 CRC32 指令不属于 aarch64 基线,需要整个程序的编译选项或运行时检测,因此不构建;它只影响 gzip 流,LLVM 写的 zlib 流以 adler32 校验。Q6 所说的"CRC"据此限定为不启用。验证:
+- 索引 CI 的四条工作区腿(Linux GCC、Linux LLVM、macOS arm64、Windows)通过;
+- 本地 `x86_64-openkal` 测试通过;
+- `aarch64-openkal` 构建中 `inflate` 调用 `inflate_fast_neon`;
+- 测试读取 madler zlib 写出的流。
+
+mcppls-clangd 的映像依赖改为 `compat.zlib-ng = "2.3.3"`,对比运行给出结论后写入 CHANGELOG。
+
+### W6 mcppls 的组件版本与按目录的 CI(T5 已确认:并入本轮;已实现)
+
+**实现记录。**
+- mcppls #79:`fd87271` 版本与系列检查(devtools `--series`/`--bump`/`--print --component`;`mcppls.base.series` 与 `initialize` 的拒绝;VS Code、CLion 的启动前检查;Zed 发送 `initializationOptions.plugin`);`b0f1c18`、`9f51d1b` 发布(资产注明组件,`release check --component/--from`,`release.yml` 的 `component` 输入与组件标签,发布器按标签筛选);`fcfd472`、`0a4f140` CI(`.github/components.tsv` 的 `@core`/`@flag`,`changes.sh` 与其用例,`server-digest` 标记与借用运行,`invariants` 与 `ci` 汇总作业)。
+- mcpp-ide #5:`91b1437`,同一分类脚本,`@core backend`,`backend-digest` 标记,`checks` 与 `ci` 汇总作业。
+- 两个仓库的 `main` 均无分支保护规则,`ci` 作为唯一必需检查在启用保护时设置。
+
+**借用的实现选择。** 设计稿写的是以 Actions 缓存保存负载。实现改为借用 main 运行的产物:main 在构建负载之后上传名为 `server-digest-<摘要>` 的标记产物;插件 PR 按名称查询标记,得到该运行的 id,插件作业以 `run-id` 下载其 payload、stage 与 kit(payload action 已有此能力,夜间运行即以此测试 PR 的字节)。这样不复制数据,也不受缓存容量限制。mcppls 的产物保留 7 天;mcpp-ide 的后端产物保留 90 天。
+
+**现状。** mcppls 的服务端与全部插件共用一个版本(`mcpp.toml` 的 `0.0.21`,`version --check` 使 VS Code、CLion、Zed、Claude Code 插件与之相同),每个 PR 都运行整个 CI。#79 的一次运行用时约 3 小时(`42385557959`,15:16–18:18),其中服务端、负载、kit、一致性与 ux 占大部分。CLion 或 Zed 的修复只改动 `editors/<插件>/`,也要重建服务端与负载,并等待全部检查。mcpp-ide 已采用系列版本(README "Versions"),但 CI 同样不按组件区分。
+
+**版本(P12)。** 版本为 `X.Y.Z`:
+- `X.Y` 为系列,由服务端与全部插件共享;`Z` 为各组件自己的补丁号。
+- 组件为:服务端(`mcppls`:单文件程序、映像、xlings 包),以及插件 VS Code、CLion、Zed、Claude Code。Neovim 插件与 Copilot CLI 的配置片段没有版本,随系列发布。
+- 系列发布的标签为 `vX.Y.0`,发布全部组件。组件补丁的标签为 `server-vX.Y.Z`、`vscode-vX.Y.Z`、`clion-vX.Y.Z`、`zed-vX.Y.Z`、`claude-code-vX.Y.Z`,只发布该组件。
+- 兼容规则:插件接受同一系列的任何服务端,拒绝其他系列,并在提示中给出两个版本与匹配的安装命令。VS Code 与 CLion 内带负载,只有在用户指定服务端路径时才会出现不匹配;Zed 与 Neovim 在 `PATH` 上找到 `mcppls` 时检查系列。插件补丁内带其系列中最新的服务端补丁,所带服务端的版本记录在包内并显示于状态报告。
+- 引擎(mcppls-clangd)保持自己的版本,服务端以精确版本锁定它;服务端补丁可以改变这一锁定。
+- 工具:`mcppls-devtools version` 支持 `--series X.Y`(全部组件置为 `X.Y.0`)与 `--bump <组件>`(只增加该组件的 `Z`);`--check` 检查全部组件同一 `X.Y`、二进制报告的常量与 `mcpp.toml` 一致。各组件的 `Z` 各自递增,所以每个市场(VS Code Marketplace、Open VSX、JetBrains、Zed)看到的版本都只增不减。
+- 发布:`packaging/release.manifest.json` 为每项资产注明所属组件。系列发布检查全部资产,组件发布只检查该组件的资产;CHANGELOG 的组件补丁小节写在系列小节之下,并注明组件。
+- 迁移:mcppls 的第 N 次发布 `0.0.N` 对应系列 `0.N`。`0.0.21` 以 `0.21.0` 发布,比 0.0.20 大,在各市场中合法;下一系列为 `0.22.0`。
+
+**CI(P12)。**
+- **组件表。** 仓库根的一张表声明组件与路径:
+ - 服务端:`src/**`、`modules/**`、`mcpp.toml`、`mcpp.lock`、`vendor/**`、`packaging/**`、`conformance/**`、`tests/**`;
+ - 各插件:`editors/vscode/**`、`editors/clion/**`、`editors/zed/**`、`editors/nvim/**`,Agent 集成为 `editors/{claude-code,copilot-cli,agents}/**`;
+ - 文档:`docs/**`、`*.md`;
+ - `.github/**`,以及表中未列出的路径,按全部组件处理。
+- **分类。** 第一个作业按表把改动的文件分到组件,输出每个组件是否受影响。分类有自己的单元测试(以文件列表为输入的样例),与 mcpp 的 `classify_changes.py` 相同。
+- **依赖传递。** 服务端受影响时运行全部作业,因为插件以新的服务端测试。只有插件受影响时,只运行该插件的构建与端到端测试,以及版本与仓库不变量的检查;只有文档受影响时,只运行文档检查。
+- **负载的重用。** 负载以服务端输入的内容摘要为键,存入 Actions 缓存;main 的运行写入,PR 可读。只改插件的 PR 按摘要取回与 main 相同的负载,不重建服务端。缓存未命中则照常构建,结果正确,只是变慢。重用以内容摘要证明负载相同,与分类是否正确无关。
+- **全量运行的场合。** 推送到 main、手动触发、夜间与发布运行全部作业,不做分类,使 main 的缓存保持有效、覆盖保持完整。
+- **汇总作业。** 一个汇总作业作为分支保护的唯一必需检查:分类要求的作业全部成功、其余作业是因分类而跳过时,汇总作业成功。
+- **性能政策。** 政策(P7)不变:被跳过的测量不算通过;发布前的对比运行仍是全量。
+
+**mcpp-ide。** 版本规则已经如此。它的 CI 以同一张表的形式区分后端(`src/**`、`protocol/**`、`spec/**`、`webui/**`、`testsupport/**`、`tests/**`、`mcpp.toml`)与 `mide-plugins/<插件>/**`,并以同样的方式重用后端的映像包。
+
+**审查。**
+
+| 视角 | 所见 | 处理 |
+|---|---|---|
+| 兼容性 | 市场、xlings 与 mcpp-ide 对版本的依赖 | `0.21.0` 比已发布的全部版本都大;xim 按 SemVer 比较,`mcppls@0.21.0` 是新的最高版本;mcpp-ide 的 CLion 插件以插件 id 依赖 mcppls 的 CLion 插件,不约束版本 |
+| 稳定性 | 重用的负载可能与源码不符 | 缓存键为服务端全部输入的内容摘要,加上 `.github/versions.env` 与运行器系统;负载内记录其摘要,测试开始时核对 |
+| 稳定性 | 分类漏掉一个依赖,插件 PR 就会在旧服务端上通过 | 未声明的路径按全部组件处理;main 每次推送全量运行,漏判在合入后的第一次运行中暴露;组件表的覆盖由一项检查保证:仓库中的每个文件至少属于一个组件 |
+| 用户体验 | 系列不匹配 | 插件给出两个版本与安装命令,不静默降级 |
+| 一致性 | mcppls 与 mcpp-ide 的规则 | 同一原则(P12)、同样的标签形式与汇总作业;两者的组件表各自声明 |
+| 测试覆盖 | 只改插件的 PR 不运行一致性与 ux | 这两组只测服务端与负载,而负载与 main 相同(由摘要证明);插件自身的端到端测试照常运行 |
+
+**预期。** 只改插件的 PR 不再构建服务端、负载与 kit,也不运行一致性与 ux,墙钟从约 3 小时降到该插件作业的时间(CLion 约 10 分钟,Zed 与 VS Code 约 20–30 分钟)。实施后以 mtest 的 CI 汇总测量(W4 §10.2)。
+
+### W7 API 的生命周期(讨论确定)
+
+**现状。** C++ 的 `[[deprecated("…")]]` 已使编译器在每个使用处警告,并随模块的 BMI 传递。它缺少四样东西:
+- 版本信息:自哪一版弃用、哪一版移除、以什么替代;
+- 汇总:依赖内部的使用同样告警,信息噪声大,`-Werror` 下直接失败;
+- 机器可读的数据:IDE、Agent 与 `mcpp update` 无从知道升级会破坏什么;
+- 更多等级:没有"实验性"一级,也无法表达整个包的弃用。
+
+mcpp 对自身清单键已有完整的同类机制:`kDeprecations` 注册表、docs/51、带替代写法的警告、`mcpp why compat`、`--strict`,以及到期即失败的测试。W7 把它推广到包与包的 API。
+
+**设计**(清单优先、核心最小:核心给出契约,编译器做编译器的事):
+1. **声明级,源码为唯一来源。** 使用标准属性,并约定 mcpp 能识别的消息语法:`[[deprecated("since 0.4.0, removed in 0.6.0: use is_openkal")]]`。编译器照常告警;mcpp 已有的模块扫描读取结构化数据。数据不写两份,因而不会漂移。
+2. **包级与模块级,在清单中。**
+ - `[package] status = "experimental" | "stable" | "deprecated"`,可带 `replaced-by`;
+ - `[lib]` 下的模块可各自声明 `status`;
+ - 实验性 API 需要使用方显式允许(`allow-experimental = ["<包>"]`)。
+3. **构建时的使用方体验。**
+ - 依赖内部的弃用告警不再重复;
+ - 使用方自身代码的使用在构建末尾汇总一次("3 处使用了 mcpplibs.os 的 2 个弃用 API,0.6.0 移除;`mcpp why deprecated`");
+ - `--strict` 下转为错误;
+ - 诊断以编译器的结构化格式读取(GCC JSON、Clang 与 MSVC 的 SARIF),不做文本匹配。
+4. **升级时。** `mcpp update` 或锁文件的变化跨过移除版本时,在构建失败之前列出将被移除的 API 与替代。
+5. **库作者的发布门禁。**
+ - `mcpp publish` 在到达移除版本而声明仍在时拒绝;
+ - 移除版本不晚于弃用版本时拒绝;
+ - 移除一个此前未弃用的导出声明时拒绝(以模块扫描得到的导出清单与上一个已发布版本比较);
+ - 发布说明可由数据生成。
+6. **数据的消费方。** `--message-format json`、`mcpp why deprecated`,以及 mcppls 中的删除线与"改用 X"的快速修复。
+7. **mcpp 自身。** `kDeprecations` 改为同一机制的一个使用者,docs/51 的表与检查保持不变。
+
+归属:mcpp 下一版本之后的独立版本,规范写入新 SPEC 与 docs/05、docs/51;W3 的 `is_openkal` 别名是它的第一个使用者。
+
+### W8 测试迁移到 mtest 与 CI 时间(T6 已确认;并入本轮)
+
+**测量(2026-11-08)。** mcpp 一次 PR 74 个作业、627 runner 分钟、墙钟 56 分钟;关键路径为 mcpp 构建(约 13 分钟)之后的 Linux 单元测试(43 分钟;16 核本机 6 分钟,构建占 210 秒)。e2e 720 个脚本在 Linux 累计约 94 分钟,按提交的耗时表分片,分片内串行(本轮一张过时的表使一个分片超时)。mcppls 一次 PR 88 个作业、446 runner 分钟、墙钟 92 分钟,关键路径为服务端与负载(约 70 分钟)之后的 stability。mtest 引擎已有 `--changed`(由程序的 `sources` 与用例的 `file:` 产物选择)、历史耗时、`resources` 互斥、`--failed`、`bisect`、`hot` 与 `path`。
+
+**设计。**
+
+| 仓库 | 内容 |
+|---|---|
+| mcpp | e2e 为 mtest 的 set:每个脚本一个用例,`# requires:` 为标签,`tests/e2e/host-requirements.tsv` 为 set 的过滤,独占点(`MCPP_HOME`、网络、端口)为 `resources`;分片按存储的历史耗时以最长优先分配,分片内按资源并行,耗时表不再提交。`mcpp test --changed ` 由构建图选择测试:测试记录的 `sources` 与传递导入覆盖变更即受影响。测试程序合成选项:一个目录的测试文件为一个程序,链接数随之减少。e2e 的覆盖映射:main 上定期以插桩的 mcpp 运行全部,得到用例到源文件的映射,存为用例的 `file:` 产物;PR 选择与变更相交的用例和冒烟集,无映射或映射过期的用例视为受影响。CI:PR 运行受影响集与冒烟集,main 与发布运行全部;单元测试按历史耗时分到多台机器 |
+| mcppls | 一致性与 ux 场景为 mtest 用例;性能与稳定性测量声明 `serial:perf`,不与其他用例并行;失败只重跑失败项(`--failed`);与 W6 的组件表与负载借用一致 |
+| mtest | 静态用例清单与清单属性:`mcpplibs.mtest.manifest` 从源文本读出 `"name"_test` 字面量、属性、命名套件与嵌套用例,`mtest list --static` 不构建而列出;属性写在 `[cases]` 或宿主 mcpp.toml 的 `[package.metadata.mtest.cases]`,优先于程序的列举。不采用构建插件:mcpp 保存工具的 `[package.metadata]`,引擎与编辑器读同一份文本,不必每次构建运行构建程序。覆盖映射(`mtest-coverage/1`:用例、源文件、提交与时间,由 GCC 的 `.gcda` 计数测得);`[discover.]` 组与 `--shard`;存储以 main 的产物在运行之间共享 |
+
+**判据。** 既有 e2e 保持一个脚本一个用例;新增与改动的 e2e 以 Bash 适配的 `mtest_case` 写出各判据,失败定位到判据并可由 `--case` 单独运行。
+
+**探针(先行)。** 一个临时 PR 测三项后定参数:分片内 2–3 路并行的墙钟与 runner 分钟;按目录合成测试程序后单元测试的时间;以 main 的 `target/` 复用时实际重跑的边数(本轮之前测得 830 条边全部重跑,其时 build 与 test 尚为两个配置)。
+
+**探针结果(#909)。**
+
+| 项 | 测得 | 决定 |
+|---|---|---|
+| 分片内并行(Linux 一个分片 81 个测试) | 一次一个 424 s;两个 220、310、384、395 s;三个 225 s。两个并行时 955 失败两次(`self env --target x86_64-openkal --format json` 的答复缺 `data`,测试已改为留下该答复) | `E2E_JOBS = 2`,三个无收益 |
+| build 之后的 test(mcpp 自身,release) | 同一检出内 test 重编 637 个目标:`trim_paths` 不作用于测试,测试是另一个配置 | `trim_paths` 作用于被测代码,测试程序自己的文件保留路径;build 与 test 为同一配置,重编 0 个 |
+| 另一检出接手 `target/` | 975 条边、255 个目标重跑(检出使源文件的时间晚于目标);源文件时间回拨后 0 条 | 不缓存 `target/`:仓库缓存的 10 GB 上限已由 bootstrap 的缓存占用(R3),收益只在源文件时间被恢复时成立 |
+| 按目录合成单元测试程序(mcpp 的 165 个单元测试,4 核 runner,冷构建) | 每文件一个程序:墙钟 2506 s,其中构建 1590 s、运行 20 s;每目录一个程序:2455 s,构建 1504 s、运行 50 s。链接不是瓶颈,编译是 | mcpp 自身保持每文件一个程序(失败定位到测试文件);`[test] programs = "directory"` 作为选项保留。单元测试的关键路径改由 `--since` 缩短:PR 只构建受影响的测试 |
+
+### W9 用例的类型、作用域与 mtest 的布局(并入本轮)
+
+**问题。** mtest 0.2.0 的属性只能写在单个用例上:`suite` 只有名字与函数体,一个一致性文件的每个用例都写 `[kind::cts]`,一组独占机器的用例只能逐个写 `[resource{"machine"}]` 或退回 `mtest.toml` 的 `[cases."glob"]`。各类别的模块(`mtest.cts`、`mtest.e2e`、`mtest.bench`)只提供函数,与类别无关:bench 用例漏写 `[kind::bench]` 即按 unit 计。共享的准备只能写在套件体内,而套件体在列举时也运行,昂贵的准备因此在 `list` 与未选中时发生。静态清单只读 `kind` 与 `tag`。用例没有自己的时限。mcppls 的一致性以 131 个目录与 6 张 CI 清单表达,变体、平台、资源与分组分散在 ci.yml、脚本与 `mtest.toml` 中。
+
+**原则。** 代码即测试,也是测试的配置:每个最小用例在代码中声明它是什么、需要什么、如何运行,runner 据此列举、选择、调度与复用,并记录所做的(资源、时间、共享对象、诊断)。`mtest.toml` 是可选的声明方式,词汇与代码的属性相同,用于不能修改源码的程序;两者冲突时以代码为准。
+
+**设计(mtest 0.3.0,mtest#4)。**
+
+| 项 | 内容 |
+|---|---|
+| 对象即用例 | `mtest::Suite` 为通用形式,`mtest::UTest`、`mtest::CTS`、`mtest::E2E`、`mtest::Bench` 以类型表示类别;对象取名字、属性与函数体。创建对象只记录配置;runner 将其列为一个用例,按属性调度,选中时运行函数体。函数体声明的用例在其名下、继承其属性并可单独选择;函数体的其他语句(其用例共享的准备)只在其中某个用例运行时执行。因此一个对象可以是单个测试、一组测试或二者兼有,一个文件中的多个对象各自受控。只因其下的用例被选中而运行的对象是该用例的作用域,只在其自身语句失败时报告(P3.4)。`suite` 保留为 0.2 的写法 |
+| 作用域属性 | `tag`、`clause`、`resource`、`artifact` 取并集,`isolate` 向内传递;`kind`、`skip_if`、`expect_failure`、`known_gap`、`timeout`、`retries`、`diagnose` 取最内层 |
+| 运行方式的属性 | `timeout{}`(按用例终止进程)、`isolate{}`(独立进程)、`retries{n}`(失败后单独重跑,之后通过为 flaky)、`diagnose{.env, .limit}`(失败后以该环境再运行一次并保存输出,不作判定,`mtest explain` 指出其位置);`Bench` 默认持有 `machine` |
+| 共享与追踪 | `shared`:第一个使用它的用例运行时构造,至多一次,随函数体结束析构;`fixture` 事件记录由哪个用例构造及耗时。`--report-json` 的 `timeline` 记录每个进程的开始、耗时、状态与持有的资源 |
+| 协议 | 版本 1 内只增字段(P9.2):P3.3 的 `timeout`、`isolation`、`retries`、`diagnose`,未声明类别的用例取其程序的类别;P3.4 的 `begin` 可带 `kind`、`tags`、`clauses`,以及作用域规则;P5.2 的 `fixture` |
+| 类别的次序 | 代码的声明(类型或 `kind::`)、`mtest.toml`、程序所在目录(`tests/unit`、`tests/e2e`、`tests/cts`、`tests/bench`,docs/layout.md)、unit;`mtest list`、`list --static`、`run` 给出相同的类别 |
+| `mtest.toml` | `[cases."glob"]` 取库的属性词汇:`kind`、`tags`、`clauses`、`resources`、`artifacts`、`timeout`、`isolation`、`retries`、`diagnose`、`skip`、`expect_fail`;程序声明的优先,列表取并集(0.2 中文件优先) |
+| 静态清单 | 独立的包 `mcpplibs.mtest.manifest`:读测试的工具(引擎、编辑器)依赖它而不链接测试程序的库。识别对象类型、文件自己从中派生的类型(`class Fixture : public CTS`)、对象的属性与 `resource`、`clause`、`artifact`;运行时才可判断的(`skip_if` 的条件、时限、派生类型在构造中添加的属性)留给程序的列举 |
+| 适配器 | Bash(`--timeout`、`--isolate`、`--retries`、`--diagnose-env`、`--diagnose-timeout`)与 C ABI 表达相同的属性,未声明类别时不再写出 unit |
+
+**宿主与编辑器。** mcpp(#906):`mcpp new` 的骨架以 `UTest` 写出;`--case` 在列举的用例之下选择其声明的用例(P3.4)——不含通配符的名字原样传给程序,含通配符的模式运行该列举的用例整体(`test_protocol::selection`,e2e 1038 F)。mcpp-ide 后端 1.2.1:mtest 的用例以 `mcpplibs.mtest.manifest` 读出,删去后端自己的读取,0.3 的对象在构建前即可见。
+
+**mcppls 的一致性(#79)。**
+
+- 一致性为工作区成员 `tests/cts`(程序 `mcppls-cts`,与运行器一同进入 `stage/`):一个 fixture 或一个变体为一个 `Fixture` 对象(`CTS` 的派生类型,带服务端日志的 `diagnose`),名字中 `@` 之后为客户端变体,平台以自定义属性 `on{...}`(标签 `on:<平台>`),计时的 fixture 以 `resource{"machine"}`;用例以 `mcppls-conformance run` 运行其 fixture,读取其度量文件,每个未通过的检查为一个 `expect` 失败,带检查的编号与原因。运行器保持为单个 fixture 的命令行,服务端模块不链接测试库。
+- 用例 `the cases and the fixtures agree` 以静态清单读 `conformance.cpp`:每个用例命名一个存在的 fixture;检查限定答复时间(`answer-within`、`within-ms`、`within-seconds`、`within`、`budget`、`initialize-within`,或 stress、typing、tokens-latency)的 fixture,其用例未持有 `machine` 即失败并说明。
+- fixture 移至 `tests/cts/fixtures`;`scenario.json` 保留 fixture 自身的内容(项目、prepare、`requires-mcpp`、`requires-cmake`、检查序列);变体、资源、平台与分组只在代码中。`requires-cmake` 使 prepare 步骤先找到所需的 CMake,服务端保持主机的 PATH,`msvc-bdb.env` 不再需要。
+- 单元测试移至 `tests/unit`,以 mtest 写出(`mcppls-testing` 删除,各成员的测试同样迁移):仓库中只有一套测试框架。
+- CI:一致性矩阵的每一行为 `mtest run --isolate --jobs 2 --tag on:<平台> [--shard i/2]`;行内其余步骤(工具链、xmake、mcpp 的数据库校验)读 `mtest plan` 给出的该行用例;首次运行导入 main 上一次成功运行的该平台各部分的存储,两个部分以相同的历史划分;第二次尝试只重跑失败的用例。`mtest.toml`、`.github/scripts/run-fixture.sh` 与 6 张清单表删除。以特定条件运行单个 fixture 的作业(共享缓存的冷热启动、PRoot、发布的负载)直接调用运行器。
+
+**依赖。** mtest 0.3.0 → mcpp-index(#732 增加 `mcpplibs.mtest`、`.manifest`、`.gtest`、`.capi` 0.3.0 的条目)→ mcpp `kMtestRelease = "0.3.0"`(#906)、mcppls #79、mcpp-ide 后端 1.2.1。mcppls 的运行器模块化与目录迁移不依赖新接口,在 mtest 的 CI 期间进行。
+
+**判据。** 既有的 mtest 程序不改即构建并给出相同的列举;同一用例的静态清单与运行时列举的类别、资源一致(mtest 的测试比较二者);mcppls 的一致性在 CI 中只由代码与 fixture 目录描述,仓库中不再有用例的清单文件。
+
+### W10 mtest 的范式与 mcppls 的测试树(并入本轮)
+
+**要求。** mcppls 的单元测试、一致性与基准收敛到 `tests/`,全面以 mtest 写出;代码树、语义表达、可读性、性能、功能、数据与分析均超过原有的自建形式;mtest 本身给出一个范式:开发者以最少的代码写出用例,自然得到选择、调度、记录与分析;基于 mtest 的测试从目录、工程到代码都带有可辨认的 mtest 特征。
+
+**mtest 的范式(mtest docs/layout.md、lib/docs/writing-tests.md)。**
+
+| 层次 | 范式 |
+|---|---|
+| 目录 | `tests/<类别>/<主题>.cpp`:`unit/`、`cts/`、`e2e/`、`bench/`;目录即类别,数据放在类别之下(`tests/cts/fixtures/`、`tests/bench/tasks/`) |
+| 工程 | 单元测试一个文件一个程序(失败定位到文件;W8 的测量表明链接不是瓶颈),或以 `[test] programs = "directory"` 一个目录一个程序;需要服务端负载等外部输入的类别为工作区成员(`tests/cts`、`tests/bench`),其程序与运行器一同进入 `stage/` |
+| 代码 | 一个文件一个主题;一个对象一个被测行为或场景(`UTest`、`CTS`、`E2E`、`Bench`),名字之后写它需要什么(资源、时限、隔离、重试、诊断、度量的界限);共享的准备是函数体内的局部对象或 `shared`;不写 `main`(mtest 的入口),需要自举子进程的文件写 `main` 并在其末尾 `return run(argc, argv)` |
+| 数据 | 用例的度量以 `metric()` 与 `bench::measure()` 报告;文件以 `produced()` 报告;共享对象的构造、进程的时间线、资源与诊断由 runner 记录;存储在运行之间以 main 的产物延续 |
+| 分析 | `mtest explain`(失败的位置、原因、复现命令、诊断输出)、`mtest analyze`(最慢、波动、失败、基准)、`mtest gate`(集合与策略)、`--report-json` 的时间线、junit;编辑器经静态清单与列举得到同一棵用例树 |
+
+**mtest 0.4.0 补足的能力。** 原有形式中由脚本判定的界限(启动时间、令牌延迟、评审的精确率与召回率)改为代码中的声明:属性 `at_most{"ready", 15}` 与 `at_least{"recall", 0.9}`(度量名与上界或下界,单位由度量给出)随用例列出(P6.5:`case` 事件与列举的 `bounds`),库在用例结束时判定,未报告或越界的度量使用例失败,失败信息给出度量与界限;`mtest.toml` 的 `[cases]` 以同名键声明,由引擎判定;与基线的比值仍由策略(P6.2)判定。度量在存储中逐次保留,`mtest analyze trend` 给出各度量跨运行的值、界限与余量。
+
+**mcppls 的测试树。**
+
+```
+tests/
+ unit/ 服务端的单元测试:每个主题一个文件、一个程序,UTest 对象为被测单元;自举子进程的文件写 main
+ cts/ 成员 mcppls-cts:cts.cppm(套件类型 Fixture)与按主题的用例文件 mcpp.cpp、targets.cpp、cmake.cpp、
+ xmake.cpp、inferred.cpp、engine.cpp、editing.cpp;一个夹具或其变体一行(@polling、@plain、@vscode、
+ @neovim、@zed、@warm 第二次会话复用第一次的缓存、@linked 经符号链接到达工作区);fixtures/、traceability.json
+ bench/ 成员 mcppls-bench:startup.cpp(冷/热启动,首次跳转以 at_most 界定;release 标签的五次中位数与
+ Windows 大型模块工程)、tokens.cpp、review.cpp(每个审查夹具一个用例,规则的精确率与召回率以
+ at_least 界定)、tasks.cpp(任务数据的检验为 UTest,代理运行为 Bench,未指定代理时 skip)、
+ ux.cpp(真实项目的各阶段为用例);tasks/、review/ 为数据
+ harness/ 成员 mcppls-harness:用例驱动服务端的方式(mcppls.harness:一致性运行器的会话与判定、环境约定),
+ 以及基准的任务、补丁、审查与代理运行模块,连同其单元测试
+```
+
+`tools/bench/` 与 `mcppls-devtools bench` 的子命令移入 `tests/bench` 与 `tests/harness`;devtools 不再有 bench,
+`measure summary` 删除。CI 的冷/热启动、审查夹具、缓存模型、符号链接工作区等附加步骤成为用例;`bench-validate`
+作业改为三平台的 `bench` 作业(`mtest run --tag on:<平台> --exclude-tag release --exclude-tag ux`),release-checks
+的性能作业运行 `--tag 'on:<平台>&release'`,`ux` 作业与 nightly 的 ux-systems 运行 `--case <场景>`;各作业保存存储并
+取 main 上一次的存储,`mtest analyze trend` 在步骤摘要中给出各度量的历史与界限。运行器的 `--navigation-budget` 与
+`timing_evidence.py` 删除,界限由用例声明。nightly 的 ux 轮次(候选与基线交替 A B B A、重复编辑阶段、双核轮次)是
+校准预算与比较发布的实验,其统计(分布、合并离散度下的劣化判定)由 `measure budgets`、`measure compare` 在度量文件上
+给出,不属于用例的判定。
+
+实现中补足的 mtest 0.4.1:用例体不得返回值(编译期拒绝,由 `main` 改写为对象时的 `return 1;` 暴露);静态清单跨文件
+读取项目的套件类型(`types_of`,`list --static` 先读项目全部 C++ 源文件);标签选择 `a&b` 表示同时具有两个标签。
+mcpp 2026.11.8.1 补足:既是依赖又是开发依赖的框架只提供一次测试入口。mcpp-ide 1.2.1 以项目 tests/ 中声明的套件类型
+读取用例。
+
+**与原有形式的比较(判据)。**
+
+| 维度 | 原有形式 | mtest 形式 |
+|---|---|---|
+| 代码树 | 测试散在 `tests/`、`conformance/`、`tools/bench/`、`modules/testing/` 与 devtools | `tests/{unit,cts,bench}`,一个类别一处 |
+| 语义表达 | `main` 中按顺序执行的 `_test`;fixture 的元数据在 CI 清单与脚本中 | 对象即用例,类型即类别,需要什么写在名字之后 |
+| 可读性与清晰 | 用例的平台、资源、客户端变体与预算需读 ci.yml 与 Python | 一个对象一行,平台、资源、变体、界限都在代码中 |
+| 性能 | 一致性逐个串行;基准每作业重建 | 按资源并行、按历史分片、失败用例单独重跑 |
+| 功能 | 以退出码判定;无列举、无按用例选择、无重试与诊断 | 列举、选择、隔离、时限、重试、诊断、共享对象、时间线 |
+| 数据 | 各自的 JSON 文件,格式各异 | 协议事件与存储,度量、产物、时间线统一 |
+| 分析 | 人读日志;预算由脚本比较 | `explain`、`analyze`、`gate`、junit、编辑器用例树 |
+
+判据:mcppls 仓库中不再有测试框架、用例清单或预算脚本的自建实现;`mtest list` 列出全部用例且与静态清单一致;CI 的一致性、单元、基准作业均经 mtest 运行并保存存储。
+
+## 3. 依赖
+
+```
+W1: clangd-0.0.21 ──(engine lock)──▶ mcppls-0.0.21 ──▶ xim bump(mcppls)
+ ▲ ▲
+ │ └── W5 deflate (若探针确认)
+ └── win32 zlib 验证
+ plugins-0.21.0 ──▶ mcpp-index ──▶ close #887
+ mcpp-ide-1.2.0 ──▶ xim bump(mcpp-ide, mcpp-ide-nvim)
+ 全部发布 ──▶ xlings 全生态验证 ──▶ issue 关闭与汇报
+
+W2 (mcpp next) ──┬─▶ W4 mtest 的 mcpp 集成与 `mcpp new` 模板
+ └─▶ #903/#904 后,生态清单与锁不再需要手工修正
+W3 (os 0.4.0) ──▶ mcppls / mcpp-ide 迁移(S2 写法、检查禁止 is_<平台>)
+W4 (mtest) ──▶ 首批五个项目迁移 ──▶ CI 时间目标的测量
+```
+
+W2、W3、W4 互不阻塞,可在 W1 的 CI 等待期间并行设计与实现;W4 的 mcpp 集成依赖 W2 的协议运行器。
+
+## 4. 尚未收敛的事项
+
+| 编号 | 事项 | 状态 |
+|---|---|---|
+| U1 | Q5:`is_<平台>` 与两层判断并存 | 已确认 |
+| U2 | darwin 比值超限的原因与修法 | 已确认为 zlib;W5 实施中 |
+| U3 | CLion 2026.2 无 JCEF 的说明不可见 | 第一轮诊断:`visible=false selected=Module Graph`;第二轮记录可见性时间线、窗口类型与自动隐藏 |
+| U4 | xlings 全生态验证的方式 | 已确认:临时目录中重定向 `HOME` 等变量(W1 的验证步骤) |
+| U5 | mcpp-vscode 的归档说明 | 先前约定只在确认后执行 |
+| U6 | mcppls-clangd 的 CHANGELOG 记录哪次对比运行 | 已确定(维护者决定):记录运行 42464678787;darwin-arm64 三次测量均因 runner 离散度无效,以与 0.0.20 的比较放行(darwin 绝对冷启动与处理器时间不高于 0.0.20;本机两版镜像交替四对,各量差异不超过 3%),并在 CHANGELOG 与 performance-policy 中注明;CI 产物经固定的 draft release `ci-artifacts` 供本地分析 |
+| U8 | `os::is_image` 的语义不明确 | 已确定:改名 `is_openkal`,`is_image` 为弃用别名(W3) |
+| U9 | API 生命周期的声明与提示 | 已确定:W7 |
+| U7 | mcpplibs/os#19 的 hosted 行 `effective_user()` 为 1000 而 `id -u` 为 1001 | 已修复(`ee1d71e`):hosted 安排中 C 库为 openkal-musl,其 `geteuid` 为常量 1000、无 `umask`;Linux 与 macOS 改为内核调用(与描述符、信号相同的 `src/posix/kernel.h`);`unshare -r` 下旧实现为 1000/0,新实现为 0/0;CI 全部通过 |
+
+## 5. 全局与生态视角的审查
+
+| 视角 | 审查所见 | 处理 |
+|---|---|---|
+| 一致性 | 原则 P3–P6 刚在四个应用仓库落地,mcpp 本体与 os 尚未(mcpp 仍有带引号的键与 `"=0.3.1"`;os 依赖 openkal 用范围) | W2 统一 mcpp 本体;os 的 openkal 范围由平台版本组给出精确性(D12),在 os 文档中写明,不改为精确版本以免与组冲突 |
+| 一致性 | 生态有四套测试方式与三套自写测试工具 | W4 收敛为 mtest;迁移在各项目的版本 PR 中整体完成,不保留双套 |
+| 架构 | 平台判断、平台事实、系统接口三条规则分属 os、应用检查与插件文档 | 规则的权威文本在 os 文档(P4、P5)与 mcpp docs(P3);各仓库的检查引用同一文本 |
+| 架构 | mtest 的通用性可能被 mcpp 的需求带偏 | 规范与引擎不引用 mcpp 的概念;mcpp 是宿主适配之一 |
+| 稳定性 | GCC 16 的模块缺陷已多次出现(#905;早先的 BMI 次序、MSVC 的 path 迭代器) | 规避写入插件开发规则("经由 os 的函数不内联");W4 的 mtest 引擎同样遵循;#905 跟踪上游 |
+| 稳定性 | 映像为唯一发布形态,任何映像特有的缺陷都直接到达全部用户 | 测试以映像在三个系统上运行(D18);对比运行在发布前度量映像与原生的差距(S6);P7 使政策不被绕过 |
+| 性能 | 映像相对原生的冷启动与解压差距(F6) | 上限不放宽;W5 在映像中提供与系统 libz 相当的 deflate |
+| 兼容性与无感升级 | os 0.4.0、mcpp 下一版本、mtest 的引入都触及生态的公共写法 | 只增加接口;精确版本使升级显式;L0 使现有测试无须改动;mcpp 的现有 `mcpp test` 行为保持 |
+| 跨平台 | Windows 的进程启动开销影响 mtest 的隔离与 os 的可移植通道 | 隔离按声明;平台事实的查询结果缓存、只在调用时发生、移出启动关键路径 |
+| 用户体验 | 映像用户在 W3 之前失去 Dev Drive 提示与 PRoot 识别 | mcppls 0.0.21 在日志与文档中说明;W3 后恢复 |
+| Agent 可操作性 | 本轮 CI 的观察依赖手工轮询与日志检索 | W4 的 `mtest ci`、`mtest explain` 直接针对此项;在此之前,各 PR 的失败以结构化产物(如 CLion 的诊断文件)保存 |
+| 测试覆盖 | 映像中的平台事实、插件的 os 接入、IDE 的无 JCEF 路径都是本轮才被测试覆盖的路径 | os 的映像测试(W3);插件 CI 的源文件检查与各消费者;CLion 两个版本的产物诊断 |
+| 过程 | 本轮出现过违反性能政策的重跑与未确认即发布的上游 issue | 已记录;P7 使前者由工具阻止;后者已有规则(只在 mcpp 内以 upstream-bug 记录) |
+| 范围 | W2–W4 的范围大于 #885 | 三者各有独立设计与评审,不并入 W1;W1 只包含已确定的修复与收尾 |
+
+## 6. 第二轮审查(多视角)
+
+| 视角 | 审查所见 | 处理 |
+|---|---|---|
+| 依赖 | W5 的条件尚未判定:探针若排除 zlib,darwin 的修法未知,W1 的关键路径因此不确定 | 探针结果出来即判定:确认则实施 W5;排除则按冷启动的装载与页错误继续定位(冷启动 `page_faults` 比值 2.74–2.87),不放宽上限,并以新的探针缩小范围 |
+| 依赖 | mcppls 0.0.21 依赖 clangd 0.0.21 的引擎锁,clangd 的阻塞会传递给 mcppls 与其 xim bump | 两者的其余部分先完成并通过 CI;锁的更新是最后一个提交 |
+| 一致性 | #874 的主体在 2026.10.31.3 已实现,issue 仍开放,生态无人复测 | W2 以报告者的工作区复测并关闭;复测需要 10–25 GB,在 CI 运行器上进行(本机的磁盘余量不足,见下) |
+| 资源 | 本机磁盘在本轮中一度只余 4 GB(`/tmp` 100%)。清理了已完成的临时目录(探针、已推送的 mwrap 仓库、研究目录中的构建产物)、各工作树只保留最近一次构建、`cache gc --older-than 2d`(2.6 GiB)与两天前的 std 构建(约 4.3 GiB),回到约 21 GB;其他会话的项目未触及 | 继续遵守"一次一个重任务";大的复测(#874、对比运行)放在 CI;W4 的 mtest 存储设保留期 |
+| 稳定性 | 验证环境若泄漏会话的 shim 变量,会把旧版本误判为索引问题 | W1 的验证步骤明确 `env -u` 的变量与中性工作目录 |
+| 用户体验 | 验证只在 Linux 上进行,Windows 与 macOS 的安装路径没有真实覆盖 | 发布 CI 的 `ci-fresh-install` 覆盖三个系统;本机验证补 Linux 的完整链路与两个镜像 |
+| 测试覆盖 | 本轮的修复(插件的 os 接入、IDE 的 `check-sources`、mcppls 的 #80)只在各自 CI 中验证 | 全局审查时按仓库核对:每项修复有对应的测试或检查,并在 PR 描述中列出 |
+| 过程 | 主工作树曾被一条命令置为分离 HEAD(已恢复到 `main` 并快进) | 只在专用工作树中执行检出类命令 |
+
+## 7. 待审查的问题
+
+- **T4** 已确认:#874 归属本方案(W2),方案确定后与其他未收敛事项一并重新拆分实施计划。
+- **T5** 已确认:W6 并入本轮,mcppls #79 以 `0.21.0` 发布,mcpp-ide #5 加入 CI 分类,各一次发布。原问题:W6 的采用时机。建议并入本轮:mcppls #79 以 `0.21.0` 发布(版本规则、devtools、发布清单、插件的系列检查、组件表与 CI 分类、负载重用),mcpp-ide #5 加入同样的 CI 分类。这符合"确定的事项不拆到以后",代价是两个 PR 再增加一轮 CI。另一做法是作为 mcppls 0.22.0 与 mcpp-ide 1.3.0 的内容,本轮保持 `0.0.21`。
+
+- **T6** 已确认:W8 并入本轮,以追加提交进入 mcpp #906、mcppls #79,mtest 以 0.2.0(mtest#3)发布;既有 e2e 不改写,新增与改动的 e2e 以 `mtest_case` 写判据;PR 运行受影响集与冒烟集,main 与发布运行全部;参数由先行的探针测定。方案见 W8。
+
+## 8. 实施计划
+
+### 8.1 每个仓库每个版本一个 PR
+
+| 仓库 | 版本 | PR / 分支 | 内容 | 工作流 |
+|---|---|---|---|---|
+| mcppls-clangd | 0.0.21 | #18 | 现有内容;win32 zlib;W5(若探针确认);CHANGELOG 记录对比运行 | W1、W5 |
+| mcppls | 0.0.21 | #79 | 现有内容;引擎锁为 clangd 0.0.21(最后一个提交) | W1 |
+| mcpp-ide | 1.2.0 | #5 | 现有内容;CLion 无 JCEF 测试 | W1 |
+| mcpp-plugins | 0.21.0 | #52 | 现有内容(CI 已通过) | W1 |
+| mcpp-index | — | 一个 PR | plugins 0.21.0;W5 的 deflate 包(若需要);mcpplibs.os 0.4.0;mcpplibs.mtest 0.1.0 | W1、W3、W4、W5 |
+| mcpplibs/os | 0.4.0 | `release/0.4.0` | S2(`can_run_on`、`on`)、S3(R5 修订与映像中的平台事实)、文档、映像测试 | W3 |
+| mcpp-community/mtest | 0.1.0 | 新仓库 | 规范 v1、C++ 库(unit、bench、cts、e2e)、引擎(运行、存储、查询、explain、plan、ci、拓扑导出)、C ABI 报告库、GoogleTest 与脚本适配;自身以 mtest 测试;ok 映像发布 | W4 |
+| mcpp | 2026.11.8.1 | #906(草稿)`release/2026.11.8.1` | #903、#904(含源文件检查)、S1 文档与 tier 评估、#874 的复测与剩余期望、S7 的协议运行器与 `mcpp.core` 上下文注入与构建图查询、`mcpp new` 默认使用 mtest、三份设计文档 | W2 |
+| xim-pkgindex | — | 各发布的 bump PR | mcpp、mcppls、mcpp-ide、mcpp-ide-nvim | 各工作流 |
+| mtest | 0.2.0 | mtest#3 | W8 的 mtest 部分;mcpp #906 与 mcppls #79 的 W8 部分以追加提交进入;探针 #909 先行 | W8 |
+| mtest | 0.4.0 | `release/0.4.0` | W10:`at_most`/`at_least` 与 P6.5、`analyze trend`、范式文档(已发布);mcppls #79 随之更新 | W10 |
+| mtest | 0.3.0 | mtest#4 | W9:对象即用例、类型即类别、作用域属性、运行方式的属性、`shared` 与追踪、P3.3/P3.4/P5.2、`mtest.toml` 的同一词汇、静态清单、布局;mcpp-index #732、mcpp #906、mcppls #79 随之更新 | W9 |
+
+首批迁移(mcppls、mcpp-ide、mcpp-plugins、mcppls-clangd 迁移到 os 0.4.0 与 mtest)是这些项目各自的下一个版本,依赖 os 0.4.0 与 mtest 0.1.0 的发布;迁移前以 mtest 的 CI 汇总测量正式基线(W4 §10.2)。
+
+### 8.2 依赖与波次
+
+```
+波次 1(进行中) plugins 0.21.0 ─▶ index ─▶ close #887
+ mcpp-ide 1.2.0 ─▶ xim bump
+ clangd 0.0.21 ◀─ W5(条件)◀─ darwin 探针
+ └─▶ mcppls 0.0.21 ─▶ xim bump
+波次 2(与波次 1 并行)
+ os 0.4.0 ─────────────┐
+ mtest 0.1.0 ──────────┼─▶ index ─▶ mcpp 2026.11.8.1(`mcpp new` 模板引用已发布的 mtest)
+ mcpp 2026.11.8.1 的其余部分(#903、#904、S1、#874、S7 运行器)可先行实现
+波次 3 基线测量 ─▶ 首批迁移(各项目下一版本)─▶ 迁移后测量(M3)
+收尾 各波次发布 ─▶ xlings 全生态验证 ─▶ issue 关闭 ─▶ 汇报
+```
+
+### 8.3 次序约束
+
+- mtest 0.1.0 以已发布的 mcpp 2026.11.7.2 构建与测试(L0 与其自身引擎),不依赖 mcpp 2026.11.8.1;mcpp 2026.11.8.1 的协议运行器以 mtest 规范 v1 为准,两者在同一波次设计。
+- os 0.4.0 只增加接口;mtest 引擎使用其 `can_run_on` 与 `host()`,因此 mtest 0.1.0 依赖 os 0.4.0 的发布。
+- 每次发布后:取消 main CI、发布、GitCode 镜像(本地 gtc)、重新触发 main CI、索引或 bump PR 合入、CDN 生效后再进行依赖它的发布。
+
+## 9. 实施记录(第六版)
+
+### 9.1 新的发现与处理
+
+| 发现 | 处理 | 版本 |
+|---|---|---|
+| Windows 的线程以 x87 双精度(控制字 0x027F)开始,okrun 进入映像时未建立 System V 的浮点初始状态;映像中 openkal-musl 的 `strtod("0.4")` 得到 `0x1.99999999998p-2`,mtest 一致性套件 P6.1 只在 Windows 失败 | SPEC 12.2 增加"浮点状态"一行、12.3 写明 loader 的义务;okrun 在入口、初始化与终结函数和每个上下文中建立该状态,openkal-bind 的调用在其中进行并恢复调用方的状态;`fpstate` 与 `bind` 示例在四个系统上检验 | openkal#63、openkal-image 0.5.3、openkal-platform 0.24.4、okrun 0.5.3(xim) |
+| 平台组发布一个新成员后,早于 2026.11.7.1 的 mcpp 按范围取最新成员再以版本组拒绝(os 与 clangd 的 CI 用 2026.11.5.1) | 生态 CI 使用 mcpp 2026.11.7.2(在版本组内解析) | os#19、clangd#18、mcpp-index#730 |
+| mcpp 的三个原有缺陷:依赖的 entry 导入 `std` 时 std 模块不构建;有起点的测试仍链接 dev-dependency 之下的 `gtest_main.cc`;共享库形式的 dev-dependency 不链接 | 均在 mcpp 2026.11.8.1 修复(e2e 1037) | #906 |
+| 父包与其命名空间下的包在同一张表中(`mcpplibs.mtest` 与 `mcpplibs.mtest.gtest`)只能以引号键写出,而 #904 把带引号的点分键列为弃用 | 唯一写法的引号键不报弃用(e2e 1032、docs/05、docs/51) | #906 |
+| mcppls 的日志状态在进程退出时被析构,退出期间写日志的分离线程在 Linux arm64 映像中 SIGSEGV | 日志状态不析构 | mcppls#79 |
+| Linux e2e 的耗时表已过时(重命名的测试按中位数计),一个分片超出 22 分钟 | 由各分片报告重新测得三张耗时表;Linux 改为 8 个分片 | #906 |
+| `mcpp new` 以 mtest 为 dev-dependency 之后,`mcpp build` 与 `mcpp test` 成为两个配置:mtest 经 mcpplibs.os 到达 openkal 的内核接口,配置因此带上 `__OPENKAL__`;dev-dependency 的包含目录进入包自己源文件的命令。结果是被测试的代码与发布的代码命令不同、两个构建目录各编译一遍、构建之后的编译数据库没有测试条目(e2e 77、783 失败),在 build 与 test 之间切换时重编包自己的源文件 | dev-dependency 只到达测试单元(`testBuild`);只经 dev 边到达的内核接口只给 dev-only 包与测试单元,配置不变;C 库与 C++ 运行时仍属配置。`mcpp dyndep` 不改写未变的输出,规则的 `restat` 由此生效 | #906(docs/08、e2e 1037 K) |
+| 测试计划修剪依赖中定义 `main` 的源文件时覆盖了全部依赖,openkal-musl 的映像入口 `okm_image.c` 声明 `main` 而被去掉,`--target x86_64-openkal` 的测试映像缺少运行时符号(e2e 1033、951) | 只修剪 dev-dependency 及其之下的包 | #906 |
+| mcpp-ide 的发布运行 `startup_failure`:ci.yml 的作业请求 `actions: read`,被调用工作流的权限不能超过调用方 | release.yml 的 `ci` 作业授予 `actions: read` | mcpp-ide#6 |
+| mcpp-plugins 0.21.0 仍精确依赖 mcpplibs.os 0.3.1,与 mcpp、mcppls、mcpp-ide 的 0.4.0 不一致 | 改为 0.4.0 后发布 | mcpp-plugins#52 |
+
+### 9.2 S7 与 W4 的实现
+
+- **mcpp 2026.11.8.1(#906)**:
+ - 测试协议运行器:`mcpp test` 的用例记录、`--case`、`--cases`、`--seed`;未实现协议的程序收不到协议变量;`--no-run` 的记录带程序与源文件;特性 `test-protocol`。
+ - 入口键 `test-protocol`、`test-main`、`test-guide`:不定义 `main` 的测试以提供 `test-main` 的入口为起点,由文件的导入在多个提供方之间选择;`gtest_main.cc` 照旧作为测试起点。
+ - `mcpp test --guide`:列出测试框架及其指南与仓库,以及用例相关的命令;失败的运行指向它(协议 P2.5)。
+ - `mcpp::context`;`mcpp new` 的测试使用 mtest,`--template lib` 生成模块、它导出的分区及分区的实现。
+- **mtest 0.1.0**:已发布(mcpp-community/mtest#1);引擎以 ok 映像的通用包发布(xim `mtest`);`mcpplibs.mtest`、`.gtest`、`.capi` 进入 mcpp-index;协议增加 P2.5(指南)。
+- **mcpp-ide 1.2.0(#5)**:实现协议的程序由 `mcpp test --cases` 列出、由 `--case` 运行,结果取自 mcpp 的记录;mtest 的用例在构建前即由源码中的 `"name"_test` 字面量给出;未实现协议的框架仍按 `spec/test-frameworks.json`。
+- **已确认**:mtest 构建插件的语法糖(静态用例清单与清单属性优先,其余能力各自定稿),归入 W8 的 mtest 一项。
+
+### 9.3 W3 的完成
+
+mcpplibs.os 0.4.0 已发布并进入 mcpp-index;mcppls、mcpp-ide、mtest 与 mcpp 迁移到 0.4.0。mcppls 的映像经 `os::on` 询问 Dev Drive 与 PRoot,此前映像对二者均不作判断。
+
diff --git a/.agents/docs/README.md b/.agents/docs/README.md
index 83417d657..91f26e835 100644
--- a/.agents/docs/README.md
+++ b/.agents/docs/README.md
@@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```
-353 records.
+355 records.
## By subject
@@ -75,6 +75,8 @@ Records that declare one. Everything else is listed by date below.
### ecosystem
+- [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active
+- [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active
- [生态 #885 收尾设计:核心、插件、openkal、语言服务与编辑器的一轮闭合](2026-11-07-ecosystem-885-closeout-design.md) — active
- [生态设计 part 2:openkal 的最小原子与二进制映像、其上的 `mcpplibs.os`,以及 part 1 的收尾与回归记录](2026-10-26-ecosystem-part2-os-library-over-openkal-design.md) — active
- [控制契约、并发与性能、单一 C++ 运行时,以及 IDE 生态(生态级,#838、#839)](2026-10-26-control-contract-runtime-and-ide-ecosystem-design.md) — active
@@ -177,6 +179,8 @@ Records that declare one. Everything else is listed by date below.
### 2026-11
+- [生态顶层方案:#885 收尾之后的工作流、原则与依赖](2026-11-08-ecosystem-top-level-design.md) — active
+- [#885 收尾补充方案:目标角色、平台事实、系统接口与版本书写](2026-11-08-ecosystem-885-supplement-design.md) — active
- [生态 #885 收尾设计:核心、插件、openkal、语言服务与编辑器的一轮闭合](2026-11-07-ecosystem-885-closeout-design.md) — active
- [Launch configuration and named test runners](2026-11-06-launch-configuration-and-named-test-runners.md) — active
- [Compiler attempt lifetime and output recovery](2026-11-06-compiler-attempt-recovery.md) — active
diff --git a/.github/actions/e2e-mtest/action.yml b/.github/actions/e2e-mtest/action.yml
new file mode 100644
index 000000000..4a52676c7
--- /dev/null
+++ b/.github/actions/e2e-mtest/action.yml
@@ -0,0 +1,92 @@
+name: e2e-mtest
+description: >
+ mtest for the e2e suite (design 2026-11-08 W8; tests/e2e/mtest.toml): the
+ release this action pins, its sha256 checked, and the stores main's last
+ successful CI run kept for this host, taken into tests/e2e/.mtest so that
+ `--shard` divides the suite by measured time and a run orders its tests
+ longest first. Exports E2E_MTEST for run_all.sh. A run with no store of main
+ divides by count, and its own store becomes the next run's history.
+
+inputs:
+ host:
+ description: the host's name in the stores' artifact names (linux, macos, windows)
+ required: true
+ version:
+ description: the mtest release
+ required: false
+ default: '0.4.0'
+ sha256:
+ description: sha256 of mtest--openkal-bundle-all.tar.gz
+ required: false
+ default: 'b62e4ac7cd76b20dbdc48a5d041d377444e8815ee7a4ff1e7686145d13acc925'
+ token:
+ description: a token that reads this repository's actions
+ required: false
+ default: ${{ github.token }}
+
+runs:
+ using: composite
+ steps:
+ - name: mtest
+ shell: bash
+ env:
+ MTEST_VERSION: ${{ inputs.version }}
+ MTEST_SHA256: ${{ inputs.sha256 }}
+ run: |
+ set -euo pipefail
+ archive="mtest-$MTEST_VERSION-openkal-bundle-all.tar.gz"
+ temp="$RUNNER_TEMP"; python=python3
+ if [ "$RUNNER_OS" = Windows ]; then temp=$(cygpath -u "$RUNNER_TEMP"); python=python; fi
+ curl -fsSL --retry 5 --retry-all-errors --retry-delay 5 -o "$temp/$archive" \
+ "https://github.com/mcpp-community/mtest/releases/download/v$MTEST_VERSION/$archive"
+ actual=$("$python" -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$temp/$archive")
+ [ "$actual" = "$MTEST_SHA256" ] || { echo "::error::$archive: sha256 $actual, the action pins $MTEST_SHA256"; exit 1; }
+ mkdir -p "$temp/mtest" && tar xzf "$temp/$archive" -C "$temp/mtest"
+ launcher="$temp/mtest/mtest-$MTEST_VERSION-openkal-bundle-all/bin/mtest"
+ [ "$RUNNER_OS" = Windows ] && launcher="$launcher.exe"
+ "$launcher" --version
+ echo "E2E_MTEST=$launcher" >> "$GITHUB_ENV"
+ # The pull request's change, for the `pr` set: the base's tip and HEAD's
+ # parents, so that `git diff ...HEAD` has its merge base. When they
+ # cannot be fetched, mtest selects every test and says why.
+ - name: The base of the change
+ if: github.base_ref != ''
+ shell: bash
+ run: |
+ git fetch --no-tags --depth=1 origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" || true
+ git fetch --no-tags --deepen=1 origin || true
+ - name: The stores of main's last CI run
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.token }}
+ HOST: ${{ inputs.host }}
+ run: |
+ set -euo pipefail
+ run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow ci.yml --branch main --status success --limit 1 \
+ --json databaseId -q '.[0].databaseId' 2>/dev/null || true)
+ if [ -z "$run" ]; then echo "no successful run of main: the suite is divided by count"; exit 0; fi
+ rm -rf "$RUNNER_TEMP/e2e-stores"
+ if ! gh run download "$run" -R "$GITHUB_REPOSITORY" -p "e2e-store-$HOST-*" -D "$RUNNER_TEMP/e2e-stores" 2>/dev/null; then
+ echo "main's run $run kept no store for $HOST: the suite is divided by count"; exit 0
+ fi
+ for store in "$RUNNER_TEMP"/e2e-stores/*/; do
+ (cd tests/e2e && "$E2E_MTEST" store import --from "$store")
+ done
+ echo "history of main's run $run: $(ls "$RUNNER_TEMP/e2e-stores" | wc -l) store(s)"
+ # The coverage map of the last nightly measurement (e2e-coverage.yml), on
+ # the host it was measured on: the sources each test's run executed there.
+ # Another host's own sources (src/native/windows/...) run on no Linux test,
+ # so its suite has no map and every test of it is affected.
+ - name: The coverage map
+ if: inputs.host == 'linux'
+ shell: bash
+ env:
+ GH_TOKEN: ${{ inputs.token }}
+ run: |
+ set -euo pipefail
+ run=$(gh run list -R "$GITHUB_REPOSITORY" --workflow e2e-coverage.yml --status success --limit 1 \
+ --json databaseId -q '.[0].databaseId' 2>/dev/null || true)
+ if [ -z "$run" ]; then echo "no coverage map: every test is affected"; exit 0; fi
+ rm -rf "$RUNNER_TEMP/e2e-coverage"
+ gh run download "$run" -R "$GITHUB_REPOSITORY" -n e2e-coverage-linux -D "$RUNNER_TEMP/e2e-coverage" || exit 0
+ (cd tests/e2e && "$E2E_MTEST" coverage import --from "$RUNNER_TEMP/e2e-coverage/coverage.json")
diff --git a/.github/tools/check_e2e_coverage.py b/.github/tools/check_e2e_coverage.py
index c98c9869b..fae518366 100644
--- a/.github/tools/check_e2e_coverage.py
+++ b/.github/tools/check_e2e_coverage.py
@@ -30,9 +30,15 @@
An exception for a test that ran, or for a test that does not exist, also fails
the check, so that the list cannot outlive the reason it records.
+Some claims need a test to pass on particular hosts, not on some host: docs/21
+calls a target row `verified` when CI builds and runs its artefact, and the
+`*-openkal` rows claim that for every system. tests/e2e/host-requirements.tsv
+lists such a test with the hosts its result must come from; a listed test that
+did not pass on one of them, or that does not exist, fails the check.
+
With --timings-out DIR it writes `.tsv`, the measured duration of every
test that ran, merged across that host's shards, in the format run_all.sh reads
-from E2E_TIMINGS; refreshing tests/e2e/timings/ is copying those files. A report
+from E2E_TIMINGS (a run without mtest; CI's shards divide by mtest's history). A report
file is named `e2e-report--.tsv`.
Usage:
@@ -64,6 +70,19 @@ def read_reports(directory: Path):
yield m["host"], int(m["shard"]), parts[0], parts[1], ms, parts[3] if len(parts) > 3 else ""
+def read_host_requirements(path: Path) -> dict[str, set[str]]:
+ """`\t ...\t` lines: the hosts a test must pass on."""
+ out: dict[str, set[str]] = {}
+ if not path.exists():
+ return out
+ for line in path.read_text(encoding="utf-8").splitlines():
+ if not line.strip() or line.startswith("#"):
+ continue
+ parts = line.split("\t")
+ out[parts[0].strip()] = set(parts[1].split()) if len(parts) > 1 else set()
+ return out
+
+
def read_exceptions(path: Path) -> dict[str, str]:
out: dict[str, str] = {}
if not path.exists():
@@ -109,8 +128,10 @@ def main() -> int:
workflows = workflow_tokens([p.read_text(encoding="utf-8")
for p in sorted((root / ".github" / "workflows").glob("*.yml"))])
exceptions = read_exceptions(root / "tests" / "e2e" / "coverage-exceptions.tsv")
+ required = read_host_requirements(root / "tests" / "e2e" / "host-requirements.tsv")
ran: dict[str, set[str]] = defaultdict(set)
+ passed: dict[str, set[str]] = defaultdict(set)
skipped: dict[str, set[str]] = defaultdict(set)
shard_ms: dict[tuple[str, int], int] = defaultdict(int)
timings: dict[str, dict[str, int]] = defaultdict(dict)
@@ -119,6 +140,8 @@ def main() -> int:
reports += 1
if status in RAN:
ran[test].add(host)
+ if status == "pass":
+ passed[test].add(host)
shard_ms[(host, shard)] += ms
timings[host][test] = ms
elif status == "skip":
@@ -143,6 +166,15 @@ def main() -> int:
for test in exceptions:
if test not in tests:
stale.append(f"{test} is excused but does not exist")
+ unmet = []
+ for test, hosts in sorted(required.items()):
+ if test not in tests:
+ stale.append(f"{test} is required on hosts but does not exist")
+ continue
+ for host in sorted(hosts - passed[test]):
+ why = [d for d in skipped[test] if d.startswith(f"{host}: ")]
+ unmet.append(f"{test} did not pass on {host}"
+ f"{' (' + '; '.join(why) + ')' if why else (' (it ran and did not pass)' if host in ran[test] else ' (no report of it there)')}")
print(f"{len(tests)} tests: {counts['ran']} ran on a shard, {counts['job']} run by a "
f"dedicated job, {counts['excused']} excused, {len(uncovered)} uncovered")
@@ -152,6 +184,8 @@ def main() -> int:
print(f"UNCOVERED: {line}")
for line in stale:
print(f"STALE EXCEPTION: {line}")
+ for line in unmet:
+ print(f"REQUIRED ON HOST: {line}")
if args.timings_out:
out = Path(args.timings_out)
@@ -159,7 +193,7 @@ def main() -> int:
for host, table in timings.items():
(out / f"{host}.tsv").write_text(
"".join(f"{t}\t{ms}\n" for t, ms in sorted(table.items())), encoding="utf-8")
- return 1 if uncovered or stale else 0
+ return 1 if uncovered or stale or unmet else 0
if __name__ == "__main__":
diff --git a/.github/tools/check_include_regions.sh b/.github/tools/check_include_regions.sh
index f7570fb12..f16279000 100755
--- a/.github/tools/check_include_regions.sh
+++ b/.github/tools/check_include_regions.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
-# check_include_regions.sh — headers are included only where a system has no
-# module for what is needed (design 2026-11-07 D10).
+# check_include_regions.sh — headers are included, and the preprocessor
+# chooses, only where a system has no module for what is needed (design
+# 2026-11-07 D10; design 2026-11-08 S4).
#
# mcpp imports the standard library (`import std`), its dependencies as modules
# (nlohmann.json, mcpplibs.os, the mwrap wrappers of zlib, zstd, xz and of the
@@ -10,6 +11,11 @@
# directive anywhere else is a header a module should provide: depend on the
# package that wraps it, or wrap it (mcpp-index docs/mwrap.md).
#
+# The same holds for a conditional directive (`#if`, `#ifdef`, `#ifndef`,
+# `#elif`) and a macro definition (`#define`): a platform is chosen by
+# `if constexpr` over mcpplibs.os and by the per-system sources of mcpp.toml,
+# never by the preprocessor.
+#
# Text that only looks like a directive is not one: a raw string (the source
# mcpp writes for a launcher or a template) and a comment are skipped. Tests
# (`tests/`, a module's `tests/`) are outside the rule.
@@ -20,10 +26,11 @@ import re, sys
from pathlib import Path
ALLOWED = {Path("src/native/posix/lock.cpp"), Path("src/native/windows/lock.cpp")}
+DIRECTIVE = re.compile(r'#\s*(include|if|ifdef|ifndef|elif|elifdef|elifndef|define)\b')
RAW = re.compile(r'(?:u8|u|U|L)?R"([^()\\ ]{0,16})\(')
def directives(text):
- """Line numbers of `#include` directives outside comments and raw strings."""
+ """(line, directive) of each preprocessor directive outside comments and raw strings."""
out, i, n, line = [], 0, len(text), 1
at_line_start = True
while i < n:
@@ -45,7 +52,8 @@ def directives(text):
j += 2 if text[j] == '\\' else 1
i = j + 1; at_line_start = False; continue
if at_line_start and c == '#':
- if re.match(r'#\s*include\b', text[i:]): out.append(line)
+ d = DIRECTIVE.match(text, i)
+ if d: out.append((line, d.group(1)))
if not c.isspace(): at_line_start = False
i += 1
return out
@@ -54,8 +62,9 @@ bad = 0
for path in sorted(list(Path("src").rglob("*.cpp*")) + list(Path("modules").rglob("*.cpp*"))):
if "tests" in path.parts or "test" in path.parts or path in ALLOWED:
continue
- for line in directives(path.read_text(encoding="utf-8", errors="replace")):
- print(f"{path}:{line}: #include outside the system lock files; import a module instead")
+ for line, d in directives(path.read_text(encoding="utf-8", errors="replace")):
+ hint = "import a module instead" if d == "include" else "choose with if constexpr or a per-system source"
+ print(f"{path}:{line}: #{d} outside the system lock files; {hint}")
bad += 1
for path in ALLOWED:
if not path.exists():
diff --git a/.github/tools/check_modules_wiring.sh b/.github/tools/check_modules_wiring.sh
index 29d385002..70e2139a3 100755
--- a/.github/tools/check_modules_wiring.sh
+++ b/.github/tools/check_modules_wiring.sh
@@ -10,7 +10,8 @@
# when they are forgotten:
#
# 1. modules//mcpp.toml the package itself
-# 2. mcpp.toml [dependencies.mcpp] what makes it importable
+# 2. mcpp.toml [dependencies] `mcpp. = { path = "modules/" }`,
+# what makes it importable
# 3. mcpp.toml [workspace] members what makes `-p` and `mcpp test` see it
# 4. the two xmake source lists the bootstrap and the benchmark, which
# have no package manager and compile the
@@ -62,8 +63,9 @@ for d in "${dirs[@]}"; do
|| bad "$d/mcpp.toml has namespace = \"${ns:-}\"; modules of this project use \"mcpp\" so they do not squat bare registry names"
# (2) importable
- grep -qE "^[[:space:]]*$name[[:space:]]*=[[:space:]]*\{[[:space:]]*path[[:space:]]*=[[:space:]]*\"modules/$name\"" mcpp.toml \
- || bad "mcpp.toml [dependencies.mcpp] has no entry for '$name' — it will not be importable"
+ grep -qE "^[[:space:]]*mcpp\.$name[[:space:]]*=[[:space:]]*\{[[:space:]]*path[[:space:]]*=[[:space:]]*\"modules/$name\"" \
+ <(sed -n '/^\[dependencies\]/,/^\[/p' mcpp.toml) \
+ || bad "mcpp.toml [dependencies] has no 'mcpp.$name = { path = \"modules/$name\" }' — it will not be importable"
# (3) addressable
grep -qF "\"modules/$name\"" <(sed -n '/^\[workspace\]/,/^\[/p' mcpp.toml) \
diff --git a/.github/workflows/ci-linux-e2e.yml b/.github/workflows/ci-linux-e2e.yml
index 8c14dbc46..25403bbe7 100644
--- a/.github/workflows/ci-linux-e2e.yml
+++ b/.github/workflows/ci-linux-e2e.yml
@@ -1,10 +1,10 @@
name: ci-linux-e2e
# The e2e suite (tests/e2e/run_all.sh) on Linux, called by ci.yml beside
-# ci-linux.yml after the Linux build. Six shards, assigned by measured
-# duration (tests/e2e/timings/linux.tsv, rule R4 of the 2026-10-02 CI record):
-# the table holds about 72 minutes of tests (2026-10-30, with the target-set
-# and packaging tests of 2026.10.30.1), about 12 a shard,
+# ci-linux.yml after the Linux build. Eight shards, divided by mtest by the
+# measured history of main's stores (design 2026-11-08 W8; rule R4 of the
+# 2026-10-02 CI record): about 94 minutes of tests (2026-11-08), about 12 a
+# shard one test at a time and about 6 two at a time,
# each running the one binary build.yml produced (use-built-mcpp, rule R1).
# Each shard writes a per-test report that the e2e-coverage job of ci.yml reads
# (rule R5).
@@ -17,16 +17,21 @@ on:
jobs:
e2e:
- name: e2e ${{ matrix.shard }}/6 (linux x86_64, self-host)
+ name: e2e ${{ matrix.shard }}/8 (linux x86_64, self-host)
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
- shard: [1, 2, 3, 4, 5, 6]
+ shard: [1, 2, 3, 4, 5, 6, 7, 8]
env:
- E2E_SHARD: ${{ matrix.shard }}/6
- E2E_TIMINGS: tests/e2e/timings/linux.tsv
+ E2E_SHARD: ${{ matrix.shard }}/8
+ # By mtest (design 2026-11-08 W8; .github/actions/e2e-mtest): parts by
+ # the measured history of main's stores, two tests at a time (measured on
+ # one Linux shard: 220 s against 424 s one at a time, three no faster),
+ # and on a pull request the tests its change affects.
+ E2E_JOBS: '2'
+ E2E_SET: ${{ github.event_name == 'pull_request' && 'pr' || '' }}
E2E_REPORT: ${{ github.workspace }}/e2e-report-linux-${{ matrix.shard }}.tsv
MCPP_HOME: /home/runner/.mcpp
# NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that
@@ -79,6 +84,10 @@ jobs:
XLINGS_HOME="$MCPP_HOME/registry" "$MCPP_VENDORED_XLINGS" install \
mcpplibs:cmdline@0.0.1 mcpplibs:cmdline@0.0.2 -y
+ - uses: ./.github/actions/e2e-mtest
+ with:
+ host: linux
+
- name: E2E suite
# About twice the shard's budget of ten minutes (R4): reached only by a
# hang. The per-test 600 s limit in run_all.sh names the test that hung.
@@ -98,6 +107,18 @@ jobs:
"$MCPP" self config
bash tests/e2e/run_all.sh
+ # The shard's store: main's are the history the next runs divide the suite by.
+ - name: Upload the shard's store
+ if: always()
+ timeout-minutes: 5
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-store-linux-${{ matrix.shard }}
+ path: tests/e2e/.mtest
+ include-hidden-files: true
+ if-no-files-found: ignore
+ retention-days: 14
+
# One file, no glob: the report the e2e-coverage job of ci.yml reads.
- name: Upload the shard's report
if: always()
diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml
index ac180b1c3..8fa3f4650 100644
--- a/.github/workflows/ci-linux.yml
+++ b/.github/workflows/ci-linux.yml
@@ -48,9 +48,20 @@ jobs:
with:
host: linux-x86_64
+ # A pull request runs the tests its change affects (`mcpp test --since`,
+ # docs/08; design 2026-11-08 W8): the base's tip is fetched, and when it
+ # cannot be, every test runs.
+ - name: The base of the change
+ if: github.base_ref != ''
+ shell: bash
+ run: |
+ if git fetch --no-tags --depth=1 origin "+refs/heads/$GITHUB_BASE_REF:refs/remotes/origin/$GITHUB_BASE_REF"; then
+ echo "MCPP_TEST_SINCE=origin/$GITHUB_BASE_REF" >> "$GITHUB_ENV"
+ fi
+
- name: Unit + integration tests via `mcpp test`
run: |
- "$MCPP_FRESH" test
+ "$MCPP_FRESH" test ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
# Each package under `modules/` carries its own tests, and they are built
# as that package ALONE -- a configuration the root suite never produces,
@@ -81,7 +92,7 @@ jobs:
echo "members: $(echo $members | tr '\n' ' ')"
for m in $members; do
echo "::group::mcpp test -p $m"
- "$MCPP_FRESH" test -p "$m"
+ "$MCPP_FRESH" test -p "$m" ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
echo "::endgroup::"
done
diff --git a/.github/workflows/ci-macos-e2e.yml b/.github/workflows/ci-macos-e2e.yml
index 0d552eb39..abde77a9b 100644
--- a/.github/workflows/ci-macos-e2e.yml
+++ b/.github/workflows/ci-macos-e2e.yml
@@ -2,7 +2,7 @@ name: ci-macos-e2e
# The e2e suite on macOS ARM64, called by ci.yml beside ci-macos.yml after the
# macOS build. Three shards, assigned by measured duration
-# (tests/e2e/timings/macos.tsv, rule R4 of the 2026-10-02 CI record), each
+# (by mtest, from the measured history of main's stores; rule R4 of the 2026-10-02 CI record), each
# running the one binary build.yml produced (use-built-mcpp, rule R1), and
# each writing the per-test report the e2e-coverage job of ci.yml reads.
#
@@ -55,7 +55,9 @@ jobs:
# 721 installs the large NDK in its own bounded step below. Running it in
# the ordinary shard duplicates the download and can fail before linking.
E2E_EXCLUDE: 721_*.sh
- E2E_TIMINGS: tests/e2e/timings/macos.tsv
+ # By mtest (design 2026-11-08 W8; .github/actions/e2e-mtest), as on Linux.
+ E2E_JOBS: '2'
+ E2E_SET: ${{ github.event_name == 'pull_request' && 'pr' || '' }}
E2E_REPORT: ${{ github.workspace }}/e2e-report-${{ matrix.image == 'macos-15' && 'macos' || matrix.image }}-${{ matrix.shard }}.tsv
# NOTE: no MCPP_VERBOSE — the e2e suite asserts mcpp's default quiet
# output (tests 48/53).
@@ -76,6 +78,10 @@ jobs:
with:
host: macos-arm64
+ - uses: ./.github/actions/e2e-mtest
+ with:
+ host: ${{ matrix.image == 'macos-15' && 'macos' || matrix.image }}
+
- name: E2E suite
# Twice a shard's budget of about nine minutes (R4); the per-test 600 s
# limit in run_all.sh names a test that hangs.
@@ -98,6 +104,18 @@ jobs:
set -o pipefail
bash tests/e2e/run_all.sh 2>&1 | tee "$RUNNER_TEMP/e2e-suite.log"
+ # The shard's store: main's are the history the next runs divide the suite by.
+ - name: Upload the shard's store
+ if: always()
+ timeout-minutes: 5
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-store-${{ matrix.image == 'macos-15' && 'macos' || matrix.image }}-${{ matrix.shard }}
+ path: tests/e2e/.mtest
+ include-hidden-files: true
+ if-no-files-found: ignore
+ retention-days: 14
+
# One file, no glob: the report the e2e-coverage job of ci.yml reads. A
# glob upload once hung a macOS job for ten minutes, and cancelling it lost
# the job's whole log.
diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml
index 64de87fa6..0de9957de 100644
--- a/.github/workflows/ci-macos.yml
+++ b/.github/workflows/ci-macos.yml
@@ -286,9 +286,20 @@ jobs:
with:
host: macos-arm64
+ # A pull request runs the tests its change affects (`mcpp test --since`,
+ # docs/08; design 2026-11-08 W8): the base's tip is fetched, and when it
+ # cannot be, every test runs.
+ - name: The base of the change
+ if: github.base_ref != ''
+ shell: bash
+ run: |
+ if git fetch --no-tags --depth=1 origin "+refs/heads/$GITHUB_BASE_REF:refs/remotes/origin/$GITHUB_BASE_REF"; then
+ echo "MCPP_TEST_SINCE=origin/$GITHUB_BASE_REF" >> "$GITHUB_ENV"
+ fi
+
- name: Unit + integration tests via `mcpp test`
run: |
- "$MCPP_FRESH" test
+ "$MCPP_FRESH" test ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
# WS8: this host's row of docs/01 and docs/20 is checked against the
# one answer the resolver gives (`self env --format json`).
diff --git a/.github/workflows/ci-windows-e2e.yml b/.github/workflows/ci-windows-e2e.yml
index de186f00f..a058e32f3 100644
--- a/.github/workflows/ci-windows-e2e.yml
+++ b/.github/workflows/ci-windows-e2e.yml
@@ -2,7 +2,7 @@ name: ci-windows-e2e
# The e2e suite on Windows x64, called by ci.yml beside ci-windows.yml after
# the Windows build. Three shards, assigned by measured duration
-# (tests/e2e/timings/windows.tsv, rule R4 of the 2026-10-02 CI record): about
+# (by mtest, from the measured history of main's stores; rule R4 of the 2026-10-02 CI record): about
# fifteen tests of over thirty seconds took 58 to 64 percent of a round-robin
# shard, and the two shards differed by 3.6 minutes on average. Each shard runs
# the one binary build.yml produced (use-built-mcpp, rule R1) and writes the
@@ -26,7 +26,12 @@ jobs:
MCPP_HOME: C:\Users\runneradmin\.mcpp
# Round-robin slice of tests/e2e (see run_all.sh).
E2E_SHARD: ${{ matrix.shard }}/3
- E2E_TIMINGS: tests/e2e/timings/windows.tsv
+ # By mtest (design 2026-11-08 W8; .github/actions/e2e-mtest): parts by
+ # the measured history of main's stores, two tests at a time (measured on
+ # one Linux shard: 220 s against 424 s one at a time, three no faster),
+ # and on a pull request the tests its change affects.
+ E2E_JOBS: '2'
+ E2E_SET: ${{ github.event_name == 'pull_request' && 'pr' || '' }}
E2E_REPORT: ${{ github.workspace }}/e2e-report-windows-${{ matrix.shard }}.tsv
# NOTE: do NOT force MCPP_VERBOSE here. The e2e suite includes tests that
# assert mcpp's DEFAULT (quiet) output — e.g. 48_build_error_output and
@@ -93,6 +98,10 @@ jobs:
test -n "$found" || { echo "FAIL: mingw installed but not where run_all.sh looks"; exit 1; }
echo "mingw payload: $found"
+ - uses: ./.github/actions/e2e-mtest
+ with:
+ host: windows
+
- name: E2E suite
shell: bash
# About twice the shard's budget of fifteen minutes (R4): reached only by
@@ -113,6 +122,18 @@ jobs:
# Measurement legs print READING lines (#646 F2 across PE images, #649 E10
# the llvm row's recorded CRT); collected where a reader finds them.
+ # The shard's store: main's are the history the next runs divide the suite by.
+ - name: Upload the shard's store
+ if: always()
+ timeout-minutes: 5
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-store-windows-${{ matrix.shard }}
+ path: tests/e2e/.mtest
+ include-hidden-files: true
+ if-no-files-found: ignore
+ retention-days: 14
+
# One file, no glob: the report the e2e-coverage job of ci.yml reads.
- name: Upload the shard's report
if: always()
diff --git a/.github/workflows/ci-windows.yml b/.github/workflows/ci-windows.yml
index 8c9f8567e..20300efce 100644
--- a/.github/workflows/ci-windows.yml
+++ b/.github/workflows/ci-windows.yml
@@ -39,11 +39,22 @@ jobs:
shell: bash
run: MCPP="$MCPP_SELF" python .github/tools/check_windows_home_menu.py
+ # A pull request runs the tests its change affects (`mcpp test --since`,
+ # docs/08; design 2026-11-08 W8): the base's tip is fetched, and when it
+ # cannot be, every test runs.
+ - name: The base of the change
+ if: github.base_ref != ''
+ shell: bash
+ run: |
+ if git fetch --no-tags --depth=1 origin "+refs/heads/$GITHUB_BASE_REF:refs/remotes/origin/$GITHUB_BASE_REF"; then
+ echo "MCPP_TEST_SINCE=origin/$GITHUB_BASE_REF" >> "$GITHUB_ENV"
+ fi
+
- name: Unit + integration tests via mcpp test
shell: bash
run: |
export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe")
- "$MCPP_SELF" test
+ "$MCPP_SELF" test ${MCPP_TEST_SINCE:+--since "$MCPP_TEST_SINCE"}
- name: Home and runner-cache paths preserve Unicode under a legacy Windows code page
shell: bash
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2922f9b98..079f941b0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,6 +40,8 @@ permissions:
contents: read
# `changes` lists the files of the pull request.
pull-requests: read
+ # The e2e shards read the stores of main's last run (.github/actions/e2e-mtest).
+ actions: read
jobs:
changes:
@@ -100,7 +102,7 @@ jobs:
- name: Check modules/ wiring
run: bash .github/tools/check_modules_wiring.sh
- - name: Check that headers are included only in the system lock files
+ - name: Check that headers and conditional directives appear only in the system lock files
run: bash .github/tools/check_include_regions.sh
- name: Check src/build/prepare* file lengths
@@ -276,8 +278,8 @@ jobs:
- name: Every test ran, is run by a dedicated job, or is excused
run: python3 .github/tools/check_e2e_coverage.py --reports reports --timings-out timings
- # The measured durations, merged per host, in the format of
- # tests/e2e/timings/; refreshing those tables is copying these files.
+ # The measured durations, merged per host, in the format run_all.sh reads
+ # from E2E_TIMINGS (a run without mtest).
- name: Upload the measured durations
if: always()
uses: actions/upload-artifact@v4
diff --git a/.github/workflows/e2e-coverage.yml b/.github/workflows/e2e-coverage.yml
new file mode 100644
index 000000000..0ba383919
--- /dev/null
+++ b/.github/workflows/e2e-coverage.yml
@@ -0,0 +1,72 @@
+name: e2e-coverage
+
+# The coverage map of the e2e suite (design 2026-11-08 W8; mtest's docs/coverage.md):
+# mcpp built with GCC's arc counters ([profile.coverage]), the whole suite run
+# by mtest under `--coverage`, and for each test the sources its run executed.
+# A pull request's Linux shards read the last map (.github/actions/e2e-mtest)
+# and run the tests whose runs executed a file the change touches, with the
+# smoke and regression sets; a test the map does not know, a map older than
+# mtest.toml's max_age, and a change mtest cannot read select every test. Main
+# and releases run every test.
+
+on:
+ schedule:
+ - cron: "17 19 * * *"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ actions: read
+
+jobs:
+ measure:
+ name: e2e coverage (linux x86_64)
+ runs-on: ubuntu-24.04
+ timeout-minutes: 300
+ env:
+ MCPP_HOME: /home/runner/.mcpp
+ E2E_JOBS: '2'
+ E2E_COVERAGE: '1'
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ submodules: recursive
+ - uses: ./.github/actions/bootstrap-mcpp
+ - name: mcpp with arc counters
+ run: |
+ set -euo pipefail
+ "$MCPP" build --profile coverage 2>&1 | tail -2
+ bin=$(ls -t "$PWD"/target/*/*/bin/mcpp | head -1)
+ dir=$(dirname "$(dirname "$bin")")
+ echo "MCPP_COVERED=$bin" >> "$GITHUB_ENV"
+ printf '\n[coverage]\nobjects = "%s/obj"\ndatabase = "%s/compile_commands.json"\n' "$dir" "$dir" >> tests/e2e/mtest.toml
+ tail -4 tests/e2e/mtest.toml
+ - uses: ./.github/actions/e2e-mtest
+ with:
+ host: linux
+ - name: The toolchains the suite probes
+ timeout-minutes: 15
+ run: |
+ set -euo pipefail
+ "$MCPP_COVERED" toolchain install gcc 16.1.0
+ "$MCPP_COVERED" toolchain default gcc@16.1.0
+ "$MCPP_COVERED" toolchain install gcc 16.1.0-musl
+ "$MCPP_COVERED" toolchain install llvm 23.1.3
+ "$MCPP_COVERED" toolchain install mingw-cross 16.1.0
+ XLINGS_HOME="$MCPP_HOME/registry" "$XLINGS_BIN" install xim:nasm -y
+ XLINGS_HOME="$MCPP_HOME/registry" "$XLINGS_BIN" install mcpplibs:cmdline@0.0.1 mcpplibs:cmdline@0.0.2 -y
+ - name: The suite under --coverage
+ timeout-minutes: 240
+ run: |
+ set -euo pipefail
+ export MCPP="$MCPP_COVERED" MCPP_BOOT="$MCPP" MCPP_VENDORED_XLINGS="$XLINGS_BIN"
+ export MCPP_E2E_TOOLCHAIN_MIRROR=GLOBAL
+ "$MCPP" self config
+ bash tests/e2e/run_all.sh || true
+ (cd tests/e2e && "$E2E_MTEST" coverage)
+ - uses: actions/upload-artifact@v4
+ with:
+ name: e2e-coverage-linux
+ path: tests/e2e/.mtest/coverage.json
+ include-hidden-files: true
+ retention-days: 14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67ef965d2..832353891 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,140 @@
> Each `## []` section is that release's notes. Entries are written in English
> from 2026.9.28.3 on; earlier entries remain as written.
+## [2026.11.8.1] - 2026-11-08
+
+### Added
+
+- `mcpp test` runs the runner's side of the mtest test protocol, version 1
+ (docs/08 "Cases"). A test program whose entry states `test-protocol = 1`, or
+ whose file carries the marker `mtest-protocol=1`, is told the protocol's
+ variables and reports its cases: the record names the protocol and each
+ case's result and failures, a case a crash left open failed, and a failing
+ program's report names its failed cases with their places. `--case PATTERN`
+ runs the matching cases -- a name below a listed case selects the case that
+ case declares, which is reported when it runs (P3.4), and a pattern below it
+ runs the listed case whole --, `--cases` lists them, `--seed N` states the seed. A
+ program that implements no protocol is one case and receives none of the
+ variables. `--no-run` records each built test with its program and the files
+ it is compiled from; `--print-launch` states the protocol; the feature
+ `test-protocol` names all of it (e2e 1035).
+- `mcpp new` scaffolds tests with mtest 0.4.0: the skeleton's test is a file of
+ mtest's cases with no `main` (`UTest smoke{"...", [] { ... }}`: an object is a
+ case, its type its kind), with the dev-dependency `mcpplibs.mtest`, and
+ `--template lib` scaffolds a library -- a module, a partition it exports and
+ the partition's implementation, named after the package -- with its test
+ (docs/01; e2e 1037).
+- An entry with `test-main = true` begins a test file that defines no `main`.
+ The file's imports choose among offers, so in one package a file of mtest's
+ cases and a file of GoogleTest's begin each at its own; a dev-dependency's
+ source that defines `main` (`gtest_main.cc`) begins a test as before, and two
+ beginnings left for one test are refused, naming them; a framework that is
+ a dependency as well as a dev-dependency offers its beginning once
+ (docs/08; e2e 1037).
+- `mcpp test --guide` names the package's test frameworks, each with the guide
+ its entry states (`test-guide`) and its repository, and the commands that
+ list, select and explain cases; a failed run points to it and a record
+ carries `guide` (the protocol's P2.5; docs/08 "Writing a test"; e2e 1037).
+- `[test] programs = "directory"`: the files of one directory below a discover
+ glob's fixed directory are one test program, named by the directory, so a
+ suite of many files is linked once (docs/08; e2e 1038).
+- `mcpp test --since ` and `--changed ` build and run the tests a
+ change affects, read from the plan: a test built from a changed file (its own
+ files, the modules it imports transitively, its units of no module), a test
+ whose files name a changed file; a manifest, lock, build program or header
+ affects every test (docs/08; e2e 1038).
+- `mcpp::context` states the engine's values to the build-program interface in
+ one process: every accessor reads the innermost context's values, then the
+ environment, so a test of a build program's or a plugin's logic needs no
+ change of its environment (docs/30; e2e 1036).
+- `[build] target_budget` keeps `target/` within a size: after a build or a test
+ run, the directories no configuration records as current are removed whatever
+ their age, and a build whose directories in use exceed it names the three
+ largest (#874; e2e 1031).
+
+### Changed
+
+- The built-in platform of the two ok-image targets is openkal-platform 0.24.4,
+ whose okrun enters an image in its convention's floating-point state (openkal
+ SPEC 12.2): on Windows a thread began with the x87 at double precision, under
+ which an image's C library read a decimal number otherwise.
+- Both openkal rows are `verified`: mcpp's CI builds an ok image over every layer
+ of the platform and runs it on Linux, macOS and Windows (e2e 1033, required on
+ each host by `tests/e2e/host-requirements.tsv`). docs/24 states the two roles,
+ an image as a release form and a native row as a build target.
+- A dev-dependency reaches the test units and nothing else of its package: its
+ include directories and definitions are given to the files under `tests/`,
+ and a kernel interface only a dev-dependency reaches is given to the
+ dev-dependencies and the test units. The package's own sources compile under
+ `mcpp test` with the commands `mcpp build` gives them, so `mcpp build` and
+ `mcpp test` share one build directory and one compile database and switching
+ between them compiles nothing again (docs/08; e2e 77, 783, 1037 K). Before,
+ a dev-dependency over mcpplibs.os (mtest's) gave the package's sources
+ `__OPENKAL__` and made the test a configuration of its own.
+- A program that carries the protocol's marker and writes no `program` event
+ -- a file of plain `main` linked beside mtest's library -- is one case, judged
+ by its exit status, when run and when listed (e2e 1038).
+- A failed test whose file imports a module no package of the build provides
+ names the module and the dev-dependency a framework is.
+- A product an earlier build left where this one places a program's directory,
+ or a directory where it places a program, is removed before the build.
+- `trim_paths` applies to what `mcpp test` compiles as it does to what `mcpp
+ build` compiles, but for a test program's own files, which keep their paths:
+ under a shipping profile (`release`, mcpp's own default) build and test are
+ one configuration, where a test compiled the whole graph again in a
+ configuration of its own (637 of mcpp's objects; docs/04; e2e 1038 E).
+- `mcpp dyndep` leaves an unchanged output untouched, so a unit whose module
+ edges are what they were is not compiled again when the rule's command
+ changed (#874).
+- The low-space warning's threshold is the larger of 2 GiB and 1.25 times what
+ the configuration's last build wrote (#874; e2e 1030).
+- mcpp's own dependencies are named by identity in one table, each by one
+ spelling of an exact release (#904), and so are those of the launchers `mcpp
+ pack` generates; mcpp, its modules and the launchers name mcpplibs.os 0.4.0.
+
+### Deprecated
+
+- A quoted dotted dependency key (`"mcpplibs.cmdline" = "0.0.2"`) is the reading
+ `quoted-dotted-dependency-key`, reported with the unquoted key that replaces
+ it; removed in 2027.2. A quoted key that is the only spelling of a package
+ beside its parent or a child of its namespace in one table (`mcpplibs.mtest`
+ and `"mcpplibs.mtest.gtest"`) is not deprecated (#904; e2e 1032).
+
+### Fixed
+
+- Two builds that find a std module missing compile it once. The std module of a
+ toolchain and flag set is one entry of the home's build cache, and two
+ processes that found it missing compiled into the same files at once: one
+ compiler read a BMI the other was rewriting (a bus error) or found its
+ output gone, and a build went on with no std object. The entry is built
+ under its own lock; a second build waits, naming the first, and finds it
+ built (e2e 1039).
+- A dependency's source whose `main` is compiled only under a switch no command
+ defines -- zlib's `crc32.c` (`#ifdef MAKECRCH`), QuickJS's `libregexp.c`
+ (`#ifdef TEST`) -- is linked into a test that has its own `main`. Which
+ sources begin a program is read from the branches the preprocessor may
+ compile: a name the compiler or platform predefines is unknown, any other is
+ undefined; 2026.11.7.2 left such a source out, and the test's link failed
+ with undefined references (measured in Sunrisepeak/mui, #874; e2e 1037 L).
+- The warning on a quoted dotted dependency key names the key to write,
+ `xrender.graph = ...`, not an example (e2e 1032).
+- A configuration group's lock keeps the entries of the other groups (#903;
+ e2e 1027).
+- A project's state directory `.mcpp/` ignores itself in a repository (e2e 1028).
+- `mcpp cache prune --older-than` and `cache gc --older-than` collect a std
+ module unused for that long (#874; e2e 1029).
+- A test that has a beginning -- its own `main`, `[test] main` or an offered
+ entry -- links no dependency's source that defines `main`, a direct
+ dev-dependency's or one beneath it: `gtest_main.cc` of compat.gtest beneath
+ the GoogleTest adapter was linked beside the adapter's `main` (e2e 1037).
+- A test links its package's dev-dependency that is a shared library by the
+ library, as a dependency is; mtest's C ABI was left unresolved (e2e 1037).
+- A dependency's entry that imports `std` builds the std module for the
+ program that begins at it, though nothing else of the graph imports it; the
+ entry failed with "failed to read compiled module" (e2e 1037).
+- Under `--workspace`, a member's test finds the member's programs as
+ `MCPP_BIN_`, as under `-p` (e2e 1034).
+
## [2026.11.7.2] - 2026-11-07
### Changed
diff --git a/README.md b/README.md
index 262681b8b..9c3c8f01f 100644
--- a/README.md
+++ b/README.md
@@ -454,7 +454,7 @@ list` reports for this machine):
| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23 — Cortex-M4/M7 soft float, M23, M33F/M55F ² | preview |
| `aarch64-linux-gnu` | `llvm@23.1.3` | preview |
| `riscv64-linux-musl` · `x86_64-macos` | — | planned |
-| `x86_64-openkal` · `aarch64-openkal` | llvm 23 — an ok image: one artifact per architecture, run by `okrun` on Linux, macOS and Windows; the C library and C++ run-time inside it are packages ([24](docs/24-openkal-cross.md)) | preview |
+| `x86_64-openkal` · `aarch64-openkal` | llvm 23 — an ok image: one artifact per architecture, run by `okrun` on Linux, macOS and Windows; the C library and C++ run-time inside it are packages ([24](docs/24-openkal-cross.md)) | verified |
| `wasm32-emscripten` | `emsdk@6.0.9` — Emscripten ships its own sysroot and its own libc++ module surface; `mcpp run` executes the module with the `node` the payload declares (`xim:node`), not one found on PATH | verified |
| `x86_64-linux-android` | `android-ndk@30.0.16248370` — bionic from the NDK, one payload for both ABIs; ran on an API 24 x86_64 emulator image | verified |
| `aarch64-linux-android` | the same payload and the same build; ran under qemu-user over the system image's own bionic, which the platform emulator cannot do from an x86_64 host | verified |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index b11b7542b..d2673588d 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -403,7 +403,7 @@ mcpp 的身份模型有两条正交的轴:**工具链**是 `family@version`(
| `thumbv7em-none-eabi` · `thumbv8m.base-none-eabi` · `thumbv8m.main-none-eabihf` | llvm 23;Cortex-M4/M7 软浮点、M23、M33F/M55F ² | preview |
| `aarch64-linux-gnu` | `llvm@23.1.3` | preview |
| `riscv64-linux-musl` · `x86_64-macos` | — | planned |
-| `x86_64-openkal` · `aarch64-openkal` | llvm 23;ok 映像:每种架构一个产物,由 `okrun` 在 Linux、macOS 与 Windows 上运行;映像内的 C 库与 C++ 运行时是包([24](docs/zh/24-openkal-cross.md)) | preview |
+| `x86_64-openkal` · `aarch64-openkal` | llvm 23;ok 映像:每种架构一个产物,由 `okrun` 在 Linux、macOS 与 Windows 上运行;映像内的 C 库与 C++ 运行时是包([24](docs/zh/24-openkal-cross.md)) | verified |
| `wasm32-emscripten` | `emsdk@6.0.9`;Emscripten 自带 sysroot 与 libc++ 模块接口;`mcpp run` 使用 payload 声明的 `node`(`xim:node`)运行模块,不使用 PATH 上的 `node` | verified |
| `x86_64-linux-android` | `android-ndk@30.0.16248370`;bionic 来自 NDK,一个 payload 服务两个 ABI;已在 API 24 的 x86_64 模拟器镜像上运行 | verified |
| `aarch64-linux-android` | 同一个 payload、同样的构建;已在 qemu-user 上配合系统镜像自带的 bionic 运行,平台模拟器无法在 x86_64 宿主上做到这一点 | verified |
diff --git a/docs/01-getting-started.md b/docs/01-getting-started.md
index c70fc9e2f..ce92416d4 100644
--- a/docs/01-getting-started.md
+++ b/docs/01-getting-started.md
@@ -101,6 +101,40 @@ int main() {
}
```
+The test is a file of [mtest](https://github.com/mcpp-community/mtest)'s cases
+with no `main`, and the manifest names it a dev-dependency. Each object is one
+case; its type is its kind (`UTest`, `E2E`, `CTS`, `Bench`), and the attributes
+after its name state how it runs (resources, time limit, isolation, retries):
+
+```cpp
+import mcpplibs.mtest;
+using namespace mcpplibs::mtest;
+
+UTest smoke{"the project builds and its test runs", [] { expect(eq(1 + 1, 2)); }};
+```
+
+### Creating a library
+
+```bash
+mcpp new acme.greeting --template lib
+```
+
+```
+acme.greeting/
+├── mcpp.toml
+├── src/
+│ ├── greeting.cppm ← export module acme.greeting; export import :hello;
+│ ├── hello.cppm ← export module acme.greeting:hello; the declaration
+│ └── hello.cpp ← module acme.greeting; the definition
+└── tests/
+ └── hello.cpp ← mtest's cases for hello
+```
+
+The module is the package's qualified name, with `-` written `_`; a consumer
+writes `import acme.greeting;`. The partition holds one function's declaration
+and the implementation unit its definition; more functions are more partitions
+the module exports.
+
### Creating from a package template
`mcpp new --template` uses the same exact package-selector style as `mcpp add`:
diff --git a/docs/04-mcpp-toml.md b/docs/04-mcpp-toml.md
index b69bbb94c..f8d6e28b2 100644
--- a/docs/04-mcpp-toml.md
+++ b/docs/04-mcpp-toml.md
@@ -297,7 +297,8 @@ main = { from = "openmui/player" } # "/"
`extern "C"`: a declaration attached to a named module cannot be defined
outside it.
- An entry target states `main` (a file), and may state `start`,
- `windows_entry`, `cflags`, `cxxflags`, `defines` and `required_features`.
+ `windows_entry`, `cflags`, `cxxflags`, `defines`, `required_features` and
+ `test-protocol`.
Its `windows_entry` is inherited by the program that selects it; a program
stating another is refused. Its `required_features` must be active: a
selection otherwise is refused with the dependency line that enables them.
@@ -509,6 +510,9 @@ required_features = ["gui"] # only built when feature `gui` is
| `windows_subsystem` *(2026.9.12.2+)* | The PE subsystem of an executable: `"console"` (the default) or `"windows"`, a GUI program that starts without a console. Reaches this target's link and no other, and renders nothing on a target that is not PE. See the section above. |
| `windows_entry` *(2026.9.12.2+)* | The entry function the program defines: `"main"` (the default), `"wmain"`, `"WinMain"` or `"wWinMain"`. See the section above. An entry target states the function its translation unit defines, and the program that selects it inherits it. |
| `start` *(2026.10.26.2+)* | The symbol the image is entered at, on a freestanding target. See `start` above. |
+| `test-guide` *(2026.11.8.1+)* | Of an entry target: a file of the package, its guide to writing the tests it begins, which `mcpp test --guide` names ([08](08-testing.md#writing-a-test)). Refused on any other target. |
+| `test-main` *(2026.11.8.1+)* | Of an entry target: `true` offers it as the beginning of a test file that defines no `main` in a package that depends on this one (or is this one) and states no `[test] main` ([08](08-testing.md#where-a-test-program-begins)). Refused on any other target. |
+| `test-protocol` *(2026.11.8.1+)* | Of an entry target: the version of the test protocol a test program that begins here implements, `1`; `mcpp test` then lists, selects and reports the program's cases ([08](08-testing.md#cases-the-test-protocol)). Refused on any other target. |
| `windows_code_page` *(2026.9.26.1+)* | The ANSI code page of an executable on Windows: `"utf-8"`, embedded as an application manifest that Windows 10 version 1903 and later honour, or `"legacy"`, the system's code page. A program target that writes neither runs in the system's code page, except a program built as a host tool (§2.14), which defaults to `"utf-8"`. Refused on a library target; renders nothing on a target that is not PE. See *Paths and text encoding* in §2.3. |
| `windows_long_paths` *(2026.10.16.1+)* | `true` embeds `longPathAware` in the executable's application manifest: the process may use paths longer than 260 characters where Windows enables them (`LongPathsEnabled`). `false` embeds nothing. A program target that writes neither embeds nothing, except a program built as a host tool (§2.14), which defaults to `true`. Refused on a library target; renders nothing on a target that is not PE. |
| `linkage` *(2026.9.15.2+)* | A library target's **default** link form, `"static"` or `"shared"`: the form a consumer that writes no `linkage` receives. Unlike `kind = "shared"` it is not a constraint, so a consumer's explicit statement is honoured. Refused beside `kind = "shared"` and on a program target. See [`dependency_linkage`](#dependency_linkage--static-or-shared-is-the-consumers-decision). |
@@ -1676,9 +1680,11 @@ anything compiles.
checkouts of one tree then embed identical paths. A unit's map holds the fixed roots
and the roots of its own package's dependency closure, so a dependency's command line
is the same whichever workspace members a build selects (2026.10.26.2+). It applies to
- what `build`, `run`
- and `pack` produce, not to `mcpp test`: a test binary runs where it was built and
- commonly finds its fixtures through `__FILE__`. A build with debug information
+ what `build`, `run`, `pack` and `mcpp test` compile, but for a test program's own
+ files: a test binary runs where it was built and commonly finds its fixtures through
+ `__FILE__`, and the code under test is the code that ships, so `mcpp build` and
+ `mcpp test` are one configuration (2026.11.8.1+; before, a test of a shipping profile
+ was a configuration of its own and compiled the whole graph again). A build with debug information
also writes `source-map.gdbinit` and `source-map.lldbinit` beside its outputs, which
map the paths back for a debugger. cl.exe has no documented equivalent and the build
says so once. Write `ndebug = false` or `trim_paths = false` under the profile to keep
@@ -1716,6 +1722,12 @@ anything compiles.
removed instead (its objects are not reusable). After a build or a test run,
at most once a day, the directories no configuration used for `[build]
keep_days` days (config.toml; default 3, `0` keeps all) are removed.
+- **`[build] target_budget` bounds `target/` (2026.11.8.1+).** A size such as
+ `target_budget = "20GiB"` in `mcpp.toml` (or config.toml): after every build
+ or test run whose `target/` holds more, the directories no configuration
+ records as current are removed whatever their age, and when the directories
+ in use still hold more, the build warns, naming the three largest. Nothing in
+ use is removed (mcpp#874).
### 2.10 `[build] cache` — The Global Dependency Cache
@@ -2141,6 +2153,7 @@ discover = ["tests/**/*.cpp"] # the default
| `windows_long_paths` | `true` or `false` | whether every test program on Windows is long-path aware, in the meaning of the target key of the same name; the default is `false` *(2026.10.16.1+)* |
| `main` | `{ from = "/" }` | the entry target every test program begins in; each test file is then an ordinary translation unit of its program and defines no `main` (§2.2, `kind = "entry"`) *(2026.10.26.2+)* |
| `start` | symbol | the symbol every test image is entered at, on a freestanding target (§2.2, `start`) *(2026.10.26.2+)* |
+| `programs` | `"file"` or `"directory"` | `"file"` (the default): every discovered file is a program; `"directory"`: the files of one directory below a glob's fixed directory are one program, named by the directory, beginning at the file that defines `main` when one does *(2026.11.8.1+)* |
The globs use the vocabulary of `[build] sources`. A test's name is its path
relative to the fixed directory of the first glob that matched it, without the
diff --git a/docs/05-dependencies.md b/docs/05-dependencies.md
index dc9b7a322..5e0db2e78 100644
--- a/docs/05-dependencies.md
+++ b/docs/05-dependencies.md
@@ -56,6 +56,21 @@ glfw = "3.4" # Explicit namespace; no fallback search
+A quoted key with dots, `"mcpplibs.cmdline" = "0.0.2"`, is one TOML key whose
+name contains dots. mcpp reads it as the selector those dots spell, warns that
+the spelling is deprecated (since 2026.11.8.1, removed from 2027.2; docs/51)
+and names the unquoted key that replaces it: `mcpplibs.cmdline = "0.0.2"`.
+A package and a package of its namespace in one table, `mcpplibs.mtest` and
+`mcpplibs.mtest.gtest`, cannot both be written unquoted -- one name would be a
+value and a table -- so the quoted key of one of them is their spelling and is
+not deprecated:
+
+```toml
+[dev-dependencies]
+mcpplibs.mtest = "0.4.0"
+"mcpplibs.mtest.gtest" = "0.4.0"
+```
+
```toml
# Path dependency (local development)
[dependencies]
@@ -430,7 +445,7 @@ Run any command with `-v` to see the decision for each dependency and why.
gtest = "1.15.2"
```
-`mcpp build` ignores these; `mcpp test` resolves and uses them. `mcpp test` automatically discovers `tests/**/*.cpp` and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare `main`, gtest (via `[dev-dependencies]` + `gtest_main`), or any other framework all work identically, and `-- args` are forwarded to every test binary (e.g. `-- --gtest_filter=...`). Note: synthesized test target names may contain `/` (`tests/00-a/0.cpp` → `00-a/0`), unlike `[targets.*]` names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their `tests/`-relative path (`tests/00-a/0.cpp` → `00-a/0`), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and `mcpp test ` / `--message-format json` filter and machine-format the run.
+`mcpp build` ignores these; `mcpp test` resolves and uses them, for the test units only: the package's own sources compile under `mcpp test` as under `mcpp build` (2026.11.8.1+, [08](08-testing.md#tests-that-reach-packages-the-artifact-does-not)). `mcpp test` automatically discovers `tests/**/*.cpp` and compiles them into test binaries. The runner is framework-agnostic: each file is an independent binary judged by exit code — a bare `main`, gtest (via `[dev-dependencies]` + `gtest_main`), or any other framework all work identically, and `-- args` are forwarded to every test binary (e.g. `-- --gtest_filter=...`). Note: synthesized test target names may contain `/` (`tests/00-a/0.cpp` → `00-a/0`), unlike `[targets.*]` names — the two namespaces are intentionally separate (test targets never enter the manifest or publishing). Tests are named by their `tests/`-relative path (`tests/00-a/0.cpp` → `00-a/0`), each test compiles in isolation (a broken test fails alone; package/dep breakage is reported as a build error instead), and `mcpp test ` / `--message-format json` filter and machine-format the run.
## `[build-dependencies]` — Build-Time Dependencies (mcpp 2026.8.29.1+)
diff --git a/docs/08-testing.md b/docs/08-testing.md
index b8b9a7c41..03bb0160c 100644
--- a/docs/08-testing.md
+++ b/docs/08-testing.md
@@ -33,8 +33,10 @@ There is no framework and no registration. A test may use one — `[dev-dependen
is how it reaches it — but the contract mcpp holds is the exit code, which is
also why a test written for another framework needs no adapter.
-`mcpp new` scaffolds `tests/test_smoke.cpp` so a project starts with the
-directory in place.
+`mcpp new` scaffolds `tests/test_smoke.cpp`, a file of
+[mtest](https://github.com/mcpp-community/mtest)'s cases with no `main`, and the
+dev-dependency `mcpplibs.mtest` (2026.11.8.1+); `mcpp new --template lib`
+scaffolds a library and its test the same way ([01](01-getting-started.md)).
### Where tests are
@@ -53,7 +55,17 @@ the extension, so the default names `tests/unit/test_span.cpp` `unit/test_span`.
`discover = []` discovers no test. Two files whose names coincide are refused,
naming both.
-A suite compiled from several sources is a package of its own: a workspace
+`[test] programs = "directory"` (2026.11.8.1+) makes the files of one directory
+below a glob's fixed directory one program, named by the directory
+(`tests/unit/*.cpp` is the program `unit`): each file is a translation unit of
+it, its cases are all of theirs, and it begins at the file that defines `main`
+when one does, else as a file that defines none does. Two files of one directory
+that define `main` are refused, naming both; a file directly in the fixed
+directory stays a program of its own. A program of many files is linked once
+where a program per file is linked once per file, which is what a suite of many
+small files spends its build time on.
+
+A suite compiled from several sources of its own is also a package: a workspace
member whose `[build] sources` carries the suite and whose one test program
drives it, selected with `mcpp test -p `.
@@ -75,6 +87,17 @@ no `main`. The package's own name selects its own entry, which is how a framewor
tests its entry with the callbacks a test file defines. On a freestanding target,
`[test] start` states the symbol every test image is entered at.
+An entry may offer itself with `test-main = true` (2026.11.8.1+): a test file that
+defines no `main`, of a package that states no `[test] main`, then begins at it,
+and a file that defines `main` keeps its own. mtest's entry and its GoogleTest
+adapter's offer themselves, so a test needs only the dev-dependency. The choice
+is the file's: an offer whose package provides modules is this test's when the
+file imports one of them and is set aside when it imports none, so in one
+package a file of mtest's cases begins at mtest's entry and a file of
+GoogleTest's at GoogleTest's. A dependency's source that defines `main`
+(`gtest_main.cc`) begins a test as it always has. Where two beginnings remain
+for one test, the build is refused, naming them, and `[test] main` chooses.
+
## Running them
```bash
@@ -107,6 +130,25 @@ test's verdict, the output of a test that failed, the `test result` line and, fo
collects the report, and `mcpp test 2>&1 | tee test.log` collects both
([09 — Commands by scenario](09-commands-by-scenario.md#output-streams)).
+### The tests a change affects
+
+`mcpp test --since ` and `--changed ` (2026.11.8.1+) build and run
+the tests a change affects, read from the plan before anything is built:
+
+- a test is affected by a change of a file it is built from: its own files, the
+ module units it imports and the units those import in turn, and every unit
+ of its link that belongs to no module;
+- a change of a manifest, a lock, a build program, a header, or a source the
+ plan does not hold affects every test;
+- another file affects the tests whose own files name it (a fixture), and no
+ other test.
+
+`--since` reads the change from git: the paths committed since the commit,
+changed in the working tree, and new and not ignored. `Affected N of M tests`
+states the selection, with what each changed path decided; under
+`--message-format json` an `affected` record lists each test with its reason.
+The selection narrows what a pattern or `-p` names and adds nothing to it.
+
**A test finds the package's programs by name** (2026.10.27.1+). `mcpp test` builds
the package's programs first, as `mcpp build` does, and gives each test
`MCPP_BIN_`: the path of program ``, upper-cased, every character
@@ -149,6 +191,18 @@ else: it is not in the artifact, and a consumer of the package never sees it.
That is the difference between a test's dependency and the package's own, and it
is why a test framework does not become part of what a library ships.
+A dev-dependency reaches the test units and nothing else of the package
+(2026.11.8.1+). Its include directories and interface definitions are given to
+the files under `tests/`; the package's own sources are compiled under
+`mcpp test` with the commands `mcpp build` gives them, so the code under test is
+the code that ships, and `mcpp build` and `mcpp test` share one build directory
+and one compile database. A kernel interface that only a dev-dependency reaches
+(`mcpplibs.mtest` over `mcpplibs.os` reaches openkal's) is given to the
+dev-dependencies and the test units alone: they see `__OPENKAL__` and openkal's
+headers, and the package's own sources do not. A C library or a C++ runtime that
+a dev-dependency supplies is a property of every object a test links, and it
+remains the configuration's.
+
[`examples/11-features`](../examples/11-features/) declares one and uses it.
## Testing on a target this machine cannot run
@@ -238,6 +292,70 @@ workers reaching for it interleave rather than fail.
The package that owns the resource states this about itself, and `mcpp test`
then serialises those tests. A project never has to remember `-j1`.
+## Cases: the test protocol
+
+A test program may consist of cases. mcpp does not provide a framework for
+them; it implements the runner's side of a protocol that frameworks implement:
+the [mtest test protocol](https://github.com/mcpp-community/mtest/blob/main/spec/protocol.md),
+version 1 (mcpp 2026.11.8.1+). A program implements it when its entry states it,
+
+```toml
+[targets.main]
+kind = "entry"
+main = "src/main.cpp"
+test-protocol = 1
+```
+
+or when its file carries the marker `mtest-protocol=1`, as a program built
+with mtest's libraries does. A program that carries the marker and writes no
+`program` event spoke no protocol: a file of plain `main` linked beside a
+framework's library is one case, judged by its exit status, run and listed. mcpp then gives the program the protocol's
+variables (`MTEST_PROTOCOL`, `MTEST_MODE`, `MTEST_REPORT`, `MTEST_TMPDIR`, and
+`MTEST_SELECT` and `MTEST_SEED` when they apply), reads the report it writes,
+and states each case's result:
+
+```bash
+mcpp test # each case's result in the report and the record
+mcpp test --case 'fs/*' # of each such program, the cases a pattern matches
+mcpp test --cases # each program's cases, running none
+mcpp test --seed 42 # the seed of anything random in the cases
+```
+
+- The program's exit status still decides its verdict. A report whose cases
+ failed fails the program though it exited zero; a case that began and did
+ not end failed, with the program's exit status as its reason.
+- A failing program's report names each failed case with the place of its
+ first failure: `case fs/read failed at tests/fs.cpp:12: ...`.
+- A pattern of `--case` matches with `*` and `?`; a name without either
+ matches itself and the cases below it (`fs` matches `fs/read`). A case that
+ a listed case declares is reported when it runs and need not be listed
+ (protocol P3.4; mtest's `UTest fs{"fs", [] { "read"_test = ...; }}` lists
+ `fs`): a name below a listed case selects that one case, and a pattern below
+ it runs the listed case whole. Under `--case`, a program with no matching
+ case, and every program that implements no protocol, does not run.
+- A program that implements no protocol is one case, judged by its exit status,
+ and receives none of the variables, even when mcpp's own environment has
+ them.
+
+The records of `--message-format json` carry the protocol, the cases and their
+failures ([50](50-machine-output.md)); `--print-launch` states the protocol
+for a debugger that starts the program.
+
+## Writing a test
+
+```bash
+mcpp test --guide # the frameworks, their guides, and the commands for cases
+```
+
+A framework states its guide to writing tests on its entry, `test-guide = ""`
+(2026.11.8.1+; the protocol's P2.5): a file of its package, read from the installed
+package and of its version. `mcpp test --guide` names each test framework of the
+package with its guide and repository, and the commands that list, select and
+explain cases; with none, it states the contract above and how to take mtest.
+A failed run ends with `how these tests are written: mcpp test --guide (...)`,
+and a record of `--message-format json` carries `guide`. A person or an agent
+about to write a test, or reading why one failed, starts there.
+
## Reporting to a program
```bash
@@ -258,9 +376,10 @@ runner, the environment the run adds and the arguments after `--` are in it.
## Current limitations
-- A test is one `.cpp` producing one program. mcpp does not discover cases
- inside a file, so a framework's per-case selection happens inside the program,
- through arguments after `--`.
+- A test is one `.cpp` producing one program. The cases inside it are the
+ program's: mcpp lists and selects them only for a program that implements
+ the test protocol; for any other, a framework's selection happens inside the
+ program, through arguments after `--`.
- `mcpp test --list` over a manifest that does not load lists
`tests/**/*.cpp`, not the `[test] discover` set it cannot read.
- `--build-timeout` is POSIX-only.
diff --git a/docs/09-commands-by-scenario.md b/docs/09-commands-by-scenario.md
index 371c4fb66..91e375f63 100644
--- a/docs/09-commands-by-scenario.md
+++ b/docs/09-commands-by-scenario.md
@@ -99,7 +99,11 @@ default 30, `0` turns it off) is removed. A hit marks an entry used. A `path`
host tool's entry is replaced when its tree changes: the entry built from the
same tree under the previous stamp is removed as the new one is written
(2026.10.31.3+). Before a build, mcpp warns when the file system it writes to
-has less than 2 GiB free.
+has less free space than the larger of 2 GiB and 1.25 times what the
+configuration's last build wrote (recorded in `