# Issue 分析报告 —— #254 / #256 / #257 / #258 / #259 / #261

> 日期:2026-07-22
> 基线:HEAD = `af25d18`(mcpp **0.0.101**),工作树 clean
> 方法:每条 issue 的断言都回到 HEAD 源码逐行核对;#256、#257 的关键行为在本机用 mcpp 自带工具链(llvm 20.1.7 / 22.1.8 / gcc 16.1.0 / 系统 clang 18.1.3)**实证**,不依赖 issue 正文的转述。
> 架构判据沿用批次总账 §5.2:**一数据模型、多文法、单一收敛漏斗;同一决策不许两处推导**;外加一条本批次凸显的:**失败必须响,不许静默降级**。

---

## 0. 结论速览

| # | 主题 | 裁决 | 断言准确度 | 归属 | 优先级 |
|---|------|------|-----------|------|--------|
| **#261** | windows scan-deps `cmd /c` 撞 8191 | **真 bug(构造性必现)** | 全部属实 | mcpp | **P0** |
| **#257** | clang 路径 purview `#include` 不触发重建 | **真 bug(correctness,静默错误结果)** | 全部属实,且是代码里已登记的 follow-up | mcpp | **P0** |
| **#259** | llvm 装完 exec 127 | **症状真,归因错**;顺带暴露 3 个 mcpp 侧真 bug | 现象属实,**根因假设被证伪** | 需重定向(见 §5) | **P1** |
| **#258** | `[target.'cfg(os)'.build].flags` | **合理的特性请求**,且内含 1 个真 bug(键被静默丢弃) | 全部属实 | mcpp | **P1** |
| **#254** | per-OS 段按宿主而非 target 拼接 | **真 bug(设计层不自洽)**,作者已在 #253 设计文档 §2.3 自记 | 全部属实 | mcpp | **P2** |
| **#256** | clang 20/22 模块 BMI 崩溃 | **上游 clang 回归,非 mcpp 缺陷**;三条 ask 均合理 | **本机完整复现** | LLVM 上游 + mcpp 文档/CI | **P2**(canary P1) |

