agentcss 0.26.0 → 0.27.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 (44) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/responsive-demo.html +1 -1
  3. package/01-tokens/tokens.core.css +1 -1
  4. package/01-tokens/tokens.css +2 -2
  5. package/02-primitives/date-range/date-range.js +1 -1
  6. package/02-primitives/timeline/demo.html +1 -1
  7. package/03-patterns/drawer/demo.html +1 -1
  8. package/03-patterns/overlay/overlay.css +1 -1
  9. package/03-patterns/states/states.css +1 -1
  10. package/04-recipes/app-shell/README.md +1 -1
  11. package/04-recipes/app-shell/demo.html +1 -1
  12. package/04-recipes/longform/content.html +6 -6
  13. package/04-recipes/report/demo.html +2 -2
  14. package/04-recipes/table/README.md +1 -1
  15. package/04-recipes/table/demo.html +1 -1
  16. package/09-assets/bar/bar.css +1 -1
  17. package/09-assets/model-viewer/model-viewer.js +1 -1
  18. package/AGENTS.md +3 -1
  19. package/CHANGELOG.md +103 -5
  20. package/CONTRIBUTING.md +44 -3
  21. package/README.md +2 -0
  22. package/README.zh-CN.md +2 -0
  23. package/START-HERE.md +12 -5
  24. package/ai/START-HERE.md +40 -6
  25. package/ai/check-html.js +658 -0
  26. package/ai/cli.js +557 -67
  27. package/ai/components.json +1 -1
  28. package/ai/pdf.js +2 -2
  29. package/ai/recipes.json +86 -0
  30. package/ai/scaffold.js +195 -0
  31. package/ai/shot.js +345 -0
  32. package/ai/validate.js +65 -6
  33. package/dist/COSTS.md +10 -10
  34. package/docs/ACCESSIBILITY.md +2 -2
  35. package/docs/ARCHITECTURE.md +116 -0
  36. package/docs/LAYER-BASELINE.md +2 -2
  37. package/docs/PRINT.md +3 -3
  38. package/docs/RELEASE-PROCESS.md +52 -2
  39. package/docs/STABILITY.md +1 -1
  40. package/docs/VERIFY-RELEASE.md +17 -0
  41. package/index.html +3 -3
  42. package/llms-full.txt +17 -3
  43. package/llms.txt +26 -5
  44. package/package.json +2 -1
@@ -257,7 +257,7 @@ node ai/cli.js tree [--set=light|dark] [--cat=color] # 第二种
257
257
  (`contrast` / `gen-ai-contract` / `gen-tokens-tree` / `token-audience` /
258
258
  `theme-sync`)。真实事故:一条 `@layer` 让六个工具同时读错,且都没有报错。
259
259
  3. 选择器是 `:root[data-market="intl"]` (0,2,0),**不是** `:where(...)` (0) ——
260
- `:where()` 抢不过 `:root` 的 (0,1,0),写了等于没写(E135)。
260
+ `:where()` 抢不过 `:root` 的 (0,1,0),写了等于没写。
261
261
 
262
262
  > 精简档(`tierA-tokens.css`)**不含**这段交换:它是一份 `<style>` 内的
263
263
  > 令牌快照,没有配套文件可引。要国际口径就自己补一条规则 ——
@@ -76,7 +76,7 @@ p { margin: 0 0 var(--sp-4); }
76
76
  </div>
77
77
  <p style="margin-top:var(--sp-4)">
78
78
  判据很简单:<strong>这个值变了会不会导致"控件大小跟着变"?会 → 不要流体。</strong>
79
- `--fs-xs` / `--fs-sm`(UI 文本)和 `--sp-1`~`--sp-3`(小间距)都属这一类。
79
+ <code>--fs-xs</code> / <code>--fs-sm</code>(UI 文本)和 <code>--sp-1</code>~<code>--sp-3</code>(小间距)都属这一类。
80
80
  </p>
81
81
 
82
82
  <h2>三、该用断点的:布局结构</h2>
@@ -884,7 +884,7 @@
884
884
  —— 遍历所有声明、后者覆盖前者、**不看选择器**。
885
885
  把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
886
886
  于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
887
- 比漏判更难查(实测,见 E137)。
887
+ 比漏判更难查(实测)。
888
888
  ⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
889
889
  ========================================================================= */
890
890
 
@@ -926,7 +926,7 @@
926
926
  —— 遍历所有声明、后者覆盖前者、**不看选择器**。
927
927
  把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
928
928
  于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
929
- 比漏判更难查(实测,见 E137)。
929
+ 比漏判更难查(实测)。
930
930
  ⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
931
931
  ========================================================================= */
932
932
 
@@ -1117,7 +1117,7 @@ input[type="submit"],
1117
1117
  /* ⚠️ 模态的遮罩规则**不在这里**,在 03-patterns/overlay/overlay.css。
1118
1118
  原因不是洁癖:`.dialog-backdrop { position: static }` 写在这里会被
1119
1119
  overlay.css 里同特异性的 `position: fixed` 盖掉 —— 规则写了、不生效、
1120
- 不报错(E254 的同一个形状,2026-11-02 由 print-policy-gate 的 L3 抓到)。 */
1120
+ 不报错(同一个形状,2026-11-02 由 print-policy-gate 的 L3 抓到)。 */
1121
1121
 
1122
1122
  /* 不跨页切断。代价:高于半页的卡片会被整体推到下一页 ⇒ 页数可能变多。 */
1123
1123
  tr, img, figure, svg,
@@ -194,7 +194,7 @@
194
194
  }
195
195
 
