@chengzhao-dev/dsh-quarto-book 0.6.3 → 0.6.5

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.
Files changed (44) hide show
  1. package/README.en.md +3 -3
  2. package/README.md +5 -3
  3. package/lib/index.js +49 -7
  4. package/lib/tools/pass-report.js +12 -12
  5. package/lib/tools/render-check.js +253 -65
  6. package/lib/workspace.js +1 -0
  7. package/locale/en.json +1 -1
  8. package/locale/zh.json +1 -1
  9. package/package.json +9 -6
  10. package/skills/designing-theme/SKILL.md +1 -1
  11. package/skills/designing-theme/assets/theme/css/content.css +31 -8
  12. package/skills/designing-theme/references/theme-spine.md +1 -1
  13. package/skills/designing-theme/scripts/checking-layout.py +11 -3
  14. package/skills/designing-theme/scripts/measuring-pages.mjs +2 -2
  15. package/skills/writing-quarto/SKILL.md +11 -2
  16. package/skills/writing-quarto/examples/03-block-after.md +29 -0
  17. package/skills/writing-quarto/examples/03-excerpt-lead.md +10 -0
  18. package/skills/writing-quarto/examples/03-qmd-elements.md +29 -2
  19. package/skills/writing-quarto/examples/04-paragraph-cases.md +13 -0
  20. package/skills/writing-quarto/examples/07-layout-list.md +34 -0
  21. package/skills/writing-quarto/quarto/authoring.md +5 -4
  22. package/skills/writing-quarto/quarto/basics.md +1 -0
  23. package/skills/writing-quarto/quarto/coords.md +2 -1
  24. package/skills/writing-quarto/quarto/figures.md +49 -0
  25. package/skills/writing-quarto/quarto/mermaid.md +2 -2
  26. package/skills/writing-quarto/quarto/refactor.md +1 -1
  27. package/skills/writing-quarto/quarto/render.md +4 -4
  28. package/skills/writing-quarto/quarto/tables.md +79 -0
  29. package/skills/writing-quarto/references/book-journey.md +38 -0
  30. package/skills/writing-quarto/references/token-budget.md +26 -0
  31. package/skills/writing-quarto/scripts/checking-callouts.py +2 -2
  32. package/skills/writing-quarto/scripts/checking-dom.py +12 -6
  33. package/skills/writing-quarto/scripts/checking-links.py +70 -13
  34. package/skills/writing-quarto/scripts/checking-names.py +3 -1
  35. package/skills/writing-quarto/scripts/checking-output.py +3 -3
  36. package/skills/writing-quarto/scripts/checking-prose.py +11 -7
  37. package/skills/writing-quarto/scripts/testing-content.py +68 -1
  38. package/skills/writing-quarto/scripts/verifying-content.py +187 -35
  39. package/skills/writing-quarto/steps/02-structure.md +1 -1
  40. package/skills/writing-quarto/steps/03-blocks.md +19 -6
  41. package/skills/writing-quarto/steps/04-paragraphs.md +1 -0
  42. package/skills/writing-quarto/steps/06-wording.md +17 -0
  43. package/skills/writing-quarto/steps/07-layout.md +5 -1
  44. package/skills/writing-quarto/steps/08-reread.md +1 -1
@@ -51,6 +51,9 @@ TRAILING_SPACE_RE = re.compile(r"[ ]+$")
51
51
  QA_PROSE_RE = re.compile(r"(?:它|只|都|分别|各自)回答|回答(?:四类|整局|这个|哪些|什么)")
52
52
  CODE_META_RE = re.compile(r"对应的代码")
53
53
  COINED_RELATION_RE = re.compile(r"(?:组成|依赖|继承|调用)边")
54
+ CARRIER_RE = re.compile("承载")
55
+ NEXT_SECTION_RE = re.compile(r"(?:下一节|上一节)")
56
+ LADDER_RE = re.compile(r"(?:分别交给|各说一层)")
54
57
 
