Skip to content

Commit 4a5d681

Browse files
committed
docs(style): the test is an interrogative word, not a question mark
Chapter 00's section headings `背景:模块到了,工具链没跟上` and `谁在为这个落差付账`, and the analogy table's column header `大致相当于谁的活`, are all colloquial, all violate the register rule, and all passed check_docs_style.sh. Two independent defects in one check. THE PREDICATE TESTED A MARKER RATHER THAN THE PROPERTY. It matched `?`, 吗 and 呢 -- the punctuation a question usually carries. The property is the interrogative word, and neither heading carries the marker. THE OBJECT SET WAS ENUMERATED SYNTACTICALLY. It read lines beginning with `#`. The property a heading has -- it names a topic, it is read out of order, it is what a reader scans -- is held equally by a table's header cells, which were therefore never examined. - Chapter 00 rewritten. The scope of a background section is decided by the problem, not by the tool: a C++ project needs four things at once and no single tool owns all four; CMake is the de facto standard for one of them, and a de facto standard is a statement about adoption rather than about the experience of use; the environment is the layer that fails most often because it is the only one nothing checks. Modules are the fourth constraint on that structure rather than the structure. - 100 headings and 57 column headers become noun phrases, in both languages. - check_docs_style.sh: the predicate is now the interrogative word set, and a fourth rule covers column headers. Chapter 00's title is exempt by name, with the reason stated. - check_docs_structure.sh rule 9 discarded the link fragment, so a link to a renamed section resolved to the file and passed. It now computes GitHub's slug and verifies the fragment. It immediately reported two anchors in chapter 30 that were wrong when they were written. - The skill records R5 (the scope of a background section), the predicate rule and the column-header rule, each with this batch's own sentences as the worked example. Each of the four new checks was falsified before commit: an interrogative heading with no question mark, a Chinese column header, an English column header, and an interrogative inside inline code that must not fire.
1 parent c3cebe8 commit 4a5d681

57 files changed

Lines changed: 467 additions & 307 deletions

Some content is hidden

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

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,19 @@ docs/specs/ ──▶ docs/ 允许
9595

9696
### 四条产生每个决定的规则
9797

98-
| | 规则 | 它排除了什么 |
98+
| | 规则 | 排除的形态 |
9999
|---|---|---|
100100
| **R1** | **一个主题一个拥有者。** 恰好一章拥有一个主题;其余每一章只写一句话并链接过去 | 同一件事被解释两遍,然后各自过期 |
101101
| **R2** | **一章为**有任务的读者**而存在,不为**有名字的机制**而存在** | 按机制建目录,于是「测试」「依赖解析」这类**任务**没有家 |
102102
| **R3** | 每章在**前 15 行**内写明读者、它回答的那一个问题、以及它**排除**什么 | 排除是承重的:它是防止这一章重新吸收 R1 已经分配出去的主题的闸 |
103103
| **R4** | 一个部分是**某类读者的一段弧**,其内部顺序是那类读者需要它的顺序 | 字母序、按特性发布时间排序 |
104+
| **R5** | **背景一节的范围由问题决定,不由方案决定。** 先把读者实际面对的那个问题写完整,再写本工具触及其中的哪一部分 | 只写与本工具最近的那一条成因,读者据此以为问题就这么大 |
104105

105106
### 每章的设计规格
106107

107108
动一章之前,先把这张表填出来。填不出来的那一格,就是还没设计的地方。
108109

109-
|| 要回答什么 |
110+
|| 填写要求 |
110111
|---|---|
111112
| 读者 | 谁在读它。一句话说不出来就是没定位 |
112113
| 那一个问题 | 它存在的理由,一个问句 |
@@ -159,10 +160,24 @@ docs/specs/ ──▶ docs/ 允许
159160
| What you may rely on, and what changes | Stability guarantees |
160161
| 0x —— 人人都需要 | 0x —— 基础 |
161162
| 0x — Everyone | 0x — Fundamentals |
163+
| 背景:模块到了,工具链没跟上 | 背景:C++ 工程侧的工具现状 |
164+
| 谁在为这个落差付账 | 这一现状的代价 |
165+
| 大致相当于谁的活 | 可对照的工具 |
166+
| 长什么样 | 形式 |
167+
| 一个 flag 由哪根轴决定 | 决定一个 flag 的轴 |
168+
| 一条运行时搜索路径可以住在哪里 | 运行时搜索路径的允许位置 |
162169

