agentcss 0.24.0 → 0.24.2

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 (53) hide show
  1. package/01-tokens/focus-ring-demo.html +1 -1
  2. package/01-tokens/page.css +32 -0
  3. package/01-tokens/responsive-demo.html +1 -1
  4. package/01-tokens/scrollbar-demo.html +1 -1
  5. package/02-primitives/badge/demo.html +1 -1
  6. package/02-primitives/button/demo.html +1 -1
  7. package/02-primitives/card/demo.html +1 -1
  8. package/02-primitives/choice/demo.html +1 -1
  9. package/02-primitives/combobox/demo.html +1 -1
  10. package/02-primitives/date-range/demo.html +1 -1
  11. package/02-primitives/delta/demo.html +1 -1
  12. package/02-primitives/input/demo.html +1 -1
  13. package/02-primitives/metric/demo.html +1 -1
  14. package/02-primitives/popover/demo.html +1 -1
  15. package/02-primitives/progress/demo.html +1 -1
  16. package/02-primitives/select/demo.html +1 -1
  17. package/02-primitives/separator/demo.html +1 -1
  18. package/02-primitives/skeleton/demo.html +1 -1
  19. package/02-primitives/switch/demo.html +1 -1
  20. package/02-primitives/timeline/demo.html +1 -1
  21. package/03-patterns/accordion/demo.html +1 -1
  22. package/03-patterns/content/demo.html +1 -1
  23. package/03-patterns/drawer/demo.html +1 -1
  24. package/03-patterns/dropdown/demo.html +1 -1
  25. package/03-patterns/form-validation/demo.html +1 -1
  26. package/03-patterns/list/demo.html +1 -1
  27. package/03-patterns/nav/demo.html +1 -1
  28. package/03-patterns/overlay/demo.html +1 -1
  29. package/03-patterns/pagination/demo.html +1 -1
  30. package/03-patterns/states/demo.html +1 -1
  31. package/03-patterns/tabs/demo.html +1 -1
  32. package/03-patterns/tooltip/demo.html +1 -1
  33. package/03-patterns/tree/demo.html +1 -1
  34. package/04-recipes/app-shell/demo.html +6 -2
  35. package/09-assets/bar/demo.html +1 -1
  36. package/09-assets/echarts-adapter/demo.html +1 -1
  37. package/09-assets/model-viewer/demo.html +1 -1
  38. package/09-assets/sparkline/demo.html +1 -1
  39. package/AGENTS.md +1 -1
  40. package/CHANGELOG.md +112 -0
  41. package/CONTRIBUTING.md +1 -1
  42. package/README.md +3 -0
  43. package/README.zh-CN.md +6 -3
  44. package/ai/components.json +1 -1
  45. package/dist/01-tokens/page.css +1 -1
  46. package/dist/COSTS.md +1 -1
  47. package/docs/ACCESSIBILITY.md +2 -2
  48. package/docs/FACTS.md +144 -0
  49. package/docs/RELEASE-PROCESS.md +211 -0
  50. package/docs/VERIFY-RELEASE.md +167 -0
  51. package/index.html +1 -1
  52. package/llms-full.txt +2 -2
  53. package/package.json +1 -1
package/dist/COSTS.md CHANGED
@@ -53,7 +53,7 @@
53
53
  ## 三、全库
54
54
 
55
55
  - 全部组件都用上(min,含 tokens 一份):**69.0 KB**
56
- - 源码版(带注释)全库 gzip:348.5 KB
56
+ - 源码版(带注释)全库 gzip:349.2 KB
57
57
 
58
58
  > ⚠️ 源码版那个数字**不是**要付的钱:它包含我们的注释(全库 CSS 有 68% 是注释,字节口径)。
59
59
  > 它的用途只有一个 —— 让人看清"dist 到底省了多少"。
@@ -5,8 +5,8 @@
5
5
 
6
6
  | 项目 | 值 |
7
7
  |---|---|
8
- | 声明适用版本 | agentcss `0.24.0` |
9
- | 声明日期 | 2026-10-31 |
8
+ | 声明适用版本 | agentcss `0.24.2` |
9
+ | 声明日期 | 2026-11-01 |
10
10
  | 复核节奏 | 每次发版前重跑(`npm run gate` 内含全部证据) |
11
11
  | 范围 | 本库交付的 CSS / JS、全部 `demo.html`(组件页 + 图表页 + 组合页)、以及文档站生成的页面 |
12
12
  | 不在范围 | 使用者拿这些组件拼出来的业务页面(那是你的无障碍责任,不是库的) |
