Skip to content

Commit 3b9539e

Browse files
committed
docs: declarative register throughout, and a checker for the half that is decidable
The documentation had drifted into a conversational voice: headings phrased as questions or as asides ("一段话讲完", "打什么由谁决定", "Consuming one", "The whole idea in one paragraph"), figurative coinages ("姊妹篇", a package's "legs", sources that "travel"), and the reader addressed as "you"/"你" in reference material that a person reads to look something up rather than to follow along. Every heading is now a noun phrase or a declarative statement, and the second person is gone from the reference documents. Tutorials keep it — 00, 01 and 04 are read while doing, and the list is written down rather than inferred. Quoted mcpp output keeps its own wording: `did you mean 'x86_64-linux-musl'?` and `your toolchain : …` are what the program prints, so reproducing them verbatim is a requirement, not a lapse. The sweep surfaced four content gaps that no reading would have found: * docs/zh/05 was missing §2.6, §2.7, §2.7.1, §2.8 and §2.8.3 — five sections of a reference manual, including the whole of `[target.*]`; * docs/zh/05 also had `#### 表形式` filed under the namespace-resolution section instead of under `[features]`, which is where its English original sits. Same heading COUNT, different structure, which is why the check compares the level sequence and not the count; * docs/zh/10 was missing "manifest keys that need a version floor" and docs/zh/README the specifications index; * docs/12 still said `kind = "shared"` was "Linux/ELF today" in its opening example, two days after Mach-O and PE/MinGW landed. `.agents/skills/mcpp-docs-style` states the rules; `.github/tools/check_docs_style.sh` enforces the three that are decidable from the text (heading register, second person, bilingual structure) and runs in ci-linux beside the version-pin check — same placement, same reason: pure text, no toolchain, and it catches drift that is invisible to every other job. The skill says plainly which rule the script cannot check, and it is the most important one: whether a claim's strength matches its evidence.
1 parent df7a443 commit 3b9539e

22 files changed

Lines changed: 697 additions & 257 deletions
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
name: mcpp-docs-style
3+
description: Use when writing or editing anything under docs/ (English or 简体中文), README files, or long-form design records — states the register mcpp documentation is written in (declarative, precise, professional), the constructions that are not admitted (question headings, conversational asides, internet slang, figurative jargon), and the bilingual parity rules.
4+
---
5+
6+
# mcpp 文档风格规范
7+
8+
## 适用范围
9+
10+
`docs/**`(含 `docs/zh/**`)、`README.md``.agents/docs/**` 的对外部分。
11+
12+
代码注释与 commit message **不受本规范约束** —— 它们的读者、篇幅与目的都不同,
13+
那里允许并鼓励叙述「为什么」以及实测过程。本规范约束的是**面向用户的文档**
14+
15+
## 一、总原则
16+
17+
文档是**参考资料**,不是博客,也不是聊天记录。判据只有一条:
18+
19+
> 一位不认识作者、只想解决自己问题的工程师,能不能在最短时间内
20+
> 拿到准确的事实,并且不会误以为某个说法比实际更随意或更绝对。
21+
22+
由此得到三条可执行的规则:陈述、精确、克制。
23+
24+
## 二、标题
25+
26+
**标题一律是名词短语或陈述句,不使用疑问句、不使用口语片段。**
27+
28+
疑问句标题把「读者已经知道自己在找什么」这个前提丢掉了 —— 目录里一列问句,
29+
读者要先把每个问句翻译成主题才能定位。
30+
31+
| 不采用 | 采用 |
32+
|---|---|
33+
| 一段话讲完 | 概述 |
34+
| 打什么由谁决定 | 打包内容的决定依据 |
35+
| 哪些 `.cppm` 会被发布 | 发布的接口单元 |
36+
| 消费者的构建会检查什么 | 消费端的构建检查 |
37+
| 怎么消费 | 消费方式 |
38+
| 老版本 mcpp 拿到这种包会怎样 | 旧版本 mcpp 的行为 |
39+
| 为什么两者都不许裁剪 | 两个集合不可裁剪的原因 |
40+
| 这些说法验证到哪一步、在哪台机器上 | 验证范围 |
41+
| The whole idea in one paragraph | Overview |
42+
| What decides what gets packed | What determines the package contents |
43+
| Consuming one | Consuming a package |
44+
| What you may rely on, and what changes | Stability guarantees |
45+
46+
「…的原因」「…的依据」「…的范围」是把 why 型标题转成名词短语的常用形。
47+
**保留 why 本身,去掉疑问语气。**
48+
49+
## 三、词汇
50+
51+
### 不采用的类别
52+
53+
1. **网络用语与口语**:搞定、干活、坑、真香、翻车、打脸、一把梭、白给、
54+
凉了、炸了、神器、黑科技、敲黑板、划重点。
55+
2. **拟人与比喻性行话**:姊妹篇、腿(fat package 的一份产物)、travel(源码
56+
「旅行」)、picky、happy path 的中文直译。技术术语本身可以是比喻
57+
(rpath、sysroot),但**不要新造比喻**
58+
3. **填充语**:其实、说白了、简单来说、众所周知、显然、当然、值得一提的是。
59+
如果一件事显然,就不必说;如果不显然,「显然」会让读者怀疑自己。
60+
4. **含糊的程度词**:很快、非常、极其、基本上、差不多。用数字或范围替代 ——
61+
「2.42×」「64.77s」「四个平台中的三个」。
62+
63+
### 人称
64+
65+
默认**不使用第二人称**。写动作的对象,不写「你」。
66+
67+
- 不采用:你可以在 `mcpp.toml` 里写 …
68+
- 采用:在 `mcpp.toml` 中声明 …
69+
70+
例外:**教程体**文档可以使用第二人称,因为那里读者正在跟着做。教程体是
71+
**列出来的,不是推断的**:`00-getting-started.md``01-examples.md`
72+
`04-build-from-source.md`。其余全部按参考文档处理。
73+
74+
引用 mcpp 自身输出的部分不受此限:`did you mean 'x86_64-linux-musl'?`
75+
`your toolchain : …` 是程序打印的原文,**逐字复现是要求,不是文风问题**
76+
检查脚本因此会先剔除行内代码段再判定。
77+
78+
## 四、句式
79+
80+
- **陈述句优先。** 命令式仅用于操作步骤(「运行 `mcpp build`」)。
81+
- **一句话一个事实。** 从句套从句的长句拆开。
82+
- **不使用反问。**「难道不应该……吗?」没有信息量。
83+
- **不使用感叹号。**
84+
- **破折号克制使用**:插入语用逗号或括号;破折号留给「随后是对前半句的
85+
重述或收束」这一种用法。
86+
87+
## 五、断言的强度必须与证据相符
88+
89+
这是本规范里最实质的一条,也是最容易违反的一条。
90+
91+
| 证据 | 允许的表述 |
92+
|---|---|
93+
| 跑过、有输出 | 「实测」「测量得到」,并给出数字或报错原文 |
94+
| 读代码推断 | 「按 X 的实现」「由 Y 决定」 |
95+
| 未验证 | 「未验证」「尚无测试覆盖」—— **必须写出来** |
96+
97+
**不要把推断写成实测。** 反例(本仓库真实发生过):把「守卫在原生构建上失效」
98+
写成实测结论,而它是从「`targetTriple` 结构上可能为空」推断的;实际运行时
99+
它非空,结论不成立。判据:**「结构上可能」不等于「运行时确实」——
100+
要么读运行时产物,要么不要写成实测。**
101+
102+
同理,不要用「完全」「永远」「所有平台」这类全称词,除非确实逐个验证过;
103+
写「已在 Linux / macOS / Windows 验证」比写「全平台可用」更有价值,
104+
因为前者可被检验。
105+
106+
## 六、双语对照
107+
108+
`docs/X.md``docs/zh/X.md`**同一份文档的两个版本**,不是两篇文章。
109+
110+
- 章节结构、标题层级、表格行数必须一一对应;
111+
- 代码块、命令、报错原文**逐字相同**,不翻译;
112+
- 术语表统一:module interface unit / 模块接口单元、implementation partition /
113+
实现分区、import library / 导入库、install name / install name(不译)。
114+
- 改动一侧时**同时改另一侧**。只改一侧会让两份文档随时间分叉,
115+
而读者无从知道哪一份是新的。
116+
117+
## 七、结构
118+
119+
- 顶部一段引言说明**这份文档回答什么问题**,以及相关文档的链接
120+
(用「相关文档:」,不用「姊妹篇」)。
121+
- 表格用于枚举与对照,散文用于因果。**不要用散文列举**
122+
- 「当前边界 / Current limitations」一节是必要的,不是可选的:
123+
没有写出边界的文档,读者只能靠踩到才知道。
124+
125+
## 八、机器检查
126+
127+
规则里可判定的那一半由 `.github/tools/check_docs_style.sh` 执行:
128+
129+
```
130+
bash .github/tools/check_docs_style.sh
131+
```
132+
133+
它检查三条:标题不是疑问句/口语片段;参考文档不使用第二人称;
134+
`docs/X.md``docs/zh/X.md` 的标题结构一致(按层级序列比对,
135+
并剔除代码块内的 `#` 注释 —— 第一版脚本把 ```sh 块里的 `# GET, never HEAD`
136+
数成了标题,报出一个并不存在的结构分歧)。
137+
138+
**它不检查第五节** —— 断言强度与证据是否相符需要读者判断,而那是本规范里
139+
最重要的一条。脚本能做的事不等于规范的全部。
140+
141+
## 九、自检清单
142+
143+
提交文档改动前:
144+
145+
```
146+
[ ] 标题没有疑问句、没有口语片段
147+
[ ] 没有网络用语、没有新造比喻
148+
[ ] 没有第二人称(教程体除外)
149+
[ ] 每条「实测」都有数字、路径或报错原文
150+
[ ] 没有未经验证的全称断言
151+
[ ] 中英两版结构对应,代码块逐字一致
152+
[ ] 有「当前边界」一节
153+
[ ] `bash .github/tools/check_docs_style.sh` 通过
154+
```

.github/tools/check_docs_style.sh

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env bash
2+
# check_docs_style.sh — the mechanically checkable half of .agents/skills/mcpp-docs-style.
3+
#
4+
# A style rule nobody can check is a suggestion. This covers the three rules
5+
# that are decidable from the text alone:
6+
#
7+
# 1. headings are not questions and not conversational fragments
8+
# 2. reference docs do not address the reader in the second person
9+
# (tutorials do — they are listed below, not inferred)
10+
# 3. docs/X.md and docs/zh/X.md have the same heading structure
11+
#
12+
# What it deliberately does NOT check: whether a claim's strength matches its
13+
# evidence. That is the most important rule in the skill and it needs a reader.
14+
#
15+
# Usage: bash .github/tools/check_docs_style.sh
16+
set -uo pipefail
17+
cd "$(dirname "$0")/../.." || exit 1
18+
19+
fail=0
20+
bad() { echo "FAIL: $*"; fail=1; }
21+
22+
# Tutorials address the reader on purpose: the reader is following along.
23+
TUTORIALS="00-getting-started.md 01-examples.md 04-build-from-source.md"
24+
25+
# Headings outside fenced code blocks. `# …` inside a ```sh block is a shell
26+
# comment, and counting it made the first version of this script report a
27+
# parity gap in docs/10 that did not exist.
28+
headings() {
29+
awk '
30+
/^```/ { infence = !infence; next }
31+
!infence && /^#{1,6} / { print }
32+
' "$1"
33+
}
34+
35+
for f in docs/*.md docs/zh/*.md; do
36+
base="$(basename "$f")"
37+
38+
# ── 1. heading register ───────────────────────────────────────────────
39+
while IFS= read -r h; do
40+
case "$h" in
41+
*"?"*|*""*|*""*)
42+
bad "$f: question heading — use a noun phrase: $h" ;;
43+
esac
44+
case "$h" in
45+
*"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"怎么"*|*"会怎样"*|*"不许"*)
46+
bad "$f: conversational heading: $h" ;;
47+
*"The whole idea"*|*"in one paragraph"*|*"Consuming one"*|*"the thing that"*)
48+
bad "$f: conversational heading: $h" ;;
49+
esac
50+
done < <(headings "$f")
51+
52+
# ── 2. second person in reference docs ────────────────────────────────
53+
case " $TUTORIALS " in
54+
*" $base "*) ;;
55+
*)
56+
# Prose only: quoted program output keeps its own wording ("your
57+
# toolchain : …" comes out of mcpp and must be reproduced verbatim), so
58+
# fenced blocks and lines that are clearly transcript are skipped.
59+
hits=$(awk '
60+
/^```/ { infence = !infence; next }
61+
infence { next }
62+
{
63+
# Inline code spans are quoted material — mcpp prints
64+
# `did you mean ...?` and `your toolchain : ...`, and reproducing
65+
# those verbatim is required, not a style lapse. Blank them before
66+
# matching rather than exempting whole lines, so prose on the same
67+
# line is still checked.
68+
line = $0
69+
gsub(/`[^`]*`/, "", line)
70+
if (line ~ /\<you\>|\<your\>|\<yours\>/ || line ~ /你/)
71+
print FILENAME ":" FNR ": " $0
72+
}
73+
' "$f")
74+
if [[ -n "$hits" ]]; then
75+
while IFS= read -r line; do
76+
bad "$f: second person in a reference doc: ${line#*: }"
77+
done <<< "$hits"
78+
fi ;;
79+
esac
80+
done
81+
82+
# ── 3. bilingual heading parity ───────────────────────────────────────────
83+
for f in docs/*.md; do
84+
z="docs/zh/$(basename "$f")"
85+
[[ -f "$z" ]] || continue
86+
# The LEVEL SEQUENCE, not just the count: two documents can have the same
87+
# number of headings and still disagree about which are sections and which
88+
# are subsections. Comparing counts would call that identical.
89+
levels() { headings "$1" | sed -E 's/^(#+).*/\1/' | awk '{print length($0)}'; }
90+
if ! diff -q <(levels "$f") <(levels "$z") >/dev/null; then
91+
ne=$(headings "$f" | wc -l); nz=$(headings "$z" | wc -l)
92+
bad "$(basename "$f"): heading structure differs (en=$ne zh=$nz headings); first divergence:"
93+
diff <(levels "$f") <(levels "$z") | head -4 | sed 's/^/ /'
94+
fi
95+
done
96+
97+
if [[ "$fail" -eq 0 ]]; then
98+
echo "OK: docs style checks pass"
99+
fi
100+
exit "$fail"

.github/workflows/ci-linux.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ jobs:
5555
- name: Check version / xlings pin consistency
5656
run: bash .github/tools/check_version_pins.sh
5757

58+
# Same placement and the same reason: pure text, no toolchain, and the
59+
# drift it catches — a 简体中文 page that has silently fallen behind its
60+
# English original — is invisible to every other job.
61+
- name: Check documentation style and bilingual parity
62+
run: bash .github/tools/check_docs_style.sh
63+
5864
- uses: ./.github/actions/bootstrap-mcpp
5965

6066
- name: Configure mirror + Build mcpp from source (self-host)

docs/02-pack-and-release.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
> deliverable. Three routes turn it into one — and none of them uses the host's
1414
> C library.
1515
16-
## Three ways to ship
16+
## Three distribution routes
1717

1818
Every route below produces an artifact whose C runtime comes from the
1919
ecosystem, never from `/lib64`. That is deliberate: mcpp builds against a
@@ -24,16 +24,16 @@ libc to distribute would give that away at the last step.
2424
| | Route | Command | Where its C runtime comes from | Choose it when |
2525
|---|---|---|---|---|
2626
| **A** | Through the ecosystem | `mcpp emit xpkg``xlings install <pkg>` | the target machine's own xlings payloads | the target has xlings |
27-
| **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | you want a single file with no runtime at all |
27+
| **B** | One static file | `mcpp build --target x86_64-linux-musl` | nowhere — it is linked in | a single file with no runtime dependency |
2828
| **C** | Carry the runtime | `mcpp pack --mode self-contained` | shipped inside the bundle | any Linux, including older than the build machine |
2929

30-
**On route A, and the thing that surprises people.** The `PT_INTERP` baked into
31-
a freshly built binary points at *your* machine's payload, so copying that file
32-
to another machine by hand does not work — the path is not there. That is not a
33-
property of the artifact so much as of the copy: installed through `xlings`, the
34-
package's ELF files are repointed at the target machine's own payloads at
35-
install time. The baked path is a build-machine detail, not a distribution
36-
format. If you are hand-copying binaries between machines, you want B or C.
30+
**On route A.** The `PT_INTERP` recorded in a freshly built binary points at the
31+
build machine's payload, so copying that file to another machine by hand does not
32+
work: the path does not exist there. This is a property of the copy rather than of
33+
the artifact installed through `xlings`, the package's ELF files are repointed at
34+
the target machine's own payloads at install time. The recorded path is a
35+
build-machine detail, not a distribution format. Routes B and C are the ones that
36+
survive hand-copying.
3737

3838
**On route B.** `--target …-musl` implies a static link, so there is no loader,
3939
no RUNPATH and nothing to find at run time. It is the smallest and most
@@ -213,10 +213,10 @@ exec "$here/lib/ld-linux-x86-64.so.2" --library-path "$here/lib" "$here/bin/myap
213213
The layout and wrapper above use an x86_64 example. The packer derives the
214214
loader name from the target; for aarch64 it is `ld-linux-aarch64.so.1`.
215215

216-
#### Trap: `/proc/self/exe` under the bundled loader
216+
#### `/proc/self/exe` under the bundled loader
217217

218218
Being started *by* the loader has a consequence the layout above does not
219-
show: the kernel sets `/proc/self/exe` to the **loader**, not to your program,
219+
show: the kernel sets `/proc/self/exe` to the **loader**, not to the program,
220220
and `/proc/self/cmdline` carries the `--library-path` argument. Every "find my
221221
resources next to the executable" path therefore resolves against `lib/`
222222
instead of the bundle root — and it does so silently. In practice that means
@@ -244,7 +244,7 @@ own resolution, say — use `--mode vendored` instead. It repoints `PT_INTERP`
244244
at the host loader, at the cost of requiring the host's glibc to be at least
245245
as new as the one the artifact was built against.
246246

247-
### Windows (PE)a `.zip`, and the DLLs sit beside the `.exe`
247+
### Windows (PE): a `.zip`, with the DLLs beside the `.exe`
248248

249249
A Windows target produces a **`.zip`**, not a `.tar.gz`, and the layout is
250250
flat:

docs/03-toolchains.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ C++23 modules are fairly sensitive to compiler versions, and different releases
88

99
## Automatic Installation
1010

11-
The first time you run `mcpp build`, if no toolchain is configured yet, mcpp
11+
On the first `mcpp build` with no toolchain configured, mcpp
1212
installs and persists a default pair for the current host. The choice is
1313
host-aware:
1414

@@ -171,7 +171,7 @@ splits on the *env* segment, not on the OS:
171171
| `x86_64-windows-msvc` | `foo.lib` (MSVC convention) |
172172

173173
Before 2026.8.3.3 a mingw build on a Windows host emitted `foo.lib` — a GNU
174-
archive wearing an MSVC name, which MSVC cannot consume. If you have a script
174+
archive wearing an MSVC name, which MSVC cannot consume. A script
175175
that globs `*.lib` out of a `windows-gnu` build, it needs to glob `*.a` now.
176176

177177
## Linux ELF from Windows (`x86_64-linux-musl`, no WSL required)
@@ -218,10 +218,10 @@ Targets block, that host genuinely cannot serve it (implemented by
218218
An MSVC toolset reaches a build one of two ways, and the **version axis of the
219219
spec** says which:
220220

221-
| Spec | Origin | Which compiler you get |
221+
| Spec | Origin | Compiler resolved |
222222
|---|---|---|
223223
| `msvc@system` (or bare `msvc`) | the machine's own Visual Studio | whatever is installed here |
224-
| `msvc@<toolset>` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the one you named, on every machine |
224+
| `msvc@<toolset>` (e.g. `msvc@14.44.35207`) | an xlings payload mcpp installs | the named toolset, identically on every machine |
225225

226226
They are not alternatives to pick between once — they answer different
227227
questions. `msvc@system` asks *"use what this developer already has"*;
@@ -236,7 +236,7 @@ Pinned toolsets coexist with each other and with a system Visual Studio.
236236
> refusing to use what is already installed would cost more than it buys:
237237
> Visual Studio is very often present and cannot always be redistributed.
238238
> `<family>@system` for any other family is an error that names both things
239-
> you might have meant. (The family-less `[toolchain] … = "system"` — the PATH
239+
> that may have been intended. (The family-less `[toolchain] … = "system"` — the PATH
240240
> compiler — is a separate and deliberate escape hatch, and is unaffected.)
241241
242242
### `msvc@system` — the machine's own Visual Studio
@@ -267,7 +267,7 @@ Default set to msvc@system (was: llvm@20.1.7)
267267
```
268268

269269
If no Visual Studio is installed, mcpp says so and offers both routes — a
270-
pinned toolset it can install for you, or the Visual Studio Installer /
270+
pinned toolset it can install, or the Visual Studio Installer /
271271
`winget install Microsoft.VisualStudio.2022.BuildTools`.
272272

273273
`mcpp toolchain list` shows the detected MSVC in a separate `System:` section,
@@ -459,7 +459,7 @@ unaffected, so this is a regression somewhere between Clang 18 and 20.
459459
This bites the module-package pattern directly. Wrapping an upstream header
460460
whose operators are `static inline` templates, and mirroring their signatures
461461
with a trivially-true constraint (the standard mixed-TU subsumption recipe),
462-
is exactly how you hit it.
462+
is exactly how it is reached.
463463

464464
**The rule of thumb:** every template parameter should be pinned by the
465465
**first** function argument. Shapes that break this are the poisonous ones:

0 commit comments

Comments
 (0)