Skip to content

Commit 9a71c9a

Browse files
authored
docs: three trees, three audiences, and the rule for citing between them (#590)
The documentation was three collections sharing one directory. It is now three trees, each with a reader, an admission criterion and a stability promise: `docs/` is a manual for what mcpp implements, `docs/specs/` is normative, and `.agents/docs/` records designs and is immutable once landed. WHAT A READER GETS. 31 chapters in bands (0x fundamentals, 1x publishing, 2x toolchains and targets, 3x extending mcpp, 4x devices, 5x contracts, 9x mcpp itself), so the number states the position. Two indexes carry the two properties a manual needs: the bands are the reading order, and a separate lookup index maps key, command and concept to the chapter that owns it. NEW CHAPTERS, each for a reader who previously had none: 00 what mcpp is, 02 scenarios, 05 dependencies, 08 testing, and 32/33/34 for the three ecosystem authors -- payloads, runtime adapters and board-support packages. Payloads were named 84 times across 12 chapters and authored nowhere. NEW EXAMPLES, all in CI's denominator: 09-heterogeneous/boundary (an island boundary needing no device), 11-features, and 12-a-new-device-language -- a language with its own compiler, built for the build machine through `tools = [...]`, which is the repository's first example of a dependency producing a host tool. CHECKS, 13 structural and 4 register, each falsified before it was committed. They found real defects rather than confirming the tree: seven translation gaps invisible to heading parity (a Chinese section with no body, a missing target matrix), five citations that named a section in the wrong chapter, two anchors wrong since they were written, and a source layout in chapter 90 that had moved directories. THE SOURCE CHANGES ARE 36 LINES, one for one: chapter paths inside `--help` text, diagnostics and comments, which the renumbering requires and which structure check rule 3 now guards. Three items are recorded as open in .agents/docs, with a closing criterion each: eight reference chapters still lack a limits section, deliberately not fabricated; SPEC-005 (the compatibility tag) and SPEC-006 (the build.mcpp directive protocol) are promotions not yet made; and whether the specifications become English-primary with a Chinese mirror is a decision about their audience.
1 parent 6e1c65c commit 9a71c9a

157 files changed

Lines changed: 15811 additions & 7411 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/docs/2026-09-08-documentation-architecture-three-trees.md

Lines changed: 598 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-09-08-examples-curriculum-and-documentation-plan.md

Lines changed: 526 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-09-08-the-documentation-as-a-book.md

Lines changed: 634 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/README.md

Lines changed: 324 additions & 1 deletion
Large diffs are not rendered by default.

.agents/skills/mcpp-docs-style/SKILL.md

Lines changed: 436 additions & 35 deletions
Large diffs are not rendered by default.

.github/tools/build_examples.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ BUILD=(
3737
# The device payloads are NOT installed: they are gated on the accelerator,
3838
# and this builds without one.
3939
examples/09-heterogeneous/multi-backend
40+
# The island boundary with nothing on top. Its island is an ordinary C file,
41+
# so it needs no device and no device payload -- only `mcpp:plugins` with
42+
# `tools-island`. It is the one example that runs the arrangement the
43+
# plugins README records as measured: a consumer importing the GENERATED
44+
# module and linking against an implementation another compiler produced.
45+
examples/09-heterogeneous/boundary
46+
# Declares features rather than consuming them. The next step asserts the
47+
# criterion that matters -- that the default build's RESOLUTION does not
48+
# name the optional package -- which a build alone cannot show.
49+
examples/11-features/counters
50+
examples/11-features/greeter
51+
# A device language the engine does not know, and the compiler for it.
52+
#
53+
# `toyc` is built here as an ordinary package as well as by the app as a
54+
# host tool, and the two are not the same signal: this one fails at the
55+
# compiler, the app's fails somewhere in `tools = [...]` / `reexport` /
56+
# `dep_bin`, and a single line telling them apart is worth one build of a
57+
# three-file package.
58+
examples/12-a-new-device-language/toyc
59+
examples/12-a-new-device-language/app
4060
)
4161

4262
# `key|reason`.
@@ -47,6 +67,7 @@ SKIP=(
4767
"examples/07-project-subos|provisions a project-local sub-OS, which e2e 27_self_contained_home covers directly and far more cheaply"
4868
"examples/08-build-rules/rules-embed|a rule package is not a standalone build: its interface imports the bundled mcpp module, which exists only inside a consumer's build. Verified by building 08-build-rules/app, the same way mcpp-plugins verifies its own members"
4969
"examples/08-build-rules/rules-tidy|same"
70+
"examples/12-a-new-device-language/rules-toy|a rule package is not a standalone build: its module imports the bundled mcpp module, which exists only inside a consumer's build program. Verified by building 12-a-new-device-language/app"
5071
"examples/09-heterogeneous/cuda/app|needs the CUDA payload set, and a device to run; the rule package is covered by mcpp-plugins' own CI"
5172
"examples/09-heterogeneous/hip/app|same, for the HIP payloads"
5273
"examples/09-heterogeneous/sycl/app|needs the dpcpp payload (over a gigabyte) and a device its runtime accepts"
Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
#!/usr/bin/env bash
2+
# check_docs_structure.sh — the rules of .agents/skills/mcpp-docs-style that are
3+
# about WHERE a document lives rather than how its prose reads.
4+
#
5+
# check_docs_style.sh covers the register. This covers the architecture:
6+
#
7+
# 1. no user chapter cites a design record
8+
# 2. a specification cites one only in its metadata table
9+
# 3. every docs/*.md path named outside .agents/ resolves
10+
# 4. every specification is listed in all three indexes
11+
# 5. every specification has a metadata table and a change record
12+
# 6. no emoji under docs/ or in a top-level README
13+
# 7. the generated design-record index is current
14+
# 8. a new design record declares its subject and status
15+
# 9. every relative link in docs/ and examples/ resolves
16+
# 10. a translation carries the same tables and code blocks
17+
# 11. every chapter states its reader, its question and its exclusions
18+
# 12. a citation naming a section lands in the chapter that contains it
19+
# 13. every table the manifest reference documents is in the lookup index
20+
#
21+
# What it deliberately does NOT check: whether a chapter documents what is
22+
# implemented, whether an assertion's strength matches its evidence, or whether
23+
# a surface's coverage has a denominator. Those need a reader, and they are the
24+
# three most important rules in the skill.
25+
#
26+
# Usage: bash .github/tools/check_docs_structure.sh
27+
set -uo pipefail
28+
cd "$(dirname "$0")/../.." || exit 1
29+
30+
fail=0
31+
bad() { echo "FAIL: $*"; fail=1; }
32+
33+
# ── 1. no user chapter cites a design record ──────────────────────────────
34+
#
35+
# The two READMEs are exempt BY DECISION, not by accident: they are where the
36+
# three-tree architecture is published, so they name `.agents/docs/` in order to
37+
# say what it holds and that a chapter must not send a reader there. Every other
38+
# file under docs/ that names it has delegated a question it should have
39+
# answered -- see "引用方向是规则" in the skill.
40+
ARCHITECTURE_PAGES="docs/README.md docs/zh/README.md"
41+
for f in docs/*.md docs/zh/*.md; do
42+
case " $ARCHITECTURE_PAGES " in *" $f "*) continue ;; esac
43+
if grep -n '\.agents/' "$f" >/dev/null 2>&1; then
44+
while IFS= read -r hit; do
45+
bad "$f: a user chapter cites a design record: ${hit}"
46+
done < <(grep -n '\.agents/' "$f")
47+
fi
48+
done
49+
50+
# ── 2. a specification cites a design record only in its metadata table ───
51+
#
52+
# Provenance belongs in the metadata row. A citation in the body is the same
53+
# delegation rule 1 refuses, one tree over.
54+
for f in docs/specs/*.md; do
55+
[ "$(basename "$f")" = "README.md" ] && continue
56+
# The metadata table is the leading block: everything before the first `##`.
57+
body_start=$(grep -n '^## ' "$f" | head -1 | cut -d: -f1)
58+
[ -z "$body_start" ] && body_start=1
59+
if awk -v s="$body_start" 'NR >= s && /\.agents\//' "$f" | grep -q .; then
60+
bad "$f: cites a design record outside its metadata table"
61+
fi
62+
done
63+
64+
# ── 3. every docs/*.md path named outside .agents/ resolves ───────────────
65+
#
66+
# A comment in src/ naming a chapter is a citation. Six of them named chapters
67+
# that had not existed since an earlier numbering, and nothing reported it.
68+
while IFS= read -r p; do
69+
[ -f "$p" ] || bad "a document names \`$p\`, which does not exist"
70+
done < <(git grep -ohE 'docs/[0-9]{2}-[a-z0-9-]+\.md' -- ':!.agents' | sort -u)
71+
72+
# ── 4. every specification is listed in all three indexes ────────────────
73+
for f in docs/specs/*.md; do
74+
base="$(basename "$f")"
75+
[ "$base" = "README.md" ] && continue
76+
grep -q "$base" docs/README.md || bad "docs/README.md does not list docs/specs/$base"
77+
grep -q "$base" docs/zh/README.md || bad "docs/zh/README.md does not list docs/specs/$base"
78+
grep -q "$base" docs/specs/README.md || bad "docs/specs/README.md does not list $base"
79+
done
80+
81+
# ── 5. every specification has a metadata table and a change record ──────
82+
#
83+
# docs/specs/README.md has required both since the directory existed. Nothing
84+
# checked, so the requirement held only for specs whose author read the README.
85+
#
86+
# Four rows are required rather than "a table", and the fourth is why: a
87+
# specification whose implementation version is not stated cannot be judged
88+
# stale by anyone. Two spellings are in use for it and both are accepted.
89+
for f in docs/specs/*.md; do
90+
base="$(basename "$f")"
91+
[ "$base" = "README.md" ] && continue
92+
meta="$(head -30 "$f")"
93+
for row in '规范编号' '状态' '最后修改'; do
94+
printf '%s' "$meta" | grep -q "$row" \
95+
|| bad "$f: metadata table has no \`$row\` row"
96+
done
97+
printf '%s' "$meta" | grep -qE '对应实现|最低实现版本' \
98+
|| bad "$f: metadata table states no implementation version, so nothing can decide whether it is stale"
99+
grep -qE '^#{2,3} .*(变更记录|Change record|Change log)' "$f" \
100+
|| bad "$f: no change record"
101+
done
102+
103+
# ── 6. no emoji under docs/ or in a top-level README ─────────────────────
104+
#
105+
# Status is a word: 已实现 / 未实现, yes / no, verified / not verified. A symbol
106+
# needs a legend and a word does not. `.agents/docs/` is NOT checked: its
107+
# records carry four thousand of them and are immutable once their change
108+
# lands, so normalising them would edit documents whose value is that they are
109+
# not edited. New records follow the rule by review.
110+
EMOJI='[✅❌⚠⭐🎉🚀💡🔥👍✨📦🔧]'
111+
for f in docs/*.md docs/zh/*.md docs/specs/*.md README.md README.zh-CN.md; do
112+
[ -f "$f" ] || continue
113+
if grep -nP "$EMOJI" "$f" >/dev/null 2>&1; then
114+
while IFS= read -r hit; do
115+
bad "$f: emoji — state the status as a word: ${hit%%:*}: $(echo "$hit" | cut -d: -f2- | cut -c1-60)"
116+
done < <(grep -nP "$EMOJI" "$f")
117+
fi
118+
done
119+
120+
# ── 7. the design-record index is current ────────────────────────────────
121+
#
122+
# 269 records and the index was one heading. It is generated now, so it cannot
123+
# drift -- and a generated file that is checked in must be compared against the
124+
# generator or it drifts anyway.
125+
python3 .github/tools/gen_agents_index.py --check || fail=1
126+
127+
# ── 8. a new design record declares its subject and status ───────────────
128+
#
129+
# From the date the convention starts. The 268 records that predate it are not
130+
# rewritten: a record describes the moment its change was made, and a pass that
131+
# added a field nobody chose would edit documents whose value is that they are
132+
# not edited.
133+
CONVENTION_FROM="2026-09-08"
134+
for f in .agents/docs/[0-9]*.md; do
135+
[ -f "$f" ] || continue
136+
d="$(basename "$f" | cut -c1-10)"
137+
[[ "$d" < "$CONVENTION_FROM" ]] && continue
138+
head -1 "$f" | grep -q '^---$' \
139+
|| { bad "$f: a record dated $CONVENTION_FROM or later has no front matter"; continue; }
140+
fmblock="$(awk 'NR>1 && /^---$/ {exit} NR>1' "$f")"
141+
printf '%s' "$fmblock" | grep -qE '^subject: *[a-z]' \
142+
|| bad "$f: front matter declares no \`subject\`"
143+
printf '%s' "$fmblock" | grep -qE '^status: *(active|landed|superseded|abandoned) *$' \
144+
|| bad "$f: front matter declares no valid \`status\` (active | landed | superseded | abandoned)"
145+
done
146+
147+
# ── 9. every relative link in docs/ and examples/ resolves, fragment included ─
148+
#
149+
# Rule 3 catches `docs/NN-*.md` named anywhere, including from source comments.
150+
# This is the other half: a Markdown link in a document that points at a file
151+
# which is not there. Both halves are needed -- a chapter moved in this batch
152+
# would satisfy one and break the other.
153+
#
154+
# THE FRAGMENT IS PART OF THE LINK. The first version of this rule discarded
155+
# it (`(?:#[^)]*)?`), so a link to a section that had been renamed resolved to
156+
# the file and was reported correct. Renaming 100 headings for register in one
157+
# batch is exactly the change that produces those, and two hand-written anchors
158+
# in chapter 30 were already wrong before the renames began.
159+
python3 - <<'PYCHECK' || fail=1
160+
import re, pathlib, sys, unicodedata
161+
162+
def slug(heading):
163+
"""GitHub's heading slug: lowercase, drop punctuation and symbols, spaces to hyphens.
164+
165+
Category P* and S* covers what github-slugger removes -- ASCII punctuation,
166+
the em dash, the backticks around inline code, and the full-width comma and
167+
colon the Chinese chapters use -- while `-` and `_` are kept because an
168+
anchor is allowed to contain them.
169+
"""
170+
t = re.sub(r"^#+\s+", "", heading).strip().lower()
171+
keep = []
172+
for ch in t:
173+
if ch in "-_":
174+
keep.append(ch)
175+
elif ch.isspace():
176+
keep.append(" ")
177+
elif unicodedata.category(ch)[0] in "PS":
178+
continue
179+
else:
180+
keep.append(ch)
181+
return "".join(keep).replace(" ", "-")
182+
183+
def anchors_of(path):
184+
"""Every anchor the file defines, with GitHub's -1/-2 suffix for repeats."""
185+
seen, out, infence = {}, set(), False
186+
for line in path.read_text(errors="ignore").splitlines():
187+
if line.startswith("```"):
188+
infence = not infence
189+
continue
190+
if infence or not re.match(r"^#{1,6} ", line):
191+
continue
192+
s = slug(line)
193+
n = seen.get(s, 0)
194+
seen[s] = n + 1
195+
out.add(s if n == 0 else f"{s}-{n}")
196+
return out
197+
198+
files = list(pathlib.Path("docs").rglob("*.md")) + list(pathlib.Path("examples").rglob("*.md"))
199+
cache = {}
200+
bad = 0
201+
for f in files:
202+
for m in re.finditer(r"\]\(([^)\s]*?)(?:#([^)\s]+))?\)", f.read_text(errors="ignore")):
203+
t, frag = m.group(1), m.group(2)
204+
if t.startswith(("http", "mailto:")):
205+
continue
206+
target = (f.parent / t) if t else f
207+
if t and not target.exists():
208+
print(f"FAIL: {f}: link to `{t}` does not resolve")
209+
bad += 1
210+
continue
211+
if not frag or target.suffix != ".md" or not target.is_file():
212+
continue
213+
key = target.resolve()
214+
if key not in cache:
215+
cache[key] = anchors_of(target)
216+
if frag not in cache[key]:
217+
print(f"FAIL: {f}: `#{frag}` is not a heading in {target.as_posix()}")
218+
bad += 1
219+
sys.exit(1 if bad else 0)
220+
PYCHECK
221+
222+
# ── 10. a translation carries the same tables and code blocks ────────────
223+
#
224+
# check_docs_style.sh compares HEADING STRUCTURE, which is what catches a page
225+
# that has fallen a section behind. It does not see a table row or a code block
226+
# that never made it across, and two of those were sitting in the tree: the
227+
# 简体中文 `[features]` section had no body at all, and 简体中文 §2.11 was
228+
# missing the `identity` verdict table. Both predate this check and both are
229+
# invisible to every other one.
230+
python3 - <<'PYPARITY' || fail=1
231+
import pathlib, sys, re
232+
bad = 0
233+
for en in sorted(pathlib.Path("docs").glob("*.md")):
234+
zh = pathlib.Path("docs/zh") / en.name
235+
if not zh.exists():
236+
continue
237+
def count(f):
238+
rows = blocks = 0
239+
infence = False
240+
for line in f.read_text(errors="ignore").split("\n"):
241+
if line.startswith("```"):
242+
if not infence:
243+
blocks += 1
244+
infence = not infence
245+
continue
246+
if infence:
247+
continue
248+
if line.startswith("|"):
249+
rows += 1
250+
return rows, blocks
251+
er, eb = count(en)
252+
zr, zb = count(zh)
253+
if er != zr:
254+
print(f"FAIL: {en.name}: {er} table rows in English, {zr} in 简体中文")
255+
bad += 1
256+
if eb != zb:
257+
print(f"FAIL: {en.name}: {eb} code blocks in English, {zb} in 简体中文")
258+
bad += 1
259+
sys.exit(1 if bad else 0)
260+
PYPARITY
261+
262+
# ── 11. every chapter states its reader and its question ─────────────────
263+
#
264+
# `.agents/skills/mcpp-docs-style` R3: reader, the one question, and the
265+
# EXCLUSIONS, in the first fifteen lines. The exclusions are the load-bearing
266+
# half -- they are the gate that stops a chapter re-absorbing a topic another
267+
# chapter owns. Five of 24 chapters had this before the design; a rule nothing
268+
# checks is a rule that decays back to five.
269+
# Chapter 00 is exempt: it is the book's front door, and a metadata block is a
270+
# reference-chapter device. It has no "not here" to declare, because everything
271+
# else IS elsewhere -- which is what its closing paragraph says instead.
272+
for f in docs/[0-9]*.md docs/zh/[0-9]*.md; do
273+
[ -f "$f" ] || continue
274+
case "$(basename "$f")" in 00-*) continue ;; esac
275+
head -18 "$f" | grep -qE '^\*\*(Reader|读者)' \
276+
|| bad "$f: no designed opening — the first lines must name the reader"
277+
head -18 "$f" | grep -qE '(question this chapter answers|本章回答的那一个问题)' \
278+
|| bad "$f: the opening names no question"
279+
head -22 "$f" | grep -qE '^\*\*(Not here|不在这里)' \
280+
|| bad "$f: the opening states no exclusions"
281+
done
282+
283+
# ── 12. a citation that names a section lands in the chapter that has it ─
284+
#
285+
# `See *One package, one version* in [04](04-mcpp-toml.md)` survived a split
286+
# that moved the section to chapter 23, in five places and two languages. Rule 3
287+
# could not see it -- the path resolved; it was the wrong chapter. A citation
288+
# that names a section by TITLE is checkable against that chapter's headings.
289+
python3 - <<'PYCITE' || fail=1
290+
import re, pathlib, sys
291+
EN = re.compile(r"See \*([^*]{3,60})\* in \[\d{2}[^\]]*\]\((\d{2}-[a-z0-9-]+)\.md\)")
292+
ZH = re.compile(r"见\s*\[\d{2}[^\]]*\]\((\d{2}-[a-z0-9-]+)\.md\)\s*的\*([^*]{2,40})\*")
293+
bad = 0
294+
for f in list(pathlib.Path("docs").glob("[0-9]*.md")) + list(pathlib.Path("docs/zh").glob("[0-9]*.md")):
295+
text = f.read_text(errors="ignore")
296+
for m in EN.finditer(text):
297+
title, chap = m.group(1), m.group(2)
298+
t = (f.parent / f"{chap}.md")
299+
if not t.exists() or title.lower() not in t.read_text(errors="ignore").lower():
300+
print(f"FAIL: {f}: cites *{title}* in {chap}, which does not contain it"); bad += 1
301+
for m in ZH.finditer(text):
302+
chap, title = m.group(1), m.group(2)
303+
t = (f.parent / f"{chap}.md")
304+
if not t.exists() or title not in t.read_text(errors="ignore"):
305+
print(f"FAIL: {f}: cites *{title}* in {chap}, which does not contain it"); bad += 1
306+
sys.exit(1 if bad else 0)
307+
PYCITE
308+
309+
# ── 13. every table the manifest reference documents is in the lookup index ─
310+
#
311+
# docs/README.md carries a reverse index -- a key in front of a reader to the
312+
# chapter that owns it -- and it is hand-built. A key added to the reference and
313+
# not indexed is invisible: the reader concludes it is undocumented. The
314+
# denominator is the reference chapter's own `###` headings, and the comparison
315+
# is on the KEY NAME rather than on its spelling, because the two documents
316+
# legitimately write `[targets.<name>]` and `[targets.<n>]`.
317+
python3 - <<'PYLOOKUP' || fail=1
318+
import re, pathlib, sys
319+
ref = pathlib.Path("docs/04-mcpp-toml.md").read_text(errors="ignore")
320+
idx = pathlib.Path("docs/README.md").read_text(errors="ignore")
321+
keys = set()
322+
for m in re.finditer(r"^### [0-9.b]+ `([^`]+)`", ref, re.M):
323+
tok = m.group(1)
324+
name = re.sub(r"^\[|\].*$", "", tok) # [targets.<name>] -> targets.<name>
325+
name = name.split(".")[0].split(" ")[-1] # -> targets ; "[package] platforms" -> platforms
326+
keys.add(name)
327+
missing = sorted(k for k in keys if k not in idx)
328+
for k in missing:
329+
print(f"FAIL: docs/README.md lookup index does not mention `{k}`, which docs/04 documents")
330+
sys.exit(1 if missing else 0)
331+
PYLOOKUP
332+
333+
if [[ "$fail" -eq 0 ]]; then
334+
echo "OK: docs structure checks pass"
335+
fi
336+
exit "$fail"

0 commit comments

Comments
 (0)