Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
416 changes: 416 additions & 0 deletions .agents/docs/2026-07-19-mcpp-replace-xmake-research.md

Large diffs are not rendered by default.

262 changes: 262 additions & 0 deletions .agents/docs/2026-07-20-mcpp-provider-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# d2mcpp Provider 与练习脚手架 参考

- 日期:2026-07-20
- 分支:`feat/mcpp-provider`
- 配套调研:[`2026-07-19-mcpp-replace-xmake-research.md`](2026-07-19-mcpp-replace-xmake-research.md)(xmake → mcpp 的可行性论证与 rustlings/cargo 横向对照)
- d2x 侧协议规范:d2x 仓库 `.agents/docs/2026-07-20-d2x-architecture-reference.md`
- 本文定位:**当前实现的参考手册**——目录约定、生成物、判定机制、维护须知

---

## 1. 全景

xmake 已完全退役。构建链路是:

```
d2x(通用框架,零内置后端)
└─ Provider Protocol (NDJSON) ─→ d2x/buildtools/mcpp/ ← 本仓库自己的 Provider(C++26)
├─ 目录约定发现练习
├─ 生成 mcpp workspace 清单
├─ 调 mcpp build/run
└─ 读侧信道判定通过
mcpp(未做任何改动)
```

### 提交进仓库的包

```
mcpp.toml [workspace] members = ["d2x/buildtools/mcpp", "dslings/harness"]
d2x/buildtools/mcpp/ Provider,standard = "c++26"
dslings/harness/ 练习脚手架库,standard = "c++23"
```

### 生成物(gitignore)

```
.d2x/build/mcpp.toml 独立 workspace 根
.d2x/build/<std>/mcpp.toml 该标准下全部 target —— 供 clangd
.d2x/build/_current/mcpp.toml 只含当前一题 —— 供 checker
.d2x/build/_current/result.ndjson 判定用侧信道
.d2x/state.json 学员进度(d2x 写)
```

---

## 2. 引导:零脚本、Windows 安全

`.d2x.json`:

```json
{ "buildtools": "mcpp run -q -p d2x/buildtools/mcpp --" }
```

d2x 拼接后得到 `mcpp run -q -p d2x/buildtools/mcpp -- check <id>`。**从仓库根执行、无需 cd、首次自动构建 Provider**,暖开销约 26ms。

d2x 从不 `chdir`(它在静态初始化时捕获 CWD),所以免 `cd` 是硬要求。注意 `mcpp` 的 `-p` 匹配的是**目录 basename 或完整相对路径**,不是包名。

### ⚠️ 嵌套 mcpp 与 `LD_LIBRARY_PATH`

`mcpp run` 会把 `LD_LIBRARY_PATH` 指向它私有的 glibc 并注入子进程。Provider 再去 spawn 嵌套的 `mcpp` 时被迫加载错配的 glibc,**在动态链接器里段错误**(冷启动稳定复现,输出只剩 `<pid>:\t__vdso_time` 这样的 trace 残片)。

`runner.cppm` 在每次 spawn 前 `unsetenv("LD_LIBRARY_PATH")` 绕过;mcpp 会为它自己的子进程重新设置正确的值。

d2x 侧对同一问题早有相同处理(`platform.cppm`),仓库里还留着 `workaround_ld_library_path_issue` 分支——**这是 mcpp 的既有问题,值得单独向上游报**。

---

## 3. 练习发现:目录约定 + 就近指令

**没有声明文件。** 放一个 `.cpp` 进去就是全部工作。

rustlings 最贵的一课是 PR #1355:Rust edition 同时写在 `rustc` 参数和 `rust-project.json` 里,两边漂移酿成 bug。**任何独立声明文件都是第二套真相源。** 这里的真相只有两处,且都无法漂移:目录结构,和练习文件自己的头部注释。

### 目录布局

```
dslings/hello-mcpp.cpp 入门练习(chapter = intro)
dslings/<std>/NN-topic-K.cpp zh 练习
dslings/en/<std>/NN-topic-K.cpp en 练习(lang=en 时启用,与 zh 互斥)
solutions/<std>/NN-topic-K.cpp 参考答案(zh/en 共用同一份)
```

