agentcss 0.24.2 → 0.25.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 (76) hide show
  1. package/01-tokens/README.md +1 -1
  2. package/01-tokens/tokens.css +67 -1
  3. package/01-tokens/typography.css +1 -1
  4. package/02-primitives/button/demo.html +8 -0
  5. package/02-primitives/card/README.md +15 -0
  6. package/02-primitives/choice/README.md +30 -0
  7. package/02-primitives/choice/choice.css +1 -1
  8. package/02-primitives/input/README.md +1 -0
  9. package/02-primitives/select/README.md +42 -2
  10. package/02-primitives/select/demo.html +11 -6
  11. package/02-primitives/select/select.js +184 -94
  12. package/02-primitives/timeline/demo.html +2 -2
  13. package/02-primitives/timeline/timeline.css +1 -1
  14. package/03-patterns/accordion/README.md +15 -0
  15. package/03-patterns/content/README.md +11 -0
  16. package/03-patterns/drawer/README.md +1 -1
  17. package/03-patterns/dropdown/demo.html +1 -1
  18. package/03-patterns/form-validation/README.md +18 -0
  19. package/03-patterns/form-validation/form-validation.css +12 -1
  20. package/03-patterns/list/README.md +11 -0
  21. package/03-patterns/overlay/README.md +26 -6
  22. package/03-patterns/overlay/overlay.css +22 -3
  23. package/03-patterns/overlay/overlay.js +63 -17
  24. package/03-patterns/states/README.md +15 -0
  25. package/03-patterns/states/states.css +18 -1
  26. package/03-patterns/tooltip/README.md +12 -0
  27. package/04-recipes/longform/longform.html +3 -3
  28. package/04-recipes/report/README.md +9 -0
  29. package/04-recipes/report/report.css +5 -0
  30. package/04-recipes/table/README.md +16 -0
  31. package/04-recipes/tierA-tokens.css +3 -3
  32. package/09-assets/README.md +18 -0
  33. package/09-assets/bar/bar.css +25 -10
  34. package/09-assets/echarts-adapter/demo.html +3 -1
  35. package/09-assets/echarts-adapter/echarts-adapter.js +107 -34
  36. package/09-assets/scientific-plot/README.md +2 -1
  37. package/09-assets/scientific-plot/scientific-plot.js +0 -7
  38. package/AGENTS.md +2 -1
  39. package/CHANGELOG.md +317 -18
  40. package/CONTRIBUTING.md +6 -5
  41. package/README.md +6 -2
  42. package/README.zh-CN.md +19 -7
  43. package/SECURITY.md +5 -2
  44. package/START-HERE.md +7 -7
  45. package/SUPPORT.md +86 -24
  46. package/ai/START-HERE.md +3 -2
  47. package/ai/cli.js +126 -6
  48. package/ai/components.json +258 -2
  49. package/ai/components.meta.json +257 -40
  50. package/ai/components.schema.json +96 -0
  51. package/ai/contract.schema.json +11 -0
  52. package/ai/pdf-pages.js +79 -0
  53. package/ai/pdf.js +354 -0
  54. package/ai/tokens.json +2 -2
  55. package/ai/validate.js +48 -1
  56. package/dist/01-tokens/tokens.css +1 -1
  57. package/dist/02-primitives/select/select.js +158 -71
  58. package/dist/03-patterns/form-validation/form-validation.css +1 -1
  59. package/dist/03-patterns/overlay/overlay.css +1 -1
  60. package/dist/03-patterns/overlay/overlay.js +40 -11
  61. package/dist/03-patterns/states/states.css +1 -1
  62. package/dist/04-recipes/report/report.css +1 -1
  63. package/dist/04-recipes/tierA-tokens.css +1 -1
  64. package/dist/09-assets/bar/bar.css +1 -1
  65. package/dist/09-assets/echarts-adapter/echarts-adapter.js +75 -33
  66. package/dist/09-assets/scientific-plot/scientific-plot.js +0 -7
  67. package/dist/COSTS.md +37 -37
  68. package/docs/ACCESSIBILITY.md +3 -3
  69. package/docs/FACTS.md +1 -1
  70. package/docs/LAYER-BASELINE.md +28 -3
  71. package/docs/PRINT.md +450 -0
  72. package/docs/RELEASE-PROCESS.md +78 -3
  73. package/index.html +1 -1
  74. package/llms-full.txt +7 -7
  75. package/llms.txt +5 -0
  76. package/package.json +6 -1