163170
「…的原因」「…的依据」「…的范围」是把 why 型标题转成名词短语的常用形。
164171
**保留 why 本身,去掉疑问语气。**
165172

173+
**判据是疑问词,不是问号。**「谁在为这个落差付账」「打什么由谁决定」都不带问号,
174+
都是疑问句。检查脚本第一版只匹配 `?` / 吗 / 呢,两句全部通过。判据是这一组词:
175+
谁、哪、什么、多少、为何、如何、怎样、怎么。
176+
177+
**表头单元格与标题同规。** 一个列头按每一条要紧的性质都是标题:它命名一个主题、
178+
被跳读、并且是读者扫描时看的那一行。`| 部分 | 大致相当于谁的活 |` 通过了当时
179+
全部的检查,而它是全树最直白的一处违规。
180+
166181
这条同样管**部分名与段位名**,不只管章节标题。「人人都需要」描述的是受众、
167182
是一个句子片段;「基础」是这一段**是什么**。受众写在每章开头的「读者」那一行,
168183
不写在目录的骨架上。

.github/tools/check_docs_structure.sh

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,23 +144,78 @@ for f in .agents/docs/[0-9]*.md; do
144144
|| bad "$f: front matter declares no valid \`status\` (active | landed | superseded | abandoned)"
145145
done
146146

147-
# ── 9. every relative link in docs/ and examples/ resolves ──────────────
147+
# ── 9. every relative link in docs/ and examples/ resolves, fragment included
148148
#
149149
# Rule 3 catches `docs/NN-*.md` named anywhere, including from source comments.
150150
# This is the other half: a Markdown link in a document that points at a file
151151
# which is not there. Both halves are needed -- a chapter moved in this batch
152152
# 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.
153159
python3 - <<'PYCHECK' || fail=1
154-
import re, pathlib, sys
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 = {}
155200
bad = 0
156-
for f in list(pathlib.Path("docs").rglob("*.md")) + list(pathlib.Path("examples").rglob("*.md")):
157-
for m in re.finditer(r"\]\(([^)#]+?)(?:#[^)]*)?\)", f.read_text(errors="ignore")):
158-
t = m.group(1)
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)
159204
if t.startswith(("http", "mailto:")):
160205
continue
161-
if not (f.parent / t).exists():
206+
target = (f.parent / t) if t else f
207+
if t and not target.exists():
162208
print(f"FAIL: {f}: link to `{t}` does not resolve")
163209
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
164219
sys.exit(1 if bad else 0)
165220
PYCHECK
166221