### id / order / chapter 的推导

| 文件 | id | chapter | order |
|---|---|---|---|
| `dslings/cpp11/00-auto-and-decltype-0.cpp` | `cpp11-00-auto-and-decltype-0` | `cpp11/00-auto-and-decltype` | 1100000 |
| `dslings/cpp11/03-trailing-return-type.cpp` | `cpp11-03-trailing-return-type` | `cpp11/03-trailing-return-type` | 1100300 |
| `dslings/hello-mcpp.cpp` | `hello-mcpp` | `intro` | -100000 |

`order = std_rank × 100000 + 章节号 × 100 + 练习序号`。尾部 `-<数字>` 是序号,没有就当 0(仓库里 03/04 两章确实缺 `-0` 后缀,规则把它们归一化)。

### 每练习编译选项

写在练习文件自己的头部注释里(只扫前 40 行):

```cpp
// d2x:cxxflags: -O0 -fno-elide-constructors
```

仓库现存用例:`04-rvalue-references`(观察移动)、`07-constexpr-0`(`-Wpedantic -Werror` 让 VLA 成为错误)。

### id 白名单

id 只允许 `[A-Za-z0-9._-]`,违反者**被拒绝并打印到 stderr**,不进入枚举。

因为 id 有两个危险去向:d2x 拼进 shell 命令、我们写进生成的 TOML(`[targets.<id>]`)。在源头挡住比在两个下游各自转义更可靠——这类文件名本就是笔误或恶意 PR,与其想办法安全传递,不如让作者改名。

---

## 4. 双 member 布局

**硬约束:dslings 的练习默认就编译不过**(49 个带 `D2X_YOUR_ANSWER`),而 mcpp 的 `build` 和 `run` 都会先全量构建整个包——一个坏兄弟拖垮全部且零产物。实测:

```
member 含全部 3 题(后两题未做完)→ mcpp build -p → exit=1 ← 当前这题被拖挂
只生成当前这一题 → 0.071s,exit=0
```

**member 粒度(标准/特性/练习)本身不解决逐题隔离。** 所以分两个:

| 目录 | 内容 | 用途 |
|---|---|---|
| `.d2x/build/<std>/` | 该标准下全部 target | clangd —— 即使多数练习编译不过,mcpp 仍生成完整 `compile_commands.json`,IDE 对每题都有正确编译参数 |
| `.d2x/build/_current/` | 只含要验证的那一题 | checker |

实测动态改写清单代价极低:切题 0.118s、切回 0.018s,**fingerprint 目录始终只有 1 个**(改写 target 集合不会让缓存爆炸)。

### 其他生成细节

- `main` 用 `../../../` 逃逸出包根,**练习源文件原地不动**
- 脚手架走 `[dependencies] path` 依赖,而非把仓库根塞进 include 搜索路径(后者会让练习能 `#include` 仓库里任何文件)
- `-fmacro-prefix-map=<repo_root>/=` 让 `__FILE__` 相对仓库根,学员看到的报错不再顶着一长串 `/home/...`
- 所有清单**内容比对后才落盘**,不推进 mtime,保住 mcpp 的快速路径
- `check` 会先 `write_full` 再 `write_current`——全新仓库上学员直接跑 `d2x checker` 时根清单还不存在,否则 mcpp 报退出码 2

---

## 5. 判定机制

### 侧信道,不是扫 stdout

判定原先 100% 是带内信号:断言失败**并不改变退出码**(实测 exit=0),所以只能扫 stdout 找 `❌`。这会误判——一个断言全过的正确解答,只要在说明文字里打了个 `❌` 就被判失败(已复现)。反过来,输出截断或 Windows 控制台代码页不对,判定同样失效。

现在 stdout 回归「给人看」,判定走侧信道:

```jsonc
{"kind":"assert","ok":false,"expr":"a == b","expected":"1","actual":"2",
"file":"dslings/cpp11/xx.cpp","line":33}
{"kind":"wait","file":"dslings/cpp11/xx.cpp","line":40}
```