一句话总览:**六条里五条成立,一条(#259)方向要改**;而且四条(#257/#258/#259 + #261 的 `cmd /c`)共享同一条失效模式——**引擎在遇到自己没准备好的情况时选择静默降级**,详见 §7。

---

## 1. #261 —— windows scan-deps `cmd /c` 撞 8191 字符 **【真 bug,P0】**

### 1.1 核实

`src/build/ninja_backend.cppm:646-677` 的 `cxx_scan` 规则三分支:

```cpp
if (msvcDeps) {                          // /scanDependencies $out —— 文件参数,无重定向
} else if (plan.scanDepsPath.empty()) {  // GCC:-fdeps-file=$out —— 文件参数,无重定向
} else {                                 // Clang:clang-scan-deps
    if constexpr (mcpp::platform::is_windows) {
        append("  command = cmd /c \"$scan_deps -format=p1689 -- "
               "$cxx $local_includes $cxxflags $unit_cxxflags -c $in -o $compile_target > $out\"\n");
    } else { ... }
}
```

断言逐条成立:

- `cmd /c` 包裹确实存在,且**全仓仅此一处**发射到 ninja 规则(`:299` 的另一处是 `compile_commands.json` 过滤器的字符串前缀判断,非规则)。
- 触发面精确:**windows × clang 工具链 × dyndep 开启 × `clang-scan-deps` 存在于 `clang++` 旁**。GCC(`-fdeps-file=$out`)与 MSVC(`/scanDependencies $out`)因为把输出写成**参数**而不是 shell 重定向,天然不需要 `cmd /c`——这本身就说明重定向是可以避免的。
- `-o` 可用性我在本机实证(不是查文档):

  ```
  $ clang-scan-deps --help | grep '^  -o'
    -o <value>             Destination of the primary output      # 20.1.7 与 22.1.8 均有
  $ clang-scan-deps -format=p1689 -o out.ddi -- clang++ -std=c++23 -c m.cppm -o m.o
  rc=0  size=488   → 合法 P1689 JSON
  ```

  故 issue 提议的替换**功能等价且已验证**。仓库无任何最低 LLVM 版本闸(`src/toolchain/clang.cppm:246-252` 的 `find_scan_deps` 只做路径探测,不查版本;`provider.cppm:88-93` 直接 `has_scan_deps = true`),实际下限就是自带工具链(20.1.7),`-o` 从 LLVM 17 起存在,兼容面无风险。

### 1.2 架构判断:issue 的修法对,但只对了一半

`cmd /c` 是**放大器**,不是唯一病灶。长度来源是 `local_include_flags()`(`ninja_backend.cppm:102-127`)每依赖一个 `-I`,无上界。去掉 `cmd /c` 把天花板从 8191 抬到 32767 —— 4 倍余量,对 48 个 `-I` 够,但对下一个 ffmpeg/opencv 级包不一定够。而且 `cxx_object` / `cxx_module` / `c_object` 在 windows 上**同样没有任何长度兜底**(rspfile 目前只在链接三规则上,`:602` `useRsp = separateLinker || is_windows`,#247 引入)。今天 scan 先炸,只是因为它的命令行 = 编译命令行 + `$scan_deps -format=p1689 -- ` 前缀 + `> $out` 后缀,**严格更长**。

工业界基准很清楚:CMake/Ninja 生成器对**所有**可能超长的规则统一走 response file(`RULE_LAUNCH_*` / `CMAKE_NINJA_FORCE_RESPONSE_FILE`),而不是逐规则救火。仓库自己在 #247 已经建立了这个先例并写下了理由注释(`:594-602`)——只是没推广到编译/扫描边。

### 1.3 建议

- **P0(本周)**:合并两分支为 `-o $out`,消灭全仓唯一的 `cmd /c`。改动 8 行,`cxx_scan` 的 POSIX 命令文本也一并简化(去掉 `> $out`),两平台同形——符合"单一收敛漏斗"。
- **P1(随后)**:把 `useRsp` 的判据从"链接规则"上移为"windows 上所有含 `$local_includes` 的规则",`cxx_scan`/`cxx_module`/`cxx_object`/`c_object` 统一 `rspfile`。注意 #247 的教训(rsp 内容按 GNU 文法分词,反斜杠是转义符 → 节点名必须 `generic_string()`)已经在 `link_rule` 里踩过,复用即可。
- 单测:`tests/unit/test_ninja_backend.cpp:516-519` 已锁链接规则的 rsp 形状,照抄一条锁 `cxx_scan` 无 `cmd /c`。

---

## 2. #257 —— clang 路径 purview `#include` 不触发模块接口重建 **【真 bug,P0】**

### 2.1 核实:这是代码里**明写的已知缺口**

`src/build/ninja_backend.cppm:496-502`:

```cpp
const bool posixDepfile = !msvcDeps && !mcpp::platform::is_windows
    && plan.toolchain.compiler == mcpp::toolchain::CompilerId::GCC;
```

于是 clang 上 `mmd_flag` / `mmd_filter` 皆空,`append_cxx_deps()` 落到 `append_deps()` 而后者对非 MSVC 什么都不发 → **`cxx_module` 与 `cxx_object` 在 clang 上完全没有 depfile**。`.inc` 对整个构建图不可见。

这个闸不是疏忽,是 0.0.97 主动加的。`78bb1c9` "fix(build): scope #235 compile-edge depfile to GCC" 的提交信息与 `:486-495` 的注释都写着:

> Clang's module system (.pcm) does not emit those and its module-TU depfile shape is **different**; applying the GCC-shaped filter there is **untested** and could yield a wrong dep set. Until Clang is verified, scope this to GCC —— header/purview rebuild tracking on Clang is a **follow-up**.

`tests/e2e/118_purview_include_rebuild.sh:2` 是 `# requires: gcc`,所以 clang 腿零覆盖。scanner / p1689 都不回读 textual include(`p1689.cppm:355` 生成 `.dep` 却从不解析),也不存在任何源文件内容哈希/mtime 兜底——**除 depfile 外没有第二道防线**。

### 2.2 关键新证据:那个"形状不同"的顾虑,实测不成立

当年 gate 掉的唯一理由是"clang 的 module-TU depfile 形状未知,GCC 的 awk 过滤器套上去可能出错"。我在本机把两边的形状打出来了:

```
$ clang++ -std=c++23 -fmodule-output=x.pcm -MMD -MF x.d -c x.cppm -o x.o   # 20.1.7 与 22.1.8 同
x.o: x.cppm ops.inc                       ← 干净的单条 make rule,ninja 直接可吃

$ g++ -std=c++23 -fmodules -MMD -MF x.d -c x.cppm -o x.o                    # gcc 16.1.0
x.o gcm.cache/x.gcm: x.cppm ops.inc
x.c++-module: gcm.cache/x.gcm             ← 正是 awk 过滤器要剥掉的 "reversed rules"
.PHONY: x.c++-module
gcm.cache/x.gcm:| x.o
```

结论:**clang 根本不产生需要过滤的东西**。正确修法不是"把 GCC 的过滤器移植到 clang",而是把两件事拆开——

- `posixDepfile`(是否发 `-MMD` + `deps = gcc` + `depfile`)→ 放开到 **GCC ∪ Clang**;
- `mmd_filter`(awk 剥离 reversed rules)→ **保持 GCC-only**。

代码上就是把 `:496-502` 的单一 bool 拆成两个谓词。风险极低,且 e2e 118 的 `# requires: gcc` 可以直接删掉,变成两工具链共同覆盖。

### 2.3 严重性:被 issue 低估了

issue 说"Harmless for CI (cold builds) but dangerous locally"。我认为要往上提一档:

- 失效模式是**静默给出错误产物**,不是报错。构建系统的失效等级里,silent staleness 是最差的一档——它把编译期错误延后成运行期错误,而且怀疑对象总是先落到用户代码上。issue 自己就记录了它已经造成了一次误诊(在 #256 的 clang 调试过程中)。
- 影响面不止 `.inc`:任何 purview 里的 `#include`(含 `module;` 之后再包的私有头)、任何被模块接口文本包含的生成头,在 clang 上都不重建。而 clang 恰是 macOS 默认腿与 windows 托管腿的编译器。
- 从"依赖跟踪必须完备"这条构建系统第一原则看,当前状态是 mcpp 在 clang 上**不满足最小正确性契约**。0.0.97 当时的取舍(不 regress、留 follow-up)在发布压力下合理,但已经拖了 4 个版本,而实测表明代价只有几行。

---

## 3. #258 —— `[target.'cfg(os)'.build]` 缺 per-glob `flags` **【合理的特性请求 + 1 个真 bug,P1】**

### 3.1 核实

| 断言 | 结论 |
|---|---|
| `ConditionalConfig` 无 globFlags 字段 | 属实,`types.cppm:276-292` 只有 cflags/cxxflags/ldflags/sources/三类 deps |
| `[target.<pred>.build]` 只读四个键 | 属实,`toml.cppm:976-987` 四次 `read_list` |
| `merge_conditional_sources_flags` 不碰 globFlags | 属实,`prepare.cppm:429-441`,加一行 `insert` 即可 |
| xpkg 描述符**有**per-OS `flags` | 属实,机制是 `xpkg.cppm:888-894` 的**文本体拼接**——OS 段体追加到 base 体后跑同一解析循环,所以 `flags` 连同其余顶层键"免费"支持 per-OS,且 OS 条目自然排在 base 之后(last-wins 顺序天然正确) |
| 死 glob 告警按**构建的源集**判定,不看文件系统 | 属实,`globFlagHits` 只在 `apply_glob_flags` 内自增,而它只被 `all_files` 循环调用(`scanner.cppm:900/919`,`all_files` 由 `modules.sources` 展开)→ issue 评论里"磁盘上放锚点文件无效"的实测结论有代码依据 |
| `features.<n>.flags` 在 mcpp.toml 里全局-only | 属实,`toml.cppm:320-327` 在顶层 `[features]` 里;`[target.*]` 没有 `features` 子键 → **OS 轴 × feature 轴在 mcpp.toml 里不可组合** |
| 无 `optional` / 允许零匹配的逃生口 | 属实,且两个解析器对未知键都**硬错**,所以今天写 `optional = true` 是 parse error 而非被忽略 |

### 3.2 顺带发现的真 bug:`flags` 在条件段里被**静默丢弃**

`toml.cppm:976-987` 的读法是"认得的四个键各读一次",**没有未知键拒绝**。作者在 `[target.'cfg(windows)'.build]` 里写 `flags = [{...}]`,今天的行为是:解析成功、零告警、规则凭空消失、构建出一个 flags 缺失的产物。

这与本仓已确立的纪律直接冲突:xpkg 未知 feature 子键进 `xpkgUnknownKeys` 报 did-you-mean(0.0.100)、`[build].flags` 条目未知子键硬错、`scan_overrides` 零命中硬错。条件段是这条纪律的一个**破口**。即便 #258 的特性最终不做,这个静默吞键也该单独修(#227 封闭文法 allowlist 应覆盖 `target.*.build.*`)。

### 3.3 架构判断:请求成立,而且是补齐既有原则而非新增机制

- **文法平价**是本仓写在设计文档里的原则("一数据模型两文法")。今天的实况是 xpkg 描述符能表达、mcpp.toml 不能——即"manifest 包是二等公民"这个论证站得住。而且不对称的成因是**偶然**(xpkg 靠文本拼接免费获得,toml 走结构化解析所以要逐键实现),不是有意的能力分级。
- **removal 论证成立**。`-U` 之所以救不了,是因为它自身也需要 OS 条件化,递归回同一缺口;issue 正文对此的推理正确。而 `merge_conditional_sources_flags` 的 append 位置天然给出 last-wins 覆盖权,这正是表达"windows 上撤销 unix 的 `-DHAVE_UNISTD_H`"所需要的。
- **实施成本**:issue 给的 4 处清单准确无误,四处全部复用既有代码(`parse_glob_flags_value` 的签名 `(value, ctx, std::vector<GlobFlags>& dst)` 可直接指向 `cc.globFlags`;下游 scanner/backend/fingerprint 零改动,因为 0.0.101 已经把 globFlags 全量序列化进 per-package fingerprint)。唯一要盯的顺序不变量:merge 必须在 `makePackageRoot` 快照 `buildConfig` 之前(`prepare.cppm:900-909` 已有注释)——三个调用点 `:912 / :1871 / :2762` 都在其前。
- **对 703 个 stub 文件的评估**:这不是"包作者偷懒",是**引擎缺表达力把语义泄漏成了文件系统结构**。0.0.97 刚删掉过同类的 #233-era tu-stub 机制,现在因为另一个缺口在下游重新长出来——这是判断该做的强信号。

### 3.4 对 stopgap `optional = true` 的反对意见

issue 评论里提议的 `optional=true` / `?`-前缀 我**不建议做**,理由是它会引入**第二套抑制机制**,与 #253 刚确立的路线("条目按条件*存在*,所以无从告警")相悖——那正是同一决策两处推导。#253 已经证明了正确形状:让规则在不该生效的维度上**不进入 `globFlags`**。OS 轴照抄即可,不需要新语义。如果 #258 主体要拖,宁可先只做 §3.2 的静默吞键修复(半天工作量,且能让作者立刻发现自己写的 `flags` 没生效)。

---

## 4. #254 —— per-OS 段按宿主拼接而非 resolved target **【真 bug,P2】**

### 4.1 核实

两轴不自洽,逐条属实:

- `platform/common.cppm:95-105` 的 `xpkg_platform` 是 `constexpr` + `#if defined(_WIN32)/__APPLE__/__linux__` —— **mcpp 自己被编译时的宿主**,无任何运行期输入。
- `xpkg.cppm:888-889` `osKey = osOverride.empty() ? xpkg_platform : osOverride`。`osOverride` 的**全部**非测试调用点:`prepare.cppm:1589 / 1637 / 1814` 三个**构建路径**全部走默认(宿主),唯一传 override 的是 lint 命令 `mcpp xpkg parse --all-os`(`cmd_xpkg.cppm:132`)。`overrides.target_triple` 从未穿进来。
- `cfgpred::context_for`(`prepare.cppm:76-107`)注释明写 "Context = the RESOLVED target's coordinates",三个调用点(`:911/:1872/:2763`)都传 `overrides.target_triple`。
- 影响面比 issue 列的还宽一格:`xpm` 版本/资产表也走宿主键(`xpkg.cppm:711-772` 的 `list_xpkg_versions(luaContent, platform)`,`pm/resolver.cppm:108` 传 `kXpkgPlatform = xpkg_platform`),即**交叉编译时连"有哪些版本可选/下哪个 tarball"都按宿主判定**,连错误文案都把宿主平台名硬写进去。
- 交叉编译不是纸面能力:`tests/e2e/102_mingw_cross_wine.sh`、`112_build_mcpp_cross.sh` 真跑 `--target x86_64-windows-gnu` 并用 wine 执行。**但**没有一条 cross e2e 使用带 per-OS `mcpp` 段的 xpkg 依赖 → 缺口对 CI 完全不可见。

### 4.2 架构判断

这是教科书级的"**同一决策两处推导**":平台选择这一个决策,在 xpkg 通道按宿主编译期常量推,在 manifest 通道按 resolved target 运行期推。native 构建下 host == target 使两者巧合一致,于是缺陷被三平台 CI 完全掩盖——**恰恰是 0.0.99 复盘里记下的那类隐性架构债**(加新语义会变构建失败:#253 刚给 per-OS 段加了 `features`,这个错误轴上又多挂了一项)。

作者的处理方式是对的:在 #253 设计文档 §2.3 记录、明确不入该 PR、拆成独立 issue 并回引设计节。这是应该保持的工作方式。

### 4.3 建议(比 issue 的方向再收紧一步)

issue 说"pm/install 侧需单独辨析",正确但太软。建议直接上**类型层**约束,否则这类 bug 会反复:

- 引入两个不可互换的类型(如 `HostPlatform` / `TargetPlatform`,或一个带 `enum class PlatformAxis { Host, Target }` 的强类型 wrapper),让 `synthesize_from_xpkg_lua` / `list_xpkg_versions` 在签名上**必须**声明自己要哪一轴。今天二者都收 `std::string_view`,混用没有任何编译期阻力。
- 分类结论(已核实,可直接照用):`prepare.cppm:1589/1637/1814` 三处 = 编进用户构建的**源码库包** → **target**;`toolchain/lifecycle.cppm:135` = **宿主工具**(且注释已写明为何要 host)→ 保持 **host**;`pm/resolver.cppm:108` = 用户项目的库依赖 → **target**;`cmd_xpkg.cppm` = lint,遍历全 OS。
- 锁定:e2e 加一条"host == target 时拼接结果逐字节不变"的回归,再加一条 mingw-cross 腿消费带 per-OS 段的 xpkg dep。

优先级 P2 合理——今天没有真实用户被咬(交叉编译尚未与 per-OS xpkg 依赖组合使用),但它是**能力增长的路障**:每加一个 per-OS 键,错误面就扩大一次。

---

## 5. #259 —— llvm 装完 exec 127 **【症状真,根因假设被证伪 → 需重定向,P1】**

### 5.1 现象属实

本机 ELF 证据与 issue 完全一致:

```
xim-x-llvm/22.1.8/bin/clang++ → interpreter …/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2
RUNPATH …/xim-x-llvm/22.1.8/lib : …/xim-x-glibc/2.39/lib64 : …/xim-x-zlib/1.3.1/lib : …
```

解释器路径是**绝对的、机器本地的、安装时写入的**(不在发布 tarball 里)。glibc payload 缺失 ⇒ 所有 clang 二进制 exec 127。到这里都对。

### 5.2 但归因错了

issue 的判断是"looks like a **missing runtime dependency in the llvm xim package**"。核对索引:

```lua
-- xim-pkgindex pkgs/l/llvm.lua:25-31
xpm = { linux = {
    deps = { "xim:glibc@2.39", "xim:linux-headers@5.11.1", "xim:zlib@1.3.1", "xim:libxml2@2.13.5" },
```

**llvm 声明了 glibc**,自 `8e432d99`(#182,首次加 Linux x86_64 支持)起从未变过,与 gcc.lua 的声明形态一致。三份本地 checkout 全同。而且 llvm.lua 自己的 install hook 在 glibc 缺失时是**故意 fail-loud** 的(`:234-239` `log.error("glibc payload not found … refusing to write a host-dependent clang cfg"); return false`)。

所以"llvm 包漏声明 glibc"不成立。真正的问题一定在**声明有、实际没装上,而且没人喊**这条链上。目前证据指向三个 mcpp 侧的静默点:

**(a) 依赖安装结果被丢弃** —— `toolchain/lifecycle.cppm:554-568`:

```cpp
for (auto dep : {"xim:glibc", "xim:linux-headers"}) {
    auto depPayload = fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress);
    mcpp::log::debug("toolchain", std::format("dep {} result: {}", dep, depPayload ? "ok" : ...));
}
```

结果只进 **debug 日志**,不检查、不中断。裸 runner 上 glibc 安装失败,流程原样继续去装 llvm。

**(b) llvm 的 post-install fixup 在 glibc 缺失时静默跳过,还把这个状态记成"成功"** —— `post_install.cppm:364-390`:`if (!glibcLibDir.empty() && exists(patchelfBin)) { … }`,glibc 找不到时整段跳过且**一句告警都没有**(gcc 路径至少有 `:358-361` 的 warning)。更糟的是随后 `fixup_clang_cfg`(`:249-284`)会把 `bin/` 下每个 `*.cfg` 重写成**省略 glibc `-B/-L/--dynamic-linker/-isystem` 的版本**——正好覆盖掉 llvm.lua 本来会拒绝写、且会 fail-loud 的那份 cfg;然后幂等标记 `.mcpp-fixup.json` 把 `glibcLib: ""` 记录为一次**成功**的 fixup,后续重装也不会重试。

**(c) 装完不验活** —— `lifecycle.cppm:578-585` 只 `std::filesystem::exists(bin)`。全仓唯一的 `--version` 调用在**构建期** `detect.cppm:56`,它把 127 报成 `unrecognized compiler output`。`mcpp doctor`(`doctor.cppm:239-313`)会 `readelf -d` 查 **RUNPATH** 缺目录,但**从不查 PT_INTERP**(全仓 `PT_INTERP`/`interpreter` 只出现在 `pack.cppm`/`linkmodel.cppm`),而 PT_INTERP 才是致命轴;它给出的提示文案还是"其提供方 xim 包可能被删除了",在"从未装过"的场景下正好误导。

### 5.3 建议

1. **把 issue 重定向**:标题/正文改成"llvm 安装链在 glibc 运行时缺失时静默降级并记为成功",把"llvm 包漏声明 glibc"的假设撤回(附上 `llvm.lua:25-31` 的证据),同时在 xlings/xim 侧另开一条查"声明了却没装上"的实际原因(bare runner 上 `xlings install` 的 dep 解析是否被跳过/失败)。
2. **mcpp 侧三处照修**(与归因无关,无论 xim 侧结论如何都该做):
   - `lifecycle.cppm:562-567` 检查 dep 结果,失败即 hard error;
   - `llvm_post_install_fixup` 在 Linux 且 `glibcLibDir` 为空时告警或硬失败,**且不写 fixup marker**(对齐 gcc 路径与 llvm.lua 自身的 fail-loud 策略);
   - 安装末尾加 `<frontend> --version` 冒烟探针,127 直接报"glibc payload missing";`doctor` 补 PT_INTERP 存在性检查。
3. 现有 workaround("先装 gcc")继续有效,但它掩盖问题,应在修好后从 opencv-m CI 撤掉。

这条 issue 的价值其实比作者以为的更高:它暴露的不是一个包的元数据错误,而是**安装路径上一条完整的静默降级链**——三个环节各自"宽容"地放过,合起来产出一个看起来装好了、实际不能 exec 的工具链。

---

## 6. #256 —— clang 20/22 模块 BMI 里的 replacement-operator 模板毒化名字查找 **【上游 clang 回归,非 mcpp bug,P2;canary P1】**

### 6.1 本机完整复现(不是转述)

用 issue 正文的 3 文件 repro,逐一跑本机四个编译器:

| 编译器 | 带 4 参 `Matx*Matx` 模板 | 去掉该模板(对照) |
|---|---|---|
| clang 20.1.7(mcpp 工具链) | **崩溃** rc=1,`PLEASE submit a bug report` | ok rc=0 |
| clang 22.1.8(mcpp 工具链) | **崩溃** rc=1,同上 | ok rc=0 |
| clang 18.1.3(Ubuntu 系统) | ok rc=0 | — |
| gcc 16.1.0(mcpp 工具链) | ok rc=0 | — |

崩溃点与 issue 一致:`use.cpp:2:46` 解析 `main` 体内 `p * 2` 时前端崩,而 `--precompile` 生成 BMI 那步 rc=0。issue 的控制矩阵**逐格复现**,回归窗口 clang 18 → 20 成立。

### 6.2 判断

- **不是 mcpp 的缺陷**。mcpp 只是把标准 C++23 模块命令行交给 clang;`--precompile` 成功、消费端崩溃,完全在编译器内部。任何构建系统(CMake、Bazel、build2)在同样输入下都会看到同样的崩溃。
- **是 mcpp 的暴露面**。issue 指出的"模块包模式(镜像上游头里的 `static inline` 运算符模板 + 平凡真约束,做混合 TU subsumption)会直撞这个形状"是准确的架构观察——这个模式正是 mcpp 生态推荐的 header→module 封装配方,所以对本项目用户的命中率远高于一般 C++ 项目。
- 三条 ask 全部合理,但优先级应重排:
  1. **上游报告优先**(ask 2)。repro 只有 40 行、回归窗口干净(18 ok / 20+22 crash),是一份高质量的 LLVM issue。建议尽快提,并把编号回链本 issue。
  2. **e2e canary(ask 3)升到 P1**。理由:这是**沉默的能力边界**——没有 canary,未来任一次 clang bump 修好或再弄坏,mcpp 都不会知道;而 opencv-m 的经历表明发现成本是"一整轮 bisect + 一次误诊"。canary 很便宜:把 §6.1 的 3 个文件塞进 `tests/e2e/`,`# requires: clang`,断言"要么编过、要么给出可识别的诊断",崩溃即红。
  3. **文档(ask 1)**:写进模块包指南,一句可操作的规则——"运算符模板的所有模板参数应由第一个实参绑定;否则改为对整个推导操作数类型建模"。作者已经在 opencv-m `42aeb20` 里给出了可直接复制的改写范式,值得原样收进文档。
- 一个**不建议**做的方向:让 mcpp 检测子进程 139/SIGSEGV 并给专门提示。信噪比太低(前端崩溃原因千百种),canary + 文档已经覆盖了实际收益。

---

## 7. 横向观察

### 7.1 主旋律:静默降级

本批次六条里有四条的实质是**引擎遇到没准备好的情况时,选择安静地少做一点事**:

| # | 静默点 | 用户看到的 |
|---|---|---|
| #257 | clang 上不发 depfile | 构建"成功",产物用了陈旧 BMI |
| #258 | `[target.*.build]` 里的 `flags` 被丢弃 | 构建"成功",flags 从未生效 |
| #259(a) | dep 安装结果只进 debug 日志 | 安装"成功" |
| #259(b) | glibc 缺失时跳过 patchelf 并记 marker 为成功 | 安装"成功",二进制不能 exec |

而本仓在**别的**地方已经把"不许静默"做得很好:xpkg 未知键 → `xpkgUnknownKeys` + did-you-mean;`scan_overrides` 零命中 → 硬错;死 glob → 告警;#253 还专门为告警加了归属点名。也就是说,**纪律存在,只是没有被提升为跨模块不变量**。

建议把它写成一条明文架构约束(放进批次总账 §5):*任何"因为条件不满足而少做一步"的分支,必须要么报错,要么发出用户可见的告警;debug 日志不算。* 并在 code review checklist 里加一条对 `if (x.empty()) return;` / 静默 `continue` 的审视。这条约束若在 0.0.97 就存在,#257 的 gate 会带一条 clang 上的 warning,#259 的两次静默都会响。

### 7.2 CI 矩阵的盲区形状

三条 bug 的可见性条件高度相似,都是"**只在某一条腿上出现,而那条腿恰好没覆盖**":

- #261:windows × clang × 长路径(自有 CI 路径短 → 绿;作为依赖被消费 → 炸)
- #257:非 GCC(e2e 118 明写 `# requires: gcc`)
- #254:host ≠ target(cross e2e 存在,但不消费带 per-OS 段的 xpkg dep)
- #256:clang × 模块 × 该模板形状(opencv-m 的 gtest 套件此前从未用 clang 构建过)

共同点:**能力维度的笛卡尔积没有被系统地枚举**。建议做一次一次性的覆盖矩阵盘点(工具链 × OS × host/target × 依赖形态 × 消费深度),把空格子显式记录为"已知未覆盖",而不是默认绿即安全。#261 尤其值得注意:它在**包自己的 CI 上是全绿的**,只有作为依赖被深路径消费时才现形——即"消费者视角"这一整维在 CI 里缺席。

### 7.3 文法平价原则的破口

#258 与 #254 都指向同一处结构性差异:xpkg 描述符靠**文本体拼接**获得能力(任何顶层键自动 per-OS 化),mcpp.toml 靠**结构化逐键解析**,于是每加一种条件维度都要手工补一遍。这解释了为什么不对称是偶然的而非有意的,也预示了它会继续复发(下一个 per-OS 键、下一个 cfg 维度)。

值得考虑的中期收敛:让 `[target.<sel>]` 的 build 段与顶层 `[build]` 段**共用同一个解析器**(把 `ConditionalConfig` 的 build 部分换成一个完整的 `BuildConfig`,解析后按谓词合并),而不是维护一份"条件段支持哪几个键"的手工子集。那样 #258 就不再是一个需要实现的特性,而是自动成立的性质;#227 的封闭文法 allowlist 也会自动覆盖条件段,§3.2 的静默吞键随之消失。

---

## 8. 建议的处置顺序

| 顺位 | 动作 | 规模 | 依据 |
|---|---|---|---|
| 1 | #261 去 `cmd /c`,改 `-o $out`,两平台合一分支 | ~8 行 | 构造性必现,已阻塞真实 CI |
| 2 | #257 拆 `posixDepfile` / `mmd_filter` 两谓词,depfile 放开到 Clang;删 e2e 118 的 `# requires: gcc` | ~10 行 + 测试 | 正确性;实测证明当年的顾虑不成立 |
| 3 | #259 mcpp 侧三处静默修复 + issue 重定向 | 中 | 静默降级链;归因需先纠正 |
| 4 | #258 四处实施(含 §3.2 的静默吞键修复) | 中 | 解锁 opencv-m windows 腿,消除 703 stub 文件 |
| 5 | #256 上游报告 + e2e canary + 文档一节 | 小 | mcpp 侧只需 canary 与文档 |
| 6 | #261 P1:rspfile 推广到 windows 全部含 `$local_includes` 的规则 | 中 | 根治长度轴,不再逐规则救火 |
| 7 | #254 host/target 轴收敛(建议同时上强类型) | 中大 | 需独立设计 pass;先做类型再改行为 |

1、2 两条合起来不到 20 行,建议合成一个 PR 立刻发;3、4 各自一个 PR;7 单独走设计文档。

---

## 附:核实手段说明

- 源码断言:全部在 HEAD(`af25d18`)上按 file:line 逐条回读,未采信 issue 正文的行号转述。
- #256:本机跑通完整 4×2 控制矩阵(llvm 20.1.7 / 22.1.8 / 系统 clang 18.1.3 / gcc 16.1.0 × 有/无 4 参模板)。
- #257:本机对比 clang 与 gcc 的 module-TU depfile 实际字节形状,推翻"clang 形状需要过滤"的原假设。
- #261:本机验证 `clang-scan-deps -o` 在两个自带 LLVM 上均存在且产出合法 P1689 JSON。
- #259:本机 `file -L` / `readelf -d` 核 PT_INTERP 与 RUNPATH;三份本地 xim-pkgindex checkout 交叉核对 `llvm.lua` 的 deps 声明与其 git 历史。
- 未做:在 windows 上实测 8191 截断(无该平台);未实测裸 runner 上 glibc 安装为何失败(需 CI 环境)。