package/docs/FACTS.md ADDED
@@ -0,0 +1,144 @@
1
+ # 事实来源清单(G0-1)
2
+
3
+ > **这份文档登记"事实从哪来、谁在核",不登记"事实是多少"。**
4
+ > 数值只存在于一个地方:`05-audit/facts.json`(生成物)。
5
+ > 文档、README、`llms*.txt`、页面里出现的数字,全部由 `05-audit/facts-gate.py` 对着它核。
6
+
7
+ ---
8
+
9
+ ## 为什么需要这份清单
10
+
11
+ 同一个数字在仓库里有多个"手抄本",一定会漂。已经真实发生过五次:
12
+
13
+ | 时间 | 现象 | 根因 |
14
+ |---|---|---|
15
+ | 0.24.1 | `llms-full.txt` 第 13 行写 `# 39 components`,第 118 行写 `Components (40)` | 生成器里那行是**写死的字符串**,标题用的是 `len()` |
16
+ | 0.24.1 | `AGENTS.md` 第 3 行写 `39 components` | 手写文件,没有任何判据看着它 |
17
+ | 0.24.1 | 讨论文档写 pitfalls `19 + 12 = 31` | 实际 20 + 12 = 32,手抄 |
18
+ | 0.24.1 | `README.zh-CN.md` 写 `共 **37** 个(stable 37 …)` | 加粗写法让老正则失配;同文件另一处 `40 个组件` 是对的,于是"找到了数"就放行 |
19
+ | 0.24.1 | 两份 README 的组件表都只有 **37 行**,标题却写着 40 / 17 | 加组件时改了数字、没补表;中文那份连数字都没改 |
20
+ | 更早 | `llms.txt` 写 38 个组件,契约里 39 | 同类(`docs-site-gate` 判据②抓到) |
21
+
22
+ **共同点**:数字是人抄的,而人不会在每次加组件后回头改所有地方。
23
+
24
+ > 上面这张表是**历史记录**,它的数字描述的是"当时错成什么样",不是当前值;
25
+ > 和 CHANGELOG 一样,不在判据范围内。当前值只有一个来源:`05-audit/facts.json`。
26
+
27
+ ⇒ 纪律:**机器算得出来的,不许手写。手写只能写解释,不能写计数。**
28
+
29
+ ---
30
+
31
+ ## 一、事实清单(登记表)
32
+
33
+ "当前结果"一栏故意只写 `facts.json` 的键路径,不写数值——写数值就等于再造一个手抄本。
34
+
35
+ | # | 事实 | 唯一权威来源 | 提取方式 | 当前结果 | 对外声明形态 | 谁在核 |
36
+ |---|---|---|---|---|---|---|
37
+ | F1 | 组件总数 | `ai/components.json` → `components[]` | `len()` | `components.total` | **数字**:`llms.txt` ×2、`llms-full.txt` ×2、`AGENTS.md`、`index.html`、`404.html`、`guide/index.html` ×2、`README.md`、`README.zh-CN.md` ×2 | ③ |
38
+ | F2 | 按层的组件数 | 同上 → `tier` | 分组计数 | `components.byTier` | **数字 ×2 形态**:每个分节标题里的 `(17)`,以及该分节表格的**行数**(两份 README 各 4 层) | **⑤** |
39
+ | F3 | 按成熟度的组件数 | 同上 → `maturity` | 分组计数 | `components.byMaturity` | **数字**:`README.md` 的 `stable 40 / beta 0`、`README.zh-CN.md` 的 `stable 40` | ③ |
40
+ | F4 | 带行为层的组件数 | 同上 → `global` 非 null | 计数 | `components.withBehaviorLayer` | 无数字声明(`README` 只写"行为层是可选的",不报数) | — |
41
+ | F5 | 翻车点总数(组件/布局/流程) | `ai/pitfalls.json` → `pitfalls[]` | `len()` | `pitfalls.total` | 无独立声明,只作为 F7 的加数出现 | — |
42
+ | F6 | 写作类翻车点数 | `ai/pitfalls-writing.json` → `pitfalls[]` | `len()` | `pitfallsWriting.total` | 同上 | — |
43
+ | F7 | 翻车点合计 | F5 + F6 | 求和 | `pitfallsAll` | **数字**:`llms-full.txt` 的 `Known failure modes (32)` | ③ |
44
+ | F8 | 运行时依赖数 | `package.json` → `dependencies` | `len()`,且**必须为 0** | `dependencies.runtime` | **散文断言**("Zero third-party runtime dependencies"),没有数字可核 | **① 结构判据**:出现任何 `dependencies` 即红 |
45
+ | F9 | 开发依赖数 | `package.json` → `devDependencies` | `len()` | `dependencies.dev` | 无数字声明 | — |
46
+ | F10 | 版本号 | `VERSION`(并与 `package.json.version` 互校) | 读文件 + 比对 | `version` | **版本戳**(不是计数):`llms-full.txt` 头部、`docs/ACCESSIBILITY.md`、每个 `guide/*.html` 页脚 | `bump-version.py --check` + `doc-facts-gate.py`;`facts-gate` 只做 `VERSION` ↔ `package.json` 互校 |
47
+ | F11 | CLI 子命令数 | `ai/cli.js` → `module.exports.CMDS` | `node -e` 取 `Object.keys()` | `cli.subcommands` | 无数字声明(`llms.txt` 列命令,不报个数) | — |
48
+
49
+ ### 1.1 三种"没有数字声明"要分清楚
50
+
51
+ "这项事实没有消费者"和"这项事实没人看着"不是一回事。上表里 `—` 有三种不同含义:
52
+
53
+ | 情形 | 例子 | 靠什么保证 |
54
+ |---|---|---|
55
+ | 有断言、但断言里没有数字 | F8 零依赖 | ① 结构判据(`dependencies` 非空即红)—— 比核数字更硬 |
56
+ | 只有生成物在用、生成物本身派生 | F10 版本戳 | 生成器从 `VERSION` 取,天然同源;另有 `bump-version` 兜手写文档 |
57
+ | 真的没有对外声明 | F4 / F5 / F6 / F9 / F11 | 无。**这是已知空白,不是已覆盖** |
58
+
59
+ > ⚠️ 不要因为"11 项都提取了"就说"11 项都被校验了"。
60
+ > 提取覆盖 11 项;**声明校验只覆盖 F1 / F2 / F3 / F7 四项的数字形态,外加 F8 的结构约束**。
61
+
62
+ ---
63
+
64
+ ## 二、五个环节,分别验
65
+
66
+ ```
67
+ 源文件 ──提取──> facts.json ──引用──> 对外声明
68
+ │ │ │
69
+ └─ ① 结构校验 ─────┴─ ② 现算比对 ────┴─ ③ 数字比对 + ④ 未登记扫描 + ⑤ 清单核对
70
+ ```
71
+
72
+ | 判据 | 抓什么 | 在哪 |
73
+ |---|---|---|
74
+ | ① 源结构 | 缺字段 / 字段类型错(`null`·数字·字符串冒充数组)/ 顶层不是对象 / id 重复 / 非法 `tier`·`maturity`·`scope` / 翻车点引用不存在的组件 / `VERSION` 与 `package.json` 不一致 / 出现运行时依赖 / `files` 漏了 `ai/` | `_facts.validate_sources()` |
75
+ | ② 现算 vs `facts.json` | "加了组件但没跑 `--write`" | `compare_facts()` |
76
+ | ③ 声明 vs 现算 | "文档里的数写错了"(报出文件、行号、两个值) | `_facts.check_claims()` |
77
+ | ④ 未登记扫描 | "新写了一句 `41 components`,没人管它" | 同上,`SCAN_FILES` |
78
+ | ⑤ 清单核对 | "标题数字改了、表格没改"/"表格行数对、内容里有个不存在的组件"/"中文写着 15、英文写着 17" | `_facts.check_listings()` |
79
+
80
+ **为什么 ②③④⑤ 不能合并成一条**:`docs-site-gate` 的判据 ① 是「`llms-full.txt` 逐字节等于生成器输出」——它只能证明**文件与生成器一致**,不能证明**这个数是对的**。生成器写死一个错数,两边一起错,判据照样绿。**自证式判据只能证明"一起错"。**
81
+
82
+ **为什么 ③ 还要配上 ⑤**:0.24.1 的英文 README 标题写 `primitives (17)`(对)、表格只有 15 行。③ 只看那个 17,看不见表格;⑤ 数两半。反过来,中文 README 是标题 15、表格 15 行——③⑤ 都能抓 ⑤ 更准。**一条判据只能看见它量的那个东西。**
83
+
84
+ ---
85
+
86
+ ## 三、什么不算事实(明确排除)
87
+
88
+ | 排除项 | 为什么 | 该去哪 |
89
+ |---|---|---|
90
+ | 门禁道数 | 运行时统计,随分层模式变化(`--fast` 只跑静态+视觉,`full` 跑全部),不是仓库常量 —— **本文档不写这个数** | 运行时打印 |
91
+ | `stable` 到底保证了什么 | 是契约语义,不是一个数 | `ai/components.meta.json` 的 `maturityCriteria` + `docs/STABILITY.md` |
92
+ | "是否经过真机验证" | 是证据状态,不是计数 | `docs/ACCESSIBILITY.md`、讨论文档的 UNKNOWN 区 |
93
+ | 体积(145.1 KB / 172 KB) | 由 `perf-gate.py` + `budget.json` 管,已有独立账本 | `docs/BUDGET.md` |
94
+ | 源码行数、文件数 | 随重构波动,没有对外声明依赖它 | 不登记 |
95
+ | CHANGELOG 里的历史数字 | 历史记录,改了就是篡改 | 不扫描 |
96
+
97
+ **注释/文档字符串里的数字也不在判据范围内**(例如 `gen-docs.py` 顶部还写着"35 个组件的 README")。判据只管**对外声明**;注释漂移不影响读者,但要改就顺手改,不要为了它加一条判据。
98
+
99
+ ---
100
+
101
+ ## 四、新增一项事实的步骤
102
+
103
+ 1. 确认它是**机器能可靠算出来的**。算不出来的,别加(见第三节)。
104
+ 2. 在 `05-audit/_facts.py` 的 `collect()` 里加一个键,值从源文件现算。
105
+ 3. 在 `validate_sources()` 里加对应的结构约束(否则源坏了会静默算出错数)。
106
+ 4. 如果它有对外声明,在 `CLAIMS` 里登记**位置**(文件 + 正则),不登记数值。
107
+ **正则只能用 `_num()` / `_labelled()` 拼**,不要自己写 `(\d+)`。
108
+ S24 会检查每个数字捕获组是否带规定的形状(见 §五·2 的注)。
109
+ 5. 如果是"标题 + 清单"这种成对出现的声明,登记到 `TIER_HEADINGS` / `LISTING_FILES`。
110
+ 6. 跑 `python 05-audit/facts-gate.py --write`,再跑 `python 05-audit/facts-gate.py`。
111
+ 7. 在 `--selftest` 里补一条合成样本,证明新判据在该红时会红。
112
+ 8. 更新本文件的登记表(只加行,不写数值)。
113
+
114
+ ---
115
+
116
+ ## 五、已知限制(不装作没有)
117
+
118
+ 1. **未登记扫描只覆盖 `SCAN_FILES` 里的 8 个文件**。`docs/*.md` 里如果新写一句"40 个组件",不会被抓。要扩就把文件加进 `SCAN_FILES`。
119
+ 2. **数字与单位之间的形态有限**。认这些:
120
+ - `N components` / `N 个组件`
121
+ - `共 N 个`,**且 `个` 后面紧跟括号 / 句读 / 行尾**(`共 40 个(stable…)`、`共 37 个。`)
122
+ - `stable|beta|alpha N`
123
+ - 分节标题 `(N)` / `(N)`:`### primitives (17)`、`### recipes · 整页配方(5)`
124
+ - 以上**全部**容忍 Markdown 的 `**` `_` 反引号夹在标签与数字之间、或包住数字
125
+ (`stable **39**`、`### primitives (**16**)` 都认)
126
+
127
+ > 🔴 这条纪律现在是结构性的:所有数字提取只能经 `_num()`(数字在前)或
128
+ > `_labelled()`(标签在前)。`--selftest` 的 **S24** 比对的是**数字捕获组的
129
+ > 完整形状** `NUM_CORE`(前有可选强调符 + `(\d+)` + 后有可选强调符),
130
+ > 而不是"正则里出现过强调符字符类"——后者把强调符放到无关位置就能骗过。
131
+ >
132
+ > 但要说清楚它的边界:S24 证明的是"捕获组带规定形状",**不是**"这个正则由
133
+ > `_num`/`_labelled` 生成"。后者需要解析正则语法,本阶段不做(不引入 AST、
134
+ > 不加依赖)。`--selftest` 的 **S24b** 反过来检查 S24 自己:坏形状要抓、
135
+ > 好形状不许误判——否则"S24 永远返回空"也会显示为通过。
136
+
137
+ 不认:"四十个组件"、"4 打组件"。
138
+ 故意不认 `共 3 个步骤` 这类——`个` 后面接的是名词,省略的不是"组件";
139
+ 把它算进来会让「装好需要 3 个步骤」这种句子误报。要报数就写 `N 个组件`。
140
+ 也故意不认 `一共就这么 37 个`——`共` 与数字不相邻就不算声明。
141
+ 3. **清单核对只覆盖两份 README**。`guide/*.html` 里的组件表是 `gen-docs.py` 生成的,与契约同源,不重复登记。
142
+ 4. **清单只比 id 集合,不比顺序**。顺序是排版偏好(两份 README 都按字母排,但不是事实)。
143
+ 5. **`facts.json` 本身可能被手改**。判据 ② 会抓(现算 ≠ 存储),但它不是加密的——能改文件的人也能改源码。这里防的是**疏忽**,不是**恶意**。
144
+ 6. **CLI 子命令数依赖 `node`**。`node` 不可用时这条会抛错而不是跳过——宁可红,不要静默绿。
@@ -0,0 +1,211 @@
1
+ # 发布流程与版本不可变规则
2
+
3
+ > 本文规定**渠道语义、版本不可变、生成链顺序、发布状态机与恢复步骤**。
4
+ > 怎么核验一个已发布版本,见 [VERIFY-RELEASE.md](VERIFY-RELEASE.md)。
5
+
6
+ ---
7
+
8
+ ## 1. 两个交付渠道
9
+
10
+ 同一份仓库,两个出口,语义不同:
11
+
12
+ | | npm 包 | 文档站 |
13
+ |---|---|---|
14
+ | 内容 | 源码 + `dist/` + `docs/` + `ai/` `esm/` `types/` + 根级文档 | 整棵工作树(除审计脚本、内部目录等) |
15
+ | 身份 | **版本快照**:`0.24.1` 这一版就是那一版 | **项目文档**:滚动更新 |
16
+ | 触发 | 打 tag `v<版本>` | 推主分支 |
17
+ | 页脚/元数据里的版本号 | 就是该快照的版本 | 是「生成时所用库的版本」,不是站点快照号 |
18
+ | 能否领先于已发布版本 | 不能(它就是被发布的那一份) | 可以(文档修正不必等发版) |
19
+ | 版本号相同是否意味内容相同 | — | **不意味**。两个渠道文件集不同,更新节奏也不同 |
20
+
21
+ **结论只有一句:版本快照以 npm 为准;站点是持续更新的文档,不是快照的镜像。**
22
+
23
+ 明确禁止的推论:
24
+
25
+ - 不能因为站点页脚写着与 npm 相同的版本号,就认为两边内容一致。
26
+ - 不能因为两个渠道都来自同一个仓库,就认为它们发布的是同一份文件集合。
27
+
28
+ ---
29
+
30
+ ## 2. 版本不可变
31
+
32
+ ### 2.1 四条硬规则
33
+
34
+ 1. **已发布版本的产物不得修改。** 不重发、不覆盖、不在本地改完之后用同一版本号再发一次。
35
+ 2. **发现已发布版本有错 ⇒ 进下一个 patch 版本。** 错误本身要登记(见第 3 节),但修正只能向前走。
36
+ 3. **不追溯修改历史 tag。** tag 是发布锚点;改它就等于改写「当时发布了什么」。
37
+ 4. **同一版本号不得用来暗示两个渠道内容相同。** 渠道差异必须写出来,而不是靠版本号掩盖。
38
+
39
+ 这四条的目的是让「`0.24.1` 是什么」有一个**唯一且不会变**的答案。只要允许同版本号在不同时间、不同渠道悄悄演变,任何核验都没有意义——你核对的可能是另一份东西。
40
+
41
+ ### 2.2 打 tag 的唯一正确形式
42
+
43
+ ```bash
44
+ git tag -a v0.24.2 -m "source-commit: <40 位提交 SHA>"
45
+ git push origin v0.24.2 # 只推这一个,不要 git push --tags
46
+ ```
47
+
48
+ - 必须是 **annotated tag**:轻量 tag 没有 message,也就没有可核的声明。
49
+ - `source-commit` 必须是**完整 40 位**:短 SHA 有歧义空间,而这条记录的全部价值就是"没有歧义地指认一个提交"。
50
+ - 发布门禁会核三件事:tag 消息里的声明 == tag 实际指向的提交;该提交被至少一个远端分支包含;tag 内 `VERSION` 与当前版本一致。
51
+ - 从 `0.24.2` 起这是**硬性要求**;更早的 tag 没有该字段是历史事实,门禁对它们走兼容判据(只核"是否属于某个远端分支")。
52
+ - 它**不能**证明"这个提交是当时最合适的发布提交"。那条判断属于评审,不属于门禁。
53
+
54
+ ---
55
+
56
+ ## 3. 发现已发布版本有错时
57
+
58
+ 1. **确认影响面**:哪些文件、哪些消费者、是否影响 API 或行为。
59
+ 2. **在仓库与文档站修正**(这两处可以立即更新;npm 已发布包不动)。
60
+ 3. **登记已知偏差**:写进 [VERIFY-RELEASE.md](VERIFY-RELEASE.md) 的「已知偏差」一节,必须含受影响版本、文件、错误内容、正确内容、影响对象、修复状态、处理方式,并给出可复现的检查命令。
61
+ 4. **在下一个版本的 CHANGELOG 里说明**,让升级的人知道修了什么。
62
+ 5. **不覆盖历史**:不改 tag、不重发同版本、不要求 registry 删除已发布版本。
63
+
64
+ 现存的实例见 [VERIFY-RELEASE.md](VERIFY-RELEASE.md) 第 5 节。
65
+
66
+ ---
67
+
68
+ ## 4. 生成链:什么由什么生成,顺序是什么
69
+
70
+ ### 4.1 生成器与依赖顺序
71
+
72
+ 仓库里没有 `regen-all` 之类的编排脚本。顺序是**由依赖决定的**,不是约定:
73
+
74
+ | 顺序 | 生成器 | 真值输入 | 输出 | 改写源文件? |
75
+ |---|---|---|---|---|
76
+ | 1 | `gen-ai-contract.py` | `ai/components.meta.json` + 组件 CSS/JS + `ai/behaviors.json` | `ai/components.json`、`ai/tokens.json` | 否 |
77
+ | 2 | `gen-tokens-tree.py` | `01-tokens/tokens.css` | `ai/tokens.tree.json` | 否 |
78
+ | 3 | `make-tiersnippet.py` | `01-tokens/tokens.css` | `04-recipes/tierA-tokens.css` | **是**(写进源目录) |
79
+ | 4 | `gen-behavior.py` | `01-tokens/behavior/*.js` | 组件 JS 里的内联 core 段 | **是**(就地改写) |
80
+ | 5 | `gen-docs.py` | `ai/components.json` + 各组件 `README.md` | `guide/`、`index.html`、`404.html` | 否 |
81
+ | 6 | `gen-llms.py` | `ai/components.json` + `ai/pitfalls*.json` + `ai/behaviors.json` | `llms-full.txt` | 否 |
82
+ | 7 | `fix-start-here.py` | `ai/components.json` | `ai/START-HERE.md` 的表格段 | 是(局部) |
83
+ | 8 | `build-dist.py` | `01`–`04`、`09` 的 CSS/JS | `dist/` | 否 |
84
+
85
+ 两条必须记住的推论:
86
+
87
+ - **第 3、4 步写回源文件**,所以它们必须排在 `build-dist.py` **之前**。反过来做,`dist` 里装的是旧源。
88
+ - **第 1 步是第 5、6、7 步的输入**,所以 `ai/components.json` 变了却不重跑下游,页面与 `llms-full.txt` 会各自停在上一个版本。
89
+
90
+ 这不是推测。把顺序颠倒(先 `build-dist` 再 `make-tiersnippet`)后,`dist-fresh` 会明确报:
91
+
92
+ ```
93
+ 源码改过但 dist 没重建:04-recipes/tierA-tokens.css
94
+ ```
95
+
96
+ 而按正确顺序重跑同一处改动,`dist-fresh` 是绿的。失败信息点名了文件,所以能确认红的原因是**产物过期**,不是环境噪声。
97
+
98
+ ### 4.2 冻结点:发布前到底检查了什么
99
+
100
+ 发布前跑的是 `npm run publish:check`,它等于:
101
+
102
+ ```
103
+ check-all.sh --fast # 静态层(跳过所有需要浏览器的判据)
104
+ pack-smoke.sh # npm pack → 装进空项目 → 解析入口
105
+ ```
106
+
107
+ `--fast` 覆盖第 4.1 节**全部 8 个生成器**的产物新鲜度(每个生成器都有对应的 `--check` 判据;其中 `gen-docs` 与 `gen-llms` 由同一道 `docs-site` 门禁覆盖)。所以"入库产物 == 生成器现算"在发布前是被验过的。
108
+
109
+ 其中 `dist` 那条是**两段**:① 源 sha 与 `dist/manifest.json` 记录一致;② 产物字节 == 生成器现算字节(干跑一遍、逐文件比、不写盘)。第二段是后补的 —— 在此之前手改 `dist` 不会被任何判据发现,而文档却写着"不要手改 dist",等于一道不存在的保护。
110
+
111
+ 它**不覆盖**任何需要浏览器的判据。其中与发布物直接相关的是 `dist` 的语义一致性:
112
+
113
+ - `dist-parity-check.js`(CSS 经 CSSOM 逐条比对)
114
+ - `dist-js-parity-gate.js`(JS 的可观测行为)
115
+
116
+ 这两道属于 component 层,`--fast` 会跳过它们。**因此发布工作流里单独、无条件地跑这两步**(见 5.1)。这条不写在别处,只写在这里,因为"发布前跑过全部门禁"是一句会骗人的话。
117
+
118
+ ---
119
+
120
+ ## 5. 发布状态机
121
+
122
+ 两条链**不是**一条链。它们各自独立,用提交 SHA 关联,但不能互相推断。
123
+
124
+ ### 5.1 npm 链(tag 触发)
125
+
126
+ | 态 | 含义 | 探测方式(只读) |
127
+ |---|---|---|
128
+ | `TAGGED` | 存在 annotated tag `v<版本>` | `git cat-file -t v<版本>` |
129
+ | `GATES_OK` | `publish:check` + dist 语义比对全绿 | 工作流日志 |
130
+ | `NPM_PUBLISHED` | registry 上有该版本,且 provenance 指向本 tag 的提交 | `bash 05-audit/release-run.sh probe` |
131
+ | `RELEASE_CREATED` | GitHub Release 存在 | 同上 |
132
+
133
+ 工作流按这个顺序落地:
134
+
135
+ 1. 校验 tag 名 == `package.json.version`,检查 `NPM_TOKEN`;
136
+ 2. `npm ci`;
137
+ 3. **probe**(只读):输出 `npm=… release=… provenance=…`;
138
+ 4. `publish:check`;
139
+ 5. 装 Chromium,跑两道 `dist` 语义比对(**无条件**,不受第 6 步的跳过影响);
140
+ 6. `npm publish --provenance`,**仅当** `npm != present`;
141
+ 7. `gh release create`,**仅当** `release != present`;
142
+ 8. **verify**(`if: always()`):npm + provenance + Release 三者齐备才算完成。
143
+
144
+ 第 5 步之所以无条件:如果它和 `npm publish` 共用同一个 `if`,恢复一次半完成的发布会**在没跑 dist 校验的情况下**继续建 Release —— 那等于把发布验证变成可跳过的装饰。
145
+
146
+ 第 5 步还有一条**同样重要**的前提:它是浏览器判据,必须先有 `:8000` 上这一棵树的静态服务。
147
+ 第一次接线时直接 `node …` 跑,CI 上 21 个组件全部 `ERR_CONNECTION_REFUSED` ——
148
+ 那不是"dist 与源码不等价",是**观测条件没搭好**。现在统一套
149
+ `05-audit/with-server.sh`(与 `check-all` 同一个 `http-serve.py`,就绪判据是
150
+ `index.html` 字节一致 **且** 带 `X-FL-Gate-Server` 身份戳)。
151
+
152
+ ⚠️ 由此得到一条通用纪律:**浏览器判据的失败信息必须能区分"观测失败"与"结论为假"**。
153
+ 把 `ERR_CONNECTION_REFUSED` 报成"不等价",等于让一道判据在环境问题时指控产品。
154
+
155
+ 第 3 步之所以不能只看「版本存在」:`npm view` 只能回答「这个版本在不在」,不能回答「它是不是我们发的」。probe 会取 attestation 里的 `gitCommit` 与 tag 指向比对;不一致就判失败,而不是跳过。
156
+
157
+ ### 5.2 站点链(主分支触发)
158
+
159
+ | 态 | 含义 | 探测方式 |
160
+ |---|---|---|
161
+ | `COMMIT_PUSHED` | 主分支上有提交 | `git rev-parse HEAD` |
162
+ | `SITE_DEPLOYED` | Pages 部署记录指向某个提交 | `gh api repos/{owner}/{repo}/deployments?environment=github-pages` |
163
+
164
+ 这条链没有"npm 版本"的概念:它部署的是主分支的当前状态。两条链的提交**可以不同**,见第 1 节。
165
+
166
+ ---
167
+
168
+ ## 6. 发布中断后怎么恢复
169
+
170
+ 先探测,再决定,不要重头再来:
171
+
172
+ ```bash
173
+ bash 05-audit/release-run.sh probe
174
+ ```
175
+
176
+ | probe 输出 | 含义 | 该做什么 |
177
+ |---|---|---|
178
+ | `npm=absent release=absent` | 全新发布 | 正常跑完工作流 |
179
+ | `npm=present provenance=ok release=absent` | npm 已发、Release 没建 | **重跑同一条工作流**;发布步骤会自动跳过,只补 Release |
180
+ | `npm=present provenance=ok release=present` | 已完成 | 不需要动作;`verify` 会打印回执 |
181
+ | `npm=present provenance=mismatch` | registry 上的版本不是这个 tag 构建的 | **停**。这不是"已完成",是"发错了"。按第 3 节处理 |
182
+ | `npm=present provenance=absent` | 该版本不是本工作流发的(可能是手工发布) | **停**,人工判断来源 |
183
+ | `npm=absent release=present` | 异常态(Release 有、包没有) | 人工核对;不要在没弄清原因前重发 |
184
+
185
+ 禁止的恢复方式:
186
+
187
+ - **已发布之后**删除 tag 重新打 —— 那会改写"当时发布了什么"。
188
+ - `npm unpublish` 之后再发同版本 —— 版本号不可变规则不允许。
189
+ - 只手工 `gh release create` 而不跑 `verify` —— 回执是"三者同时成立"的唯一记录。
190
+
191
+ **唯一的例外,判据是"有没有产出物"**:`probe` 明确给出
192
+ `npm=absent release=absent` 时,这个 tag 从未产生任何可被消费的东西,
193
+ 它可以删掉、在同一版本号上重打。判据不是"我觉得没发出去",是那两行输出
194
+ ——两个渠道都查过,都不存在。
195
+
196
+ 这条例外有实际先例:`v0.24.2` 第一次打 tag 后,`release.yml` 在
197
+ **发布前门禁**就失败了(`selftest-all` 里 `release-gate` 的夹具在 Linux 上失真:
198
+ 夹具缺 `.gitignore`,`__pycache__` 让"工作区干净"那条正确地变红)。
199
+ npm 与 Release 都是 absent ⇒ 删 tag、修夹具、在同一提交树上重打 `v0.24.2`。
200
+ **版本号没有跳号,也没有第二份同版本内容。**
201
+
202
+ ⚠️ 反过来说:只要 `npm=present`,哪怕 Release 没建,也不许删 tag 重打 ——
203
+ 那时 registry 上已经有一个别人可以 `npm install` 的包,tag 是它唯一的来源锚点。
204
+
205
+ ---
206
+
207
+ ## 7. 本文档的边界
208
+
209
+ 本文覆盖:渠道语义、版本不可变、生成链顺序、冻结点范围、发布状态机、恢复步骤。
210
+
211
+ 本文**不**覆盖:视觉回归基线、体积预算、组件行为契约等与"发布身份"无关的门禁;也不承诺对发布物做逐文件完整性校验(见 [VERIFY-RELEASE.md](VERIFY-RELEASE.md) 第 4 节)。
@@ -0,0 +1,167 @@
1
+ # 核验一个已发布版本
2
+
3
+ > 读者:想确认「我装到的这个包、我看到的这个站点,到底来自哪个提交」的人。
4
+ > 本文只讲**核验**;发布流程与版本规则见 [RELEASE-PROCESS.md](RELEASE-PROCESS.md)。
5
+
6
+ ---
7
+
8
+ ## 0. 三个问题,三种证据
9
+
10
+ | 问题 | 证据 | 强度 |
11
+ |---|---|---|
12
+ | npm 包对应哪个提交? | registry 元数据的 `gitHead`;provenance 的 `resolvedDependencies` | 前者是自报字段,后者由 CI 签发 |
13
+ | 这个包是不是那次构建产出的? | attestation 的 subject 摘要 vs registry 的 `dist.integrity` | 密码学绑定 |
14
+ | 站点对应哪个提交? | GitHub deployment 记录的 `sha` | 部署来源,不是内容校验 |
15
+
16
+ 三条都能查,但**强度不同**。下面每一步都写明它能证明到哪一步为止。
17
+
18
+ ---
19
+
20
+ ## 1. npm 包 → 提交
21
+
22
+ ```bash
23
+ npm view agentcss@0.24.1 version gitHead --registry=https://registry.npmjs.org/
24
+ ```
25
+
26
+ 实测输出:
27
+
28
+ ```
29
+ version = '0.24.1'
30
+ gitHead = 'f090c86c1ba728a6cf904cc36dffe1a5eca887b2'
31
+ ```
32
+
33
+ - `gitHead` 是 `npm publish` 时构建目录的 git HEAD,由 npm 自动写入。
34
+ - 显式写 `--registry=https://registry.npmjs.org/`:若本机把 npm 指向了镜像,`npm view` 读到的是镜像的缓存元数据,而镜像与公共 registry 之间可能有延迟。核验时不要用镜像。
35
+ - **`gitHead` 不是签名。** 能改 registry 元数据的一方也能改它。要签名级证据,必须做第 2 步。
36
+
37
+ ---
38
+
39
+ ## 2. 证明签名描述的就是这个 tarball
40
+
41
+ 先取 provenance 里被绑定的三个字段:
42
+
43
+ ```bash
44
+ curl -s https://registry.npmjs.org/-/npm/v1/attestations/agentcss@0.24.1 \
45
+ | python3 -c '
46
+ import base64, json, sys
47
+ att = json.load(sys.stdin)["attestations"]
48
+ p = next(a for a in att if a["predicateType"].endswith("provenance/v1"))
49
+ st = json.loads(base64.b64decode(p["bundle"]["dsseEnvelope"]["payload"]))
50
+ bd = st["predicate"]["buildDefinition"]
51
+ print("workflow.ref =", bd["externalParameters"]["workflow"]["ref"])
52
+ print("gitCommit =", bd["resolvedDependencies"][0]["digest"]["gitCommit"])
53
+ print("subject.sha512 =", st["subject"][0]["digest"]["sha512"])
54
+ '
55
+ ```
56
+
57
+ 实测输出:
58
+
59
+ ```
60
+ workflow.ref = refs/tags/v0.24.1
61
+ gitCommit = f090c86c1ba728a6cf904cc36dffe1a5eca887b2
62
+ subject.sha512 = df01f6d74818c4ed1e26129ed543355d4add76ccd31a09867b520df21d91ea586c6e4bb5eef293ef61b7250567ffc73b4cc69cd0f95a68b92261397c4271d742
63
+ ```
64
+
65
+ 再取 registry 记录的 tarball 摘要,并解码成同一进制:
66
+
67
+ ```bash
68
+ npm view agentcss@0.24.1 dist.integrity --registry=https://registry.npmjs.org/ \
69
+ | tr -d "'" \
70
+ | python3 -c 'import base64,sys; s=sys.stdin.read().strip(); print(base64.b64decode(s.split("-",1)[1]).hex())'
71
+ ```
72
+
73
+ 实测输出(与上面 `subject.sha512` **逐字符相同**):
74
+
75
+ ```
76
+ df01f6d74818c4ed1e26129ed543355d4add76ccd31a09867b520df21d91ea586c6e4bb5eef293ef61b7250567ffc73b4cc69cd0f95a68b92261397c4271d742
77
+ ```
78
+
79
+ **为什么必须做这一步:** attestation 是一个独立对象,它签的是「某个摘要」,不是「这个包」。不核对 subject 与 `dist.integrity` 的对应关系,就等于拿一张别人的证件复印件说「这是我」。
80
+
81
+ 两者相等时,可以说:**这个 tarball 由 `release.yml` 在 `refs/tags/v0.24.1` 上、从 `f090c86…` 构建并签名。**
82
+ 两者不等时,不要相信任何一方,先查是不是取错了版本。
83
+
84
+ ---
85
+
86
+ ## 3. 站点 → 提交
87
+
88
+ ```bash
89
+ # 在仓库目录里执行:{owner} / {repo} 由 gh 按当前远端自动填充
90
+ gh api 'repos/{owner}/{repo}/deployments?environment=github-pages&per_page=1' \
91
+ --jq '.[0] | "\(.sha) \(.ref)"'
92
+ ```
93
+
94
+ 实测输出(`<提交> <部署来源分支>`):
95
+
96
+ ```
97
+ 200c01cee9c9e0e5a86461c0e7570262fa595974 master
98
+ ```
99
+
100
+ - 这条记录说明:**最近一次 Pages 部署取自 `200c01c…`**。
101
+ - `ref` 是 `master`,不是 `v0.24.1`。站点跟随主分支,**不是版本快照**。
102
+ - 它**不证明**落盘文件与该提交逐字节一致:部署过程有一次 `rsync`,本次没有对它做全量校验。
103
+ - 想抽查单个文件是否一致,取线上文件与本地同路径文件的 sha256 对比即可;抽查只能支撑被抽查的那几个文件。
104
+
105
+ ---
106
+
107
+ ## 4. 能证明什么、不能证明什么
108
+
109
+ | 级别 | 能回答 | 本库现状 | 不能回答 |
110
+ |---|---|---|---|
111
+ | L0 声明 | 「我声明它来自 X」 | 文档、CHANGELOG、tag 消息 | 声明是否为真 |
112
+ | L1 抽样 | 「我抽查了 N 个文件,都与 X 一致」 | 有人做过小样本抽查 | 未抽查的文件是否一致 |
113
+ | L2 全量 | 「每一个文件都与 X 的对应文件一致」 | **不存在** | 构建过程是否被污染 |
114
+ | L3 密码学 | 「这个 tarball 由这条 workflow、这个提交构建」 | npm provenance(SLSA provenance v1) | 仓库之外的东西 |
115
+
116
+ 三条必须说清的话:
117
+
118
+ 1. **没有 L2。** 既没有逐个文件比对过 tarball 与提交,也没有比对过站点全量。因此「包内每个文件都来自该提交」这句话目前**说不出口**。
119
+ 2. **站点不是 npm 包的镜像。** 两个渠道发布的文件集本来就不同(包里有 `dist/`、没有 `guide/`;站点有 `guide/` 与演示页、没有构建清单)。
120
+ 3. **同一版本号不保证同一内容。** 历史上出现过一次,见下一节。
121
+
122
+ ---
123
+
124
+ ## 5. 已知偏差:0.24.1
125
+
126
+ | 项 | 内容 |
127
+ |---|---|
128
+ | 受影响版本 | `0.24.1`(npm 上已发布的包) |
129
+ | 受影响文件 | 包内 `llms-full.txt` |
130
+ | 错误内容 | 第 13 行的命令示例注释写成 `39 components` |
131
+ | 正确内容 | 同一文件第 24 行写的是 `Components (40)`;`ai/components.json` 当时也是 40 条 |
132
+ | 影响对象 | 读取该文件、按其中数字理解组件规模的 AI 编码助手与自动化工具 |
133
+ | 修复状态 | 仓库与文档站已修正;**npm 上已发布的 0.24.1 未修正**(已发布版本不覆盖) |
134
+ | 处理方式 | 由下一个 patch 版本自然带走;本文件保留记录,不追溯改历史 |
135
+
136
+ 复现(两条命令,前者读包内文件,后者读线上文档):
137
+
138
+ ```bash
139
+ SITE=$(npm view agentcss homepage --registry=https://registry.npmjs.org/)
140
+
141
+ curl -sL https://registry.npmjs.org/agentcss/-/agentcss-0.24.1.tgz \
142
+ | tar xzO package/llms-full.txt | grep -n "components, joined"
143
+
144
+ curl -s "$SITE/llms-full.txt" | grep -n "components, joined"
145
+ ```
146
+
147
+ 实测输出(同一版本号,两个渠道,两个数字):
148
+
149
+ ```
150
+ 13:npx agentcss ai components --json # 39 components, joined with pitfalls
151
+ 13:npx agentcss ai components --json # 40 components, joined with pitfalls
152
+ ```
153
+
154
+ 两份文件的 sha256(实测,供交叉核对):
155
+
156
+ ```
157
+ a3d36f35ee748632ba05705e669ce950260dd81a3d32b5f3f46fadae0bd084ef # npm 包内
158
+ 530de90aa18c2e36b6463409566ba4db4a47dc2212c7fd0bab6a11850b7d845d # 文档站 / 仓库当前
159
+ ```
160
+
161
+ **这不是「版本快照可以变」的证据**,恰恰相反:它是「快照已经发出、错误只能在下一个版本修正」的实例。规则见 [RELEASE-PROCESS.md](RELEASE-PROCESS.md) 第 2 节。
162
+
163
+ ---
164
+
165
+ ## 6. 更早的版本
166
+
167
+ provenance 不是从第一个版本就有的。实测 `0.10.0` 的 attestation 接口返回 404 —— 这类版本只能核对 `gitHead`(L0),不要把它当作签名证据。
package/index.html CHANGED
@@ -97,7 +97,7 @@ li a:active { background: var(--surface-sunken); }
97
97
  </p>