55
58
  TREE_CATEGORY_COMMENTS = frozenset({
56
59
  "头文件", "源文件", "源码", "实现", "测试", "配置文件",
@@ -89,7 +92,8 @@ class Issue:
89
92
 
90
93
 
91
94
  def read_text(path: Path) -> str:
92
- return path.read_text(encoding="utf-8", errors="replace")
95
+ # utf-8-sig 兼容带 BOM 的合法 UTF-8 文件;BOM 不进入行号与正文。
96
+ return path.read_text(encoding="utf-8-sig")
93
97
 
94
98
 
95
99
  def strip_frontmatter(text: str) -> tuple[str, str]:
@@ -139,9 +143,9 @@ def parse_chapters(quarto_yml: Path) -> list[str]:
139
143
  if stripped and not raw.startswith((" ", "\t", "-")):
140
144
  in_chapters = False
141
145
  continue
142
- if not stripped.startswith("-"):
146
+ if not stripped:
143
147
  continue
144
- item = stripped[1:].strip()
148
+ item = stripped[1:].strip() if stripped.startswith("-") else stripped
145
149
  key, sep, value = item.partition(":")
146
150
  if sep and re.fullmatch(r"[a-z_]+", key.strip()):
147
151
  # 只接受会落到页面的键:href 与 part 的文件形式。
@@ -153,6 +157,17 @@ def parse_chapters(quarto_yml: Path) -> list[str]:
153
157
  return chapters
154
158
 
155
159
 
160
+ def collect_all_qmd(root: Path) -> list[Path]:
161
+ excluded = {"node_modules", "_book", ".quarto", ".git", "_freeze", "_site"}
162
+ found: list[Path] = []
163
+ for candidate in sorted(root.rglob("*.qmd")):
164
+ rel_parts = candidate.relative_to(root).parts
165
+ if any(part in excluded or part.startswith(".") for part in rel_parts):
166
+ continue
167
+ found.append(candidate)
168
+ return found
169
+
170
+
156
171
  def collect_chapter_files(root: Path, chapters: list[str]) -> list[Path]:
157
172
  found: list[Path] = []
158
173
  for rel in chapters:
@@ -164,15 +179,8 @@ def collect_chapter_files(root: Path, chapters: list[str]) -> list[Path]:
164
179
  found.append(index)
165
180
  if chapters:
166
181
  return found
167
- # No chapter table: audit every tracked .qmd outside generated/vendor dirs.
168
- excluded = {"node_modules", "_book", ".quarto", ".git", "_freeze", "_site"}
169
- for candidate in sorted(root.rglob("*.qmd")):
170
- rel_parts = candidate.relative_to(root).parts
171
- if any(part in excluded or part.startswith(".") for part in rel_parts):
172
- continue
173
- if candidate not in found:
174
- found.append(candidate)
175
- return found
182
+ # No chapter table: audit every .qmd outside generated/vendor dirs.
183
+ return collect_all_qmd(root)
176
184
 
177
185
 
178
186
  def check_heading(path: Path, line: int, level: int, text: str, issues: list[Issue]) -> None:
@@ -425,6 +433,38 @@ def check_tree_block(path: Path, block: list[tuple[int, str]]) -> list[Issue]:
425
433
  return issues
426
434
 
427
435
 
436
+ def check_html_figure_block(path: Path, block: list[tuple[int, str]]) -> list[Issue]:
437
+ """原生 HTML/SVG 图要用 <figure> 包住,<figcaption> 放在图后。"""
438
+ issues: list[Issue] = []
439
+ svg_line: int | None = None
440
+ figure_line: int | None = None
441
+ caption_line: int | None = None
442
+ for number, raw in block:
443
+ if "<figure" in raw and figure_line is None:
444
+ figure_line = number
445
+ if "<svg" in raw and svg_line is None:
446
+ svg_line = number
447
+ if figure_line is None:
448
+ issues.append(Issue(
449
+ path, number,
450
+ "原生 SVG 图要用 <figure> 包住,并在图后写 <figcaption>",
451
+ ))
452
+ if "<figcaption" in raw and caption_line is None:
453
+ caption_line = number
454
+ if svg_line is not None and figure_line is not None:
455
+ if caption_line is None:
456
+ issues.append(Issue(
457
+ path, svg_line,
458
+ "原生 SVG 图缺 <figcaption>(图注要落在图下)",
459
+ ))
460
+ elif caption_line < svg_line:
461
+ issues.append(Issue(
462
+ path, caption_line,
463
+ "<figcaption> 要放在图后,不能放在图前",
464
+ ))
465
+ return issues
466
+
467
+
428
468
  def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> list[Issue]:
429
469
  issues: list[Issue] = []
430
470
  text = read_text(path)
@@ -436,6 +476,10 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
436
476
  elif front and not parse_frontmatter_title(front):
437
477
  issues.append(Issue(path, 1, "frontmatter 缺少 title"))
438
478
 
479
+ # 保留 frontmatter 的行数,后续行号才是文件里的真实行号。
480
+ leading = text[: len(text) - len(body)] if body else ""
481
+ if leading:
482
+ body = "\n" * leading.count("\n") + body
439
483
  lines = body.splitlines()
440
484
  in_fence = False
441
485
  fence_lang = ""
@@ -443,8 +487,11 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
443
487
  fence_lines: list[tuple[int, str]] = []
444
488
  fence_blocks: list[list[tuple[int, str]]] = []
445
489
  mermaid_blocks: list[list[tuple[int, str]]] = []
490
+ html_blocks: list[list[tuple[int, str]]] = []
491
+ fence_is_html = False
446
492
  paragraph: list[str] = []
447
493
  paragraph_start = 0
494
+ last_fence_end: int | None = None
448
495
 
449
496
  def flush_paragraph() -> None:
450
497
  nonlocal paragraph
@@ -459,24 +506,35 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
459
506
  if not in_fence:
460
507
  in_fence = True
461
508
  fence_start = number
509
+ if last_fence_end is not None and number == last_fence_end + 1:
510
+ issues.append(Issue(
511
+ path, number,
512
+ "相邻围栏之间至少留一个空行(DOC-E11);空行不等于桥接正文",
513
+ ))
462
514
  fence_lang = (fence.group(2) or "").strip()
463
515
  fence_lines = []
516
+ fence_is_html = False
464
517
  if not fence_lang:
465
518
  issues.append(Issue(path, number, "代码围栏未声明语言"))
466
519
  elif fence_lang == "mermaid":
467
520
  issues.append(Issue(path, number, "plain mermaid 围栏(改用 ```{mermaid})"))
468
521
  elif fence_lang.startswith("{"):
469
522
  inner = fence_lang.strip("{}").strip()
523
+ fence_is_html = inner == "=html"
470
524
  if inner and not inner.startswith((".", "=")) and "mermaid" not in inner:
471
525
  # ```{python} style attribute fences are valid.
472
526
  pass
473
527
  else:
474
528
  in_fence = False
529
+ last_fence_end = number
475
530
  if fence_lines:
476
531
  fence_blocks.append(fence_lines)
477
532
  if "mermaid" in fence_lang:
478
533
  mermaid_blocks.append(fence_lines)
534
+ if fence_is_html:
535
+ html_blocks.append(fence_lines)
479
536
  fence_lines = []
537
+ fence_is_html = False
480
538
  continue
481
539
 
482
540
  if in_fence:
@@ -504,7 +562,12 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
504
562
  flush_paragraph()
505
563
  continue
506
564
 
507
- if raw.lstrip().startswith((">", "|", "-", "*", "+", "1.")):
565
+ if raw.lstrip().startswith("|"):
566
+ flush_paragraph()
567
+ # 表格行按列组织,不套散文句长阈值;列数与表头一致性由表格检查负责。
568
+ continue
569
+
570
+ if raw.lstrip().startswith((">", "-", "*", "+", "1.")):
508
571
  flush_paragraph()
509
572
  check_prose_block(path, number, re.sub(r"^\s*(?:[>|]|[-*+]|\d+\.)\s*", "", raw), issues)
510
573
  continue
@@ -515,6 +578,9 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
515
578
 
516
579
  flush_paragraph()
517
580
 
581
+ if in_fence:
582
+ issues.append(Issue(path, fence_start, "代码围栏未闭合"))
583
+
518
584
  for block in fence_blocks:
519
585
  issues.extend(check_tree_block(path, block))
520
586
 
@@ -522,21 +588,63 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
522
588
  issues.extend(check_mermaid_block(path, block))
523
589
  issues.extend(check_mermaid_wrapper(path, lines, block))
524
590
 
591
+ for block in html_blocks:
592
+ issues.extend(check_html_figure_block(path, block))
593
+
594
+ fenced_lines: set[int] = set()
595
+ inside_fence = False
596
+ for number, raw in enumerate(lines, start=1):
597
+ if FENCE_RE.match(raw):
598
+ fenced_lines.add(number)
599
+ inside_fence = not inside_fence
600
+ elif inside_fence:
601
+ fenced_lines.add(number)
602
+
603
+ blob_links: dict[str, list[int]] = {}
604
+
525
605
  for index, raw in enumerate(lines, start=1):
526
- match = LINK_RE.search(raw)
527
- if not match:
528
- continue
529
- target = match.group(1).strip()
530
- if target.startswith(("http://", "https://", "mailto:", "#", "data:")):
606
+ if index in fenced_lines:
531
607
  continue
532
- clean = target.split("#", 1)[0].split("?", 1)[0]
533
- if not clean or "<" in clean:
534
- continue
535
- if not (path.parent / clean).exists() and not (root / clean).exists():
536
- issues.append(Issue(path, index, f"本地链接不可达:{target}"))
608
+ for match in LINK_RE.finditer(raw):
609
+ target = match.group(1).strip()
610
+ if target.startswith(("http://", "https://", "mailto:", "#", "data:", "//")):
611
+ if target.startswith("https://github.com/") and "/blob/" in target:
612
+ blob_links.setdefault(target, []).append(index)
613
+ continue
614
+ clean = target.split("#", 1)[0].split("?", 1)[0]
615
+ if not clean or "<" in clean:
616
+ continue
617
+ if not (path.parent / clean).exists() and not (root / clean).exists():
618
+ issues.append(Issue(path, index, f"本地链接不可达:{target}"))
619
+
620
+ for target, numbers in blob_links.items():
621
+ if len(numbers) > 1:
622
+ issues.append(Issue(
623
+ path, numbers[1],
624
+ f"同章重复文件链接:{target}(首次在第 {numbers[0]} 行);每章每条仓库路径至多一条",
625
+ ))
537
626
 
538
- if text.count("```") % 2 != 0:
539
- issues.append(Issue(path, len(lines), "代码围栏数量为奇数,存在未闭合围栏"))
627
+ # 章引言(第一个标题之前)只留承接上一章的一个章节链接;其余章节引用下沉到正文。
628
+ # 着陆页 index.qmd 是目录页,不受这条预算约束。
629
+ if path.name != "index.qmd":
630
+ first_heading = next(
631
+ (n for n, raw in enumerate(lines, start=1) if HEADING_RE.match(raw)),
632
+ len(lines) + 1,
633
+ )
634
+ intro_links: list[tuple[int, str]] = []
635
+ for index, raw in enumerate(lines, start=1):
636
+ if index >= first_heading or index in fenced_lines:
637
+ continue
638
+ for match in LINK_RE.finditer(raw):
639
+ target = match.group(1).strip()
640
+ if target.endswith(".qmd"):
641
+ intro_links.append((index, target))
642
+ if len(intro_links) > 1:
643
+ issues.append(Issue(
644
+ path, intro_links[1][0],
645
+ f"章引言有 {len(intro_links)} 个章节链接({'、'.join(t for _, t in intro_links)});"
646
+ "只留承接上一章的一个,其余移到正文首次用到处",
647
+ ))
540
648
 
541
649
  for term in GENERIC_FORBIDDEN_TERMS:
542
650
  for index, raw in enumerate(lines, start=1):
@@ -571,6 +679,24 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
571
679
  path, index,
572
680
  f"自造关系词「{match.group(0)}」;用组成、调用、依赖、继承等通行词",
573
681
  ))