@@ -15,175 +15,392 @@
15
15
  "$whyFourth": "🔴 为什么 M8 要加第四条(0.7.6):只用前三条时 27 个组件**全是 stable** ⇒ ladder 没有任何鉴别力(对标 Primer 六档阶梯,每档都有硬判据)。补上行为欠账这一条后:stable 20 / beta 7。",
16
16
  "$strictImpact": "⚠️ strict 档位只放行 stable ⇒ 这 7 个组件在 strict 下不可用(accordion / combobox / drawer / dropdown / overlay / select / tooltip)。这是**有意的**:它们的行为还有手写副本或缺一个该有的行为。要用就切 standard 档。"
17
17
  },
18
+ "printDefaults": {
19
+ "$comment": "库级打印约定:不属于任何组件,只由 01-tokens/tokens.css 的 @media print 兑现。policy 取值与组件相同(见 components.schema.json 的 printRule)。",
20
+ "rules": [
21
+ {
22
+ "selector": ".no-print",
23
+ "policy": "omit",
24
+ "why": "使用者显式排除操作区;全库唯一的 !important"
25
+ },
26
+ {
27
+ "selector": ".print-only",
28
+ "policy": "reveal",
29
+ "why": "只在纸上出现(打印版标题 / 落款 / 水印)"
30
+ },
31
+ {
32
+ "selector": "tr",
33
+ "policy": "keep-together",
34
+ "why": "切开的表行在下一页没有表头,读不出来"
35
+ },
36
+ {
37
+ "selector": "img",
38
+ "policy": "keep-together",
39
+ "why": "图片切一半等于没有这张图"
40
+ },
41
+ {
42
+ "selector": "figure",
43
+ "policy": "keep-together",
44
+ "why": "图与图注是一个整体"
45
+ },
46
+ {
47
+ "selector": "svg",
48
+ "policy": "keep-together",
49
+ "why": "图表都是 svg;切开后右半页是空白"
50
+ },
51
+ {
52
+ "selector": ".print-allow-break",
53
+ "policy": "allow-break",
54
+ "why": "退出开关:高块想省纸就加它(520px 块实测 −48% 页)"
55
+ }
56
+ ]
57
+ },
18
58
  "components": {
19
59
  "accordion": {
20
60
  "rootClass": "accordion",
21
- "purpose": "可折叠分组"
61
+ "purpose": "可折叠分组",
62
+ "printPolicy": [
63
+ {
64
+ "selector": ".accordion .accordion__panel[hidden]",
65
+ "policy": "expand",
66
+ "why": "收起的面板也要展开;display / 行轨道 / visibility 三条缺一就一个字不印"
67
+ },
68
+ {
69
+ "selector": ".accordion__marker",
70
+ "policy": "omit",
71
+ "why": "展开箭头是交互提示;只藏箭头,不藏标题"
72
+ }
73
+ ]
22
74
  },
23
75
  "analysis-report": {
24
76
  "rootClass": "report",
25
77
  "tier": "recipe",
26
- "purpose": "组合型研究报告页面:正文与辅助栏并排,窄屏自然退回单栏"
78
+ "purpose": "组合型研究报告页面:正文与辅助栏并排,窄屏自然退回单栏",
79
+ "printPolicy": [
80
+ {
81
+ "selector": ".report__aside",
82
+ "policy": "expand",
83
+ "why": "屏幕 sticky + 限高 + 可滚;不放开就只印可视区那几行"
84
+ }
85
+ ]
27
86
  },
28
87
  "app-shell": {
29
88
  "rootClass": "shell",
30
89
  "tier": "recipe",
31
- "purpose": "应用外壳:侧栏 + 顶栏 + 主区三段式;sticky 侧栏自滚、min-width 防撑破、窄屏收走侧栏"
90
+ "purpose": "应用外壳:侧栏 + 顶栏 + 主区三段式;sticky 侧栏自滚、min-width 防撑破、窄屏收走侧栏",
91
+ "printPolicy": []
32
92
  },
33
93
  "badge": {
34
94
  "rootClass": "badge",
35
- "purpose": "状态标签:计数、分类、结果标记"
95
+ "purpose": "状态标签:计数、分类、结果标记",
96
+ "printPolicy": []
36
97
  },
37
98
  "bar": {
38
99
  "rootClass": "bar",
39
100
  "tier": "asset",
40
- "purpose": "柱状图:堆叠/分组两种语义、超量程裁剪、图例"
101
+ "purpose": "柱状图:堆叠/分组两种语义、超量程裁剪、图例",
102
+ "printPolicy": []
41
103
  },
42
104
  "button": {
43
105
  "rootClass": "btn",
44
- "purpose": "动作触发:主/次/危险/纯文字四档"
106
+ "purpose": "动作触发:主/次/危险/纯文字四档",
107
+ "printPolicy": []
45
108
  },
46
109
  "card": {
47
110
  "rootClass": "card",
48
- "purpose": "内容容器:媒体+正文+操作区三段"
111
+ "purpose": "内容容器:媒体+正文+操作区三段",
112
+ "printPolicy": [
113
+ {
114
+ "selector": ".card",
115
+ "policy": "keep-together",
116
+ "why": "切开后下半张没有标题"
117
+ }
118
+ ]
49
119
  },
50
120
  "choice": {
51
121
  "rootClass": "choice",
52
- "purpose": "单选/多选/原生 select 外观的统一控件"
122
+ "purpose": "单选/多选/原生 select 外观的统一控件",
123
+ "printPolicy": []
53
124
  },
54
125
  "combobox": {
55
126
  "rootClass": "combo",
56
- "purpose": "可搜索 + 可多选标签的输入控件"
127
+ "purpose": "可搜索 + 可多选标签的输入控件",
128
+ "printPolicy": []
57
129
  },
58
130
  "content": {
59
131
  "rootClass": "prose",
60
- "purpose": "长文排版容器(标题/段落/列表/引用/代码)"
132
+ "purpose": "长文排版容器(标题/段落/列表/引用/代码)",
133
+ "printPolicy": []
61
134
  },
62
135
  "date-range": {
63
136
  "rootClass": "drange",
64
- "purpose": "日期区间选择,含预设快捷范围"
137
+ "purpose": "日期区间选择,含预设快捷范围",
138
+ "printPolicy": []
65
139
  },
66
140
  "delta": {
67
141
  "rootClass": "delta",
68
- "purpose": "变化量:涨跌方向 + 符号 + 颜色。只描述往哪边变,不判断好坏(等于好恶是 badge 的事,会计正负是 table 的事)"
142
+ "purpose": "变化量:涨跌方向 + 符号 + 颜色。只描述往哪边变,不判断好坏(等于好恶是 badge 的事,会计正负是 table 的事)",
143
+ "printPolicy": []
69
144
  },
70
145
  "drawer": {
71
146
  "rootClass": "drawer",
72
- "purpose": "侧边抽屉,含遮罩"
147
+ "purpose": "侧边抽屉,含遮罩",
148
+ "printPolicy": []
73
149
  },
74
150
  "dropdown": {
75
151
  "rootClass": "dd",
76
- "purpose": "下拉菜单"
152
+ "purpose": "下拉菜单",
153
+ "printPolicy": []
77
154
  },
78
155
  "form-validation": {
79
156
  "rootClass": "form",
80
- "purpose": "表单校验反馈与错误汇总"
157
+ "purpose": "表单校验反馈与错误汇总",
158
+ "printPolicy": []
81
159
  },
82
160
  "input": {
83
161
  "rootClass": "field",
84
- "purpose": "文本输入及其标签/帮助/错误三件套"
162
+ "purpose": "文本输入及其标签/帮助/错误三件套",
163
+ "printPolicy": []
85
164
  },
86
165
  "list": {
87
166
  "rootClass": "list",
88
- "purpose": "键值列表/条目列表"
167
+ "purpose": "键值列表/条目列表",
168
+ "printPolicy": []
89
169
  },
90
170
  "model-viewer": {
91
171
  "rootClass": "mv",
92
172
  "tier": "asset",
93
- "purpose": "3D 模型查看器:令牌驱动样式,不含渲染库本体"
173
+ "purpose": "3D 模型查看器:令牌驱动样式,不含渲染库本体",
174
+ "printPolicy": [
175
+ {
176
+ "selector": ".mv__btn",
177
+ "policy": "omit",
178
+ "why": "「加载 3D 模型」按钮由 JS 生成,纸上点了没反应"
179
+ },
180
+ {
181
+ "selector": ".mv__hint",
182
+ "policy": "omit",
183
+ "why": "拖拽旋转的操作提示在纸上不成立"
184
+ }
185
+ ]
94
186
  },
95
187
  "metric": {
96
188
  "rootClass": "metric",
97
- "purpose": "指标块:一个标签 + 一个值 + 一句上下文。数字等宽、单位分离、缺数是一等状态"
189
+ "purpose": "指标块:一个标签 + 一个值 + 一句上下文。数字等宽、单位分离、缺数是一等状态",
190
+ "printPolicy": [
191
+ {
192
+ "selector": ".metric",
193
+ "policy": "keep-together",
194
+ "why": "数字、单位、标签是一个整体,切开就分家"
195
+ }
196
+ ]
98
197
  },
99
198
  "nav": {
100
199
  "rootClass": "nav",
101
- "purpose": "顶部导航与目录"
200
+ "purpose": "顶部导航与目录",
201
+ "printPolicy": [
202
+ {
203
+ "selector": ".nav__list",
204
+ "policy": "omit",
205
+ "why": "导航链接是纯导航;不藏 .nav__brand(可能是标题)"
206
+ },
207
+ {
208
+ "selector": ".nav__toggle",
209
+ "policy": "omit",
210
+ "why": "汉堡按钮只在窄屏交互中存在"
211
+ }
212
+ ]
102
213
  },
103
214
  "overlay": {
104
215
  "rootClass": "toast",
105
- "purpose": "吐司提示与模态遮罩"
216
+ "purpose": "吐司提示与模态遮罩",
217
+ "printPolicy": [
218
+ {
219
+ "selector": ".toast",
220
+ "policy": "omit",
221
+ "why": "通知是瞬时反馈,纸上没有对应物"
222
+ },
223
+ {
224
+ "selector": ".dialog-backdrop",
225
+ "policy": "expand",
226
+ "why": "fixed 半透明遮罩印出来盖满整页;拉回流式而不是隐藏(隐藏会吞掉弹窗内容)"
227
+ },
228
+ {
229
+ "selector": ".dialog",
230
+ "policy": "expand",
231
+ "why": "弹窗自己限高 100vh 且可滚;纸上不限高,否则只印可视区那几行"
232
+ }
233
+ ]
106
234
  },
107
235
  "pagination": {
108
236
  "rootClass": "pagination",
109
- "purpose": "分页控件"
237
+ "purpose": "分页控件",
238
+ "printPolicy": [
239
+ {
240
+ "selector": ".pagination",
241
+ "policy": "omit",
242
+ "why": "纸上翻不了页;印出来只会让人以为还有下一页"
243
+ }
244
+ ]
110
245
  },
111
246
  "popover": {
112
247
  "rootClass": "popover",
113
- "purpose": "锚定到触发元素的浮层"
248
+ "purpose": "锚定到触发元素的浮层",
249
+ "printPolicy": [
250
+ {
251
+ "selector": ".popover",
252
+ "policy": "omit",
253
+ "why": "同 tooltip;且 position:fixed 会盖住正文"
254
+ }
255
+ ]
114
256
  },
115
257
  "progress": {
116
258
  "rootClass": "progress",
117
- "purpose": "进度条与进度环"
259
+ "purpose": "进度条与进度环",
260
+ "printPolicy": []
118
261
  },
119
262
  "report": {
120
263
  "rootClass": "rep",
121
264
  "tier": "recipe",
122
- "purpose": "报告版式系统 v1:共享基础 + 三套版式(杂志/科研技术/金融研究),只换一个 rep--* 类即可切换取舍"
265
+ "purpose": "报告版式系统 v1:共享基础 + 三套版式(杂志/科研技术/金融研究),只换一个 rep--* 类即可切换取舍",
266
+ "printPolicy": [
267
+ {
268
+ "selector": ".rep__aside",
269
+ "policy": "omit",
270
+ "why": "边注是并排布局的产物;纸上没有第二栏,留着会被挤成 12em 一行"
271
+ },
272
+ {
273
+ "selector": ".rep__fig",
274
+ "policy": "keep-together",
275
+ "why": "图与图注同页;高于一整页时 avoid 会被静默忽略"
276
+ },
277
+ {
278
+ "selector": ".rep table",
279
+ "policy": "keep-together",
280
+ "why": "表格不从行中断;跨页表头由 HTML 默认重复"
281
+ }
282
+ ]
123
283
  },
124
284
  "scientific-plot": {
125
285
  "rootClass": "splot",
126
286
  "tier": "asset",
127
- "purpose": "二维科学绘图:折线/区间带/对数轴,纯令牌驱动、零依赖"
287
+ "purpose": "二维科学绘图:折线/区间带/对数轴,纯令牌驱动、零依赖",
288
+ "printPolicy": [
289
+ {
290
+ "selector": ".splot",
291
+ "policy": "keep-together",
292
+ "why": "图、坐标轴、图例必须同页"
293
+ }
294
+ ]
128
295
  },
129
296
  "select": {
130
297
  "rootClass": "select",
131
- "purpose": "自定义下拉选择(非原生)"
298
+ "purpose": "自定义下拉选择(非原生)",
299
+ "printPolicy": []
132
300
  },
133
301
  "timeline": {
134
302
  "rootClass": "timeline",
135
- "purpose": "时间轴:按时间排的事件序列(公告 / 大事记 / 版本记录)。顺序即信息,用 ol;状态交给 badge,颜色在这里会失效"
303
+ "purpose": "时间轴:按时间排的事件序列(公告 / 大事记 / 版本记录)。顺序即信息,用 ol;状态交给 badge,颜色在这里会失效",
304
+ "printPolicy": []
136
305
  },
137
306
  "separator": {
138
307
  "rootClass": "separator",
139
- "purpose": "分隔线,可带中间文字"
308
+ "purpose": "分隔线,可带中间文字",
309
+ "printPolicy": []
140
310
  },
141
311
  "skeleton": {
142
312
  "rootClass": "skeleton",
143
- "purpose": "加载占位骨架"
313
+ "purpose": "加载占位骨架",
314
+ "printPolicy": [
315
+ {
316
+ "selector": ".skeleton",
317
+ "policy": "omit",
318
+ "why": "加载占位符;纸上永远是「还没加载完」的假象"
319
+ }
320
+ ]
144
321
  },
145
322
  "sparkline": {
146
323
  "rootClass": "sparkline",
147
324
  "tier": "asset",
148
- "purpose": "迷你趋势图:无坐标轴、纯形状,适合表格内嵌的一行走势"
325
+ "purpose": "迷你趋势图:无坐标轴、纯形状,适合表格内嵌的一行走势",
326
+ "printPolicy": [
327
+ {
328
+ "selector": ".sparkline",
329
+ "policy": "keep-together",
330
+ "why": "趋势线切开就是两条断线"
331
+ }
332
+ ]
149
333
  },
150
334
  "states": {
151
335
  "rootClass": "state",
152
- "purpose": "空/加载/错误/成功四种整块状态"
336
+ "purpose": "空/加载/错误/成功四种整块状态",
337
+ "printPolicy": []
153
338
  },
154
339
  "switch": {
155
340
  "rootClass": "switch",
156
- "purpose": "开关切换"
341
+ "purpose": "开关切换",
342
+ "printPolicy": []
157
343
  },
158
344
  "upload": {
159
345
  "rootClass": "upload",
160
- "purpose": "文件上传:零 JS 的原生形态 + 拖放/多文件/校验的增强形态"
346
+ "purpose": "文件上传:零 JS 的原生形态 + 拖放/多文件/校验的增强形态",
347
+ "printPolicy": []
161
348
  },
162
349
  "table": {
163
350
  "rootClass": "table",
164
351
  "tier": "recipe",
165
- "purpose": "数据表格:排序/加载/空/错误四态;三种用途预设(财务/学术/统计)"
352
+ "purpose": "数据表格:排序/加载/空/错误四态;三种用途预设(财务/学术/统计)",
353
+ "printPolicy": [
354
+ {
355
+ "selector": ".table",
356
+ "policy": "expand",
357
+ "why": "屏幕是横向滚动容器(70vh 限高);不放开只印可视区,最经典的静默截断"
358
+ }
359
+ ],
360
+ "pagination": "client"
166
361
  },
167
362
  "tabs": {
168
363
  "rootClass": "tabs",
169
- "purpose": "选项卡切换"
364
+ "purpose": "选项卡切换",
365
+ "printPolicy": [
366
+ {
367
+ "selector": ".tabs .tabs__panel[hidden]",
368
+ "policy": "expand",
369
+ "why": "不展开就只印第一个 tab(实测少印 68% 字);前缀丢了这个规则等于没写"
370
+ },
371
+ {
372
+ "selector": ".tabs__list",
373
+ "policy": "omit",
374
+ "why": "标签栏是导航;只藏它,面板是内容"
375
+ }
376
+ ]
170
377
  },
171
378
  "reveal": {
172
379
  "rootClass": "reveal",
173
- "purpose": "入场动效:一个 data-reveal 属性六个方向,无 JS 时内容全可见"
380
+ "purpose": "入场动效:一个 data-reveal 属性六个方向,无 JS 时内容全可见",
381
+ "printPolicy": []
174
382
  },
175
383
  "tooltip": {
176
384
  "rootClass": "tooltip",
177
- "purpose": "悬停/聚焦提示"
385
+ "purpose": "悬停/聚焦提示",
386
+ "printPolicy": [
387
+ {
388
+ "selector": ".tooltip",
389
+ "policy": "omit",
390
+ "why": "气泡靠 hover 出现,纸上没有 hover"
391
+ }
392
+ ]
178
393
  },
179
394
  "tree": {
180
395
  "rootClass": "tree",
181
- "purpose": "可展开树形结构"
396
+ "purpose": "可展开树形结构",
397
+ "printPolicy": []
182
398
  },
183
399
  "detail-page": {
184
400
  "rootClass": "detail-page",
185
401
  "tier": "recipe",
186
- "purpose": "实体详情页骨架:字段齐全 / 部分缺失 / 无数据三种状态同构,无数据是一等状态"
402
+ "purpose": "实体详情页骨架:字段齐全 / 部分缺失 / 无数据三种状态同构,无数据是一等状态",
403
+ "printPolicy": []
187
404
  }
188
405
  }