98
98
  </main>
99
99
  <footer class="wrap">
100
- <p class="note">0.24.0 · 零第三方运行时依赖</p>
100
+ <p class="note">0.24.2 · 零第三方运行时依赖</p>
101
101
  </footer>
102
102
  </body>
103
103
  </html>
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # agentcss — full reference (one file)
2
2
 
3
- > Generated from `ai/components.json` + `ai/pitfalls*.json` + `ai/behaviors.json` at version **0.24.0**.
3
+ > Generated from `ai/components.json` + `ai/pitfalls*.json` + `ai/behaviors.json` at version **0.24.2**.
4
4
  > Do not edit by hand: run `python 05-audit/gen-llms.py` (freshness is gated).
5
5
  > Short version: `llms.txt`. Machine contract: `ai/*.json`.
6
6
 
@@ -10,7 +10,7 @@ No build step, no framework, no third-party runtime dependency. Class names are
10
10
  ## 1 · Query instead of guessing
11
11
 
12
12
  ```bash
13
- npx agentcss ai components --json # 39 components, joined with pitfalls
13
+ npx agentcss ai components --json # 40 components, joined with pitfalls
14
14
  npx agentcss ai components --json --maturity=stable
15
15
  npx agentcss ai pitfalls --json --component=table
16
16
  npx agentcss ai tokens --json --set=dark
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcss",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
4
4
  "description": "移动 Web 组件库:CSS + 可选行为脚本,零第三方运行时依赖。样式用 bundler 引入,交互行为用 script 标签引入。",
5
5
  "keywords": [
6
6
  "css",