@chengzhao-dev/dsh-quarto-book 0.6.17 → 0.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +3 -3
- package/lib/tools/pass-report.js +6 -5
- package/lib/tools/render-check.js +1 -1
- package/package.json +2 -2
- package/skills/designing-theme/SKILL.md +24 -12
- package/skills/writing-quarto/SKILL.md +52 -39
- package/skills/writing-quarto/examples/02-structure-shape.md +1 -1
- package/skills/writing-quarto/examples/03-excerpt-lead.md +19 -0
- package/skills/writing-quarto/examples/04-paragraph-cases.md +17 -13
- package/skills/writing-quarto/examples/06-wording-cases.md +1 -1
- package/skills/writing-quarto/examples/07-layout-list.md +17 -1
- package/skills/writing-quarto/quarto/authoring.md +3 -3
- package/skills/writing-quarto/quarto/comments.md +1 -0
- package/skills/writing-quarto/quarto/coords.md +1 -1
- package/skills/writing-quarto/quarto/prose.md +2 -3
- package/skills/writing-quarto/quarto/refactor.md +3 -3
- package/skills/writing-quarto/quarto/terminal.md +2 -0
- package/skills/writing-quarto/references/book-journey.md +14 -14
- package/skills/writing-quarto/references/fence-semantics.md +6 -6
- package/skills/writing-quarto/references/personas.md +15 -0
- package/skills/writing-quarto/references/rules-cards.md +3 -3
- package/skills/writing-quarto/references/token-budget.md +2 -2
- package/skills/writing-quarto/scripts/testing-content.py +83 -2
- package/skills/writing-quarto/scripts/verifying-content.py +251 -4
- package/skills/writing-quarto/steps/01-task.md +25 -42
- package/skills/writing-quarto/steps/02-structure.md +29 -54
- package/skills/writing-quarto/steps/03-blocks.md +112 -133
- package/skills/writing-quarto/steps/04-paragraphs.md +2 -1
- package/skills/writing-quarto/steps/05-sentences.md +45 -70
- package/skills/writing-quarto/steps/06-wording.md +11 -9
- package/skills/writing-quarto/steps/07-layout.md +3 -1
- package/skills/writing-quarto/steps/08-reread.md +9 -26
- package/skills/writing-quarto/steps/09-check.md +4 -3
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
- 步骤:`steps/02-structure.md`
|
|
54
54
|
|
|
55
55
|
**C10 章节按五步走。** 现象 → 最小例子 → 机制 → 边界 → 自测。
|
|
56
|
-
-
|
|
56
|
+
- 正:章文件依次讲空棋盘、落子、越界、判胜、自测。
|
|
57
57
|
- 反:先给完整类声明,再逐行解释。
|
|
58
58
|
- 步骤:`steps/02-structure.md`
|
|
59
59
|
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
- 步骤:`steps/03-blocks.md`
|
|
64
64
|
|
|
65
65
|
**C12 一节一个关键。** 一章只解决一个问题,多出来的挪到下一章。
|
|
66
|
-
-
|
|
67
|
-
-
|
|
66
|
+
- 正:讲棋盘的章只讲棋盘;游戏流程留给下一章。
|
|
67
|
+
- 反:讲棋盘的章顺手把 `Game` 的状态机也讲了。
|
|
68
68
|
- 步骤:`steps/02-structure.md`
|
|
69
69
|
|
|
70
70
|
**C13 章首导语 60–146 字。** 一段说清这章解决什么、读者读完能做什么。
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
| `assets/**/meta.md` | 2000 |
|
|
19
19
|
| 全部技能 Markdown 合计 | 140000 |
|
|
20
20
|
|
|
21
|
-
单次激活上限:`SKILL.md` + 1 个步骤 + 1
|
|
21
|
+
单次激活上限:`SKILL.md` + 1 个步骤 + 1 个被点名文件(案例、条文或脚本路径)。**九步同走**只用于新章、整章改写或用户明确要求全章处理;同轮不把同号案例与条文一起打开。
|
|
22
22
|
|
|
23
23
|
## 怎么用
|
|
24
24
|
|
|
25
25
|
- 新增规则先判断属于哪一层,再写进已有文件;同一判定只成文一处,其他位置写引用。
|
|
26
26
|
- 文件将超限时,先删重复、合并同源条目,再考虑拆成同目录的新文件;拆文件后同步入口和 `scripts/checking-skill-links.mjs`。
|
|
27
|
-
-
|
|
27
|
+
- 单点修改只加载 `SKILL.md`、当前一步与该步点名的一个文件;不要为了省 token 把整本书的规则塞进 `SKILL.md`。
|
|
28
28
|
- 预算不是目标:没必要的条文直接删,不靠缩写把句子压成暗语。
|
|
@@ -122,7 +122,7 @@ title: 入门
|
|
|
122
122
|
下面是一张只画类型和边的类图,读者可以对照箭头理解类型之间的依赖。
|
|
123
123
|
|
|
124
124
|
::: {.class-diagram}
|
|
125
|
-
```{mermaid}
|
|
125
|
+
```{mermaid role="img" aria-label="类型 A 依赖类型 B"}
|
|
126
126
|
%%| fig-cap: "类型 A 依赖类型 B。实线箭头从使用者指向被使用者。"
|
|
127
127
|
%%{init: {"fontFamily": "LXGW Bright Medium, system-ui, sans-serif", "class": {"hideEmptyMembersBox": true}}}%%
|
|
128
128
|
classDiagram
|
|
@@ -178,12 +178,17 @@ def main() -> int:
|
|
|
178
178
|
needle: str,
|
|
179
179
|
project: bool = False,
|
|
180
180
|
checks_json: str | None = None,
|
|
181
|
+
files: dict[str, str] | None = None,
|
|
181
182
|
) -> None:
|
|
182
183
|
nonlocal passed_cases
|
|
183
184
|
passed_cases += 1
|
|
184
185
|
with tempfile.TemporaryDirectory() as raw:
|
|
185
186
|
root = Path(raw)
|
|
186
187
|
make_book(root, chapter)
|
|
188
|
+
for relative, content in (files or {}).items():
|
|
189
|
+
target = root / relative
|
|
190
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
191
|
+
target.write_text(content, encoding="utf-8")
|
|
187
192
|
if project:
|
|
188
193
|
skill_dir = root / ".agents" / "skills" / "quarto-book-project"
|
|
189
194
|
skill_dir.mkdir(parents=True)
|
|
@@ -293,6 +298,18 @@ def main() -> int:
|
|
|
293
298
|
"---\ntitle: 入门\n---\n\n## 目标\n\n```mermaid\ngraph LR\n```\n",
|
|
294
299
|
1, "plain mermaid",
|
|
295
300
|
)
|
|
301
|
+
expect(
|
|
302
|
+
"mermaid 围栏带前导点",
|
|
303
|
+
"---\ntitle: 入门\n---\n\n## 目标\n\n"
|
|
304
|
+
'```{.mermaid role="img" aria-label="流程"}\ngraph LR\n```\n',
|
|
305
|
+
1, "不会载入图表脚本",
|
|
306
|
+
)
|
|
307
|
+
expect(
|
|
308
|
+
"mermaid 围栏正确写法",
|
|
309
|
+
"---\ntitle: 入门\n---\n\n## 目标\n\n"
|
|
310
|
+
'```{mermaid role="img" aria-label="流程"}\ngraph LR\n```\n',
|
|
311
|
+
0, "OK",
|
|
312
|
+
)
|
|
296
313
|
expect(
|
|
297
314
|
"未声明围栏",
|
|
298
315
|
"---\ntitle: 入门\n---\n\n## 目标\n\n```\nprint(1)\n```\n",
|
|
@@ -316,6 +333,67 @@ def main() -> int:
|
|
|
316
333
|
0, "OK",
|
|
317
334
|
)
|
|
318
335
|
|
|
336
|
+
excerpt_url = "https://github.com/your-org/your-book/blob/main/games/board.h"
|
|
337
|
+
excerpt_source = (
|
|
338
|
+
"/// 判断两个坐标是否相同。\n"
|
|
339
|
+
"///\n"
|
|
340
|
+
"/// @param lhs 左操作数。\n"
|
|
341
|
+
"/// @return 相同返回 true。\n"
|
|
342
|
+
"bool Same(int lhs, int rhs);\n"
|
|
343
|
+
)
|
|
344
|
+
excerpt_chapter = VALID_CHAPTER + (
|
|
345
|
+
f"\n声明见 [board.h]({excerpt_url})。\n\n"
|
|
346
|
+
)
|
|
347
|
+
expect(
|
|
348
|
+
"正文讲解文件头",
|
|
349
|
+
VALID_CHAPTER + "\n每个源文件开头都有一段 Doxygen 文件头。\n",
|
|
350
|
+
1, "文件级",
|
|
351
|
+
)
|
|
352
|
+
expect(
|
|
353
|
+
"围栏摘录 @file",
|
|
354
|
+
excerpt_chapter
|
|
355
|
+
+ "```{.cpp filename=\"board.h\"}\n/// @file board.h\n/// @brief 棋盘。\n```\n",
|
|
356
|
+
1, "文件级",
|
|
357
|
+
)
|
|
358
|
+
expect(
|
|
359
|
+
"围栏注释截断",
|
|
360
|
+
excerpt_chapter
|
|
361
|
+
+ "```{.cpp filename=\"board.h\"}\n"
|
|
362
|
+
"/// 判断两个坐标是否相同。\n"
|
|
363
|
+
"bool Same(int lhs, int rhs);\n"
|
|
364
|
+
"```\n",
|
|
365
|
+
1, "只摘了",
|
|
366
|
+
files={"games/board.h": excerpt_source},
|
|
367
|
+
)
|
|
368
|
+
expect(
|
|
369
|
+
"围栏注释整段一致",
|
|
370
|
+
excerpt_chapter
|
|
371
|
+
+ "```{.cpp filename=\"board.h\"}\n"
|
|
372
|
+
+ excerpt_source
|
|
373
|
+
+ "```\n",
|
|
374
|
+
0, "OK",
|
|
375
|
+
files={"games/board.h": excerpt_source},
|
|
376
|
+
)
|
|
377
|
+
expect(
|
|
378
|
+
"围栏整段不摘注释",
|
|
379
|
+
excerpt_chapter
|
|
380
|
+
+ "```{.cpp filename=\"board.h\"}\n"
|
|
381
|
+
"bool Same(int lhs, int rhs);\n"
|
|
382
|
+
"```\n",
|
|
383
|
+
0, "OK",
|
|
384
|
+
files={"games/board.h": excerpt_source},
|
|
385
|
+
)
|
|
386
|
+
expect(
|
|
387
|
+
"输出围栏不比对注释",
|
|
388
|
+
VALID_CHAPTER
|
|
389
|
+
+ "\n```text filename=\"运行效果\"\n"
|
|
390
|
+
"/// 判断两个坐标是否相同。\n"
|
|
391
|
+
"bool Same(int lhs, int rhs);\n"
|
|
392
|
+
"```\n",
|
|
393
|
+
0, "OK",
|
|
394
|
+
files={"games/board.h": excerpt_source},
|
|
395
|
+
)
|
|
396
|
+
|
|
319
397
|
expect("目录树合格", TREE_CHAPTER, 0, "OK")
|
|
320
398
|
expect("原生 SVG 缺图注", HTML_FIGURE_BAD, 1, "figcaption")
|
|
321
399
|
expect("原生 SVG 图注合格", HTML_FIGURE_GOOD, 0, "OK")
|
|
@@ -379,7 +457,10 @@ def main() -> int:
|
|
|
379
457
|
expect("类图图注写在 init 之后", wrong_order, 1, "init 之前")
|
|
380
458
|
expect(
|
|
381
459
|
"类图缺少页面容器",
|
|
382
|
-
MERMAID_CHAPTER.replace(
|
|
460
|
+
MERMAID_CHAPTER.replace(
|
|
461
|
+
'::: {.class-diagram}\n```{mermaid role="img" aria-label="类型 A 依赖类型 B"}',
|
|
462
|
+
'```{mermaid role="img" aria-label="类型 A 依赖类型 B"}',
|
|
463
|
+
).replace(
|
|
383
464
|
"```\n:::", "```"
|
|
384
465
|
),
|
|
385
466
|
1, ".class-diagram",
|
|
@@ -78,6 +78,12 @@ TEST_RE = re.compile(r"测试|断言|用例|gtest|CTest")
|
|
|
78
78
|
# 围栏与正文的配对:声明围栏只配契约文字。正文出现实现词而小节里没有定义围栏时
|
|
79
79
|
# 报 decl-without-def,规则与正反例见 references/fence-semantics.md。
|
|
80
80
|
FENCE_FILENAME_RE = re.compile(r'filename="([^"]+)"')
|
|
81
|
+
# 读者正文不摘文件级 Doxygen 头;成员注释的 @param、@return 不受这条限制。
|
|
82
|
+
EXCERPT_HEADER_RE = re.compile(r"@file\b|@brief\b|Doxygen\s*文件头")
|
|
83
|
+
REF_DEFINITION_RE = re.compile(r"^\s*\[([^\]]+)\]:\s*(\S+)\s*$")
|
|
84
|
+
REF_USE_RE = re.compile(r"\]\[([^\]]+)\]")
|
|
85
|
+
BLOB_URL_RE = re.compile(r"^https://github\.com/[^/]+/[^/]+/blob/[^/]+/(.+)$")
|
|
86
|
+
DOC_COMMENT_RE = re.compile(r"^\s*///")
|
|
81
87
|
IMPL_MARKER_RE = re.compile(r"复用|委托给|扫描|遍历|对每个|短路|表达式|状态位|缓存|不再单独|递归")
|
|
82
88
|
FENCE_SKIP_SECTION_RE = re.compile(r"常见错误|自测|回顾|练习|小结")
|
|
83
89
|
DECL_SUFFIXES = (".h", ".hpp", ".hh", ".hxx")
|
|
@@ -105,6 +111,8 @@ HEADING_PERSONAL_RE = re.compile(r"自己|咱们|我们|你|您|跟我|试试|
|
|
|
105
111
|
FENCE_RE = re.compile(r"^\s*(```+|~~~+)\s*(\{[^}]*\}|[^\s`]*)?")
|
|
106
112
|
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)\)")
|
|
107
113
|
LINK_LABEL_RE = re.compile(r"\[([^\]]*)\]\([^)]*\)")
|
|
114
|
+
LIST_LINE_RE = re.compile(r"^\s*(?:[-*+]|\d+\.)\s+")
|
|
115
|
+
GUIDED_ITEM_RE = re.compile(r"^\*\*[^*\n]{1,30}\*\*:")
|
|
108
116
|
CJK_RE = re.compile(r"[\u3400-\u9fff]")
|
|
109
117
|
TITLE_RE = re.compile(r"^title:\s*(.+?)\s*$", re.MULTILINE)
|
|
110
118
|
|
|
@@ -295,6 +303,70 @@ def check_prose_block(
|
|
|
295
303
|
))
|
|
296
304
|
break
|
|
297
305
|
|
|
306
|
+
if not is_list and CJK_RE.search(compact):
|
|
307
|
+
sentences = [
|
|
308
|
+
sentence.strip()
|
|
309
|
+
for sentence in re.split(r"(?<=[。!?])", compact)
|
|
310
|
+
if sentence.strip()
|
|
311
|
+
]
|
|
312
|
+
seen_names: set[str] = set()
|
|
313
|
+
for index, sentence in enumerate(sentences):
|
|
314
|
+
if index > 0:
|
|
315
|
+
if sentence.startswith("它") and len(sentence) > 1 and sentence[1] != "们":
|
|
316
|
+
issues.append(Issue(
|
|
317
|
+
path, start_line,
|
|
318
|
+
"过简断裂:「它」开头的句子没有接住前句句末的对象;写回原对象名",
|
|
319
|
+
))
|
|
320
|
+
break
|
|
321
|
+
lead = INLINE_NAME_RE.match(sentence)
|
|
322
|
+
if lead and lead.group(1) not in seen_names:
|
|
323
|
+
issues.append(Issue(
|
|
324
|
+
path, start_line,
|
|
325
|
+
f"过简断裂:后句以新标识符「{lead.group(1)}」开头,前句没有承接;先用回指词接住",
|
|
326
|
+
))
|
|
327
|
+
break
|
|
328
|
+
for match in INLINE_NAME_RE.finditer(sentence):
|
|
329
|
+
seen_names.add(match.group(1))
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def visible_length(text: str) -> int:
|
|
333
|
+
visible = LINK_LABEL_RE.sub(r"\1", text)
|
|
334
|
+
visible = re.sub(r"[*`]", "", visible)
|
|
335
|
+
return len(re.sub(r"\s+", "", visible))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def check_list_block(
|
|
339
|
+
path: Path, items: list[tuple[int, str]], issues: list[Issue], landing: bool = False
|
|
340
|
+
) -> None:
|
|
341
|
+
if len(items) < 2:
|
|
342
|
+
return
|
|
343
|
+
guided = [bool(GUIDED_ITEM_RE.match(text)) for _, text in items]
|
|
344
|
+
if any(guided) and not all(guided):
|
|
345
|
+
issues.append(Issue(
|
|
346
|
+
path, items[0][0],
|
|
347
|
+
"同一列表混用普通句子和「**引导词**:」句式;统一成多数项使用的那一种",
|
|
348
|
+
))
|
|
349
|
+
return
|
|
350
|
+
lengths = [
|
|
351
|
+
(line, visible_length(text))
|
|
352
|
+
for line, text in items
|
|
353
|
+
if text.strip()
|
|
354
|
+
]
|
|
355
|
+
lengths = [(line, length) for line, length in lengths if length > 0]
|
|
356
|
+
if len(lengths) < 2 or landing:
|
|
357
|
+
return
|
|
358
|
+
joined = "".join(text for _, text in items)
|
|
359
|
+
if "出错的原因" in joined and "正确的做法" in joined:
|
|
360
|
+
return
|
|
361
|
+
longest = max(lengths, key=lambda pair: pair[1])
|
|
362
|
+
shortest = min(lengths, key=lambda pair: pair[1])
|
|
363
|
+
if longest[1] < 2 * shortest[1]:
|
|
364
|
+
return
|
|
365
|
+
issues.append(Issue(
|
|
366
|
+
path, longest[0],
|
|
367
|
+
f"列表项字数失衡:最长 {longest[1]} 字,最短 {shortest[1]} 字;删长项多余中心或把长项收成一句",
|
|
368
|
+
))
|
|
369
|
+
|
|
298
370
|
|
|
299
371
|
def display_width(text: str) -> int:
|
|
300
372
|
return sum(2 if unicodedata.east_asian_width(char) in {"W", "F"} else 1 for char in text)
|
|
@@ -653,6 +725,159 @@ def check_fence_semantics(path: Path, lines: list[str]) -> list[Issue]:
|
|
|
653
725
|
return issues
|
|
654
726
|
|
|
655
727
|
|
|
728
|
+
def normalize_code(text: str) -> str:
|
|
729
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def collect_excerpt_links(lines: list[str]) -> list[tuple[int, str]]:
|
|
733
|
+
"""Return GitHub blob links in reading order, including reference-style uses."""
|
|
734
|
+
definitions: dict[str, str] = {}
|
|
735
|
+
for raw in lines:
|
|
736
|
+
definition = REF_DEFINITION_RE.match(raw)
|
|
737
|
+
if definition:
|
|
738
|
+
definitions.setdefault(definition.group(1), definition.group(2))
|
|
739
|
+
links: list[tuple[int, str]] = []
|
|
740
|
+
for number, raw in enumerate(lines, start=1):
|
|
741
|
+
for match in LINK_RE.finditer(raw):
|
|
742
|
+
links.append((number, match.group(1).strip()))
|
|
743
|
+
for match in REF_USE_RE.finditer(raw):
|
|
744
|
+
target = definitions.get(match.group(1))
|
|
745
|
+
if target:
|
|
746
|
+
links.append((number, target))
|
|
747
|
+
links.sort()
|
|
748
|
+
return links
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def excerpt_fence_blocks(lines: list[str]) -> list[tuple[int, str, list[tuple[int, str]]]]:
|
|
752
|
+
"""Return (opening line, filename, body) for every fence with a filename."""
|
|
753
|
+
blocks: list[tuple[int, str, list[tuple[int, str]]]] = []
|
|
754
|
+
in_fence = False
|
|
755
|
+
start = 0
|
|
756
|
+
filename = ""
|
|
757
|
+
body: list[tuple[int, str]] = []
|
|
758
|
+
for number, raw in enumerate(lines, start=1):
|
|
759
|
+
fence = FENCE_RE.match(raw)
|
|
760
|
+
if fence:
|
|
761
|
+
if not in_fence:
|
|
762
|
+
in_fence = True
|
|
763
|
+
start = number
|
|
764
|
+
opening = FENCE_FILENAME_RE.search(raw)
|
|
765
|
+
filename = opening.group(1) if opening else ""
|
|
766
|
+
body = []
|
|
767
|
+
else:
|
|
768
|
+
in_fence = False
|
|
769
|
+
if filename:
|
|
770
|
+
blocks.append((start, filename, body))
|
|
771
|
+
filename = ""
|
|
772
|
+
body = []
|
|
773
|
+
continue
|
|
774
|
+
if in_fence:
|
|
775
|
+
body.append((number, raw))
|
|
776
|
+
return blocks
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
def source_doc_above(source: list[str], index: int) -> list[str]:
|
|
780
|
+
"""Collect the contiguous `///` block that sits directly above index."""
|
|
781
|
+
block: list[str] = []
|
|
782
|
+
cursor = index - 1
|
|
783
|
+
while cursor >= 0:
|
|
784
|
+
raw = source[cursor]
|
|
785
|
+
if not DOC_COMMENT_RE.match(raw) or raw.lstrip().startswith("///<"):
|
|
786
|
+
break
|
|
787
|
+
block.append(raw)
|
|
788
|
+
cursor -= 1
|
|
789
|
+
block.reverse()
|
|
790
|
+
return block
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def check_excerpts(path: Path, root: Path, lines: list[str]) -> list[Issue]:
|
|
794
|
+
"""Reader text carries no file headers; kept doc comments match the source."""
|
|
795
|
+
issues: list[Issue] = []
|
|
796
|
+
for number, raw in enumerate(lines, start=1):
|
|
797
|
+
if EXCERPT_HEADER_RE.search(raw):
|
|
798
|
+
issues.append(Issue(
|
|
799
|
+
path, number,
|
|
800
|
+
"读者正文不摘文件级 Doxygen 头(@file、@brief、文件范围段);"
|
|
801
|
+
"删掉这段,围栏从它之后开始",
|
|
802
|
+
))
|
|
803
|
+
|
|
804
|
+
links = collect_excerpt_links(lines)
|
|
805
|
+
for start, filename, body in excerpt_fence_blocks(lines):
|
|
806
|
+
candidates = [
|
|
807
|
+
(number, target)
|
|
808
|
+
for number, target in links
|
|
809
|
+
if number < start and target.rsplit("/", 1)[-1] == filename
|
|
810
|
+
]
|
|
811
|
+
if not candidates:
|
|
812
|
+
continue
|
|
813
|
+
target = candidates[-1][1]
|
|
814
|
+
blob = BLOB_URL_RE.match(target)
|
|
815
|
+
if not blob:
|
|
816
|
+
continue
|
|
817
|
+
relative = blob.group(1)
|
|
818
|
+
source_path = (root / relative).resolve()
|
|
819
|
+
if root not in source_path.parents or not source_path.is_file():
|
|
820
|
+
continue
|
|
821
|
+
source = read_text(source_path).splitlines()
|
|
822
|
+
|
|
823
|
+
pending: list[tuple[int, str]] = []
|
|
824
|
+
pending_start = start
|
|
825
|
+
for number, raw in body:
|
|
826
|
+
stripped = raw.strip()
|
|
827
|
+
if DOC_COMMENT_RE.match(raw) and not stripped.startswith("///<"):
|
|
828
|
+
if not pending:
|
|
829
|
+
pending_start = number
|
|
830
|
+
pending.append((number, raw))
|
|
831
|
+
continue
|
|
832
|
+
if not pending:
|
|
833
|
+
continue
|
|
834
|
+
if not stripped:
|
|
835
|
+
pending = []
|
|
836
|
+
continue
|
|
837
|
+
declaration = normalize_code(raw)
|
|
838
|
+
fence_block = [item.strip() for _, item in pending]
|
|
839
|
+
pending = []
|
|
840
|
+
if not declaration or declaration.startswith(("#", "}", "{", "...")):
|
|
841
|
+
continue
|
|
842
|
+
matches = [
|
|
843
|
+
index for index, item in enumerate(source)
|
|
844
|
+
if normalize_code(item) == declaration
|
|
845
|
+
]
|
|
846
|
+
if not matches:
|
|
847
|
+
continue
|
|
848
|
+
# 同一行声明可能在多个类里重复(如各实现类的 override);任一处注释
|
|
849
|
+
# 与围栏整段一致即通过,避免把第一个同名声明当成唯一出处。
|
|
850
|
+
source_blocks = [
|
|
851
|
+
[item.strip() for item in source_doc_above(source, index)]
|
|
852
|
+
for index in matches
|
|
853
|
+
]
|
|
854
|
+
if any(block == fence_block for block in source_blocks):
|
|
855
|
+
continue
|
|
856
|
+
if not fence_block:
|
|
857
|
+
continue
|
|
858
|
+
prefixes = [
|
|
859
|
+
block for block in source_blocks
|
|
860
|
+
if len(block) > len(fence_block) and block[: len(fence_block)] == fence_block
|
|
861
|
+
]
|
|
862
|
+
if prefixes:
|
|
863
|
+
issues.append(Issue(
|
|
864
|
+
path, pending_start,
|
|
865
|
+
f"围栏只摘了 {filename} 注释的前 {len(fence_block)} 行;"
|
|
866
|
+
"整段补上 @param、@return 与空 /// 行,或整段不摘",
|
|
867
|
+
))
|
|
868
|
+
elif all(not block for block in source_blocks):
|
|
869
|
+
issues.append(Issue(
|
|
870
|
+
path, pending_start,
|
|
871
|
+
f"围栏带有注释,但 {filename} 对应声明上方没有 /// 注释;整段删掉",
|
|
872
|
+
))
|
|
873
|
+
else:
|
|
874
|
+
issues.append(Issue(
|
|
875
|
+
path, pending_start,
|
|
876
|
+
f"围栏注释与 {filename} 对应声明上方的不一致;整段照抄或整段不摘",
|
|
877
|
+
))
|
|
878
|
+
return issues
|
|
879
|
+
|
|
880
|
+
|
|
656
881
|
def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> list[Issue]:
|
|
657
882
|
issues: list[Issue] = []
|
|
658
883
|
text = read_text(path)
|
|
@@ -682,7 +907,9 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
682
907
|
fence_is_html = False
|
|
683
908
|
paragraph: list[str] = []
|
|
684
909
|
paragraph_start = 0
|
|
910
|
+
list_items: list[tuple[int, str]] = []
|
|
685
911
|
last_fence_end: int | None = None
|
|
912
|
+
landing = path.resolve() == (root / "index.qmd").resolve()
|
|
686
913
|
|
|
687
914
|
def flush_paragraph() -> None:
|
|
688
915
|
nonlocal paragraph
|
|
@@ -690,10 +917,17 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
690
917
|
check_prose_block(path, paragraph_start, " ".join(paragraph), issues)
|
|
691
918
|
paragraph = []
|
|
692
919
|
|
|
920
|
+
def flush_list() -> None:
|
|
921
|
+
nonlocal list_items
|
|
922
|
+
if len(list_items) >= 2:
|
|
923
|
+
check_list_block(path, list_items, issues, landing=landing)
|
|
924
|
+
list_items = []
|
|
925
|
+
|
|
693
926
|
for number, raw in enumerate(lines, start=1):
|
|
694
927
|
fence = FENCE_RE.match(raw)
|
|
695
928
|
if fence:
|
|
696
929
|
flush_paragraph()
|
|
930
|
+
flush_list()
|
|
697
931
|
if not in_fence:
|
|
698
932
|
in_fence = True
|
|
699
933
|
fence_start = number
|
|
@@ -712,7 +946,12 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
712
946
|
elif fence_lang.startswith("{"):
|
|
713
947
|
inner = fence_lang.strip("{}").strip()
|
|
714
948
|
fence_is_html = inner == "=html"
|
|
715
|
-
if inner
|
|
949
|
+
if inner.startswith(".") and "mermaid" in inner:
|
|
950
|
+
issues.append(Issue(
|
|
951
|
+
path, number,
|
|
952
|
+
"Mermaid 围栏写成 `{.mermaid}`,Quarto 不会载入图表脚本,图会退化成代码块;改成 `{mermaid ...}`",
|
|
953
|
+
))
|
|
954
|
+
elif inner and not inner.startswith((".", "=")) and "mermaid" not in inner:
|
|
716
955
|
# ```{python} style attribute fences are valid.
|
|
717
956
|
pass
|
|
718
957
|
else:
|
|
@@ -742,6 +981,7 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
742
981
|
heading = HEADING_RE.match(raw)
|
|
743
982
|
if heading:
|
|
744
983
|
flush_paragraph()
|
|
984
|
+
flush_list()
|
|
745
985
|
level = len(heading.group(1))
|
|
746
986
|
heading_text = heading.group(2)
|
|
747
987
|
if level == 1:
|
|
@@ -760,26 +1000,33 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
760
1000
|
|
|
761
1001
|
if raw.lstrip().startswith("|"):
|
|
762
1002
|
flush_paragraph()
|
|
1003
|
+
flush_list()
|
|
763
1004
|
# 表格行按列组织,不套散文句长阈值;列数与表头一致性由表格检查负责。
|
|
764
1005
|
continue
|
|
765
1006
|
|
|
766
1007
|
if raw.lstrip().startswith((">", "-", "*", "+", "1.")):
|
|
767
1008
|
flush_paragraph()
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1009
|
+
item_text = re.sub(r"^\s*(?:[>|]|[-*+]|\d+\.)\s*", "", raw)
|
|
1010
|
+
if LIST_LINE_RE.match(raw):
|
|
1011
|
+
list_items.append((number, item_text))
|
|
1012
|
+
else:
|
|
1013
|
+
flush_list()
|
|
1014
|
+
check_prose_block(path, number, item_text, issues, is_list=True)
|
|
771
1015
|
continue
|
|
772
1016
|
|
|
1017
|
+
flush_list()
|
|
773
1018
|
if not paragraph:
|
|
774
1019
|
paragraph_start = number
|
|
775
1020
|
paragraph.append(raw.strip())
|
|
776
1021
|
|
|
777
1022
|
flush_paragraph()
|
|
1023
|
+
flush_list()
|
|
778
1024
|
|
|
779
1025
|
if in_fence:
|
|
780
1026
|
issues.append(Issue(path, fence_start, "代码围栏未闭合"))
|
|
781
1027
|
|
|
782
1028
|
issues.extend(check_fence_semantics(path, lines))
|
|
1029
|
+
issues.extend(check_excerpts(path, root, lines))
|
|
783
1030
|
|
|
784
1031
|
for block in fence_blocks:
|
|
785
1032
|
issues.extend(check_tree_block(path, block))
|
|
@@ -1,65 +1,48 @@
|
|
|
1
1
|
# 第 1 步:定任务
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
产出:一句读者做完能完成的事,加上本章要覆盖的任务清单。
|
|
4
|
+
这一步不动标题层级,也不改句子;两条规则冲突时按 `references/priority.md` 排级,本步先让位给读者任务与事实。
|
|
5
5
|
|
|
6
6
|
## 通用情况
|
|
7
7
|
|
|
8
|
-
###
|
|
8
|
+
### 读者与术语门槛
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
读者有编程基础,可能第一次接触本书使用的语言、构建工具或自动化测试。每页只解决一个任务;正文按书的实际技术栈写,不预设某一种语言或操作系统,需要环境差异时在页面里就地交代。
|
|
11
|
+
|
|
12
|
+
- 不假设读者知道工具名、命令参数、目录布局或错误输出代表的含义。
|
|
13
|
+
- 标准术语首次出现时用一句话解释或就近术语卡,再进入命令;解释写成可观察行为,缩写先写全称(写法见 `quarto/readability.md`)。
|
|
14
|
+
- 只保留判断成功所需的输出,不解释对当前操作没有影响的内部格式。
|
|
15
|
+
- 面向新人的正文不堆砌入门前置名词;标题只用读者此刻已认识的词。
|
|
11
16
|
|
|
12
17
|
### 叙事范围(硬边界)
|
|
13
18
|
|
|
14
|
-
本书定位是可用工程的用法与验收;编程语言机制的系统讲解不属于正文。`content/**`
|
|
19
|
+
本书定位是可用工程的用法与验收;编程语言机制的系统讲解不属于正文。`content/**` 正文只写五类内容:领域语义、分层与依赖、接口契约、构建与测试验收、可验证动作。
|
|
15
20
|
|
|
16
|
-
-
|
|
21
|
+
- **语言机制与风格指南的取舍先看项目技能**:项目技能写明禁止时,正文不展开枚举、结构体、命名空间、`inline`、`constexpr`、翻译单元、ODR,也不逐条教命名大小写、前缀或 include guard。项目没写时按读者门槛判断,不要默认开语言课。
|
|
17
22
|
- 枚举取值列表的位置见 `steps/03-blocks.md`「三级标题最小模块」。
|
|
18
23
|
- **禁止**在工程章(非设计章)展开数值选型旁白:不写「用有符号整型承载…」「某字段为什么选定宽有符号整型…」,也不为选型链到外部语言笔记。`int` 可出现在代码摘录中,旁白不解释「为什么有符号」。
|
|
19
|
-
- **禁止课堂类比**:工程坐标系、API
|
|
20
|
-
- **禁止突兀跨仓依据句**:不写「写法依据见 / 依据见 / 机制见 / 选型见 / 承诺见 / 用法见 / 陷阱见」+
|
|
21
|
-
-
|
|
24
|
+
- **禁止课堂类比**:工程坐标系、API 与数据结构只用程序事实与可观察行为表述,不写「与数学课的直角坐标系正好相反」这类跨学科课堂对照;图注已交代原点与轴向的,正文不复述对照。
|
|
25
|
+
- **禁止突兀跨仓依据句**:不写「写法依据见 / 依据见 / 机制见 / 选型见 / 承诺见 / 用法见 / 陷阱见」+ 外部语言笔记。工程约定由本书拥有,外部教材不是工程写法的依据机关。
|
|
26
|
+
- **跨仓外链默认零条**:工程叙事须自洽。仅当本小节工程点已讲完、且确有系统学语法需求时,可在小节末单独成句尾置可选深潜,口吻为「若要系统阅读…,见对应章节(GitHub 绝对 URL)」,同一章宜 ≤2,能删则删;仅设计章可保留一次分册级链接。本书固定采用外部章节的构建约定时,用「本书固定采用…」陈述,仍放工程点之后。
|
|
22
27
|
- **允许**代码块逐字展示源码(关键字照抄真实代码);旁白回答工程问题:这段公共定义解决什么问题、谁读取或调用它、如何验证。
|
|
23
|
-
-
|
|
28
|
+
- **禁止接口清单压缩段**:不把私有成员、公开成员名单、`const`、`[[nodiscard]]` 等接口属性压进同一段旁白;定稿形态为场景(动作 → 点名 → 边界)→ 代码 → 块后只补注释未覆盖的工程信息(对照见 `examples/04-paragraph-opening.md`「接口清单压缩段」)。
|
|
24
29
|
- 判定口诀:句子的主语应是工程对象(模块、接口、数据、文件),不是语言构造(`enum class`、翻译单元)。反例与改写对照见 `examples/01-language-scope.md` 与 `examples/04-paragraph-cases.md`。
|
|
25
30
|
|
|
26
|
-
###
|
|
27
|
-
|
|
28
|
-
章节出现取舍时,按下面的顺序判断,不用较低层级的格式或样式要求覆盖较高层级的教学目标:
|
|
29
|
-
|
|
30
|
-
1. 读者任务与章节边界
|
|
31
|
-
2. 信息顺序和渐进披露
|
|
32
|
-
3. 页面块与段落布局
|
|
33
|
-
4. 技术正确性和可验证性
|
|
34
|
-
5. 标题、列表、Callout 和代码块等表达形式
|
|
35
|
-
6. 句子精简、术语和标点
|
|
36
|
-
7. 视觉样式与装饰
|
|
37
|
-
|
|
38
|
-
新增内容先确定它在既有页面块序列中的位置,再写入相邻小节。修改后回读完整小节及前后切口,确认动作、结果和下一步仍然相邻。只有步骤存在先后依赖时使用有序列表;同级选项使用无序列表;连续因果解释保留为短段落。
|
|
31
|
+
### 定任务的做法
|
|
39
32
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
|
|
48
|
-
### 定任务:先让新人完成任务
|
|
49
|
-
|
|
50
|
-
- 开篇场景的写法见 `steps/02-structure.md`「六段块序列」。
|
|
51
|
-
- 每页一条成功路径与后置内容的判定见 `steps/06-wording.md`;主线闭合见 `steps/02-structure.md`。
|
|
52
|
-
- 标准术语的首次解释与术语卡落点见 `steps/02-structure.md`(写法见 `quarto/authoring.md`);缩写先写全称。
|
|
53
|
-
- 不假设读者知道工具名、命令参数、目录布局或错误输出代表的含义。
|
|
54
|
-
- 目录式堆砌禁令与清单下沉判据见 `steps/02-structure.md`。
|
|
55
|
-
- 标题与引言的单事承诺、承诺用词与 title 变更同步见 `steps/02-structure.md`。
|
|
56
|
-
- 语言课禁令、否定式预告、不写「下一节」都在这一步判:它们决定本章能不能出现某句,不是措辞问题(细则见 `steps/05-sentences.md`「句子规则」、本文件「叙事范围」)。
|
|
33
|
+
- 任务写成读者做完能完成的一件事,不写成语法清单或功能列表;每页一条成功路径。
|
|
34
|
+
- 开篇场景的写法见 `steps/02-structure.md`「六段块序列」;标准术语的首次解释与术语卡落点同见该节(写法见 `quarto/authoring.md`)。
|
|
35
|
+
- 目录式堆砌禁令与清单下沉判据见 `steps/02-structure.md`;标题与引言的单事承诺、承诺用词与 title 变更同步同见该文件。
|
|
36
|
+
- 语言课禁令、否定式预告、不写「下一节」在这一步判:它们决定本章能不能出现某句,不是措辞问题(细则见 `steps/05-sentences.md`「句子规则」与本文件「叙事范围(硬边界)」)。
|
|
37
|
+
- 新增内容先确定它在既有页面块序列中的位置,再写入相邻小节;修改后回读完整小节及前后切口,确认动作、结果和下一步仍然相邻。
|
|
38
|
+
- 只有步骤存在先后依赖时用有序列表;同级选项用无序列表;连续因果解释保留为短段落。
|
|
39
|
+
- 目录树先核对磁盘,再只保留当前任务需要的文件,注释与读法句见 `steps/03-blocks.md`「情况:目录树」。GitHub blob 链接嵌进同节导语句子,不单独成行贴在围栏上方。线性构建流程用标题和文字说明,不用 Mermaid 代替。
|
|
57
40
|
|
|
58
41
|
## 情况
|
|
59
42
|
|
|
60
43
|
### 情况:语言课
|
|
61
44
|
|
|
62
|
-
|
|
45
|
+
讲语言特性时,任务写成「读者能读懂一段真实代码」,不写成语法清单;叙事范围跟着读者的语言门槛收窄。对照 `examples/01-language-scope.md`。
|
|
63
46
|
|
|
64
47
|
### 情况:否定式预告
|
|
65
48
|
|
|
@@ -71,7 +54,7 @@
|
|
|
71
54
|
|
|
72
55
|
### 情况:没有项目技能
|
|
73
56
|
|
|
74
|
-
书里没有 `.agents/skills/quarto-book-project/`
|
|
57
|
+
书里没有 `.agents/skills/quarto-book-project/` 时跳过书名、读者和章节表,不猜也不补;任务只从当前文件与 `_quarto.yml` 读到的内容定。
|
|
75
58
|
|
|
76
59
|
## 正反例
|
|
77
60
|
|