682
+ match = CARRIER_RE.search(raw)
683
+ if match:
684
+ issues.append(Issue(
685
+ path, index,
686
+ "类型或字段不用「承载」描述;改用表示、保存、记录或对应",
687
+ ))
688
+ match = NEXT_SECTION_RE.search(raw)
689
+ if match:
690
+ issues.append(Issue(
691
+ path, index,
692
+ f"小节导航「{match.group(0)}」;删除后用新节首句承接,章末预告保留",
693
+ ))
694
+ match = LADDER_RE.search(raw)
695
+ if match:
696
+ issues.append(Issue(
697
+ path, index,
698
+ f"拉链句「{match.group(0)}」;拆句或改成一项一行",
699
+ ))
574
700
 
575
701
  required = project.get("chapter_titles", {}).get(rel)
576
702
  if required:
@@ -605,6 +731,7 @@ def load_project(root: Path) -> tuple[bool, dict]:
605
731
  def main() -> int:
606
732
  parser = argparse.ArgumentParser(description="检查 Quarto Book 章节源码")
607
733
  parser.add_argument("--root", default=".", help="书根目录(默认当前目录)")
734
+ parser.add_argument("--paths", nargs="+", help="只检查这些 qmd 路径(相对 --root)")
608
735
  parser.add_argument("--strict", action="store_true", help="把未列入目录的文件也视为错误")
609
736
  parser.add_argument("--quiet", action="store_true", help="只输出汇总")
610
737
  args = parser.parse_args()
@@ -621,27 +748,52 @@ def main() -> int:
621
748
  return 2
622
749
 
623
750
  quarto_yml = root / "_quarto.yml"
624
- chapters = parse_chapters(quarto_yml)
751
+ try:
752
+ chapters = parse_chapters(quarto_yml)
753
+ except (OSError, UnicodeDecodeError) as exc:
754
+ print(f"失败 读取目录表失败: {exc}")
755
+ return 1
625
756
  files = collect_chapter_files(root, chapters)
757
+ issues: list[Issue] = []
758
+ if args.paths:
759
+ requested: list[Path] = []
760
+ for raw in args.paths:
761
+ candidate = (root / raw).resolve()
762
+ if root not in candidate.parents and candidate != root:
763
+ issues.append(Issue(quarto_yml, 1, f"--paths 落在书根之外:{raw}"))
764
+ elif not candidate.is_file():
765
+ issues.append(Issue(candidate, 1, f"--paths 指定的文件不存在:{raw}"))
766
+ else:
767
+ requested.append(candidate)
768
+ files = requested
626
769
  if not files:
627
770
  print(f"失败 {root} 下没有 .qmd 章节(确认 --root 指向书根目录)")
628
771
  return 1
629
772
 
630
- has_project, project = load_project(root)
773
+ try:
774
+ has_project, project = load_project(root)
775
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
776
+ print(f"失败 读取项目规则失败: {exc}")
777
+ return 1
631
778
 
632
779
  missing = [rel for rel in chapters if not (root / rel).is_file()]
633
- issues: list[Issue] = [
780
+ issues.extend([
634
781
  Issue(quarto_yml, 1, f"目录中的章节不存在:{rel}") for rel in missing
635
- ]
782
+ ])
783
+
784
+ if args.strict:
785
+ extras = [item for item in collect_all_qmd(root) if item not in files]
786
+ for item in extras:
787
+ issues.append(Issue(item, 1, "未列入 _quarto.yml(strict 模式)"))
788
+ files.extend(extras)
636
789
 