.github/tools/check_docs_style.sh

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
# 2. reference docs do not address the reader in the second person
99
# (tutorials do — they are listed below, not inferred)
1010
# 3. docs/X.md and docs/zh/X.md have the same heading structure
11+
# 4. a table's header cells follow the same register as a heading
12+
#
13+
# Rule 4 exists because rule 1 read only lines beginning with `#`, and a column
14+
# header is a heading by every property that matters: it names a topic, it is
15+
# read out of order, and it is what a reader scans. `| 部分 | 大致相当于谁的活 |`
16+
# passed every check in this file while being the plainest register violation
17+
# in the tree.
1118
#
1219
# What it deliberately does NOT check: whether a claim's strength matches its
1320
# evidence. That is the most important rule in the skill and it needs a reader.
@@ -43,19 +50,69 @@ for f in docs/*.md docs/zh/*.md docs/specs/*.md; do
4350
base="$(basename "$f")"
4451

4552
# ── 1. heading register ───────────────────────────────────────────────
53+
#
54+
# An interrogative WORD is the test, not a question mark. `谁在为这个落差付账`
55+
# and `打什么由谁决定` carry no `?` and are questions all the same, and the
56+
# first version of this rule matched `?`/`吗`/`呢` and passed both.
57+
#
58+
# THE ONE EXEMPTION IS BY NAME. `# 00 —— mcpp 是什么` mirrors the English
59+
# `What mcpp Is`, which is a noun clause rather than a question, and a chapter
60+
# title is the chapter's registered name: it appears in three indexes and in
61+
# every citation of the chapter. Exempting it here states the decision;
62+
# narrowing the rule to `##` and deeper would have hidden it.
4663
while IFS= read -r h; do
47-
case "$h" in
48-
*"?"*|*""*|*""*)
64+
if [[ "$f" == "docs/zh/00-what-mcpp-is.md" && "$h" == "# 00 —— mcpp 是什么" ]]; then
65+
continue
66+
fi
67+
# Inline code is quoted material: `cxx_stdlib` and `mcpp.why.toolchain`
68+
# must not be read for register.
69+
bare="$(sed -E 's/`[^`]*`//g' <<< "$h")"
70+
case "$bare" in
71+
*"?"*|*""*|*""*|*""*)
4972
bad "$f: question heading — use a noun phrase: $h" ;;
73+
*""*|*""*|*"如何"*|*"为何"*|*"什么"*|*"多少"*|*"怎样"*|*"怎么"*)
74+
bad "$f: interrogative heading — use a noun phrase (…的原因 / …的依据 / …的范围): $h" ;;
5075
esac
51-
case "$h" in
52-
*"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"怎么"*|*"会怎样"*|*"不许"*)
76+
case "$bare" in
77+
*"一段话"*|*"讲完"*|*"姊妹篇"*|*"干活"*|*"会怎样"*|*"不许"*|*"跟上"*|*"付账"*|*"长什么样"*)
5378
bad "$f: conversational heading: $h" ;;
5479
*"The whole idea"*|*"in one paragraph"*|*"Consuming one"*|*"the thing that"*)
5580
bad "$f: conversational heading: $h" ;;
5681
esac
5782
done < <(headings "$f")
5883

84+
# ── 4. table header register ──────────────────────────────────────────
85+
#
86+
# A header row is the row directly above the `|---|---|` separator, so the
87+
# separator is what identifies it; matching every `|` line would read data.
88+
while IFS= read -r cell; do
89+
case "$cell" in
90+
*"?"*|*""*|*""*|*""*)
91+
bad "$f: question in a table header — use a noun phrase: $cell" ;;
92+
*""*|*""*|*"如何"*|*"为何"*|*"什么"*|*"多少"*|*"怎样"*|*"怎么"*)
93+
bad "$f: interrogative table header — use a noun phrase: $cell" ;;
94+
esac
95+
shopt -s nocasematch
96+
if [[ "$cell" =~ ^(what|which|who|how|why|where|whether)([[:space:]]|$) ]]; then
97+
bad "$f: interrogative table header — use a noun phrase: $cell"
98+
fi
99+
shopt -u nocasematch
100+
done < <(awk '
101+
/^```/ { infence = !infence; prev = ""; next }
102+
infence { next }
103+
/^\|[[:space:]:|-]+\|[[:space:]]*$/ && prev ~ /^\|/ {
104+
line = prev
105+
gsub(/`[^`]*`/, "", line) # quoted material, as above
106+
n = split(line, cells, "|")
107+
for (i = 2; i < n; i++) {
108+
c = cells[i]
109+
gsub(/^[[:space:]]+|[[:space:]]+$/, "", c)
110+
if (c != "") print c
111+
}
112+
}
113+
{ prev = $0 }
114+
' "$f")
115+
59116
# ── 2. second person in reference docs ────────────────────────────────
60117
case " $TUTORIALS " in
61118
*" $base "*) ;;

0 commit comments

Comments
 (0)