@ccoalm/ccl-skills 0.3.0 → 0.5.0

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 (79) hide show
  1. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/classify_envelope.py +34 -3
  2. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_abort_leak_state_helpers.sh +148 -0
  3. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_classify_envelope.sh +28 -0
  4. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_parse_review_json.sh +7 -0
  5. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +13 -0
  6. package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate_abort_leak.sh +271 -34
  7. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-data-acquisition.md +3 -1
  8. package/dist/assets/marketplace/plugins/ccl-skills/skills/multi-perspective-research/references/public-disclosure-channels.md +2 -0
  9. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +12 -12
  10. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +8 -0
  11. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/eval-routing.md +7 -8
  12. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/external-practice-controls.md +12 -0
  13. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +3 -2
  14. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/firing-point-placement.md +8 -0
  15. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/harness-patterns-and-eval.md +7 -6
  16. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/recurring-anti-patterns-checklist.md +18 -0
  17. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +47 -2
  18. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +28 -29
  19. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +165 -1
  20. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/eval-health.rb +23 -10
  21. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +303 -1
  22. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_impact_chain_refscripts.sh +222 -91
  23. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +12 -0
  24. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +85 -0
  25. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +123 -0
  26. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_dateless_host.sh +6 -1
  27. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_gate_verdict_differential.sh +1 -1
  28. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_round_attribution.sh +12 -12
  29. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +455 -0
  30. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_source_refuted.sh +20 -20
  31. package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_liveness_predicate_gate.sh +288 -0
  32. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/SKILL.md +4 -4
  33. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/deliverable-doc-genre-skeletons.md +133 -0
  34. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/doc-charter-first.md +2 -0
  35. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/references/figure-and-table-craft.md +318 -0
  36. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/AGENTS.md +46 -0
  37. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/doc-lint.py +246 -0
  38. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/figure-lint.py +1092 -0
  39. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/mutation_probe.sh +100 -0
  40. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/test_figure_and_doc_lint.sh +375 -0
  41. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/control.md +10 -0
  42. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/empty-header.md +6 -0
  43. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fake-header.md +13 -0
  44. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fenced-noise.md +14 -0
  45. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-dangling.md +5 -0
  46. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan-captioned.md +11 -0
  47. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig-orphan.md +9 -0
  48. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/fig.png +0 -0
  49. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/imbalance.md +41 -0
  50. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/no-unit.md +8 -0
  51. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/should-be-chart.md +11 -0
  52. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/tables-only-clean.md +35 -0
  53. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/unfilled.md +7 -0
  54. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/doc/wide-table.md +5 -0
  55. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/bad-viewbox.svg +9 -0
  56. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/blackmarker.svg +9 -0
  57. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/control.svg +12 -0
  58. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/crossings.svg +12 -0
  59. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/cvd-confusable.svg +9 -0
  60. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/decorative-line.svg +10 -0
  61. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-no-arrow.svg +11 -0
  62. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/edge-vague.svg +12 -0
  63. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-contract.json +21 -0
  64. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/figure-is-a-list.svg +12 -0
  65. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/flow-mixed.svg +13 -0
  66. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/low-contrast.svg +12 -0
  67. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/malformed.svg +1 -0
  68. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-aria.svg +9 -0
  69. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-group.svg +10 -0
  70. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-legend.svg +9 -0
  71. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-title.svg +12 -0
  72. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/no-viewbox.svg +9 -0
  73. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/offcontract-shape.svg +13 -0
  74. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/overflow.svg +13 -0
  75. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/transformed.svg +9 -0
  76. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/ungrouped-card.svg +12 -0
  77. package/dist/assets/marketplace/plugins/ccl-skills/skills/tighten-doc/scripts/tests/svg/unlabeled-edge.svg +13 -0
  78. package/dist/assets/release.json +276 -31
  79. package/package.json +1 -1