637
790
  for path in files:
638
- issues.extend(check_file(path, root, project, chapters))
791
+ try:
792
+ issues.extend(check_file(path, root, project, chapters))
793
+ except (OSError, UnicodeDecodeError) as exc:
794
+ issues.append(Issue(path, 1, f"无法按 UTF-8 读取:{exc}"))
639
795
 
640
- if args.strict:
641
- for issue in list(issues):
642
- if "未列入 _quarto.yml" in issue.message:
643
- issue.message = issue.message.replace("(目录会漏章)", "")
644
- else:
796
+ if not args.strict:
645
797
  issues = [issue for issue in issues if "未列入 _quarto.yml" not in issue.message]
646
798
 
647
799
  if not args.quiet:
@@ -17,7 +17,7 @@
17
17
 
18
18
  **章际衔接(定结构检查项)**:第 N 章的引言、任务节与自测题只用第 1..N-1 章已建立的名字与概念;本章新术语在首次真正使用处就近落术语卡或行内括号;术语卡的升级判据见 `writing-quarto` 技能 `quarto/authoring.md`「Callout 升级判据」,写法见 `steps/03-blocks.md`。某概念接不上前章已建立的名字时,把它下沉回首次使用它的章建立,不在本章临时补课。
19
19
 
20
- - **无标题引言**:第一个 `##` 之前,写成**连贯读者文**回答读者三问——从哪来(承接上一章已成立的结果)、这一章做什么(本章要解决的问题与读者将获得的能力,正面陈述;范围能并则并,不写否定式后章预告)、代码在哪(可选:若本章有主源文件,用**同段末句**自然语言点名并挂全章 blob 链接,不另起「代码写在…里」独立段)。第一句写出谁在什么情况下做本章这件事,类章同时写出类名;类型怎么实现放到后面的句子。不另开背景节。**形态硬限(用户决定)**:首选**恰好 1 个自然段**(通常 2–4 句);必要时允许 **1 个自然段 + 1 个列表**(列表前引导句写在同一段末,或段落后紧跟列表;列表不算第二散文段);**禁止**出现两个散文自然段并立,更禁止 ≥3 段、单句孤儿段、结尾导航段。列表仅用于并列范围/对象,任务细节下沉正文 `##`,不用顿号串交付物冒充散文。**摘录免责句永不写进读者正文**:「本章摘录源码服务于讲解」「片段不保证与源文件逐字一致」「不等于文件内顺序」类作者旁白一律删除(旧「示例位置句开口子」已废止),同源保证交给 `verifying-content.py`;摘录免责只保留在 技能的权威文件与检查脚本里。不放 Callout,不写后续章节内容,不用「后面的 X」式前向指称引入后续章节才建立的结构件(如「实现放在后面的编号目录里」)。引言容纳多个独立概念时**并段或改列表**,禁止拆成两个散文段;拆分判据见 `steps/04-paragraphs.md`「拆分判据」。内容量硬限:token 预算见本文件「分层上限」;范围边界能并入承接段时不成列表。承接中引用其他章节一律用章节标题链接,不用「第 N 章」;本章回顾的下一章预告不写链接,直述预告一句(页面导航已有前进按钮),两者均见 `steps/06-wording.md`。任务概览默认并入承接句,正文小节就是任务的展开;预览列表仅当任务没有对应正文小节时使用(示例章四项工作列表已按此删除)。原 front matter `description` 的定位信息并入引言承接句,信息只保留引言一处、不同义两存;章节 qmd 不写 `description`(字段出处见 basics.md)。着陆页 `index` 按 landing 规则组织,不硬套本契约。
20
+ - **无标题引言**:第一个 `##` 之前,写成**连贯读者文**回答读者三问——从哪来(承接上一章已成立的结果)、这一章做什么(本章要解决的问题与读者将获得的能力,正面陈述;范围能并则并,不写否定式后章预告)、代码在哪(可选:若本章有主源文件,用**同段末句**自然语言点名并挂全章 blob 链接,不另起「代码写在…里」独立段)。第一句写出谁在什么情况下做本章这件事,类章同时写出类名;类型怎么实现放到后面的句子。不另开背景节。**形态硬限(用户决定)**:首选**恰好 1 个自然段**(通常 2–4 句);必要时允许 **1 个自然段 + 1 个列表**(列表前引导句写在同一段末,或段落后紧跟列表;列表不算第二散文段);**禁止**出现两个散文自然段并立,更禁止 ≥3 段、单句孤儿段、结尾导航段。列表仅用于并列范围/对象,任务细节下沉正文 `##`,不用顿号串交付物冒充散文。**摘录免责句永不写进读者正文**:「本章摘录源码服务于讲解」「片段不保证与源文件逐字一致」「不等于文件内顺序」类作者旁白一律删除(旧「示例位置句开口子」已废止),同源保证交给 `verifying-content.py`;摘录免责只保留在 技能的权威文件与检查脚本里。不放 Callout,不写后续章节内容,不用「后面的 X」式前向指称引入后续章节才建立的结构件(如「实现放在后面的编号目录里」)。引言容纳多个独立概念时**并段或改列表**,禁止拆成两个散文段;拆分判据见 `steps/04-paragraphs.md`「拆分判据」。内容量硬限:token 预算见本文件「分层上限」;范围边界能并入承接段时不成列表。承接中引用其他章节一律用章节标题链接,不用「第 N 章」;**链接预算**:引言只挂承接上一章的那 1 个章节链接和本章主源文件的 blob 链接,其余章节链接下沉到正文真正用到它的句子,每处只挂当下需要的那个目标,同一段不堆多个章节链接(Google 技术写作指南把每个链接视为一次读者可能离开当前页的机会,链接越多,认知负担越重);本章回顾的下一章预告不写链接,直述预告一句(页面导航已有前进按钮),两者均见 `steps/06-wording.md`。任务概览默认并入承接句,正文小节就是任务的展开;预览列表仅当任务没有对应正文小节时使用(示例章四项工作列表已按此删除)。原 front matter `description` 的定位信息并入引言承接句,信息只保留引言一处、不同义两存;章节 qmd 不写 `description`(字段出处见 basics.md)。着陆页 `index` 按 landing 规则组织,不硬套本契约。
21
21
  - **任务序列**:每节一个可验证结果。判定标准是任务结束时存在一个读者能自己复述的检查动作;只有「读完了解释」的内容并入相邻任务。概念 → 最小示例 → 操作 → 验证 → 边界,先让读者看到行为再解释规则;单次操作按「做什么 → 执行什么 → 看到什么 → 简短说明」推进。各章的任务节以该章标题里的对象为准,不把每一章都写成「目录、设计、构建、测试」四节。设计范围以这一章能在 `src/` 里核对的源文件收束。内容量硬限:任务级 `##` 通常不超过 4 个;`##` 与 `###` 开头的承接句数见本文件「分层上限」,写完直接进入代码、命令或列表;超出即为凑字数信号,禁令见 `steps/04-paragraphs.md`「凑字数禁令」。有 `###` 子节时,二级标题(`##`)的引言须承接上文;恰好 2 个子节时一句点一个,3 个或 4 个时不再逐个点名,点名交给三级标题的句式见本文件「标题分层」,点名用读者已认识的词,不点后文才定义的类型名;一句之内禁止把两个子节对象配成「由 X 定位,并保存 Y」「分别交给」式的两组对应(对照见 `examples/05-sentence-frame.md`「拉链句」)。交付清单或纯边界说明不单独设 `##`,并入引言或相邻工程总览节;同一头文件中连续的值类型优先按一个可验证结果归并到同一任务节。任务节内的讲述顺序按「类型 → 简单函数 → 更难的函数」排列,最难的组装放在最后,并满足先定义后调用:读者要跟着读懂的行内代码标识符,其行为必须已经出现过——上一章、上文,或同一小节里紧挨着的声明和一句行为——上一章已建立的名字视为已知;未定义名字的调用链、节首概览与编号步骤整段删除。`##` 导语只用白话,不点后文才定义的函数名。章内顺序的依据见 `SKILL.md`「九步」的定结构一条,本文件不复述。