189
406
  }
@@ -28,6 +28,24 @@
28
28
  "maturityCriteria": {
29
29
  "type": "object"
30
30
  },
31
+ "printDefaults": {
32
+ "type": "object",
33
+ "required": [
34
+ "rules"
35
+ ],
36
+ "properties": {
37
+ "rules": {
38
+ "type": "array",
39
+ "items": {
40
+ "$ref": "#/definitions/printRule"
41
+ }
42
+ },
43
+ "$comment": {
44
+ "type": "string"
45
+ }
46
+ },
47
+ "$comment": "库级打印约定:不属于任何组件,只由 01-tokens/tokens.css 兑现(.no-print / tr / img 这些)。"
48
+ },
31
49
  "summary": {
32
50
  "type": "object"
33
51
  },
@@ -213,6 +231,84 @@
213
231
  },
214
232
  "behaviorRef": {
215
233
  "type": "string"
234
+ },
235
+ "pagination": {
236
+ "type": "string",
237
+ "enum": [
238
+ "none",
239
+ "client",
240
+ "server"
241
+ ],
242
+ "$comment": [
243
+ "内容是否被分页裁过(缺省 = none)。",
244
+ "",
245
+ "🔴 server 不许与 printPolicy 里的 expand 同时出现:",
246
+ " 服务端分页时 DOM 里只有当前一页的行,展开它 = 印出一页却看起来完整。",
247
+ " 页数正常、对比度合格、内容少九成 —— 这是静默截断的教科书案例。",
248
+ " 由 gen-ai-contract.py 与 print-policy-gate.js 双向拦。"
249
+ ]
250
+ },
251
+ "printPolicy": {
252
+ "type": "array",
253
+ "items": {
254
+ "$ref": "#/definitions/printRule"
255
+ },
256
+ "$comment": [
257
+ "这个组件在纸上会发生什么。空数组 = 原样保留(**明确值**,不是漏填)。",
258
+ "",
259
+ "🔴 为什么是记录数组而不是一句话:",
260
+ " 一个组件可以同时有多种打印行为(tabs = 展开面板 + 隐藏标签栏)。",
261
+ " 写成单个枚举就必须撒谎,而撒谎的契约比没有契约更坏。"
262
+ ]
263
+ }
264
+ }
265
+ },
266
+ "printRule": {
267
+ "type": "object",
268
+ "required": [
269
+ "selector",
270
+ "policy",
271
+ "why"
272
+ ],
273
+ "additionalProperties": false,
274
+ "properties": {
275
+ "selector": {
276
+ "type": "string",
277
+ "$comment": [
278
+ "单个 CSS 选择器(不许逗号)。",
279
+ "",
280
+ "🔴 为什么必须是单个:一条记录只认领一条声明。",
281
+ " 带逗号的记录会让「谁有权改坏它」没有答案,",
282
+ " 而 print-policy-gate 的 L2(实现 → 声明)正是按这条线判的。"
283
+ ]
284
+ },
285
+ "policy": {
286
+ "type": "string",
287
+ "enum": [
288
+ "omit",
289
+ "expand",
290
+ "reveal",
291
+ "keep-together",
292
+ "allow-break"
293
+ ],
294
+ "$comment": [
295
+ "有限枚举。每个值对应一组**可在浏览器里量**的断言(print-policy-gate L3):",
296
+ " omit → 打印态 display === none",
297
+ " expand → 打印态 display≠none、visibility≠hidden、position≠fixed、",
298
+ " max-height=none、overflow 不裁切、高度>0(有文本时)",
299
+ " reveal → 屏幕 display===none 且打印态 display≠none",
300
+ " keep-together → break-inside === avoid",
301
+ " allow-break → break-inside === auto",
302
+ "",
303
+ "⚠️ 没有 omit-parts:隐藏粒度由 selector 自己说明",
304
+ " (等于 rootClass ⇒ 整块;是 __ 部件 ⇒ 少一块)。",
305
+ " 两个名字指同一件事、判据一模一样,只会多一处争论。"
306
+ ]
307
+ },
308
+ "why": {
309
+ "type": "string",
310
+ "minLength": 8,
311
+ "$comment": "必须能回答「不这么做会怎样」。写不出来的策略,多半是不该有的策略。"
216
312
  }
217
313
  }
218
314
  }