**逐条追加而非退出时统一写** —— 练习段错误时,崩溃之前的断言结果照样保留。

`D2X_RESULT_FILE` 环境变量未设置时 harness 只打印不写文件:**学员直接跑二进制零摩擦**。

### 判定顺序

| 侧信道内容 | Verdict |
|---|---|
| 有 `ok:false` | `fail` + 每条失败转一个 `Diagnostic` |
| 无失败但退出码非 0 | `fail` |
| 无失败、有 `wait` | `blocked` |
| **文件不存在**(练习没用 harness) | 退回「编译通过 + 退出 0 = `pass`」 |

最后一条让 **harness 自动变成可选的**,不需要额外机制:纯观察型练习(现有 18 个只用 `D2X_WAIT`)可以写成零依赖的纯 C++ 文件,学员能原样拷进 Compiler Explorer。

### 路径:展示 vs 定位

侧信道里的 `file` 因 `-fmacro-prefix-map` 是相对路径(学员看着舒服),但协议要求绝对路径(d2x 靠它开编辑器、监听变更)。**在协议边界上还原成绝对。**

---

## 6. 练习脚手架 `dslings/harness/`

按库设计,目前住在本仓库,将来可整体抽成独立 mcpp 包给其他 C++ 课程复用。

### 两条路径不等价

| 设施 | `#include <d2x/cpp/common.hpp>` | `import d2x.harness;` |
|---|---|---|
| 断言 | `d2x_assert` / `d2x_assert_eq` 宏 | `d2x::check` / `check_eq`,`source_location` 自动带 file/line |
| 路障 | `D2X_WAIT` | `d2x::wait()` |
| 填空占位 | `D2X_YOUR_ANSWER` | **无等价物** |

**宏无法跨模块导出**,这是 C++ 的硬约束而非疏漏。`D2X_YOUR_ANSWER` 必须展开为空才能制造编译错误,本质就是宏。**cpp20/cpp23 等模块化章节需要另设填空约定——这是个尚未决策的缺口。**

`include/` 下保持 `d2x/cpp/common.hpp` 这个既有路径,所以 104 个练习和书本一行都不用改。路径改名可以以后单独做。

### 维护须知

`d2x_assert_eq` 的**所有分支**都必须走 `d2x::detail::show()`,不能用裸 `std::to_string`——后者没有 `std::string` / `const char*` / scoped enum 的重载,一旦有练习比较这些类型就会硬编译失败。(曾经上报分支用了 `show()` 而日志分支没用,是个潜伏的地雷。)

---

## 7. 测试

### `d2x/buildtools/mcpp/tests/e2e.sh`

断言两件事,缺一不可:**每个练习未完成时不通过**、**每个参考答案放进去后通过**。等价于 rustlings 的 `cargo dev check --require-solutions`。

走的是 `d2x checker` 内部同一条 Provider 路径,所以绿灯等价于学员本地能跑通,而非另一条平行路径。

**两道防线:**

1. **防空转**——`pass == 0` 时直接判失败。旧 CI 就是这么绿了很久的:它只挑 `-ref` 目标,而 `solutions/` 早被注释掉,循环一次都没进,job 照样退出 0;第 60 行的 `wc -l` 对空串还打印 "Found 1 reference targets" 把空转掩盖了。同类问题在本脚本自己身上也犯过一次(`${sol#en/}` 剥离顺序错,导致所有英文答案静默 SKIP)。

2. **脏树检查**——练习目录有未提交改动时**拒绝运行**并列出文件。脚本会把参考答案覆盖到练习上再还原,天然会吃掉未提交的改动,这个陷阱咬过两次。

### 当前状态

| 项 | 结果 |
|---|---|
| Provider 枚举 | 52 题(zh:1 + 49 + 2;en 同样 52) |
| 端到端 zh | 51/51 参考答案通过 |
| 端到端 en | 51/51 参考答案通过 |
| d2x session 单测 | 28/28 |
| 事件流 | 纯 JSON,0 污染 |
| 注入防护 | 恶意文件名被拒绝,命令未执行 |