22
22
  - **常见错误**:服务失败后回来的读者。每个症状一个 `###`,标题后第一句写触发条件和可观察现象。误用型条目一段成文写原因与正确做法;原因与做法各需一句完整交代时(如原因牵涉私有段的设计动机),用症状句导语加「出错的原因/正确的做法」引导词列表,列表可作为节导语的合法收尾,导语预算只约束症状句;排查型故障展开定位 → 修复 → 验证三步。形态分野与细则是 `writing-quarto` 技能 `quarto/terminal.md`「常见错误条目形态与三步诊断流」。入门章节只保留最可能遇到的 2-3 项,不是完整 FAQ。
23
23
  - **自测问题**:服务刚跑通主线、需要确认理解的读者。每章 0-7 道题,题目问本章已经写明的一件可观察结果(会怎样、是哪一个、从几开始),没有这种结果就不设题;只有本章写了原因、而且情景已经说全时,才用「为什么」。题面用本章已经用过的词说全,不用本章未定义的概念,不问「为什么不做成另一种」;题干是合上书的一问,只看本题题干、不看答案与上一题;题干代词的判法见 `steps/05-sentences.md`「代词不能代替未写出的对象」,允许「什么」——它在要一个名字或一句文字,不是指上文没出现的东西;题面里每个细节都要改变答案,答案不受细节影响时不单点某一种情形。题面只描述一个自洽局面,每个事实都要能同时成立,禁止把互相矛盾的局面写成题干(同一句里先说轮到甲方、又说这一步的用户是 O,读者听到的是程序自相矛盾,不是一次非法调用)——「轮到谁」和「这一步的用户」若是同一个对象,只写一次。要考拒绝,用正文已经用过的那条规则问可观察结果(正例:「空状态面板开局,轮到甲方。没轮到的一方来操作时,状态面板会改变吗?」,答案首句「不会。操作的用户不是当前用户,`Place` 返回 `kRejected`,状态面板和轮次都保持原样。」);不为了出题制造正文没走过的矛盾局面。一道题只问一件可观察结果,不把两个观察捆进同一题(如「状态面板是空的吗」与「轮到谁」并列),拆开或删去一个。固定导语「不要展开答案,试着自己把回答完整说出来,写完之后展开对照;说不出来的部分,回到对应小节对照正文。」;答案写在 `::: {.answer}` 围栏 div 内,由 `scripts/answering-disclosure.lua` 渲染为默认收起的原生 `<details>`;答案只使用本章已讲过的概念。题目同样必须能由本章正文回答:答案依据须是正文已写明的内容,正文没讲的概念不设题;若问题值得问,先在正文补上依据再设题。答案先答所问:首句直接回答题面,一到两句,不补题面之外的对比、延伸或重复题面(对照与归属由正文承载)。问答中的文件、路径与标识符指称统一用行内代码,不挂链接(`verifying-content.py` 对自测节链接记 WARN);所属目录在答案前文已确立时,路径可省去重复的顶层前缀(如先写 `example/`,后文写 `include/state.h`)。
@@ -29,7 +29,7 @@
29
29
 
30
30
  渐进披露不进正文:语法细节延后展开是 Agent 侧写作策略,禁止把策略写成对读者的话——「语法细节留到后续章节真正用到时再展开」「本章只需认清……」这类元叙述一律不出现在 `content/**`;正文直接陈述对象和结果(如「这段程序对外可见的结果只有两处:……」),需要延后的内容保持沉默即可。改写对照见 `examples/06-wording-cases.md`,禁用词见 `steps/06-wording.md`。
31
31
 
32
- 列表与表格同律:表格前用一句完整的话交代它回答什么问题,表格后补一行读者该取走的结论;列表通则见 `steps/04-paragraphs.md`「段落规则」。命令、输出与正文三分见本文件「命令、输出与细节各司其职」。
32
+ 列表与表格同律:表格前用一句完整的话交代它回答什么问题,表格后补一行读者该取走的结论;列表通则见 `steps/04-paragraphs.md`「段落规则」。表格还必须与上一个代码块、图或列表之间隔一段正文,写清两者关系:定义刚给出就接对照表,读者对不上表头和围栏。表头、列宽比例与单元格写法见 `quarto/tables.md`。命令、输出与正文三分见本文件「命令、输出与细节各司其职」。
33
33
 
34
34
  多命令块:块后逐条讲解的格式见 `steps/04-paragraphs.md`「段落规则」,引导词加粗与行内代码不叠加的边界见 `steps/07-layout.md`。引言与收束按上段列表通则,不再按命令分段成多个段落。
35
35
 