196
196
  function paintSummary(s) {
197
- /* 🔴 不拼 innerHTML(E299):`s` 来自两个 <input> 的 value,
197
+ /* 🔴 不拼 innerHTML:`s` 来自两个 <input> 的 value,
198
198
  而 `type=date` 在 Chromium 97 旧内核会回退成文本输入 ⇒ value 可为任意串。 */
199
199
  if (!summary) return;
200
200
  summary.textContent = '';
@@ -90,7 +90,7 @@
90
90
  </div>
91
91
  <p class="small">
92
92
  <code>timeline--compact</code> 只做两件事:条目间距从 <code>--sp-4</code> 收到
93
- <code>--sp-2</code>,时间挪回标题同一行。**它不改字号** ——
93
+ <code>--sp-2</code>,时间挪回标题同一行。<strong>它不改字号</strong> ——
94
94
  在侧栏里把字缩小,等于承认这块内容不重要,而事实往往相反。
95
95
  </p>
96
96
 
@@ -74,7 +74,7 @@ ul.demo-list li { margin: var(--sp-1) 0; }
74
74
  <span class="dr-bar__title">筛选</span>
75
75
  <button class="dr-tiny" type="button" tabindex="-1" aria-label="关闭">×</button>
76
76
  </div>
77
- <p class="cmp__why">关闭是**独立目标**:抽屉滑进来之后,用户第一件事就是找它。24px 的叉在触屏上要瞄准,点偏了就点到了背后的内容。</p>
77
+ <p class="cmp__why">关闭是<strong>独立目标</strong>:抽屉滑进来之后,用户第一件事就是找它。24px 的叉在触屏上要瞄准,点偏了就点到了背后的内容。</p>
78
78
  </div>
79
79
  <div class="cmp__pane cmp__good" data-good="button.drawer__close|h|>=44">
80
80
  <h3>✅ 本库:<code>drawer__close</code> 44px</h3>
@@ -289,7 +289,7 @@ body[data-scroll-locked] { overflow: hidden; }
289
289
  隐藏遮罩会把弹窗里的字一起吞掉(静默截断)。实测:屏幕开着 dialog 时
290
290
  打印,遮罩是 `position: fixed; inset: 0; rgba(21,24,28,.4)`,整页 40% 黑。
291
291
  ⚠️ 必须写在这个文件里:写进 tokens.css 会被这里同特异性的 `position: fixed`
292
- 盖掉 —— 规则写了、不生效、不报错(E254;由 print-policy-gate L3 抓到)。
292
+ 盖掉 —— 规则写了、不生效、不报错(由 print-policy-gate L3 抓到)。
293
293
  ========================================================================= */
294
294
  @media print {
295
295
  .dialog-backdrop {
@@ -137,7 +137,7 @@
137
137
  ⇒ 减少动态效果的用户照样看到闪烁的微光;
138
138
  · 与 `02-primitives/skeleton/skeleton.css` 同特异性 ⇒ 谁后加载谁定生死。
139
139
 
140
- 即"要不要尊重减少动态效果"取决于 CSS 顺序。与 E254 同类。
140
+ 即"要不要尊重减少动态效果"取决于 CSS 顺序。与同类。
141
141
  ⚠️ 这里与 skeleton.css 是**两份实现**(已登记在 css-ownership.json),
142
142
  修的时候两边都要看。 */
143
143
  @media (prefers-reduced-motion: no-preference) {
@@ -243,7 +243,7 @@ rail 下硬塞一个 `.shell__side-label` 会被 CSS 藏掉(兜底,防止它
243
243
 
244
244
  - **❌ 侧栏 `position: fixed` + 主区 `margin-inline-start`** —— 看起来能用,但侧栏被摘出
245
245
  grid 流,主区宽度靠**手抄的数字**维持(改侧栏宽就要改两处);`inset-inline-start`
246
- 与 `margin-inline-start` 只要有一处漏改,RTL 下侧栏跑到右边、主区仍在右边留白。**
246
+ 与 `margin-inline-start` 只要有一处漏改,RTL 下侧栏跑到右边、主区仍在右边留白。
247
247
 
248
248
  正确的做法见上面「侧栏为什么是 `sticky` 而不是 `fixed`」。
249
249
 
@@ -48,7 +48,7 @@
48
48
 
49
49
  <h1>应用外壳 · app-shell</h1>
50
50
  <p class="lede">工具型页面的骨架:左边一列常驻导航、上面一条工具栏、右边是主区。
51
- 它存在的理由不是"少写几个 div",而是**两个每个人都会踩的坑** —— 见下面两节。</p>
51
+ 它存在的理由不是"少写几个 div",而是<strong>两个每个人都会踩的坑</strong> —— 见下面两节。</p>
52
52
 
53
53
  <h2>1 · 标准三段式</h2>
54
54
  <div class="demo">
@@ -934,7 +934,7 @@
934
934
  —— 遍历所有声明、后者覆盖前者、**不看选择器**。
935
935
  把一段**条件性**的交换规则放在这里,它们会把它当成默认值叠上去,
936
936
  于是报告里「暗色 涨」显示成绿色,而浏览器里其实是对的 —— 门禁冤枉代码,
937
- 比漏判更难查(实测,见 E137)。
937
+ 比漏判更难查(实测)。
938
938
  ⇒ 本文件只放「无条件声明」与「暗色媒体段」,条件规则一律外置。
939
939
  ========================================================================= */
940
940
 
@@ -1125,7 +1125,7 @@ input[type="submit"],
1125
1125
  /* ⚠️ 模态的遮罩规则**不在这里**,在 03-patterns/overlay/overlay.css。
1126
1126
  原因不是洁癖:`.dialog-backdrop { position: static }` 写在这里会被
1127
1127
  overlay.css 里同特异性的 `position: fixed` 盖掉 —— 规则写了、不生效、
1128
- 不报错(E254 的同一个形状,2026-11-02 由 print-policy-gate 的 L3 抓到)。 */
1128
+ 不报错(同一个形状,2026-11-02 由 print-policy-gate 的 L3 抓到)。 */
1129
1129
 
1130
1130
  /* 不跨页切断。代价:高于半页的卡片会被整体推到下一页 ⇒ 页数可能变多。 */
1131
1131
  tr, img, figure, svg,
@@ -1460,12 +1460,12 @@ body {
1460
1460
  指令与数据混在一起,地址按字计。</p>
1461
1461
  <h2>存储器构成</h2>
1462
1462
  <p>不同 Block II 的分段数有差异,引用时应以具体的手册为准。</p>
1463
- <table>
1463
+ <table aria-label="存储器构成">
1464
1464
  <thead>
1465
1465
  <tr>
1466
- <th>区域</th>
1467
- <th>容量</th>
1468
- <th>用途</th>
1466
+ <th scope="col">区域</th>
1467
+ <th scope="col">容量</th>
1468
+ <th scope="col">用途</th>
1469
1469
  </tr>
1470
1470
  </thead>
1471
1471
  <tbody>
@@ -289,8 +289,8 @@
289
289
  <!-- ======================================================================== -->
290
290
  <h2 class="rule-top">4 · 🔴 本轮明确不做(不是"还没做",是"这一版不管")</h2>
291
291
  <ol>
292
- <li><strong>分页 PDF / 印刷分页</strong>: @page 页边距、页眉页脚、页码、跨页不断行、
293
- 孤行寡行控制 —— 全部不在本版范围内,需要 `@page`、实物打样与 PDF 引擎验证。
292
+ <li><strong>分页 PDF / 印刷分页</strong>:<code>@page</code> 页边距、页眉页脚、页码、跨页不断行、
293
+ 孤行寡行控制 —— 全部不在本版范围内,需要 <code>@page</code>、实物打样与 PDF 引擎验证。
294
294
  CSS 里只保留了"打印时不更糟"的最小保障(隐藏边注、图表不跨撕裂)。</li>
295
295
  <li><strong>自动目录、尾注/脚注、交叉引用、参考文献样式</strong>:图/表编号用 CSS counter
296
296
  做到"写第几就显示第几",再往上的引用图(「详见图 3」这种自动维护)不支持。</li>
@@ -185,7 +185,7 @@ python -m http.server 8000
185
185
  会让同一格同时受两套颜色支配,谁赢取决于选择器权重 —— 那是最难查的一类 bug。
186
186
  `table-check` 的判据 ⑬ 会挡住给 `table.css` 添 `num--up` / `num--down` 的做法。
187
187
 
188
- ### 零值:两边都不许有符号(0.12.1 修,E145)
188
+ ### 零值:两边都不许有符号(0.12.1 修)
189
189
 
190
190
  `format: 'signed-percent'` 的正号由类名触发,而旧实现判的是 `v < 0` ⇒
191
191
  `v === 0` 落进 else 分支得到 `num--pos`,渲染成「**+0.0%**」——
@@ -260,7 +260,7 @@
260
260
  /* 符号:负数总是有 −(数值语义 + 无障碍第二线索);
261
261
  正数只有 signed-* 才带 + —— 这就是"按列语义处理"。
262
262
 
263
- 🔴 但符号必须**跟打印出来的数字同号**(0.12.1 修,E145):
263
+ 🔴 但符号必须**跟打印出来的数字同号**(0.12.1 修):
264
264
  旧实现判的是 `v < 0`,于是 `v === 0` 掉进 else 分支拿到 `num--pos`,
265
265
  渲染成「+0.0%」—— 一个没变化的数被念成"涨了"。
266
266
  同一条错还会在**四舍五入**处复现:0.0004 在 1 位精度下打印成
@@ -167,7 +167,7 @@
167
167
  这里原来有一段针对 `prefers-color-scheme: dark` 的媒体查询块,
168
168
  里面只有两条:`.bar` 的 background 与 `.bar__label` 的 stroke。
169
169
  ⚠️ 那段文字在这里**故意不照抄**:多个门禁拿这个字符串当锚点,
170
- 注释里出现字面量会把它们带偏(E199 踩过一次)。
170
+ 注释里出现字面量会把它们带偏(踩过一次)。
171
171
  它做的只有一件事:把 `var()` 的**兜底值**从 #fff 换成暗色值。
172
172
  令牌在的时候(本库的正式用法)两条声明解析结果完全相同 ⇒ 纯冗余。
173
173
 
@@ -116,7 +116,7 @@
116
116
  this.ui.appendChild(this.btn);
117
117
 
118
118
  this.hint = el('p', 'mv__hint');
119
- /* 🔴 文件名来自 `src`,可能是调用方拼出来的 URL(E299)。
119
+ /* 🔴 文件名来自 `src`,可能是调用方拼出来的 URL。
120
120
  原来用 innerHTML 直接拼 ⇒ `<img onerror>` 这类值会被解析成节点。
121
121
  改成 textContent 组装:`<code>` 与 `<b>` 仍然存在,但文本永远只是文本。 */
122
122
  var fname = el('code', null, this.src.split('/').pop() || '—');
package/AGENTS.md CHANGED
@@ -4,6 +4,8 @@
4
4
  - **Known failure modes:** `ai/pitfalls.json`, or `npx agentcss ai pitfalls --json --component=<id>`.
5
5
  - **Print / PDF:** `npx agentcss ai pdf <page.html> --out page.pdf [--paper=a4|letter|a5] [--margin="18mm 15mm"] [--max-pages=N]`. `@page` belongs to the exporter, never to the library. To hold a document to paper standards use `ai check <doc.json> --medium=paper` — it then requires `pageBudget` and refuses `pagination:"server"` components.
6
6
  - **Query:** `npx agentcss ai components --json [--maturity=stable]` · `ai tokens --json [--set=dark]` · `ai tree --json`.
7
- - **Validate:** `npx agentcss ai validate <file> --schema=components --json`. Every subcommand accepts `--json`; failures print `{"ok":false,…}` and exit non-zero.
7
+ - **Validate, then branch on the exit code:** `npx agentcss ai validate <file> --schema=components --json`. Every subcommand accepts `--json`; failures print exactly one document `{"ok":false,"command","error":{"kind","message"}}`. Codes are one table for the whole CLI: `0` pass · `1` what you checked is bad (a verdict) · `2` bad flag or value — unknown flag, unknown enum value, empty value and a repeated flag all land here · `3` environment missing (no browser / no `node`) · `4` render failed · `5` over budget · `6` tool bug. A typo can never degrade into "empty result, exit 0".
8
+ - **Check the page you actually wrote:** `npx agentcss ai check <page.html> [--recipe=report/finance]`. Static — no network, no browser. It reports class names the library does not define, stylesheets that were never linked (naming the file that defines the class), missing `img alt` / table name / `th scope`, skipped heading levels, and the per-layout obligations in `ai/recipes.json`. `npx agentcss ai scaffold <recipe> --out <page.html>` gives a minimal shell that already passes.
9
+ - **See it before shipping:** `npx agentcss ai shot <page.html> --out shot-{width}.png --width=390,1440 --full`. The viewport is exactly the width you ask for (a cropped window is not the same thing), and it reports horizontal-overflow culprits plus JS errors.
8
10
  - **Markup ground truth:** each component's `demo.html`. Restyle through tokens (CSS variables); never rewrite component selectors.
9
11
  - **Docs are Chinese:** `ai/START-HERE.md` (navigation). English entry points: `README.md`, `llms.txt`, `llms-full.txt`.
package/CHANGELOG.md CHANGED
@@ -12,9 +12,107 @@
12
12
  - **次版本**:新增组件 / 新增能力(下游不改代码也能升级)
13
13
  - **修订号**:修 bug、修文案(下游无需任何改动)
14
14
 
15
+ > **关于历史条目里的 `E123` 这类编号**:那是 0.26.0 及更早版本沿用的
16
+ > 内部失误台账索引,台账本身不随包分发。从 0.27.0 起,本文件只描述
17
+ > **改了什么、影响谁**,不再引用内部编号 —— 读者不需要一份查不到的索引。
18
+
15
19
  ## [Unreleased]
16
20
 
17
- (无)
21
+
22
+ ## [0.27.0] - 2026-11-06
23
+
24
+ ### Added
25
+
26
+ - **`ai check <page.html>` —— 校验产物,而不只是计划。**
27
+ 此前 `ai check` 只吃 `doc.json`(节点类型、组件档位),管不到"写出来的那一页"。
28
+ 新增 16 条判据,查的都是**不会报错、只会静默失效**的东西:
29
+ 类名库里没有 / 类名库里有但这页没引到定义它的样式表(指名到文件)/
30
+ 引的样式表一个都读不到 / `<img>` 缺 `alt` / 表没有可访问名称 /
31
+ `th` 缺 `scope` / id 重复 / 标题跳级 / 表单控件没有可读名称 /
32
+ `target="_blank"` 缺 `rel="noopener"` / 缺 `lang`、`charset`、`viewport`。
33
+ 纯静态:不联网、不启动浏览器。按扩展名分派,`check x.json` 的行为不变。
34
+ - **`ai shot <page.html> --out shot-{width}.png` —— 出 PNG。**
35
+ 用 Chrome 的调试管道(不开端口、不打包浏览器)把视口**设成**要的宽度,
36
+ 再截图。为什么不用 `--window-size`:Windows 上窗口有最小宽度,
37
+ `--window-size=390` 实际按 500px 排版、只按 390 裁一刀 —— 图是好的,
38
+ 内容不对,且不报错。输出里同时给出横向溢出(指名元素)、内容高度与 JS 报错。
39
+ 同一台机器、同样参数两次逐字节一致(门禁验的就是这条)。
40
+ - **`ai scaffold <版式> --out page.html` —— 三个最小壳**(`report/finance`、
41
+ `report/technical`、`report/magazine`):只给正确的骨架与必有件的位置,
42
+ 不含内容。三个壳都由门禁跑一遍 `ai check --recipe=<自己>`。
43
+ - **`ai/recipes.json`** —— 版式"必有件"的机器可读事实:能从标记判定的写进
44
+ `requires`(少一件就报错),判不了的写进 `unchecked`(只打印,不判过)。
45
+ - **`prose-gate`** —— 检查页面的**可见文本**:未渲染的 `**加粗**` / 反引号 / `](链接)`、
46
+ 整页括号不配平(通常是把 `(…)` 切断),以及生成器是否切在语义边界上。
47
+ 自带反向控制:四类注入必须全部抓到,注释与代码块里的同样字符不许误报。
48
+ - **诊断可分辨**:`npm pack` 失败时 `pack-closure` 会打印 npm/node 版本、cwd、
49
+ stdout/stderr 尾,并区分"产品失败(package.json 坏了)"与"清单没拿到(无法判定)";
50
+ `check-all.sh` 在 `node` 不在 PATH 时一次性说明并整体退出 `3`,
51
+ 而不是让 5 道 python 门禁各抛一个 `WinError 2` 堆栈。
52
+
53
+ ### Changed
54
+
55
+ - **退出码统一成一套,并且可以按码分支。** 以前同一个 `--profile` 写错,
56
+ `pdf` 给 2、其它子命令给 1;而 `1` 同时表示"页面有错"和"工具崩了"。
57
+ 现在全 CLI 一张表:
58
+
59
+ | 码 | 含义 |
60
+ |---|---|
61
+ | 0 | 通过 |
62
+ | **1** | **判定不通过**(页面/文档确实有错 —— 是结论,不是工具坏了) |
63
+ | **2** | 用法错误(开关名、取值、重复、空值) |
64
+ | 3 | 环境不具备(没有浏览器 / 没有 node) |
65
+ | 4 | 渲染失败 |
66
+ | 5 | 超出预算(产物完整) |
67
+ | 6 | 工具内部错误 |
68
+
69
+ 失败的 JSON 也统一成**一种形状**:`{"ok":false,"command","error":{"kind","message"}}`
70
+ (`pdf` 以前发的是顶层 `code`/`message`,调用方得写两个分支)。
71
+ - **枚举开关的非法值一律报错,不再退化成"空结果"。** 以前:
72
+
73
+ ```
74
+ ai tokens --set=not-a-theme --json → stdout 是字面量 undefined,退出码 0
75
+ ai tree --set=not-a-theme --json → 同上
76
+ ai components --maturity=not-a-level → [],退出码 0
77
+ ```
78
+
79
+ 对 Agent 这不是"没输出",而是**错误的控制流**:它会把拼错的参数读成
80
+ "这个库没有 beta 组件",然后据此写结论。现在非法值 / 空值 / 同一个开关给两次
81
+ / 开关后面吞掉另一个开关,全部 `2` 退出,并在 message 里列出合法取值。
82
+ 合法取值**从数据现算**(`profiles.json` 的 `allowedMaturity`、`tokens.json` 的
83
+ `sets`、`tokens.tree.json` 的 `meta.categories`、`recipes.json` 的版式),
84
+ 不在这里另抄一份。
85
+ - **与本命令无关的开关不再被静默忽略。** `ai components --set=dark` 以前退出码 0、
86
+ 什么都不说 —— 调用方会以为"按 dark 过滤过了"。现在每个子命令声明自己认识的开关
87
+ (`ai/cli.js` 的 `FLAGS` 表),不认识的给 `2`。
88
+ - **`ai check <page.html> --profile` / `--medium` 明确报错**:这两个开关只对
89
+ `doc.json` 有效。以前静默忽略 ⇒ 调用方以为"按 strict 档查过了"。
90
+
91
+ ### Fixed
92
+
93
+ - **Schema 校验器实现了 `uniqueItems` 与 `not`。** 这两个关键字一直挂在
94
+ "已实现"白名单里却没有实现:`validate({type:'array',uniqueItems:true},[1,1])`
95
+ 返回 `ok:true`,`validate({type:'number',not:{const:1}},1)` 同样。
96
+ 白名单只保证"允许出现",不保证"真的校验" —— 现在逐个关键字都有正/负契约用例,
97
+ 新加关键字却不写用例,门禁直接红。
98
+ - **能力预扫从 4 份 schema 扩到全部 8 份。** 以前只扫 contract/components/tokens/patch,
99
+ `behaviors` / `deprecations` / `tokensAudience` / `tokensTree` 从来没被扫过 ——
100
+ 它们里面出现未知关键字也不会有人知道。扩上去当场撞出一个真 bug:
101
+ `patternProperties` 的**键是正则**,被当成关键字检查,于是
102
+ `tokensAudience` 报"用了没实现的关键字"(schema 本身完全合法)。
103
+ - **生成器不再按字符硬切句子。** 首页摘要以前是 `purpose[:56]`,于是
104
+ `delta` 结尾是「会计正负是 ta」、`timeline` 是「颜色在」,首页还因此括号不配平。
105
+ 现在按语义边界切(句末标点 > 分句边界 > 硬切 + 省略号),并且括号必须配平。
106
+ `aria-label` 里的小节名走同一条规则 —— 读屏会把它逐字念出来。
107
+ - **可见文本里不再有未渲染的 Markdown。** 修了 5 处:
108
+ `timeline` / `drawer` / `app-shell` 的 `**加粗**`,`responsive-demo` 与
109
+ `report` 页里的反引号代码;另有 `guide/app-shell.html` 的一个孤立 `**`
110
+ (源头是 `04-recipes/app-shell/README.md` 里一处没闭合的加粗)。
111
+ - **`md-render.py` 产出的表格补 `scope="col"` 与可访问名称**
112
+ (取最近一节的小节标题作 `aria-label`)。此前每一份从 Markdown 出来的页面
113
+ 都缺这两件 —— GFM 的表格语法里没有它们可写,而缺了不会报错,
114
+ 只会让读屏用户听到一串没有表头的数字。
115
+ - `04-recipes/longform/content.html` 随之重生成(差 4 行,纯属性)。
18
116
 
19
117
  ## [0.26.0] - 2026-11-04
20
118
 
@@ -141,7 +239,7 @@ F8(demo 对比度)、F10(model-viewer 小溢出)、F12(缺 SemVer/废
141
239
  按真值更正,历史段落只加注记、不改写。
142
240
 
143
241
  上述每一项都跑了正向、反向控制与真实发布包验证;发布前全量门禁 0 FAIL。
144
- 新增的失误与纠正规则登记在内部 `ERRORS.md`(不随包分发)。
242
+ 新增的失误与纠正规则登记在内部失误台账(不随包分发)。
145
243
 
146
244
  ---
147
245
 
@@ -335,7 +433,7 @@ README 里「想知道我们踩过哪些坑」的指向,从内部文件改到
335
433
 
336
434
  ### 修复:9 处指向不存在文件的引用
337
435
 
338
- `ERRORS.md`(在内部目录,不入库)、`docs/ROADMAP.md`、`docs/GUIDANCE.md` 被
436
+ 内部失误台账(不入库)、`docs/ROADMAP.md`、`docs/GUIDANCE.md` 被
339
437
  README / CONTRIBUTING / 05-audit/README 引用,但文件不存在。
340
438
  现在全部改指向真实文件,或去掉编号(内部条号对外部读者没有意义)。
341
439
 
@@ -1339,7 +1437,7 @@ G0d 前提:G4 的替换真的改到了文件
1339
1437
  现在:
1340
1438
 
1341
1439
  - 词表统一走 `terms`(原来 outsider-audit 自己抄了一份,与 leak-scan 口径不同)
1342
- - 真实代号与用户名模式搬进 `internal/private-terms.txt`
1440
+ - 真实代号与用户名模式移出公开树
1343
1441
  (`.gitignore` 里,也不在 npm `files` 里 ⇒ 不进包)
1344
1442
  - 读不到时**不假装扫过**:`terms.private_status()` 由三道扫描器打印
1345
1443
  「私有词表未加载 ⇒ 本次未覆盖」
@@ -2084,7 +2182,7 @@ Actions → release → Run workflow → 勾 `shoot_baseline`,
2084
2182
  ### 文档对齐
2085
2183
 
2086
2184
  - `CONTRIBUTING.md`:补「全量为什么**不**在 CI 上跑」的完整理由与责任边界;
2087
- 补 `rc=3` 三态表;补「失败时先读 `internal/tmp/selftest-fail-*.log`」。
2185
+ 补 `rc=3` 三态表;补「失败时先读本地门禁日志」。
2088
2186
  - `docs/A11Y-MANUAL-PROTOCOL.md`:结果表补 `accordion` / `drawer` / `dropdown`
2089
2187
  三行 —— 三个 beta 组件此前一个都不在表内,而「beta 升 stable」的判据之一
2090
2188
  就是行为零欠账,人工读屏是唯一的用户侧证据。**状态仍写「未执行」**:
package/CONTRIBUTING.md CHANGED
@@ -27,23 +27,45 @@
27
27
  npm ci # 按锁文件装(验证 lockfile 真能装)
28
28
  npm run gate # 全量:含浏览器类
29
29
  npm run gate:fast # 只跑静态层,不用装浏览器
30
+ npm run gate:pre-commit # 提交前:五道秒级判据(实测约 7s)
30
31
  ```
31
32
 
33
+ 三档不是"快慢"的区别,是**判断边界**的区别:
34
+
35
+ | 档 | 什么时候跑 | 能证明什么 |
36
+ |---|---|---|
37
+ | `gate:pre-commit` | 每次 `git commit` 前 | 契约 / 页面结构 / 文案 / 行为核心 / 类型没被改坏 |
38
+ | `gate:fast` | push 前 | 加上静态层的全部判据(不含浏览器) |
39
+ | `gate` | 收尾、发版前 | 全部,含浏览器、视觉基线、打包闭包 |
40
+
41
+ `gate:pre-commit` 走绿只说明"可以提交",**不说明可以发版**。想让它自动跑,
42
+ 把 `bash 05-audit/check-all.sh --pre-commit` 写进 `.git/hooks/pre-commit` 即可;
43
+ 本仓不替使用者装 hook —— 提交节奏是个人选择。
44
+
32
45
  全量的门禁条数由 `05-audit/check-all.sh` 实算,**不在文档里写死** —— 新增一道门禁
33
46
  就会变,写下来的第二天就是第二个会漂的真值(I-7)。默认 `min(核数, 6)` 路并发;
34
47
  组件层要起浏览器,还要逐张比浅色 / 暗色 / RTL 三套视觉基线(按平台分集,
35
48
  只比本平台那套)。条数、耗时、并发效率一律以**本轮门禁打印的结果**为准。
36
49
  只想确认没改坏静态契约的话用 `gate:fast`,几秒到几分钟。
37
50
 
38
- ### 全量为什么**不**在 CI 上跑(这不是缺口,是结论)
51
+ ### CI 跑什么、不跑什么(边界,不是缺口)
39
52
 
40
53
  `.github/workflows/gate.yml` 分三层:`quick`(`MODE=fast`,静态 + 视觉)、
41
- `contract`(API 契约,需 Chromium)、`browsers`(nightly,三引擎矩阵)。
54
+ `contract`(API 契约,需 Chromium)、`browsers`(三引擎矩阵)。
42
55
  **没有任何一处是 `MODE=full`**,而且不该有 —— 实测在 Linux runner 上跑全量会
43
56
  FAIL 10+ 道(字体渲染不同 ⇒ 视觉 diff 必然不一致;路径/编码不同 ⇒ 体积类误报;
44
57
  shallow clone ⇒ 依赖 git 状态的判据语义不同;浏览器版本不同 ⇒ 截图基线不匹配)。
45
58
 
46
59
  **一个长期红的 CI 比没有 CI 更糟**:大家会习惯性忽略它,真正的新问题也一起被忽略。
60
+
61
+ 触发面(2026-11-05 调整):
62
+
63
+ | 事件 | 跑哪些 | 为什么 |
64
+ |---|---|---|
65
+ | 直推 `master` | 只 `quick` | 这个仓库一直是直推,之前**完全没有 CI 保护**;quick 约 3–4 分钟,能挡住"本地没跑就推" |
66
+ | PR | `quick` + `contract` | 合并前要契约 |
67
+ | nightly / 手动 | 全部三个 job | 三引擎矩阵成本高,不该每次 push 都烧 |
68
+
47
69
  所以边界是:
48
70
 
49
71
  - **CI 负责**:跨平台可靠的那部分(静态契约、类型、API 形态、单引擎行为)
@@ -52,10 +74,29 @@ shallow clone ⇒ 依赖 git 状态的判据语义不同;浏览器版本不同
52
74
  `browsers` job 已覆盖三引擎的组件行为契约,但它**不替代**本机全量:
53
75
  它不跑视觉基线(基线按平台分集)、不跑 RTL/响应式/无障碍全量。
54
76
 
77
+ ### 私有词表(维护者;缺失时 leak 判据会变弱)
78
+
79
+ `leak-scan` 的「真实代号 / 用户名」两类要靠一份**不随包分发**的词表才能生效。
80
+ 它放在 `internal/private-terms.txt`(已被 `.gitignore` 挡住,也不在 `package.json`
81
+ 的 `files` 里)。文件不在时,脚本会打印:
82
+
83
+ ```
84
+ ⚠️ 私有词表未加载(0 条)⇒ 真实代号 / 用户名判据**本次未覆盖**
85
+ ```
86
+
87
+ **这不是通过**。本地开发把它放回去即可;CI 上因为拿不到这个文件,需要:
88
+
89
+ 1. GitHub 仓库 → Settings → Secrets and variables → Actions → 新建
90
+ `FL_PRIVATE_TERMS`,值就是词表全文(多行);
91
+ 2. `gate.yml` 的 `quick` job 会把它写进 `internal/private-terms.txt` 再跑门禁;
92
+ 3. 没配时日志里会有一条 `::notice`,明确写着"这一类在 CI 上不覆盖"。
93
+
94
+ ⚠️ 不要把词表直接提交进仓库 —— 那等于把要防的东西本身公开了。
95
+
55
96
  ### 失败时先读哪里
56
97
 
57
98
  `selftest-all` 报红时,除屏幕上那 4 行摘要,**完整子输出会落盘**到
58
- `internal/tmp/selftest-fail-<脚本名>.log`(`internal/` 已被忽略,不进仓库)。
99
+ `internal/tmp/selftest-fail-<脚本名>.log`(`internal/` 是本地忽略目录,不进仓库)。
59
100
  不可复现的红最需要这个 —— 摘要往往不够定位。
60
101
 
61
102
  只想验一条反向控制时,用 `SKIP_SELFTESTS="a.py,b.py" python3 05-audit/selftest-all.py`
package/README.md CHANGED
@@ -31,6 +31,8 @@ The JS entry points (`esm/*.mjs`) are **ESM only** — `exports` has no `require
31
31
 
32
32
  - **Every demo page, one index** — https://owain323.github.io/frontend-lib/
33
33
  - **Component documentation with search** — https://owain323.github.io/frontend-lib/guide/
34
+ - **How this project is organized** — [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
35
+ (what each layer is, how the automated checks work — in plain language)
34
36
 
35
37
  Both are generated from `ai/components.json` and the per-component `README.md`
36
38
  files, so a page cannot quietly go missing from the index when a component is
package/README.zh-CN.md CHANGED
@@ -15,6 +15,8 @@
15
15
 
16
16
  - **全部示例页,一个索引** —— https://owain323.github.io/frontend-lib/
17
17
  - **带搜索的组件文档** —— https://owain323.github.io/frontend-lib/guide/
18
+ - **这个项目是怎么组成的** —— [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
19
+ (四层组件、自动检查系统,用平话讲)
18
20
 
19
21
  两者都由 `ai/components.json` 与各组件的 `README.md` **生成**:
20
22
  新增组件时索引不会悄悄漏掉它,文档页里的每条链接都指向真实存在的文件。
package/START-HERE.md CHANGED
@@ -122,11 +122,15 @@
122
122
  | `report/` | `04-recipes/report/` | 报告版式系统 v1:共享基础 + 三种版式取舍(杂志 / 科研技术 / 金融研究) |
123
123
  | `table/` | `04-recipes/table/` | 专业数据表格:列级格式与单位,三种用途预设 |
124
124
 
125
- ## 抄完先做这三件事
126
-
127
- 1. **看效果** —— 每个组件目录下有 `demo.html`,抄完对照着比
128
- 2. **跑一遍无障碍** —— 浏览器控制台里(或用 axe)确认对比度达标
129
- 3. **别改类名** —— 类名是 API(`.btn` `.btn--primary` `.field__input`),
125
+ ## 抄完先做这几件事
126
+
127
+ 1. **查一遍结构** —— `node ai/cli.js check 你的页面.html`
128
+ 查的是产物:类名在库里存不存在、引没引到定义它的样式表、图有没有 alt、
129
+ 表有没有名字、标题有没有跳级。**它不联网、不开浏览器,一秒出结果。**
130
+ 2. **看一眼** —— `node ai/cli.js shot 你的页面.html --out shot-{width}.png --width=390,1440 --full`
131
+ 390 那张是按真 390 宽度排的(不是把宽图裁一刀),顺手报横向溢出与 JS 报错。
132
+ 3. **跑一遍无障碍** —— 浏览器控制台里(或用 axe)确认对比度达标
133
+ 4. **别改类名** —— 类名是 API(`.btn` `.btn--primary` `.field__input`),
130
134
  改名等于换组件。**要改样式就改令牌**,全库会一起变
131
135
 
132
136
  ---
@@ -146,6 +150,9 @@ CSS 类跨框架通用,React 组件会绑死框架。
146
150
 
147
151
  > **抄 `tierA-tokens.css`(A 档)或 `tokens.css`(B/C 档)→ 再抄你要的那几个 CSS → 保留类名。**
148
152
 
153
+ **让模型来写页面时**,另有一份按「它需要知道什么」组织的清单:`ai/START-HERE.md`
154
+ (`ai components` / `ai pitfalls` / `ai check` / `ai shot` / `ai scaffold` 都在那里)。
155
+
149
156
  ---
150
157
 
151
158
  ## 你可能还想找的(有 / 没有,一次说清)
package/ai/START-HERE.md CHANGED
@@ -61,7 +61,9 @@
61
61
  | 令牌名与取值 | `node ai/cli.js tokens --mode=dark`、`node ai/cli.js tree`(分层视图) |
62
62
  | 组件真实长什么样 | 该组件目录下的 `demo.html` |
63
63
  | 英文一页速览 | `llms.txt` / `llms-full.txt`(发现层;本文件是中文详版) |
64
- | 页面怎么搭 | `04-recipes/` 下的示例 |
64
+ | 页面怎么搭 | `04-recipes/` 下的示例;要写报告/长文就先 `node ai/cli.js scaffold list` |
65
+ | 写完了对不对 | `node ai/cli.js check <页面.html>`(类名/结构/版式必有件) |
66
+ | 写完好不看 | `node ai/cli.js shot <页面.html> --out shot-{width}.png --width=390,1440 --full` |
65
67
  | 令牌怎么改 | README「主题定制」(覆盖 CSS 变量,不需重新编译) |
66
68
 
67
69
  **类名即 API**(`.btn`、`.btn--primary`)。改样式改**令牌**,不要改类名,
@@ -71,7 +73,7 @@
71
73
 
72
74
  | 信号 | 怎么看 |
73
75
  |---|---|
74
- | 成熟度 | `ai/components.json` 的 `maturity` 是**算出来的**(判据在 `ai/components.meta.json`),不是人写的 |
76
+ | 成熟度 | `ai/components.json` 的 `maturity` 是**算出来的**(判据在 `ai/components.meta.json`),不是人写的。它判的是**机制完整**:有 demo、有专属门禁、没有旧 `.is-*` 状态类、没有行为债。**它不判**视觉质量、真实模型生成效果、真机表现 —— `40/40 stable` 不等于 `40/40 好看`,更不等于有人在生产里用过 |
75
77
  | 有没有 demo | 组件目录下有 `demo.html` 就能直接跑起来对照 |
76
78
  | 有没有门禁守 | `05-audit/check-all.sh`(`npm run gate`)一次跑全,逐项打印 `PASS`/`FAIL`/`SKIP` |
77
79
  | 浏览器支持 | README「浏览器支持」表:桌面 Chrome/Edge/Firefox 已验证(自动化);iOS Safari / 微信 / 企微 / QQ 等 7 个入口**人工实测过能力探针**(`🔍`,无回归保护);**Android 系统 WebView 无数据** |
@@ -92,11 +94,42 @@
92
94
  npm ci # 按锁文件安装(首次)
93
95
  npm test # 单元测试
94
96
  npm run test:types # 类型契约:正例必过 + 反例必挂
95
- npm run gate:fast # 静态检查(约 45 秒,改代码时跑)
96
- npm run gate # 全量检查(含浏览器,提交前跑)
97
- node ai/cli.js check <doc.json> # 校验契约文档(读 JSON,不解析原始 HTML)
97
+ npm run gate:pre-commit # 提交前档(约 7 秒:契约 / 页面 / 文案 / 行为核心 / 类型)
98
+ npm run gate:fast # 静态层全部(约 4 分钟,不含浏览器类判据)
99
+ npm run gate # 全量(含浏览器与视觉基线;**发版前**跑,不是提交前)
100
+ node ai/cli.js check <doc.json> # 校验契约文档(计划:节点类型 / 组件档位)
101
+ node ai/cli.js check <页面.html> # 校验产物(类名 / 结构 / 版式必有件)
102
+ node ai/cli.js shot <页面.html> --out shot-{width}.png --width=390,1440 --full
103
+ # 截图(真实视口),顺带报横向溢出与 JS 报错
104
+ node ai/cli.js scaffold report/finance --out report.html
105
+ # 生成最小壳(正确骨架 + 必有件位置),不含内容
98
106
  ```
99
107
 
108
+ **退出码可以直接分支**(全部子命令同一套):
109
+
110
+ | 码 | 含义 | 你该做什么 |
111
+ |---|---|---|
112
+ | 0 | 通过 | — |
113
+ | 1 | **判定不通过**(页面/文档确实有错) | 读 `findings` / `errors`,改产物 |
114
+ | 2 | 用法错误(开关名、取值、重复、空值) | 改调用 |
115
+ | 3 | 环境不具备(没有浏览器 / 没有 node) | 换机器或跳过,别改代码 |
116
+ | 4 | 渲染失败 | 看 message |
117
+ | 5 | 超出预算(产物是完整的) | 拆文档或放宽预算 |
118
+ | 6 | 工具内部错误 | 报 bug,别改自己的参数 |
119
+
120
+ > `1` 和 `2` 分开是有原因的:把「页面有 3 个错」和「`--profile` 拼错了」都返回 1,
121
+ > 自动修复循环会去改**错的那一边**。
122
+
123
+ **计划与产物是两件事,判据不重叠**:
124
+
125
+ | 查什么 | 命令 | 能发现 | 发现不了 |
126
+ |---|---|---|---|
127
+ | 计划(doc.json) | `check <doc.json>` | 节点类型、组件是否在档位内、介质义务 | 写出来的 HTML 对不对 |
128
+ | 产物(.html) | `check <页面.html>` | 类名未定义 / 引错样式表 / 缺 alt、caption、scope / 标题跳级 / 版式缺件 | 页面**好看不好看**(那要 `shot` 看一眼) |
129
+
130
+ > 两个命令都**不联网、不启动浏览器**;`shot` 用你机器上已装的 Chrome/Edge,
131
+ > 不打包浏览器(理由同 `ai pdf`)。
132
+
100
133
  改动本库**自身**时的分层(决定该动哪一层):
101
134
 
102
135
  | 层 | 文件 | 性质 |
@@ -112,7 +145,8 @@ node ai/cli.js check <doc.json> # 校验契约文档(读 JSON,不解析原
112
145
 
113
146
  有人问过要不要把 `ai/*.json` 包成一个 MCP server。结论是**不做**:
114
147
 
115
- 1. 十一个子命令**全是只读查询** —— MCP 版本只是换一层传输,一个新能力都没有。
148
+ 1. 十四个子命令**全是本机命令**(读契约 / 校验文件 / 调本机浏览器)——
149
+ MCP 版本只是换一层传输,一个新能力都没有。
116
150
  2. 它会是**第二份真值**:命令怎么调、参数叫什么、返回什么形状,都得再写一遍。
117
151
  CLI 改了 server 没改,就多一处只有用户能发现的错(`--json` 那次已经踩过同类)。
118
152
  3. 用户要多信任一个常驻进程(安装、权限、升级),而 CLI 离线、可审计、跑完即退。