---

## 8. C++ 标准与教学漂移

**所有练习按 `c++23` 编译。** mcpp 目前硬拒 `c++11/14/17/20`(`src/manifest/types.cppm` 的白名单),本方案选择不改 mcpp 上游。因此 `cppNN/` 目录现在表示**特性引入于哪个标准**,不再改变编译参数。

### 已知代价

`04-rvalue-references` 的移动构造教学点。C++17 起 prvalue 直接初始化目标(保证复制省略),`-fno-elide-constructors` 再也无法让 `Object obj = Object();` 产生移动构造。

**修法:** 改成从具名对象 `std::move`(任何标准下都必然调用移动构造),并补一条 `d2x_assert(move_ctor_calls >= 1)`。

**更重要的是那条断言。** 漂移之所以能静默发生,正是因为从前没有任何断言检查它——输出少一行没人发现。现在它是 CI 可检测的事实。

> **教训:教学点如果没有断言覆盖,它的消失就是不可见的。** 新增练习时应问:这一课的核心观测点,有断言在守着吗?

量化过的风险面:51 个参考答案在「原标准 vs c++23」下逐一编译并 diff 运行输出,**50 个完全一致,仅此 1 个漂移**。但随着课程往 cpp17/20 扩写,这类静默漂移只会变多。

---

## 9. 撰稿流程的变化

`.agents/skills/d2mcpp-authoring/` 已同步更新:

- **练习不再需要注册**——放进 `dslings/<std>/` 即被目录约定发现
- 新增 `<std>` 章节只需建目录 + 改两个 `SUMMARY.md`,无构建文件接线
- 每练习编译选项改为文件头部 `// d2x:cxxflags:` 指令
- 验证命令改为 `mcpp run -q -p d2x/buildtools/mcpp -- check <id>` 或整体 `bash d2x/buildtools/mcpp/tests/e2e.sh`

---

## 10. 已知缺口

| 缺口 | 影响 |
|---|---|
| **模块化练习的填空占位符无约定** | `D2X_YOUR_ANSWER` 是宏,跨不了模块边界;cpp20/cpp23 章节需先决策 |
| **macOS / Windows 从未验证** | `_popen`、`_putenv_s` 的 `#ifdef` 分支全是纸面推断 |
| **新 CI 从未真跑过** | workflow 是手写的,`xlings install -y` 在 CI 环境能否装上 mcpp 未验证 |
| **`diagnostics` 只覆盖运行期断言** | 编译错误尚未解析成结构化诊断 |
| **Provider 侧无单测** | discovery 的 id/order/chapter 推导、manifest 生成都只被端到端间接覆盖 |
| **`hello-mcpp` 无参考答案** | e2e 只能 SKIP 它,入门练习实际未被验证 |
| **`solutions/` 无 en 镜像** | 设计如此(共用一份),但意味着 en 专有的措辞错误无法被答案校验发现 |
14 changes: 7 additions & 7 deletions .agents/skills/d2mcpp-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Authoring conventions, design principles, and file formats for the d2mcpp
(D2X) Modern C++ tutorial project. Use this whenever you add or edit a C++
language-feature lesson — a book chapter, a dslings exercise, a reference
solution, or when registering them in SUMMARY/xmake/changelog. Trigger it for
solution, or when registering them in SUMMARY/changelog. Trigger it for
any request like "add a chapter for X", "write a dslings exercise for fold
expressions", "add the constexpr lesson", "fill the cpp14 section", or
anything touching book/src, book/en/src, dslings/, or solutions/. Even small
Expand All @@ -19,7 +19,7 @@ quartet: **Book (mdBook) + Code (dslings exercises) + Solution + Auto-checker
(`d2x checker`)**, all of it **bilingual (zh + en)**. A "lesson" is never one
file — it is a coordinated set across several directories. The cardinal sin here
is producing a half-set (a chapter with no exercise, a zh file with no en
counterpart, an exercise not registered in xmake). This skill exists so every
counterpart, an exercise missing from solutions/). This skill exists so every
lesson lands complete and consistent.