@@ -57,7 +57,7 @@ int main() {
57
57
  - **filename 不写链接**:`filename="..."` 是纯文本短名,禁止塞 Markdown 链接;`resolve_fragment_source` 的解析只认正文句子里的链接,标题栏写链接既渲染不出可点样式,也会被 `verifying-content.py` 判失败。
58
58
  - **blob 链接嵌进导语句**:GitHub blob 链接写在同节导语的完整句子里(正例:「apps/CMakeLists.txt 用一行目标属性固定运行时库查找」,其中文件名是 GitHub 链接的锚文本),禁止整行仅为链接单独成行贴在代码围栏上方(可夹空行,`verifying-content.py` 判失败)。悬空的裸链接行既不像正文也不像说明,读者扫读时只剩一个锚点。
59
59
  - **摘录链接每章每条仓库路径至多一条**:章引言是全章链接位,首次点名源文件并挂链接(如「声明在头文件 `state.h`,实现在源文件 `state.cpp`」,这两个名字是该章仅有的源文件链接);后文和各节导语改成行内代码,不再包 `[]()`,导语讲块里要看的标识符;同一章摘录另一阶段目录的同名文件时,在切换处再挂一条不同 URL(对照见 `examples/03-excerpt-lead.md`「摘录链接密度」;`verifying-content.py` 对同章同 URL 重复判失败)。本条挂在写作步骤的定版式。
60
- - 片段内注释硬限:每处实现注释(`//` 或 `#`)不超过 2 行、单行不超过 100 列(与源文件 `ColumnLimit: 100` 同一口径;正文散文的 40 字限不套到代码注释),只写这段代码做什么;CMake/Shell 等命令型片段每条命令保留一行意图注释。Doxygen `///` 声明块(概述、空 `///` 与细节)是声明骨架的一部分,摘录随源保留,不受此限。命名惯例、官方建议、未来演化等规范说理一律不进代码块;术语卡和可跳过的操作提醒可以进 callout,注释怎么写、行尾先写什么、`///` 与 `@` 标签怎么分工只留在 `.agents`,面向作者的进知识文件。
60
+ - 片段内注释硬限:每处实现注释(`//` 或 `#`)不超过 3 行(与源文件注释规范的 1–3 行同一口径)、单行不超过 100 列(与源文件 `ColumnLimit: 100` 同一口径;正文散文的 40 字限不套到代码注释),只写这段代码做什么;CMake/Shell 等命令型片段每条命令保留一行意图注释。Doxygen `///` 声明块(概述、空 `///` 与细节)是声明骨架的一部分,摘录随源保留,不受此限。命名惯例、官方建议、未来演化等规范说理一律不进代码块;术语卡和可跳过的操作提醒可以进 callout,注释怎么写、行尾先写什么、`///` 与 `@` 标签怎么分工只留在 `.agents`,面向作者的进知识文件。
61
61
  - 首讲某段源码的章节不省略源码既有的职责注释:注释是读者读到的第一层讲解;「注释与空行可以省略」只服务后续章节复现已见代码。源码缺注释时交给项目 C++ 流程补齐,本技能不替源码补注释。
62
62
  - 讲解段只解释片段中实际出现的代码;片段没有展示的内容要么补进片段、要么进 callout,不在正文列表里讲代码外面的东西。
63
63
  - include 短代码降级为可选手段:仅用于确实需要完整展示且很短的文件,并仍须包在带语言标注的围栏代码块内(包裹规则见 `quarto/render.md`)。
@@ -78,7 +78,19 @@ int main() {
78
78
 
79
79
  摘录 `class` 或多函数片段时**保留**源码中函数之间的空行,禁止为省行数坍缩成函数注释贴着下一声明;省略部分成员时,被保留的相邻函数之间仍空 1 行;同一 access 段内最后一组数据成员与函数之间空 1 行,数据成员组内不空行。与「逐字摘录」冲突时以源码空白为准:教学缩略可删成员,不可删「函数间距」惯例。
80
80
 
81
- ### 三级标题最小模块
81
+ ### 正文与代码注释分工
82
+
83
+ 代码块是源文件的逐字摘录,它承载理解这段代码所需的细节:契约、设计理由、边界与失败方式都留在块内注释里。正文不再转述这些细节,只写读者必须带走的判断。
84
+
85
+ - **块内注释负责细节**:`///` 契约、`@param`/`@return`、实现注释里的次序依赖与边界,都随摘录保留。正文删掉注释已写明的返回值和参数含义。
86
+ - **正文只写三类信息**:为什么这样设计(取舍)、读者接下来要用它做什么、几个块之间的工程关系。一条简单的 `return`、一个参数名、一个枚举取值,正文一句带过或直接不写。
87
+ - **不复述成列表**:把注释里的三条契约改写成正文三条项目符号,等于把代码读了两遍。需要并列判断时,先看能不能并成一句;并不成再用列表。
88
+ - **不写注释元叙述**:正文不交代「类说明没有逐个列出成员」「每个成员的用法写在行尾注释里」「理由写在类型注释里」这类关于注释怎么写的策略;读者需要的事实直接陈述,作者怎么组织注释是技能规则,不进正文。
89
+ - **正文长度随注释密度反向走**:块内注释越完整,块后段落越短。块后只剩一句读者该取走的结论时,就只写一句,不必凑段。
90
+
91
+ 改写对照见 `examples/03-block-after.md`「块后复述注释」。
92
+
93
+ ## 三级标题最小模块
82
94
 
83
95
  一个三级标题下面的正文由一到两个最小模块组成。一个模块是一个整体,次序固定:标题已经给出对象;标题下一句导语;有并列取值时跟着列表,一项一个取值、一项一句;代码块、表格或图放在列表和收束段之间;没有列表时放在导语和收束段之间;收束段只写各项共同的契约;Callout 可有可没有,写了就放在收束段之后。短模块可以只有导语和代码块。示例章「操作与成败」里的 `Move` 与 `PlaceResult` 各是一个短模块,下一个短模块用自己的导语接上。第二个模块用来解释前一个时,衔接句就是第二个模块的导语。示例章「数据项坐标」用「下面的图标出这块状态面板上可用的 `(row, col)`。」接上图。讲一个函数时,先一个模块讲 `.h`,再用导语接 `.cpp`。列表写选法,围栏与源文件逐字一致,收束段不把列表再写一遍。禁止「每个状态码对应」,也禁止以 `` `类型::kName`: `` 开头的释义项。正面例子是 示例章「操作的强度」。
84
96
 
@@ -100,11 +112,12 @@ int main() {
100
112
 
101
113
  ### 代码与图的桥接
102
114
 
103
- 代码块引出 `{mermaid}` 图或 `{python}` 坐标图时按固定序列组织:代码块 → 桥接句 → 图(含 `fig-cap`)→ 图后新结论。固定序列的完整写法见 `writing-quarto` 技能 `quarto/authoring.md`「代码与图的桥接」,两条硬边界:
115
+ 代码块引出 `{mermaid}` 图或 `{python}` 坐标图时按固定序列组织:代码块 → 桥接句 → 图(含 `fig-cap`)→ 图后新结论。固定序列的完整写法见 `writing-quarto` 技能 `quarto/authoring.md`「代码与图的桥接」,边界如下:
104
116
 
105
117
  - 代码块与图之间必须有桥接句,说明图要核对代码已讲清的哪条关系;中间只有空行视为堆叠(`verifying-content.py` 的 DOC-E11 只要求相邻围栏留空行,空行不等于桥接正文)。
106
118
  - 图后先给「从图带走的结论」或图外新信息,再进入下一块;小节以图收尾时,结论可并入桥接段,不在图后孤悬一句。图注已写明的信息不复述。坐标图计入盒子密度,口径见 `quarto/coords.md` 与本文件。
107
- - 一图一事、同主题相邻、图不承载正文未写的新信息:一张图只核对一条已讲清的关系;代码与图主题不同时收束本对象再拆 `###`,禁止「代码 A → 短句拐弯 → 图讲 B → 再贴代码 C」的同节混排(三条边界的写法见 `quarto/authoring.md`「代码与图的桥接」)。
119
+ - 图注与图前句分工:图注写图面内容与读法,落在图下;图前只写引出关系的一句,不写“下图标出……”这类图面说明。三类图(Mermaid、坐标图、原生 HTML/SVG)的写法见 `quarto/figures.md`。
120
+ - 一图一事、同主题相邻、图不承载正文未写的新信息:一张图只核对一条已讲清的关系;代码与图主题不同时收束本对象再拆 `###`,禁止「代码 A → 短句拐弯 → 图讲 B → 再贴代码 C」的同节混排(上述边界的写法见 `quarto/authoring.md`「代码与图的桥接」)。
108
121
  - 图前段只讲本页可见的对象,且单一主题:描述版式的句子其对象必须同页可见,引用前章画面时复述读者需要的关键事实(如行列取值 0 到 2),不指称前章画面的版式(「画面上方是列号」式会让读者翻回前章);调用方行为等第二主题移出图前段(示例章定义坐标范围节两段并一段的改稿对照)。
109
122
 
110
123
  图不能替代结论句:桥接句与图带走的结论缺任何一处,读者只有图没有判断依据。正反对照见 `examples/03-qmd-elements.md`「代码与图桥接对照」。
@@ -225,7 +238,7 @@ int main() {
225
238
 
226
239
  ### 情况:图是 Mermaid 或坐标轴图
227
240
 
228
- 要画流程图、类图或时序图才打开 `quarto/mermaid.md`(类图先按内容预算删节点和成员),坐标轴网格图打开 `quarto/coords.md`。不排图不打开。
241
+ 要画流程图、类图或时序图才打开 `quarto/mermaid.md`(类图先按内容预算删节点和成员),坐标轴网格图打开 `quarto/coords.md`,图注与原生 HTML/SVG 图打开 `quarto/figures.md`。不排图不打开。
229
242
 
230
243
  ### 情况:Callout
231
244
 
@@ -12,6 +12,7 @@
12
12
  - 每段只有一个主题或中心句;中心句放在段首,后面的句子都服务于它。段内句子改完后,段首仍是中心句,一段仍只有一个对象;句子的六个成分见 `steps/05-sentences.md`「六个成分」。
13
13
  - 一段一对象:同一段不夹杂两个并列对象,对象切换必须新起段;配套列表只服务本段对象,不得跨对象拼装列表项(对照见 `examples/03-block-after.md`「单段混并列对象」)。
14
14
  - 段落不超过 7 行,最佳 4 行以内;超过即拆或改列表。
15
+ - 同一可数事实全章只交代一次:格子数、尺寸、类型、成员数量这类事实第一次出现时写全(如引言里的「3 行 3 列,共九个棋盘格子」),后面用「这些格子」「其余格子」回指,不复述数字或同一个名词短语;代码块前后的散文也不重复块内注释已经写过的数量与类型(对照见 `examples/04-paragraph-cases.md`「重复交代可数事实」)。
15
16
  - 多个独立概念禁止塞进同一段:承接、目标、范围边界、阅读顺序、术语解释各归各段。
16
17
  - 段落之间用一个空行分隔,段首不缩进。
17
18
  - 代码块后的逐条讲解用无序列表:每项格式为「行内代码标识符 + 全角冒号 + 一句职责」(如 `` `enable_testing()`:开启 CTest。``),描述从简,全页标记方式一致;列表前保留一句引言交代工程语境,列表后需要时补一句收束或尾置 Callout。有先后顺序的排查步骤仍用有序列表;块前导语直接点名对象做什么,不用「先…再…最后…」,元计数禁令见 `steps/03-blocks.md`。
@@ -31,6 +31,22 @@
31
31
 
32
32
  「只读」「只写」是读写性质的固定术语,保留;`const` 成员函数首次称「只读」时,写明它不改动对象。安全、协议或验收条件确实需要排他时,写成可核对的条件(「`PlaceCell` 在目标格子非空时返回 `false`」),不靠「只」承担论证。
33
33
 
34
+ ### 表示、保存、记录,不用「承载」
35
+
36
+ 读者正文不把类型或字段写成「承载」某个值。「承载」是抽象能力或文档职责的说法,套到 `struct`、`enum`、字段上会让读者分不清它表示什么、运行时保存什么。
37
+
38
+ 按对象选动词:
39
+
40
+ - 类型表达概念:`CellPosition` **表示**一对行列,`CellType` **表示**格子里的取值。
41
+ - 对象保存数据:`Board` 的 `cells_` **保存**九个 `CellType`。
42
+ - 文件或协议留下记录:快照 **记录**落子次数、当前玩家和结果。
43
+ - 名称与值对应:`kX` **对应** X 标记。
44
+
45
+ - 不佳:坐标由 `CellPosition` 承载,取值由 `CellType` 承载。
46
+ - 改写:`CellPosition` 表示一对行列,`CellType` 表示格子里的取值。
47
+ - 不佳:这份配置承载三个字段。
48
+ - 改写:这份配置保存三个字段。
49
+
34
50
  ### 读者正文禁无信息提问与自造关系词
35
51
 
36
52
  读者正文不向读者发问。没有任务指向的提问(「总览图为什么不列成员?」「这条边表示什么?」)只是把作者自检提纲搬进正文,删掉后读者仍能拿到同样信息;需要读者判断的内容改写为陈述句,或放进自测。
@@ -100,6 +116,7 @@
100
116
  - 动词选用与主动语态见「目标语气」;「由 X 完成」在这一步改成主动句,改完露出第二个核就退回定段落。
101
117
  - 「同一概念只使用一个称呼」的判定见「七个质量维度」;英文标识保持大小写。
102
118
  - 「游戏」指作品或玩法整体,「对局」指具体一场:定义「井字棋是…游戏」用「游戏」,规则、落子与结果语境用「对局」;同一页不把两者当同义词换用(对照见 `examples/06-wording-cases.md`「游戏与对局」)。
119
+ - 项目名在同一页首次写全(如「井字棋棋盘」),后文改用对象名(「棋盘」「动态库」「对局」);不逐段重复项目名,也不在同一页反复解释已经说过的事。
103
120
  - 无信息修饰的删除判定见「七个质量维度」;禁用词以 `scripts/verifying-content.py` 词表为准。
104
121
  - 压缩动词写全:`重建`、`重编`、`重跑` 这类两字词,读者可能读成「重新创建」;写「重新生成」「重新编译」「重新运行」,同一页后续也不回退到压缩词。
105
122
  - 连续出现三个「的」时的改写判定见「七个质量维度」第 5 条。
@@ -20,6 +20,7 @@
20
20
 
21
21
  - **无序列表**:并列概念、范围边界、要点枚举——各项之间没有先后或因果;代码块后的逐条讲解格式见 `steps/04-paragraphs.md`「段落规则」。
22
22
  - **有序列表**:有先后顺序的步骤或链路——执行顺序本身就是信息(如配置 → 编译 → 测试 → 运行)。
23
+ - **规则和条件不编号**:并列的许可、禁止、判定条件写自然段或分句;只有读者需要按顺序执行动作时才用编号。把「越界 / 占用 / 格式错误」写成 1、2、3,会让读者以为要按顺序检查。一段同时有规则和流程时,规则先成段,流程另起有序列表。
23
24
  - 列表项结构平行:起首用同类的词(都是名词短语,或都是动词短语),长度相近。
24
25
  - **列表字数带**:同一列表各项保持视觉齐长,避免扫读节奏断裂。计量:引导词汉字数、冒号后汉字数(行内代码按标识符计)。失衡判定:冒号后最长 ≥ 最短 × 2,或引导词相差 ≥ 4。改写顺序:删长项多余中心 → 拆多中心 → 过短项按简洁边界补全 → 停止。豁免:出错原因/正确做法;有序步骤;着陆页卡片。列表齐长服从句段主规则,假拆禁令见 `steps/04-paragraphs.md`。
25
26
  - 列表项起首用加粗引导词标记中心语,读者扫读时先看到分类再决定是否细读;关键词取同类词(如「显示状态面板」「读入坐标」),并按对象的发生顺序排列,读者扫读即得顺序与内容。
@@ -52,13 +53,16 @@
52
53
  - 华为《中文技术文档写作风格指南》:标题层级与句式;段落长度条文统一指向 `steps/04-paragraphs.md`。
53
54
  - GB/T 15834—2011《标点符号用法》:顿号层级与标点用法。
54
55
  - 阿里 f2e-spec writing:中英混排与标点。
56
+ - Microsoft Writing Style Guide「Lists」:列表 2–7 项、结构平行;编号只用于有顺序的步骤或优先级。
57
+ - ONS content style guide「Lists」:编号列表只给必须按顺序执行的步骤,其余并列用无顺序列表。
58
+ - Quarto「Lists」:连续列表项之间保留空行;编号与项目符号按语义选择。
55
59
 
56
60
  ### 格式取舍
57
61
 
58
62
  - 行内代码与平台名词的版式判定见 `steps/02-structure.md`。
59
63
  - 可复制命令使用指定语言的代码围栏;命令与输出分开的判定见 `steps/02-structure.md`。
60
64
  - 标题与导航不用行内代码的判定见 `steps/02-structure.md`;中英文之间的空格见 `steps/06-wording.md`。
61
- - 列表用于并列或顺序关系,选择判定见本文件「列表选择」。表格只用于需要对多个属性进行比较的内容:同一主题的属性合并成一张表,不按小节拆成多张薄表;只有一两列信息或右侧大量留白时改用句子或列表。列表是版式选择,不是新的一段说明。
65
+ - 列表用于并列或顺序关系,选择判定见本文件「列表选择」。表格只用于需要对多个属性进行比较的内容:同一主题的属性合并成一张表,不按小节拆成多张薄表;只有一两列信息或右侧大量留白时改用句子或列表。表头、列宽比例与表格前后的过渡见 `quarto/tables.md`。列表是版式选择,不是新的一段说明。
62
66
  - Callout 只承载可跳过提示的判定见 `steps/03-blocks.md`。
63
67
 
64
68
  具体元素写法见 `quarto/authoring.md`,终端步骤见 `quarto/terminal.md`。
@@ -59,7 +59,7 @@
59
59
 
60
60
  ### 润色流程:沿着步骤走
61
61
 
62
- 润色任务沿 `SKILL.md` 的写作步骤执行,从定任务走到通过检查,跳步视为未完成;判定标准见 `steps/06-wording.md`。两种走法共用 1–9:**增量润色**保持最小改动,只改判为不合格的句子,不做风格重塑;**全章重构**按 `SKILL.md`「两种走法」走——章级(1–2)先定删留不动句子,节级(3–4)只拆块和段,句级(5–7)逐句拆开再写回去,该层全部重组,不走最小改动。执行时遵守定段落节的既有判定:分句骨架在定句子判为不合格,不在定段落里顺手改句子。
62
+ 润色任务沿 `SKILL.md` 的写作步骤执行,从定任务走到通过检查,跳步视为未完成;判定标准见 `steps/06-wording.md`。书根 `README.md` 与 `README.en.md` 是仓库说明页,按 `SKILL.md` 的例外只走第 5–8 步,这个例外优先于本步的跳步判定。两种走法共用 1–9:**增量润色**保持最小改动,只改判为不合格的句子,不做风格重塑;**全章重构**按 `SKILL.md`「两种走法」走——章级(1–2)先定删留不动句子,节级(3–4)只拆块和段,句级(5–7)逐句拆开再写回去,该层全部重组,不走最小改动。执行时遵守定段落节的既有判定:分句骨架在定句子判为不合格,不在定段落里顺手改句子。
63
63
 
64
64
  **先切模块,再套规则。** 任何一步动笔前先把文本切成单位,围栏、YAML 与链接 URL 跳过(脚本按同一口径统计,计数见 `verifying-content.py` 输出):
65
65