@@ -0,0 +1,318 @@
1
+ # 图与表的起草期契约
2
+
3
+ `deliverable-doc-genre-skeletons.md` §4 定「哪个族画哪几张图」、§5 定「画了的图怎么验收」。
4
+ 本文件补的是**图与表画成什么样才合规**,以及**哪些判据能机械跑**。三者不重述:族与候选看 §4,
5
+ 验收面看 §5,本文件只管形态与可判定性。
6
+
7
+ ## §1 判据分三档,混档就会被反驳
8
+
9
+ 落任何一条图/表规则前先归档,档位决定它能不能被"你这数字哪来的"问倒:
10
+
11
+ | 档 | 含义 | 用法 |
12
+ |---|---|---|
13
+ | `[外]` | 有权威一手源 | 可声称行业基线,引用时点名来源 |
14
+ | `[工]` | 工程约定 + 观测到的失败类 | 能防返工,**不得声称是业界最佳实践** |
15
+ | `[禁]` | 查证后确认无可靠来源 | 谁拿数字立规则,先要出处 |
16
+
17
+ **`[禁]` 档(已核,勿再立)**:加粗/高亮密度;每 N 字一图的图表密度(唯一数字是学术期刊的
18
+ **印刷页数配额**,成因是版面成本不是可读性);"一行不超过 40 汉字"引 WCAG(该条的 CJK 40 是从
19
+ 拉丁文 80 折半推导,且原文要求是「提供**机制**让用户改」不是「正文必须排这么宽」);"句子不超过
20
+ 25 词";"留白提升理解约 20%"(错误引用链)。
21
+
22
+ **同体裁实测分布 ≠ 规范阈值**:可以说"本稿在同体裁公开样本分布的哪个位置",不能由此推出"写得好"。
23
+ 分布定位的合法输出是描述不是裁决——这条与 `tighten-doc` SKILL.md「外部基线」条同源,按那条执行。
24
+
25
+ ## §2 选哪种图:按主张的形态
26
+
27
+ `[外]` 依据 UML 2.5 图种语义。**判别式是「有没有显式事件触发」**:
28
+
29
+ - 有事件触发,且关心对象的生命周期与合法/非法迁移 → **状态机图**
30
+ - 有跨参与者的消息往返,且顺序本身就是主张 → **时序图**
31
+ - 无事件触发,随动作完成自动流转(含判定分支)→ **流程图 / 活动图**
32
+ - 主张是「谁包含谁、谁依赖谁」的静态结构 → **架构图**(先定抽象层)
33
+
34
+ 反向同样是规则:没有时间顺序不画时序图,没有非法迁移不画状态机。把静态包含关系画成流程图,
35
+ 箭头承载不了「包含」,读者必须回正文才懂——这类图必然过不了 §5 的独立性验收。
36
+
37
+ **架构/方案族的视图三分** `[外]`:C4(Context/Container/Component、Dynamic、Deployment)、
38
+ arc42(§5 Building Block / §6 Runtime / §7 Deployment)、4+1(Logical+Development / Process /
39
+ Physical)三个来源各自独立、收敛到同一组三分:**静态结构 / 运行时行为 / 部署拓扑**。
40
+ 缺哪一类视图就是缺一类受众。完整性判据用 §5 的关切覆盖(每个关切至少被一个视图回答)。
41
+
42
+ ## §3 画了就必须满足的硬约束 `[外]`
43
+
44
+ 通用(C4 记法与其 21 项评审清单):
45
+
46
+ - 标题说清**图的类型与范围**(不是「架构图」,是「XX 系统的容器图」)
47
+ - **有图例**,解释形状、颜色、边框、线型、箭头
48
+ - **每条线单向且标签具体**——禁 `Uses`/`调用`/`依赖` 这类单词,要说清意图与方向
49
+ - 跨进程连线标出协议/技术;每个元素标类型 + 一句职责
50
+ - **配色一致性跨图成立**(原文:consistent within and across diagrams),且要能过黑白打印与色觉障碍
51
+
52
+ 按图种补充:
53
+
54
+ - **时序图**:lifeline 是参与者不是函数;同步/异步用不同箭头并进图例;**错误分支要么画、要么在图注
55
+ 声明本图只画 happy path**——不声明即隐瞒
56
+ - **状态机**:状态穷尽(含初态、终态、异常态);**非法迁移显式**(画禁止边或图注列出不存在的迁移);
57
+ 每条边标**触发事件 + 守卫条件**,缺一即不可验证
58
+ - **流程图**:每个判定框**出边覆盖全部取值**;不用它表达静态包含关系
59
+
60
+ **图注写主张,不写内容** `[工]`:「图 1:系统架构」是废话;「图 1:入口层与 API 路径分离,
61
+ AI 能力跨云调用不与主云绑定」才是主张。图注承担 §5 独立性验收的一半。
62
+
63
+ ## §4 调研/报告族的图与表 `[外]`
64
+
65
+ 依据 ICD 203 九条分析标准中的第 9 条,原文:
66
+
67
+ > visual presentations should be used when information or concepts (e.g., spatial or temporal
68
+ > relationships) **can be conveyed better in graphic form (e.g., tables, flow charts, images) than
69
+ > in written text** … **Analytic content in visual information should also adhere to other analytic
70
+ > tradecraft standards.**
71
+
72
+ 两个可执行推论:
73
+
74
+ 1. **判据是「图形式是否比文字更能传达」,不是「要不要配图」。** 空间关系与时间关系优先出图;
75
+ 不满足这个条件的图是装饰。**反向也成立:量级对比与结构关系压进表格是载体错配**——表适合
76
+ 逐条查证与精确取值,看不出量级差异。
77
+ 2. **图与表里的分析内容同样受其余八条约束**——图上的判断也要能追到来源、表达不确定性、
78
+ 区分信息与假设。落成可跑的判据:图上实体名在证据正文的命中数 / 在标题的命中数 /
79
+ 引用的证据编号是否存在,三项全空即**撤框**,不留「看起来合理」的框。
80
+
81
+ **档位必须拆开标,别整条挂在 `[外]` 名下。** 上面第 1 条的**载体选择原则**是 `[外]`;
82
+ 但检查器用来识别「哪张表算量级对比」的**触发阈值**(行数、数值列占比、列数)是 `[工]`——
83
+ 本检查器自定的保守下界,无外部依据。把两者一起标成 `[外]`,正是 §1 要防的混档,
84
+ 而且本轮独立评审就是在这一条上抓到了本文件自己:一条声称按依据分档的规则自己标错了档。
85
+
86
+ 同族另两条对本轮问题直接相关:**第 2 条**要求不确定性用固定七档词表
87
+ (`almost no chance / very unlikely / unlikely / roughly even chance / likely / very likely /
88
+ almost certain`)并说明什么指标会改变不确定性水平;**第 7 条**要求解释本版判断相对上版的
89
+ 变化或一致性——**这把修订轮从"打磨消耗"变成必答内容,没写是缺项不是勤奋**。
90
+
91
+ 报告中图表与表格的视觉记法另有国际标准 **ISO 24896:2026《Notation for business reporting》**
92
+ (2026-06-11 发布;IBCS 2.0 与之对齐)。本文件不搬其条文,只登记为该族的权威面。
93
+
94
+ ## §4b 图的布局:唯一有实证的那一支 `[外]`
95
+
96
+ §1 判定「图表密度」「行宽」这类无可靠来源是对的,但**图的布局属性另有一支真正做过实验的文献**,
97
+ 本仓此前完全没有覆盖——它是少数几个既有实证依据、又能在 SVG 里精确计算的属性。
98
+
99
+ **Purchase, "Which aesthetic has the greatest effect on human understanding?"(Graph Drawing 1997)**
100
+ 以理解时间与错误数双指标测五项美学,原文结论:
101
+
102
+ > 减少边交叉是**迄今为止最重要**的美学特征,而最小化弯折数与最大化对称性影响较小。
103
+ > **最大化节点出边间最小夹角、以及把边与节点固定到正交网格,其效果在统计上不显著。**
104
+
105
+ **Huang, Eades, Hong, "Larger crossing angles make graphs easier to read"(JVLC 25(4), 2014)**:
106
+ 更大的交叉角让图更易读,且**不必是直角**。
107
+
108
+ 两条可执行推论:
109
+
110
+ 1. **冲突时的取舍序**:边交叉 > 弯折 > 对称。**不要为了对齐正交网格或加大出边夹角去牺牲前三者**——
111
+ 后两项统计上不显著,为它们做取舍没有实证支持。这半句同样是 `[外]`,它是一条"别做什么"的依据。
112
+ 2. **不可避免的交叉,角度越大越好**,但不必强求 90°。
113
+
114
+ **三条边界必须一起引,否则就是误引**:
115
+
116
+ - **实验对象是抽象点线图,不是带标签的架构图。** 受试回答的是图论问题(连通、最短路径),
117
+ 不是「这个系统怎么运作」。架构图的节点有名字、有语义分组、有分层——**外推需声明,不得当作已验证**。
118
+ - **它给的是排序,不是阈值。** "边交叉最重要"不等于"交叉数 ≤ N"。
119
+ 由它推出任何具体数字,就变成了 §9 删掉那四条谓词的同类。
120
+ - **只有前两项可机械计算**:边交叉数(线段求交)与弯折数(path 折点数)可精确算;
121
+ 对称性不可靠,不做。
122
+
123
+ 据此 `figure-lint` 提供 `GRAPH-CROSSINGS`(**WARN,只报数不设阈**):给出连线两两交叉数与最小交叉角,
124
+ 把事实交给人判断。**没有阈值依据就不设阈**——这条谓词的存在形态本身就是 §1 分档纪律的体现。
125
+
126
+ ## §4c 色觉障碍:语义只落在颜色上就会失效 `[外]` 机制 / `[工]` 判据
127
+
128
+ **机制(一手)**:protanopia 源于长波(L)视锥的视蛋白基因缺失或改变,
129
+ 所以 protanope 容易把**红色与黑色**混淆——不是"红变成别的颜色",是长波整体感光下降。
130
+ 色度学上,同一条**混淆线**(confusion line)上的颜色对该类型不可分;
131
+ 标准模拟法是把颜色投影到 LMS 空间的不变平面(Brettel/Viénot/Mollon 1997;Viénot/Brettel/Mollon 1999)。
132
+
133
+ **由此得出的判据不是「避开某些颜色」,而是「别让语义只落在同一条混淆线上的色对」。**
134
+
135
+ - **实算优于模式匹配。** 按"红橙/蓝黄/低饱和"这类色对模式去查,在本仓真实 token 集上
136
+ **只能命中三分之一**——另两对(主色/静默色 protan 距 14、主色/正常态 tritan 距 19)
137
+ 不属于任何预设模式,却同样不可分。
138
+ - **参照点用实测给**:Okabe-Ito 八色两两最小二色视距离 **38**;随机八色 **10**。
139
+ 本仓原 token 集 **14**——落在随机附近。
140
+ - **色不是唯一编码**:语义还应有形状、图标或文字冗余。
141
+ - **只测契约里显式声明为语义的色,且只报数不下判定**(`CVD-DISTANCE`)。两条都是被评审打回来才改对的:
142
+ ①早先比的是图里**全部渲染色**(含装饰、边框、底色),等于假定任意两色都编码不同语义——
143
+ 装饰色撞在一条混淆线上不构成缺陷,所以语义色必须由 `figure-contract.json` 的 `semantic_colors` **声明**,
144
+ 声明缺失就不报,而不是替作者猜;
145
+ ②早先虽写着"只报数",代码里却拿 25 当触发条件——**那就是阈**。
146
+ 上面那三个参照点(38 / 14 / 10)是量出来的,`25` 不是;现在无条件报出距离与参照点,可否接受由人判断;
147
+ ③"只报数"若走 `WARN` 档,退出码就变成非零——**声明会被退出码当场推翻**。
148
+ 故本条单列 `INFO` 档:打印、但不影响退出码。没有阈值依据的测量一律走这一档。
149
+ - **`semantic_colors` 必须受契约校验**:写成数组/字符串会让下游 `.values()` 直接崩;
150
+ 写成含非法颜色的映射会被静默丢成空集,于是"没报 CVD"既可能是"距离没问题"、
151
+ 也可能是"压根没测"——两者必须可区分。现在类型、`#RRGGBB` 语法、以及
152
+ "必须是 `color_tokens` 的子集"三项都在 `validate_contract` 里判,违者 `CONTRACT-INVALID`。
153
+
154
+ **验证锚点也可能是错的(本文件踩过)**:曾用"红色模拟后应变暗"去验实现,结果亮度上升,
155
+ 差点判一个正确算法有 bug。原因是模拟把红投影到 **575nm 黄色不变轴**——
156
+ 亮度上升是算法的正确行为;而"红色看起来暗"说的是**红与黑难分**,是另一个量。
157
+ **用错的尺子量对的实现,比实现出错更难发现**,因为其余锚点会通过。
158
+
159
+ `[外]` 调色板出处:Okabe & Ito,*Color Universal Design (CUD)*,J\*Fly(<https://jfly.uni-koeln.de/color/>);
160
+ Bang Wong 2011 年在 *Nature Methods* 使其广为人知,故常被称作 "Wong palette",但原创归 Okabe 与 Ito。
161
+
162
+ ## §4d 图值不值得画,以及它退化成什么 `[工]`
163
+
164
+ - **一句话说得更快,就写那句话。** 图的价值在于让冷读者看见一个**否则要从散文里拼出来的机制**。
165
+ - **画机制,不画名字。** 一个写着「缓存」的框比散文信息还少;请求穿过它的路径、它夹在哪两个存储之间、
166
+ 移除它会消失哪条箭头——这些才是文字说不清的。
167
+ - **一组互不相连的标注框不是对比,是列表的图形版**(本仓 `FIGURE-IS-A-LIST` 查这条)。
168
+ 比较方案就画**差异**:并排两版、前后对照、每个选项各加/减的那一条边。
169
+ - **图例只在同一编码重复出现时才值得**;否则把含义直接写在标记上。
170
+ - **流向一致**:L→R 或 T→B,不混用(`FLOW-DIRECTION-MIXED`)。该判据**依赖坐标**,
171
+ 所以图里存在 transform 等解析不了的呈现时整条跳过(否则旋转/镜像后的组会被判成另一个方向);
172
+ 两端都有箭头的双向边不计入分布,只有 `marker-start` 的边按反向计;
173
+ **两端都没箭头的连接直接跳过**——按 `d` 的书写顺序给它定向是凭空造方向,
174
+ 三条视觉上无向的线仅因端点书写顺序不同就能触发混合流向。
175
+ - **过期的图比没有图更糟——它主动误导。** 改动了图所描述的东西,同一次提交里改图;
176
+ 评审时即使超出本次范围,也要标出发现的过期图。
177
+ - **载体按规模选**(实践者做法,非实证):≤10 元素用 mermaid;<15 且要布局控制用 flowchart;
178
+ >15 或有持续交叉用 plantuml(其方向提示可修交叉)。
179
+ - **边交叉的实践者目标值**:复杂图 <5、简单图 0。**本仓不设阈**——`GRAPH-CROSSINGS` 只报数,
180
+ 实证(§4b)给的是排序不是门槛,这个目标值记在这里供参考,不作判定。
181
+
182
+ **不可机械判定、故只作提示**:视觉层级(系统边界应最显眼)、Gestalt 邻近性(相关元素成组)。
183
+ 二者归视觉判断,本文件不设检查器;真要判需 `product-ui-ux-design` 的渲染验收。
184
+
185
+ ## §5 版式契约:起草前冻结 `[工]`
186
+
187
+ **一致性本身是 `[外]` 要求**(C4 明文:consistent within and across diagrams;ICD 203 §2:
188
+ consistency in the terms used is critical)。**但「冻结成哪几档」是团队自选**——所以规则是
189
+ **契约符合性**,不是阈值。写成魔数("字号不超过 6 档")是错的:6 无依据,而偏离契约有依据。
190
+
191
+ 首稿动手前写死 `figure-contract.json`,与图源同目录或其上层:
192
+
193
+ ```json
194
+ {
195
+ "canvas_ratios": [1.778],
196
+ "font_scale": [12, 13, 15, 18, 19, 36],
197
+ "color_tokens": { "ink": "#111827", "muted": "#667085", "primary": "#356A8A",
198
+ "ok": "#12805C", "warn": "#B54708", "critical": "#B42318",
199
+ "surface": "#FFFFFF", "surface-2": "#F7F8FA" },
200
+ "semantic_colors": { "ok": "#12805C", "warn": "#B54708", "critical": "#B42318" },
201
+ "themes": { "light": { "surface": "#FFFFFF" }, "dark": { "surface": "#0B0F19" } },
202
+ "_retired": { "#98A2B3": "对白底 2.58:1,不达 WCAG AA 4.5:1,已退役" }
203
+ }
204
+ ```
205
+
206
+ - **契约缺失本身是缺陷**:没有冻结的版式,一致性不可判定,每轮内容变动都要重调几何
207
+ - **退役项写明理由**:下次有人想用,看到的是「为什么不能用」而不是「不在表里」
208
+ - 改契约 = 改规范,不是改一张图
209
+ - 契约还需覆盖 §4 未定的两项:**单格文本上限**(按"必须能在一行放下"定,不按字数拍)与
210
+ **分组单元**(每个卡片 = 形状 + 其文字包一个组)
211
+ - `semantic_colors` 是 `color_tokens` 的**子集声明**:哪些 token 承载语义区分(§4c 只测这些)。
212
+ 不声明 = 不测,不是"测出来没问题"
213
+ - `themes.<名>.surface` 必须是 `#RRGGBB`。写成 `"black"` 或非字符串,早先只判真值会让它通过校验、
214
+ 随后因不是 `#` 开头被静默跳过——**声明了主题却一次都没测,报出来的是绿**。现在按语法拒绝
215
+ - **只有直接压在主题底色上的文字才按主题判**(`THEME-CONTRAST`)。压在局部卡片上的文字,
216
+ 实际底色由卡片决定:深底上一块不随主题换的白卡片 + 深色字是**可读**的,拿全局底色去判会误报。
217
+ 判据是"这块底板是不是覆盖了整幅画布"——整幅底板算页面底色,明显小于画布的才是卡片。
218
+ 另两条前提同样是被评审打回来才补的:**`fill="none"` 的描边框不是底板**
219
+ (否则一个框住低对比文字的边框就把它豁免了);**底色解析不可信时整条跳过并显式报
220
+ `THEME-UNASSESSED`**——圆形/路径/渐变卡片不会进 `rects`,此时"查不到卡片"会被当成
221
+ "文字压在主题底上",判出来的是假红。**未判定必须说成未判定,不能沉默成绿**
222
+
223
+ ## §6 可读性与渲染:两条硬事实 `[外]`
224
+
225
+ - **WCAG 2.2 SC 1.4.3**:正文对底 ≥4.5:1;大字(≥18pt 或 ≥14pt 粗)≥3:1;例外只有纯装饰与 logo。
226
+ 图里的说明小字最容易违反,因为它又小又浅——所以 §5 的 token 表要求标注对比度
227
+ - **WCAG 2.2 SC 1.3.1(Level A)**:加粗若承载信息,必须有语义标记或文字替代。管的是「加粗有没有
228
+ 意义」,不是「加粗了多少」。对表格即:**真表头,不能用加粗行冒充**
229
+ - **SVG `<text>` 默认不换行**:规范行为不是偏好。超出容器的文本不折行、直接溢出,所以
230
+ 「单格文本上限」是功能要求不是审美要求
231
+
232
+ **目标端渲染的三条** `[工]`:
233
+
234
+ 1. **验收以目标端回读为准,不认本地检查器**——本地全绿而目标端丢字是已发生过的事,检查器不模拟
235
+ 目标端的写入行为
236
+ 2. **卡片单元必须分组**——至少一个目标端会对未分组的「形状+文字」做 z 序重排把矩形排到文字上层
237
+ 盖字,且每次覆写受害者不同;载荷顺序正确无用,重排在写入端
238
+ 3. **交互源导成静态图前全部展开再导**(可折叠节点、悬浮标签、滚动区、分页表),导出后对照交互源
239
+ 清点实体
240
+
241
+ 多载体时的载体登记与同步义务归 `multi-perspective-research`,本文件不重述。
242
+
243
+ ## §7 图文一致
244
+
245
+ `[工]`,但它兑现的是 §5 独立性验收与 ICD 203 §6(清晰且合乎逻辑的论证):
246
+
247
+ - 正文引用的图号必须存在——引用悬空即缺陷
248
+ - 采用编号图注约定后,每条图注都应被正文引用;有图从不被正文提及 = 图与正文各说各的,
249
+ 读者不知道该在哪一步看图
250
+ - 单张随文插图不强制编号(否则会把正常写法误判为缺陷)
251
+
252
+ ## §8 检查器自身的规则 `[工]`
253
+
254
+ 这一节的存在本身是教训:本轮第一版对比度检查器把压在色块上的白字判成 1.0:1(假阳性),
255
+ 后又出现过图注把自己算成正文引用(假阴性)。
256
+
257
+ 1. **先证明它能报失败再信它的绿**——逐谓词 fixture + 干净控制组,控制组被误报即检查器有缺陷
258
+ 2. **改判据必同步改 fixture**:本轮修完误报后,orphan 那条谓词的 fixture 当场失效却仍"通过"——
259
+ 这是最容易静默丢覆盖的一步
260
+ 3. **对比度检查必须解析文本实际压着的底色**,不能假定白底
261
+ 4. **谓词钉在结构与实体上**,不钉在文件名或标题词表上——改名后词表谓词要么误报,要么在检查器
262
+ 仍读旧文件时静默通过,后者更危险
263
+ 5. **派生产物新鲜度要两道判据**:实体数与 canonical 一致(抓内容过期)+ 文件时间不早于 canonical
264
+ (抓"改了名字但数量没变")。只查数量会在纯改名的轮次里全绿
265
+ 6. **派生产物不进闸,就是下一个静默过期的载体**——新增派生产物的当刻就加进闸
266
+
267
+ ## §9 已删除的谓词(勿再加回)
268
+
269
+ 以下四条曾经存在,**已删除**:标题过长、单元格塞整段、段落内并列枚举、列表项多句。
270
+
271
+ 删除理由不是"太严",是**判据错了**:它们都拿宽度或计数当「表达好不好」的代理,
272
+ 而这类阈值经查证无可靠来源(见 §1 `[禁]` 档)——等于把 `[禁]` 档的东西改个名字重新立一遍。
273
+
274
+ 代价是实测出来的:在 392 份正常文档上全量试跑,这四条产生 **318 条 ERROR、涉及 150 份文件**,
275
+ 抽样中**没有一条**是其声称的那个缺陷;命中最多的是围栏代码块里的 ASCII 分隔线与示例标题。
276
+ 删除后总发现从 718 条降到 23 条、ERROR 归零、涉及文件 13/392。
277
+
278
+ **一条真正的缺陷判据不会在正常仓库里命中 38% 的文件。** 命中率本身就是判据是否成立的证据,
279
+ 新增谓词前先在真实语料上量一次。
280
+
281
+ 顺带修的两条前提性错误(同样由全量试跑暴露):
282
+ - **围栏代码块与其内容不参与结构分析**——代码块里的分隔线、示例标题、示例表格都不是文档结构
283
+ - **「引用悬空」的前提是文档确实在用图系统**——一张图都没有的文档里出现「图 N」是在谈论图,
284
+ 不是在引用;本文件自己就因此被误判过一次
285
+
286
+ ## §9b 两条代理谓词为何是非阻断(读结果前先知道)
287
+
288
+ `C4-LEGEND` 与 `C4-EDGE-LABEL` 不是精确判据,它们的失败方向不同,报出来的数字含义也不同:
289
+
290
+ - **`C4-LEGEND` 实测为真阳性。** 在 18 张真实架构图上报 17 张缺图例;抽 3 张核对,
291
+ 确实是 0 个小色块、无图例关键字、尾部文本全是内容而非图例项。报什么就是什么。
292
+ - **`C4-EDGE-LABEL` 倾向漏报,不是误报。** 它把「连线中点 90px 内的框外自由文本」当作标签,
293
+ 而实测中框外自由文本大多是**分区标题**(「入口层」「数据层」)与**旁注**,不是边标签。
294
+ 一条真正无标签的连线,只要中点附近恰好有个分区标题,就会被判为有标签。
295
+ **所以它报出的数量是下界,真实情况只会更差**——读结果时不要当成全部。
296
+
297
+ **两条都已降为 WARN(非阻断)。** 对抗评审实测出它们**两个方向都会错**:
298
+ `C4-LEGEND` 拒掉只有两个条目的合法图例,而注释 / `<desc>` / class 里随便出现 `legend` 字样
299
+ 就能让没有图例的图通过;`C4-EDGE-LABEL` 会被中点附近的分区标题掩盖掉未标注的连线,
300
+ 又会拒掉标在连线别处的合法标签。**C4 的规则本身是 `[外]`,但用邻近与色块计数去认它是 `[工]` 代理**——
301
+ 代理不该阻断他人提交。要恢复阻断,需要结构化关联(契约声明的组 / `aria-labelledby` / `textPath`),
302
+ 而不是把阈值调得更准。
303
+
304
+ 把下界当全量会给人虚假的安心。
305
+ `est_width` 同理:它是保守估算(CJK 1em / 拉丁 0.55em),只判「明显装不下」,临界宽度不可靠。
306
+
307
+ ## §10 可跑的检查
308
+
309
+ 同目录 `../scripts/figure-lint.py`(SVG)与 `../scripts/doc-lint.py`(Markdown 表格与结构)实现了
310
+ 上述可机械判定的部分;`../scripts/tests/` 是逐谓词 fixture 与控制组,按 §8 第 1 条先自测再信。
311
+
312
+ ```
313
+ python3 skills/tighten-doc/scripts/figure-lint.py <svg 目录或文件>
314
+ python3 skills/tighten-doc/scripts/doc-lint.py <文档.md>
315
+ ```
316
+
317
+ **不可机械判定的仍归人**:图种选得对不对、图注写的是不是主张、独立性验收、
318
+ 调研族图上实体能否回指证据。检查器只挡机械项,不替代 §5 的验收。
@@ -0,0 +1,46 @@
1
+ # skills/tighten-doc/scripts Agent Contract
2
+
3
+ 这里的脚本对交付文档的**图与表**做起草期确定性检查:`figure-lint.py` 查 SVG,
4
+ `doc-lint.py` 查 Markdown 的表格与结构。判据与其依据档位定义在
5
+ `../references/figure-and-table-craft.md`。
6
+
7
+ Rules:
8
+
9
+ - **每条谓词必须标依据档位**,并与 `figure-and-table-craft.md` §1 的分档一致:
10
+ `[外]` 有权威一手源、`[工]` 工程约定(不得声称行业最佳实践)、`[禁]` 查证后确认无来源。
11
+ 新增谓词若属 `[工]`,其阈值必须在代码注释里明说是拍的下界,不得写成像有依据的样子。
12
+ - **不引入"可读性阈值"类的无据数字**。加粗密度、图表密度、行宽字数、句长这几类已核实无可靠来源
13
+ (见 `figure-and-table-craft.md` §1 的 `[禁]` 档),不得以任何形式重新引入。
14
+ - **一致性判为契约符合性,不判魔数**:偏离已声明的 `figure-contract.json` 才报,
15
+ "几档算多"不是判据;契约缺失单独报 `CONTRACT-MISSING`。
16
+ - **对比度检查必须解析文本实际压着的底色**,不得假定白底——假定白底会把压在色块上的白字误报。
17
+ - 谓词钉在**结构与实体**上,不钉在文件名或标题词表上。
18
+ - 两个 linter 的退出码语义一致(含 `--json` 模式):有 ERROR 返回 1、仅 WARN 返回 2、干净返回 0。
19
+ 改其中一个必须同步另一个,否则调用方会把正常结果当执行失败。
20
+
21
+ Validation(缺一不可):
22
+
23
+ - `bash skills/tighten-doc/scripts/test_figure_and_doc_lint.sh` —— 逐谓词差分 + 契约三态正负例。
24
+ **改判据必须同步改 fixture**:本目录踩过"修完误报后 fixture 当场失效却仍通过"。
25
+ - **新增或修改谓词后必须做突变实测**:跑 `bash skills/tighten-doc/scripts/mutation_probe.sh`——
26
+ 它逐谓词把 code 字面量替换掉使其在结果中消失,确认套件转红;任一谓词仍绿即无覆盖,脚本退出非 0。
27
+ 该脚本是可重跑的证据,不是一次性命令:评审方可自行执行核对。两条已踩过的坑——① 突变要作用在 oracle 实际观测的维度上
28
+ (改 severity 而断言比 code 集合 = 无效突变,会全绿假通过);② 突变判据要看**退出码**,
29
+ 只看有无 `FAIL` 行会把脚本崩溃读成绿。
30
+ - **新增谓词前先在真实语料上量命中率**:fixture 只证明谓词**能**报,不证明它报得**对**——
31
+ fixture 是作者造的,天然符合作者的假设。判据是否成立要看它在**没有为它准备的**真实文档上的分布。
32
+ 本目录实测:四条阈值型谓词在 392 份正常文档上产生 318 条 ERROR、命中 38% 的文件,
33
+ 抽样中无一条是其声称的缺陷,已全部删除。**一条真正的缺陷判据不会在正常仓库里命中 38% 的文件。**
34
+ 参考命令:`python3 doc-lint.py $(find docs skills -name '*.md' -not -path '*/tests/*') --json`。
35
+ - **已删除的谓词勿再加回**:标题过长、单元格塞整段、段落内并列枚举、列表项多句——
36
+ 理由与实测代价见 `../references/figure-and-table-craft.md` §9。
37
+ - 断言不得写成 OR(三选一命中即过)——那会让其余维度永久失去覆盖。
38
+ - **期望表必须全量对应**:报告里出现而期望表未列的文件、或期望表列了而报告里没有的文件,都判红。
39
+ 只断言点名的 fixture 会让意外文件(通配到非目标文件、新增 fixture 忘记登记)的发现无人过问。
40
+ - **改函数签名必须同步改调用点**:曾把参数加进签名却漏改调用点,第一条期望被当成参数吃掉,
41
+ 控制组变脏仍全绿。参数个数不足现在会直接判红。
42
+ - 该测试已注册进 `Makefile` 的 `test-repo-gates`;新增 `test_*.sh` 同样要注册,
43
+ 本目录**不在** `skill-extraction-workflow` 那道注册闸的覆盖范围内,不注册就静默不跑。
44
+ - shell 脚本里凡在字符串中插值一律用 `${var}`:CJK 标点紧跟 `$var` 会被 bash 吞进变量名
45
+ (本目录踩过两次)。
46
+ - `bash skills/skill-extraction-workflow/scripts/check-ccl-skills.sh .`
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """doc-lint — 交付文档的表格与结构的起草期确定性检查(Markdown)。
3
+
4
+ 判据来源:
5
+ [外] WCAG 2.2 SC 1.3.1 (Level A) — 通过视觉呈现传达的信息与关系必须可被程序确定。
6
+ 对表格即:真表头,不能用加粗行冒充表头。
7
+ [外] ICD 203 §9 — tables 属于 visual information;且「图形式比文字更能传达
8
+ 空间/时间关系时应当出图」。据此判「该出图却压成表」。
9
+ [外] ICD 203 §1/§3 — 承载判断的内容要能追到来源,并区分信息与假设。
10
+ [工] 其余为工程判断(列数过多、占位符未填、数值列无单位、图文引用一致)。
11
+
12
+ 已删除的谓词(勿再加回):标题过长、单元格塞整段、段落内并列枚举、列表项多句。
13
+ 它们都拿宽度或计数当「表达好不好」的代理,而这类阈值经查证无可靠来源;
14
+ 全仓 392 份文档试跑时它们产生 318 条 ERROR,抽样中无一条是其声称的缺陷。
15
+
16
+ 用法: doc-lint.py <file.md>... [--json]
17
+ """
18
+ import sys, re, os, json, collections
19
+
20
+ CJK = re.compile(r'[\u4e00-\u9fff]')
21
+
22
+ def width(s):
23
+ """近似显示宽度:汉字 2,其余 1。"""
24
+ return sum(2 if CJK.match(c) else 1 for c in s)
25
+
26
+ def parse_tables(lines):
27
+ """返回 [(start_line, header_cells, sep_ok, rows)]"""
28
+ tables = []
29
+ i = 0
30
+ while i < len(lines):
31
+ if lines[i].lstrip().startswith('|') and i + 1 < len(lines):
32
+ sep = lines[i + 1].strip()
33
+ sep_ok = bool(re.match(r'^\|?[\s:\-\|]+\|[\s:\-\|]*$', sep)) and '-' in sep
34
+ if sep_ok:
35
+ header = [c.strip() for c in lines[i].strip().strip('|').split('|')]
36
+ rows = []
37
+ j = i + 2
38
+ while j < len(lines) and lines[j].lstrip().startswith('|'):
39
+ rows.append([c.strip() for c in lines[j].strip().strip('|').split('|')])
40
+ j += 1
41
+ tables.append((i + 1, header, sep_ok, rows))
42
+ i = j
43
+ continue
44
+ i += 1
45
+ return tables
46
+
47
+ PLACEHOLDER = {'-', '—', '–', 'N/A', 'n/a', 'TBD', 'TODO', '待定', '待补', '?', '待填'}
48
+ NUMRE = re.compile(r'^[¥$€]?\s*-?[\d,]+(\.\d+)?\s*[%‰]?$')
49
+ UNIT_HINT = re.compile(r'[%‰]|元|美元|万|亿|GB|TB|MB|ms|s\b|次|人|天|月|年|条|个|倍|Ki?B|/')
50
+
51
+ FENCE = re.compile(r'^\s*(```|~~~)')
52
+
53
+ def strip_fences(lines):
54
+ """把围栏代码块内的行替换成空行(保留行号)。
55
+
56
+ 代码块里的内容不是文档结构:ASCII 分隔线、缩进的示例标题、
57
+ 示例表格都会被结构谓词误判。全仓试跑时 `── Report ──` 这类
58
+ 分隔线被当成标题,就是漏了这一层。
59
+ """
60
+ out = []
61
+ in_fence = False
62
+ for ln in lines:
63
+ if FENCE.match(ln):
64
+ in_fence = not in_fence
65
+ out.append('')
66
+ continue
67
+ out.append('' if in_fence else ln)
68
+ return out
69
+
70
+ def lint(path):
71
+ # 非 UTF-8 文本(图片等)不是本检查器的对象:跳过而不是崩——
72
+ # 调用方按目录通配传入时,目录里混着图片是常态。
73
+ # 与 figure-lint 对齐:坏文件报 READ 并继续整批,不能静默判干净——
74
+ # 早先返回空发现集 + skipped=true,等于让损坏的 .md「干净通过」。
75
+ try:
76
+ src = open(path, encoding='utf8').read()
77
+ except (UnicodeDecodeError, OSError) as e:
78
+ return ([{'level': 'ERROR', 'code': 'READ',
79
+ 'msg': f'无法读取: {type(e).__name__}: {e}', 'line': None}],
80
+ {'tables': 0, 'figures': 0, 'lines': 0})
81
+ raw_lines = src.split('\n')
82
+ # mermaid 图是**图**不是代码:必须在剥离围栏前数,否则恒为 0,
83
+ # 既会制造 CARRIER-IMBALANCE 假报,又让图文引用检查漏检。
84
+ n_mermaid = sum(1 for ln in raw_lines if re.match(r'^\s*(```|~~~)\s*mermaid\b', ln))
85
+ lines = strip_fences(raw_lines)
86
+ src = '\n'.join(lines) # 后续按剥离围栏后的正文分析
87
+ F = []
88
+ def add(level, code, msg, line=None):
89
+ F.append({'level': level, 'code': code, 'msg': msg, 'line': line})
90
+
91
+ # ---- 表格 ----
92
+ tables = parse_tables(lines)
93
+ fake_header_rows = 0
94
+ for (ln, header, sep_ok, rows) in tables:
95
+ ncol = len(header)
96
+
97
+ # [外] WCAG 1.3.1:表头必须是真表头
98
+ if all((not h) or h in PLACEHOLDER for h in header):
99
+ add('ERROR', 'WCAG-131-TABLE', f'表格表头为空——表头必须可被程序确定,不能靠视觉暗示', ln)
100
+
101
+ # [工] 列数过多
102
+ if ncol > 8:
103
+ add('WARN', 'TABLE-WIDE', f'{ncol} 列——超出一屏可读范围,考虑拆表或转置', ln)
104
+
105
+ # [工] 占位符未填
106
+ cells = [c for row in rows for c in row]
107
+ if cells:
108
+ ph = sum(1 for c in cells if c in PLACEHOLDER or not c)
109
+ if ph / len(cells) > 0.25:
110
+ add('WARN', 'TABLE-UNFILLED',
111
+ f'{ph}/{len(cells)} 个单元格为空或占位符({ph/len(cells)*100:.0f}%)——表未填完', ln)
112
+
113
+ # [工] 数值列缺单位/口径
114
+ for c_i in range(ncol):
115
+ col = [row[c_i] for row in rows if c_i < len(row)]
116
+ nums = [c for c in col if NUMRE.match(c)]
117
+ if len(nums) >= 3 and len(nums) / max(len(col), 1) > 0.6:
118
+ head = header[c_i] if c_i < len(header) else ''
119
+ if not UNIT_HINT.search(head) and not any(UNIT_HINT.search(c) for c in nums):
120
+ add('WARN', 'TABLE-NO-UNIT',
121
+ f'数值列「{head or f"第{c_i+1}列"}」表头与单元格均无单位/口径', ln)
122
+
123
+ # 载体选择原则是 [外](ICD 203 §9:图形式更能传达空间/时间关系时应出图),
124
+ # 但「≥6 行、数值占比 >0.8、≤3 列」这三个识别阈值是 [工]——本检查器自定的
125
+ # 保守下界,无外部依据。两者档位不同,不能一起挂在 [外] 名下。
126
+ if len(rows) >= 6:
127
+ numcols = 0
128
+ for c_i in range(ncol):
129
+ col = [row[c_i] for row in rows if c_i < len(row)]
130
+ if col and sum(1 for c in col if NUMRE.match(c)) / len(col) > 0.8:
131
+ numcols += 1
132
+ if numcols == 1 and ncol <= 3:
133
+ add('WARN', 'ICD203-9-SHOULD-BE-CHART',
134
+ f'{len(rows)} 行 × {ncol} 列且仅一列数值——量级对比用图形式更能传达。'
135
+ f'[外] 载体选择原则来自 ICD 203 §9;[工] 触发阈值(≥6 行 / 数值占比 >0.8 / ≤3 列)'
136
+ f'为本检查器自定的保守下界,无外部依据', ln)
137
+
138
+ # ---- 文档级:表图配比 ----
139
+ n_fig = len(re.findall(r'!\[', src)) + n_mermaid + len(re.findall(r'<img', src))
140
+ n_tab = len(tables)
141
+ # [工] 计数阈值是工程启发式,不是 ICD 203 的内容。ICD 203 §9 的判据是
142
+ # 「图形式是否比文字更能传达」——那取决于内容,不取决于表的个数。
143
+ # 八张查询表 / 模式表 / 证据表本来就不需要图,所以这条只报 WARN 不阻断,
144
+ # 且只在存在「量级对比型」表格(已由 ICD203-9-SHOULD-BE-CHART 认定)时才提示。
145
+ chart_worthy = any(x['code'] == 'ICD203-9-SHOULD-BE-CHART' for x in F)
146
+ if n_tab >= 8 and n_fig == 0 and chart_worthy:
147
+ add('WARN', 'CARRIER-IMBALANCE',
148
+ f'{n_tab} 个表、0 张图,且其中有适合出图的量级对比表——'
149
+ f'信息可能整体压给了表格(计数阈值为工程启发式,非外部标准)')
150
+ elif n_tab >= 10 and n_fig and n_tab / n_fig > 8 and chart_worthy:
151
+ add('WARN', 'CARRIER-IMBALANCE',
152
+ f'{n_tab} 表 / {n_fig} 图,比值 {n_tab/n_fig:.1f}——偏表(工程启发式)')
153
+
154
+
155
+ # ---- 图文一致 ----
156
+ # 正文引用的图号 vs 实际图数;以及有图从不被正文引用(孤图)
157
+ # 采集「正文引用」时必须排除图注行本身,否则图注会把自己算成引用(假阴性)
158
+ CAPTION_LINE = re.compile(r'^\s*[*_]*图\s*\d{1,2}\s*[::]')
159
+ fig_refs = set()
160
+ for _ln in lines:
161
+ if CAPTION_LINE.match(_ln):
162
+ continue
163
+ for m in re.finditer(r'图\s*(\d{1,2})', _ln):
164
+ fig_refs.add(int(m.group(1)))
165
+ # 图注锚(![...] 的 alt、或「图 N:」形式的说明行)
166
+ caption_nums = set()
167
+ for m in re.finditer(r'^\s*\*?图\s*(\d{1,2})\s*[::]', src, re.M):
168
+ caption_nums.add(int(m.group(1)))
169
+ n_fig_local = n_fig
170
+ # 采用编号图注约定时按**实际编号**比对;早先按数量比(n > max(图数, 图注数))
171
+ # 两个方向都会错:引用图 2 而图注只有 1 和 3 不报,唯一图注是图 10 却误报。
172
+ if fig_refs and caption_nums and not n_fig_local:
173
+ # 有编号图注、却没有任何真实图实例:图注在描述不存在的图。
174
+ add('ERROR', 'FIG-REF-DANGLING',
175
+ f'文档有编号图注 {sorted(caption_nums)} 但没有任何图片或 mermaid 图——图注指向的图不存在')
176
+ elif fig_refs and caption_nums:
177
+ missing = sorted(fig_refs - caption_nums)
178
+ if missing:
179
+ add('ERROR', 'FIG-REF-DANGLING',
180
+ f'正文引用了「图 {missing}」但文档没有对应编号的图注'
181
+ f'(现有图注编号 {sorted(caption_nums)})——引用悬空')
182
+ elif fig_refs and not caption_nums and n_fig_local:
183
+ # 「引用悬空」的前提是文档确实在用图系统。一张图都没有的文档里出现「图 N」,
184
+ # 那是在**谈论**图(举例、引用规范条文),不是在引用本文档的图——
185
+ # 本检查器自己的判据文档就因此被误判过一次。
186
+ missing = sorted(n for n in fig_refs if n > n_fig_local)
187
+ if missing:
188
+ add('ERROR', 'FIG-REF-DANGLING',
189
+ f'正文引用了「图 {missing}」但文档只有 {n_fig_local} 张图且无编号图注——引用悬空')
190
+ # 只有当文档已采用编号图注约定,或图多到需要索引(>=3)时,才要求正文引用。
191
+ # 单张随文插图不强制编号——否则控制组会被误报。
192
+ if n_fig_local and not fig_refs and (caption_nums or n_fig_local >= 3):
193
+ add('WARN', 'FIG-ORPHAN',
194
+ f'{n_fig_local} 张图,正文一次也没引用「图 N」——图与正文各说各的,读者不知道该在哪一步看图')
195
+ if caption_nums and fig_refs:
196
+ never_ref = sorted(caption_nums - fig_refs)
197
+ if never_ref:
198
+ add('WARN', 'FIG-ORPHAN', f'图注存在但正文未引用:图 {never_ref}')
199
+
200
+ # ---- 加粗行冒充小节标题 ----
201
+ for idx, ln in enumerate(lines, 1):
202
+ s = ln.strip()
203
+ if re.match(r'^\*\*[^*]{2,40}\*\*[::]?$', s):
204
+ fake_header_rows += 1
205
+ if fake_header_rows >= 5:
206
+ add('WARN', 'WCAG-131-FAKE-HEADING',
207
+ f'{fake_header_rows} 处「独占一行的加粗短语」——若充当小节标题,结构无法被程序确定(WCAG 1.3.1),且目录不可用')
208
+
209
+ return F, {'tables': n_tab, 'figures': n_fig, 'lines': len(lines)}
210
+
211
+
212
+ def main(argv):
213
+ as_json = '--json' in argv
214
+ files = [a for a in argv[1:] if not a.startswith('--')]
215
+ if not files:
216
+ print('用法: doc-lint.py <file.md>... [--json]', file=sys.stderr)
217
+ return 2
218
+ out = {}
219
+ n_err = n_warn = 0
220
+ for f in files:
221
+ F, st = lint(f)
222
+ out[f] = {'findings': F, 'stats': st}
223
+ n_err += sum(1 for x in F if x['level'] == 'ERROR')
224
+ n_warn += sum(1 for x in F if x['level'] == 'WARN')
225
+ if as_json:
226
+ print(json.dumps(out, ensure_ascii=False, indent=1))
227
+ else:
228
+ for f, r in out.items():
229
+ st = r['stats']
230
+ print(f"\n{os.path.basename(f)} [{st['lines']} 行 / {st['tables']} 表 / {st['figures']} 图]")
231
+ seen = collections.Counter()
232
+ for x in r['findings']:
233
+ seen[x['code']] += 1
234
+ if seen[x['code']] <= 3:
235
+ loc = f"L{x['line']}" if x['line'] else '--'
236
+ print(f" {x['level']:5} {x['code']:26} {loc:>6} {x['msg']}")
237
+ for c, n in seen.items():
238
+ if n > 3:
239
+ print(f" ... {c:26} 另有 {n-3} 处")
240
+ if not r['findings']:
241
+ print(' ✓ 无发现')
242
+ print(f'\n合计: {n_err} ERROR, {n_warn} WARN')
243
+ return 1 if n_err else (2 if n_warn else 0)
244
+
245
+ if __name__ == '__main__':
246
+ sys.exit(main(sys.argv))