@@ -52,6 +52,17 @@
52
52
  ],
53
53
  "$comment": "详见 ai/profiles.json。三档决定『允许多不成熟的东西进入文档』。"
54
54
  },
55
+ "pageBudget": {
56
+ "type": "integer",
57
+ "minimum": 1,
58
+ "$comment": [
59
+ "页数上限(建议值)。只在 paper 介质下必填 —— screen 介质里它是多余的字。",
60
+ "🔴 没有默认值:页数取决于内容,任何全局默认都是猜的。",
61
+ " 猜一个数比不写更坏:它会让『这份报告该印几页』变成一个没人负责的数字。",
62
+ "语义是**报错**,不是警告:超了要停下来给出路(拆文档 / 进附录 / 自己删),",
63
+ " 而不是悄悄印出 40 页。导出时对应 `agentcss pdf --max-pages=N`。"
64
+ ]
65
+ },
55
66
  "capabilities": {
56
67
  "$ref": "#/definitions/capabilities",
57
68
  "$comment": [
@@ -0,0 +1,79 @@
1
+ /*
2
+ * pdf-pages.js — 从 PDF 字节里读页数与纸幅。零依赖、不联网。
3
+ *
4
+ * ============================================================================
5
+ * 🔴 为什么要有这个文件(而不是各写一份)
6
+ * ============================================================================
7
+ * 现在有两个地方要回答"这份 PDF 几页":
8
+ * · `05-audit/print-gate.js` —— 判分页有没有被改坏
9
+ * · `ai/pdf.js` —— 判导出产物有没有超出 `--max-pages`
10
+ *
11
+ * 各写一份的代价不是"多几行",而是**两个数会在某次改动后不一致,且没人发现**:
12
+ * 门禁说 7 页、导出器说 8 页,两边各自"通过"。同一个判定写两遍必然漂移(E31)。
13
+ *
14
+ * ============================================================================
15
+ * ⚠️ 两种读法,必须一致
16
+ * ============================================================================
17
+ * ① 数 `/Type /Page` 对象 —— Chromium 每页一个,直方图式可靠
18
+ * ② 读页树根 `/Count` —— 语义上的权威值
19
+ *
20
+ * 不一致意味着"这个文件长了两套说法",此时**不能挑一个继续**:
21
+ * 预算判错的代价是"多印 40 页纸,而工具说通过"。
22
+ * 所以 `inspect()` 把两个值都返回,由调用方决定:
23
+ * · 门禁(判分页变化)用 ①(历史基线就是按 ① 录的)
24
+ * · 导出器(判预算)要求 ①② 一致,不一致就报错退出
25
+ *
26
+ * ⚠️ 本解析器**只认**未压缩的对象表。Chromium 的产物实测 679 个对象、0 个
27
+ * `/Type /ObjStm`;若将来出现对象流,①② 会同时偏小 —— 这一条写在这里,
28
+ * 是为了让"某天页数突然变 0"能被想到,而不是当成新 bug 查一整天。
29
+ */
30
+ 'use strict';
31
+
32
+ /** 数 `/Type /Page` 对象。正则与 print-gate 历史实现**逐字相同**(基线按它录的)。 */
33
+ function countPageObjects(raw) {
34
+ return (raw.match(/\/Type\s*\/Page[^s]/g) || []).length;
35
+ }
36
+
37
+ /** 页树根声明的 `/Count`(取第一个;没有则 null)。 */
38
+ function countFromTree(raw) {
39
+ const m = /\/Count\s+(\d+)/.exec(raw);
40
+ return m ? +m[1] : null;
41
+ }
42
+
43
+ /** 第一张纸的 `/MediaBox [x0 y0 x1 y1]` → `{width, height}`(pt,1/72 英寸)。 */
44
+ function mediaBox(raw) {
45
+ const m = /\/MediaBox\s*\[\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*\]/.exec(raw);
46
+ if (!m) return null;
47
+ const x0 = +m[1];
48
+ const y0 = +m[2];
49
+ const x1 = +m[3];
50
+ const y1 = +m[4];
51
+ return {
52
+ width: Math.round((x1 - x0) * 100) / 100,
53
+ height: Math.round((y1 - y0) * 100) / 100,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {Buffer|String} raw PDF 字节(String 时按 latin1 处理)
59
+ * @returns {{pages:number, countFromTree:number|null, agree:boolean, mediaBox:object|null}}
60
+ */
61
+ function inspect(raw) {
62
+ const s = Buffer.isBuffer(raw) ? raw.toString('latin1') : String(raw);
63
+ const pages = countPageObjects(s);
64
+ const tree = countFromTree(s);
65
+ return {
66
+ pages,
67
+ countFromTree: tree,
68
+ /* 没有 /Count 时不算分歧(有的生成器省它);有就必须相等 */
69
+ agree: tree === null || tree === pages,
70
+ mediaBox: mediaBox(s),
71
+ };
72
+ }
73
+
74
+ /** 只要页数。等价于 `inspect(buf).pages`,给"只关心页数"的调用方用。 */
75
+ function countPages(raw) {
76
+ return inspect(raw).pages;
77
+ }
78
+
79
+ module.exports = { inspect, countPages, countPageObjects, countFromTree, mediaBox };