Read `references/anatomy.md` for the full directory map and the exact list of
Expand Down Expand Up @@ -48,7 +48,8 @@ Honor that axis:
remark, P0136 inheriting-constructor semantics fix.
- **Compile each chapter at its introduction standard.** Do not globally pin a
newer standard. If one exercise genuinely needs a later standard, make it an
**explicit, commented exception** in xmake — never a bare `-- TODO` hack.
**explicit, commented exception** via a `// d2x:cxxflags:` header directive
in the exercise file — never a bare `TODO` hack.

Background and the full per-feature evolution analysis live in
`.agents/docs/2026-06-08-cpp11-feature-evolution-and-cxx20-baseline.md`. Consult
Expand All @@ -70,8 +71,7 @@ When adding a feature numbered `NN` with slug `topic` (e.g. `06-scoped-enums`):
3. `dslings/<std>/NN-topic-K.cpp` — one or more exercises (`K` = 0,1,2…), zh comments
4. `dslings/en/<std>/NN-topic-K.cpp` — en exercises (translate comments only)
5. `solutions/<std>/NN-topic-K.cpp` — reference solution per exercise
6. Register each exercise target in `dslings/<std>/xmake.lua`
7. Register each solution target in `solutions/xmake.lua`
6. (nothing to register — exercises are discovered by directory convention)
8. Add the chapter line to **both** `book/src/SUMMARY.md` and `book/en/src/SUMMARY.md`
9. Add a changelog entry to **both** `book/src/changelog.md` and `book/en/src/changelog.md`

Expand Down Expand Up @@ -106,7 +106,7 @@ Use `assets/chapter.zh.md` / `assets/chapter.en.md`. Required structure, in orde
6. `## 二、真实案例` corroborates the feature with **real, verbatim STL code**
quoted from the in-repo `msvc-stl/` basis — see "The 真实案例 section" below.
7. `## 四、练习代码` lists **every** exercise topic of the chapter (`-0`, `-1`, …,
matching the dslings/xmake count) with dslings links as an index, but the checker
matching the dslings file count) with dslings links as an index, but the checker
block shows only the **single entry** command `d2x checker <name>` — `d2x` auto-
advances through the remaining exercises, so do NOT list one command per exercise.
Then a `### 练习交流讨论` link to the chapter's forum thread. Each exercise's own
Expand Down Expand Up @@ -179,4 +179,4 @@ Use `assets/solution.cpp`. It is the corrected exercise with:
Before claiming a lesson complete, verify the whole set exists and is wired up,
and that the exercise/solution actually build & check. See
`references/anatomy.md` for the verification commands (`d2x checker <name>` and
the xmake build) — run them; do not assert success without the output.
the mcpp build) — run them; do not assert success without the output.
2 changes: 1 addition & 1 deletion .agents/skills/d2mcpp-authoring/assets/chapter.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@

<explanation>

<!-- Practice Topics list ALL exercises (-0, -1, …, matching the dslings/xmake count)
<!-- Practice Topics list ALL exercises (-0, -1, …, matching the dslings file count)
as an index; but the checker block shows only the SINGLE entry command
`d2x checker <name>` — d2x auto-advances through the rest, no need to list each.
Each exercise's own checker name + a Tips line specific to that exercise live in
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/d2mcpp-authoring/assets/chapter.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@

<讲解>

<!-- 练习主题列出本章【全部】练习(-0、-1、…,与 dslings/xmake 注册数一致),作为索引;
<!-- 练习主题列出本章【全部】练习(-0、-1、…,与 dslings 练习文件数一致),作为索引;
但检测命令只放【入口一条】 d2x checker <name> —— d2x 会自动逐个往下推进,无需逐条列出。
各练习自己的 checker 名 + 针对本练习的 Tips 放在对应练习文件的头注释里。
讨论帖取各练习头注释里的「练习交流讨论」链接(全章通常同一帖)。 -->
Expand Down
Loading
Loading