收紧 AriaProps 的未知键,并按门测量把 widget 全文件 + i18n 五个形状判为 no door(#4001 批 16) - #5069
Merged
Conversation
…+ five i18n shapes as no-door (#4001 批 16) Fifteen strip sites measured per the #4852 remeasure; the counts held exactly (widget 9/9, i18n 6/6, confirmed against the ledger gate's own AST counter). Per-schema door measurement then split them one / fourteen. Closed with strictObject: ui/i18n.zod.ts AriaPropsSchema The highest-fan-out single site the ui/ wave has closed — carried as `aria:` on ~30 shapes under six metadata-type roots (ListView, Page, PageComponent, DashboardWidget, ChartConfig, Action, 20 SDUI component defs). Through the view root, `aria: { label, describedBy }` parsed clean and came back `aria: {}`: the accessible name existed in the source file and nowhere else. Those two spellings are objectui's own ARIA_KEY_ALIASES, i.e. what stored view metadata carries. Reclassified as no door, NOT tightened (14 sites): ui/widget.zod.ts all six shapes / nine sites ui/i18n.zod.ts I18nObject, PluralRule, NumberFormat, DateFormat, LocaleConfig Measured three ways with positive AND negative controls in the same run: no carrier key (nothing imports widget.zod but the barrel), unreachable in a 4766-node BFS from the 24 roots + defineStack, zero parse in all three repos (cloud checked by code search against a proven-working index). A synthetic carrier flips all of them, so the verdict is a fact about the graph. Curation anchored on named sibling contracts, not edit distance: the two legacy spellings on objectui's normalizer, and `ariaRole` on this shape's own inconsistent prefix (two of three keys carry `aria`, `role` does not). The four the distance fallback already reaches were measured first and left to it. `live` and `ariaLabelledBy` get a prescription rather than a rename — finding 7 discipline. `live` is real and rendered by objectui's ListView alone, which declares it via `AriaPropsSchema.extend({ live })`, so that surface keeps accepting it and now inherits this error map; promoting it into the shared shape would advertise aria-live on 29 renderers that do not implement it (#5058). A `.strip()` was added to four files this batch does not otherwise touch: animation, dnd (x2), keyboard, touch build their shapes as `z.object({…}).merge(AriaPropsSchema.partial())`, and `.merge()` adopts the incoming posture — closing AriaProps would have silently closed all five of those no-door shapes with zod's generic message and no changeset. Pinned. Ledger finding 20: the campaign's own BFS reported widget.zod.ts as REACHABLE. Zod's `.describe()` returns a clone sharing the original `_zod.def`, so a bridge firing on any ONE shared property under a shared name links unrelated shapes via `name`/`label`. 2 keys of 20. The error is one-directional — it can only make a batch tighten something dead (#4583). Corrected to whole-shape overlap in ui/door-reachability.testkit.ts, pinned as a regression test, filed as #5056. Instruments proven red before trusting green: six sabotages, one per assertion class (walker, clone bridge, strictness, curation, finding-7 guidance, merge-strip), each reverted. Ledger: ui/ 118 strip of 198, authorable subtotal 82 — both recomputed from the surviving rows, never decremented by this batch's count. Refs #4001. Filed out of scope: #5055 (ADR-0049 for the 14 no-door sites), #5056, #5058, #5059.
…#5043 landed) Ledger ui/ section was the only conflict, as expected for a three-way contended table. Every side's rows kept; header and authorable subtotal RECOMPUTED from the surviving rows rather than resolved in favour of a side: rows: 29+20+9+2+7+5+4+4+4+3+1+1+1 = 90 strip of 198 authorable = 90 - 40 no-parse (38 no door + 2 no gate) = component 29 + view 20 + app 1 = 50 Neither side's number survives — 批 16 computed 118 against a tree where 批 14's and 批 15's rows still existed, main carried 91. Eighth instance of the pattern this table documents; recorded as such. Merge resolution detail: main split the combined `chart / i18n` triage row into two, so 批 16's i18n verdict was rehomed onto main's standalone i18n row, and main's 批 15 chart row was kept verbatim. `theme.zod.ts` left the remaining-strip map on main's reverse pin (0 strip) and stays gone. os-regen four-step done: merge → restore the seven driver-managed artifact paths from origin/main → rebuild spec → `check:generated --fix` (all 8 already current; strictness changes no generated snapshot, same as 批 14/15) → sibling entries asserted surviving (批 13/14/15/16 changesets all present, theme/chart/dataset/ sharing strictObject counts intact). Post-merge verification (§10, overlapping packages): @objectstack/spec 301 files / 7709 tests pass, tsc --noEmit clean, check:strictness-ledger green.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
xuyushun441-sys
marked this pull request as ready for review
August 4, 2026 03:51
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 4, 2026
Ledger `ui/` section conflicted as expected — batch 16 (#5069) landed while batch 18 was in flight and both edit it. Resolved by taking main's section wholesale (so batch 16's `widget` no-door reclass, the `i18n` split row and the AriaProps closure survive verbatim) and re-applying batch 18's three edits on top. Header and subtotal recomputed FROM THE SURVIVING ROWS, not carried from either side: 29+4+9+2+7+5+4+4+4+3+1+1+1 = 74 strip, authorable 34 of 74. Batch 18 is the ninth wrong-on-both-sides instance — it computed 84 against a tree where batch 16's rows still existed, batch 16 computed 90 against one where view was still 20, and the merge is neither. Recorded in the section's own prose, since the two batches moved the same numbers for OPPOSITE reasons (16 reclassified 14 sites it never touched; 18 closed 16 it did). check:strictness-ledger green on the merged tree: 36 open file(s) / 233 strip site(s). Part of #4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 4, 2026
冲突一处:账本 ui/ 小计段(批 16 与 批 17 都重写了它)。保留 main 的
批 16 叙述与其发现段(AriaProps 收紧 + widget/i18n 重分类),把 批 17 作为
第九次实例追加,再合成单一小计并从存活行重算。
批 16 关掉 AriaProps(i18n 6 → 5 strip)并把 widget(9)与 i18n 其余 5 项
移出 authorable,总数 91 → 90。批 17 再把 component 的 29 从 authorable
移入 no gate:
存活行 = 29+20+9+2+7+5+4+4+4+3+1+1+1 = 90
no door = touch 7 + animation 4 + dnd 4 + keyboard 4
+ offline 3 + sharing 1 + notification 1
+ widget 9 + i18n 5 = 38
no gate = component 29 + chart 2 = 31
authorable = view 20 + app 1 = 21
21 + 38 + 2 + 29 = 90 ✓
由此:ui/ 剩余 90 个 strip 站点里有 69 个(超过四分之三)不是本 ratchet
的工作。批 17 自身在同一分支上把这行算错了两次(47 of 100 → 36 of 91 →
21 of 90),三次对各自的树都正确、对合并都错误,已如实写下——九次全中。
os-regen 四步:merge(未 rebase)→ 从 origin/main 取回生成物 →
install --frozen-lockfile + 重建 spec + 整体重生成 → check:generated 8/8
全绿,并断言九组兄弟条目共存(批 13 responsive 8 / 批 14 sharing 13 +
notification 3 / 批 15 theme 12 + chart 44 / 批 16 aria 3 + widget 40 +
i18n 25 / 批 17 component 26 键)。
Part of #4001
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
xuyushun441-sys
pushed a commit
that referenced
this pull request
Aug 4, 2026
#5069(批 16)关掉 AriaProps 并把 widget/i18n 移出 authorable,总数 91 → 90。批 17 的 changeset 正文会作为 CHANGELOG 发给使用者,不能停留在 上一次合并时的数值:authorable 50 → 21 of 90,两个 no-parse 类合计 69 of 90(38 no door + 31 no gate)。 Part of #4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 4, 2026
…k-ai#4001 批 17) (objectstack-ai#5071) * docs(spec): 把 ui/component.zod.ts 的 29 个站点判定为 no gate 而非收紧 (objectstack-ai#4001 批 17) SDUI 组件 props 是 ui/ 目录最大的待收紧面,批 17 的测量结果是它根本没有 parse:.strict() 在这里不会强制任何东西,只会花掉一次 v17 破坏性变更换来 objectstack-ai#4583 所说的「一个被精确校验的死槽位」。 三条独立测量,控制组同轮为真: - 承载键 PageComponentSchema.properties 是 z.record(z.string(), z.unknown()); PageComponentSchema 自 ADR-0089 D3a 起是 .strict(),但严格性不递归。 - 从 24 个 metadata-type root + ObjectStackSchema 做 BFS(复用 build-schemas.ts 的 objectstack-ai#4650 闭包,6899 节点),52 个目标全部 UNREACHABLE;六个正控制组全部 root-graph;批 13 的 no-door 形状保持 unreachable。 - 三个仓库里对本文件的 parse/safeParse 全部落在本文件自己的单测内。 经验证据(definePage() 即 PageSchema.parse()):example 语料 10/10 页面上, 写进 components[].properties 的未声明键原样通过并被保留;同一个键放到 properties 的兄弟位上 10/10 被拒(负控制组)。 判定是 no gate 而非 no door —— 词汇是活的(objectui SchemaRenderer 把 properties 里每个键 spread 成 React prop),因此不能按 ADR-0049 退役。修法是 把 parse 接到承载键的闸门上,已立为 objectstack-ai#5068。 判定写在三处:文件头、component.test.ts 的钉子(含一条 properties 一旦获得 类型化分派就变红的断言)、账本 ui/ 两张表。账本 authorable strip 76 → 47, no gate 2 → 31,均从存活行重算。 Part of objectstack-ai#4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(spec): 把 批 17 判定从 JSDoc 改为行注释,避免泄漏进面向用户的生成文档 build-docs.ts 会把文件第一个 JSDoc 块当作模块描述,原样发布到 content/docs/references/ui/component.mdx 和 skills/objectstack-ui/references/ _index.md。内部战役判定不该出现在协议参考文档里(而且生成器会把段落打散, 渲染也是坏的)。 改为 imports 之后的 // 注释块,与 批 13 在 touch.zod.ts 的做法一致 —— touch.mdx 至今仍显示 "Touch Target Configuration Schema",证明这个位置对 生成器不可见。check:generated 8/8 恢复全绿。 WIP checkpoint:实现 + 测试钉子 + 账本 + changeset 已完成并已验证; 待办 = 全量 typecheck/test、三个 example app 的 validate、os-regen 四步、 开 draft PR。 Part of objectstack-ai#4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(spec): 记录 objectstack-ai#5056 桥接缺陷对 批 17 判定不适用的理由 批 16 发现 derived-clone 桥(共享 .describe() 克隆)会把死形状误报为 REACHABLE。其误差方向与本判定相反——只可能掩盖 no-gate 发现,不可能制造 一个。且本判定不依赖该桥:六个正控制组全部 root-graph(自身实例在闭包内), 52 个目标 root-graph 与 derived-clone 双双未命中。两条非 BFS 测量(开放 承载键 + 三仓零 parse 站点)独立成立。 Part of objectstack-ai#4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(changeset): 把 批 17 changeset 的账本算术更新为合并后数值 合并 objectstack-ai#5042(批 14)后总数 100 → 91,authorable 65 → 36,两个 no-parse 类 合计 55 of 91。changeset 正文会作为 CHANGELOG 发给使用者,不能停留在 合并前的分支局部数值。 Part of objectstack-ai#4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 * docs(changeset): 把 批 17 changeset 算术更新为合并 批 16 后的数值 objectstack-ai#5069(批 16)关掉 AriaProps 并把 widget/i18n 移出 authorable,总数 91 → 90。批 17 的 changeset 正文会作为 CHANGELOG 发给使用者,不能停留在 上一次合并时的数值:authorable 50 → 21 of 90,两个 no-parse 类合计 69 of 90(38 no door + 31 no gate)。 Part of objectstack-ai#4001 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #4001 —— ui/ 波次批 16(T1 必保清单序首位),
widget+i18n两文件。账面 15 站点,AST 复测分毫不差(widget 9/9,i18n 6/6,用台账门禁自己的计数器)。逐 schema 门测量后一分为十四:收紧 1 个,改判 14 个。
收紧(1 站点)
ui/i18n.zod.ts·AriaPropsSchema→strictObject这是 ui/ 波次目前收紧的扇出最大的单个站点:作为
aria:挂在约 30 个活形状上,横跨六个 metadata-type 根 ——ListViewSchema/PageSchema/PageComponentSchema/DashboardWidgetSchema/ChartConfigSchema/ActionSchema,以及 20 个 SDUI component def。它在真门上真的在剥。经
view根:两个键整个消失,报告合法。作者写的无障碍名只存在于源文件里,屏幕阅读器用户听到的是 DOM 默认值,而工具链从头到尾没有一句话提过。
而且这两个拼法不是假想的:它们逐字就是 objectui
normalize-list-view.ts里ARIA_KEY_ALIASES折叠的那两个(objectui#2890),也就是存量视图元数据实际携带的拼法。处方(逐条锚定具名姊妹契约,不靠编辑距离)
labelariaLabelnormalizeListViewSchema在 ListView 边界折叠describedByariaDescribedByariaRolerolearia前缀,role不带arialabel/ariaLabell/ariadescribedby/aria-label/roles留给编辑距离兜底 —— 先测了它够得着才不手写:一条距离已经能到的别名是誊抄,不是判断。两个键给处方而不给重命名(finding 7 纪律)
live—— 它是真的,而且真的在渲染:objectui 的ListView.tsx:1984读schema.aria?.live并落成aria-live;objectui 用AriaPropsSchema.extend({ live })声明它,所以那个面继续接受它(并且现在继承本批的错误映射)。在别的面上,消息告诉作者live在哪里有效,而不是指向一个语义不同的已声明键。把它加进共享形状 = 对另外 29 个不实现 aria-live 的渲染器宣告一个不兑现的能力(Prime Directive chore: version packages #10)。收编问题另开 AriaProps 与 WAI-ARIA 1.2 的两处缺口:aria.live只活在 objectui 的扩展里,aria-labelledby全无对应 #5058。ariaLabelledBy/labelledBy——aria-labelledby引用的是另一个元素的 id,与ariaLabel(字面串)不是一回事,没有可以重命名过去的目标。所以点名缺口,同样 AriaProps 与 WAI-ARIA 1.2 的两处缺口:aria.live只活在 objectui 的扩展里,aria-labelledby全无对应 #5058。顺带在四个本批不改的文件里加了
.strip()animation.zod.ts/dnd.zod.ts(×2)/keyboard.zod.ts/touch.zod.ts用z.object({…}).merge(AriaPropsSchema.partial())构形,而.merge()采纳传入 schema 的未知键姿态 —— 收紧AriaProps会把这五个形状一并悄悄收紧:用 zod 的通用文案(不是本战役的)、没有 changeset、而且违背 #4988 实测出的「没有任何东西 parse 它们」。显式.strip()守住这条线,i18n.test.ts钉住。这是 finding 16 的
.extend()陷阱换了个方法名出现。改判为 no door(14 站点,⛔ 不收紧)
ui/widget.zod.ts全文件 6 个 schema / 9 站点,ui/i18n.zod.ts的I18nObject/PluralRule/NumberFormat/DateFormat/LocaleConfig5 站点。三条独立测量,正对照与负对照在同一轮跑:
packages/spec/src下除ui/index.ts桶文件外没有任何模块 importwidget.zod;field.widget是个z.string(),命名的是已注册组件,从不指向WidgetManifest。defineStack出发的 BFS(4766 节点)全都够不到;PageSchema/ObjectListViewSchema同轮解析为 direct,一个新建z.object和一个刻意长得像的负对照都判不可达,注入合成载体后全部翻转为 direct。objectstack/objectui/cloud三仓,除各自单测外零.parse()。objectui 只 re-export 推导 TYPE 且改了名(RuntimeWidgetManifest/FieldWidgetComponentProps);cloud 用 code search 对全部符号 0 命中,正对照"@objectstack/spec"同仓 345 命中(索引可用)。.strict()是 parse 的属性;没有 parse 就什么都不闸,只会把死槽装扮成受校验的活槽(#4583)。ADR-0049 enforce-or-remove 另开 #5055(与 #4988 同类)。NumberFormat/DateFormat确有载体(LocaleConfig.numberFormat/.dateFormat),但载体自己无门,所以整棵子树是no door而不是no gate。重测的那条标注警告,落点比警告本身更有用
台账原文警告「i18n 的 label 形状按设计是宽开 record —— 收紧前必验」。验完的结论是警告没错,但瞄偏了一层:
I18nObject.params,插值参数袋,键空间就是消息模板命名的任何东西。但它从来不是那 6 个站点之一(它是z.record,不是z.object),所以本棘轮压根没有东西可关。AriaProps),测出来是本目录扇出最大的活形状。即:标准警告与实测指向相反方向,而两边都被记了下来(schema 头注释 + 测试钉 + 台账行,三处标准)。
台账 finding 20 —— 战役自己的门测量会误报「可达」(#5056)
第一轮 BFS 把
WidgetManifestSchema报成 REACHABLE,而这个文件全仓没人 import。差点据此对一个死文件做 9 站点的 breaking 收紧。机制:批 15 引入的 derived-clone bridge 在「任意一个同名属性 def 相同」时判可达 —— 它存在是有真实理由的(
.extend()克隆不共享身份但共享逐属性实例)。可是 zod 的.describe()返回的克隆共享同一个_zod.def对象(实测defOf(z.string().describe('x')) === defOf(z.string())为 true),于是SnakeCaseIdentifierSchema/I18nLabelSchema这类共享叶子在全仓 def-同一,而name/label几乎是每个可授权形状都有的两个键。20 个键里命中 2 个(重合度 11%),判可达。误差只朝一个方向:只会造门,不会藏门 —— 它全部的失效预算都花在让批次去收紧没人 parse 的东西上。
修法与阈值都写进
ui/door-reachability.testkit.ts:问「共享了多少形状」而不是「有没有共享」,阈值对着实测区间两端定(批 15 的真派生远在其上,批 16 的假阳远在其下)。widget.test.ts有回归钉。批 15 留在chart.test.ts的那份拷贝属 #5056 —— 战役自己反复在记的「真相的第二份拷贝」,这次出现在它的仪器里。方法论上值得记一句:批 15 的擦肩(
typeof v !== 'object'跳过所有 lazy Proxy)是正对照抓到的;这一条对正对照完全隐形(每个根都照样解析),要负对照 + 合成载体翻转才抓得到。一次门测量欠三样,同一轮跑:一个已知活的、一个已知死的、一个注入后必须翻转的载体。验证
先证仪器会红,再信绿 —— 六次破坏,每个断言类目一次,逐次回退:
typeof v !== 'object'unreachable≠direct)。注意 no-door 断言仍绿 —— 这正是为什么需要这条对照expected 1 to be less than 0.2)AriaProps退回z.objectlive的 guidance 改成错误 alias.strip().merge()不外溢钉零破坏实证:
@objectstack/spec301 文件 / 7709 用例通过,tsc --noEmit干净check:*闸门全绿(8 个生成物 + 8 个源审计,含check:docs—— 本批的判决全部写成//行注释,不会顶替参考页正文,批 15 的教训)objectstack validate三个示例应用 exit=0validate对页面内嵌形状是空转 ——objectstack build/validate从不按 PageSchema 解析页面元数据:ADR-0089 D3a 早就该拒绝的键一路通过,#4001 的「三个示例应用 validate 全过」对 page 面是空证 #5000):遍历三个应用构建产物dist/objectstack.json,按载体槽路径追踪 ——aria槽 0 个,即零破坏;探针的负对照(一个带旧拼法的块)证明会红。另 250 个同名槽命中经 provenance 核对全部属于别的 schema(翻译包 / 校验规则的 JSON-Schemaproperties/ dashboardwidgets/ actionparams/ 连接器 IO schema),没有一个是本批形状的载体台账
ui/ 段三方竞争如预期冲突。两边的行全留,表头与可授权小计从存活行重算,不向任一方解决:
哪一方的数字都不是最终值 —— 批 16 在还有批 14/15 行的树上算出 118,main 上写着 91。这是该表自己记录的这个模式的第八次,已按例记进去。
os-regen 四步做完:merge → 从 origin/main checkout 七条 driver 托管路径 → 重建 spec →
check:generated --fix(八个全部已是最新;收紧不改任何生成快照,与批 14/15 一致)→ 断言姊妹条目存活(批 13/14/15/16 changeset 齐全,theme/chart/dataset/sharing 的strictObject计数完好)。范围外发现(已建 issue,本 PR 不改)
.describe()共享 def 对象,任意单属性 bridge 把无关形状连起来 #5056 —— 上文的门测量仪器缺陷,含把 BFS 抽成一份共享工具的建议aria.live只活在 objectui 的扩展里,aria-labelledby全无对应 #5058 ——AriaProps对 WAI-ARIA 1.2 的两处缺口(aria.live只活在 objectui 扩展里 /aria-labelledby全无对应),additive,建议进 v18 地图content/docs/references/{data/mapping,system/translation}.mdx两张公开参考页的正文被本战役的内部 history 注释顶替(feat(spec): reject unknown keys on an action param instead of stripping them (#3405) #3746 陷阱 1 已实际发生两次,check:docs结构上看不见),附一条可做的窄门禁建议已知限制,诚实记录
chart.test.ts里批 15 那份 BFS 拷贝仍是旧的 any-property 版本。它对 chart 自己的断言无害(那些 schema 确实可达),但它是 #4001 战役的门测量 BFS 会误报「可达」:zod.describe()共享 def 对象,任意单属性 bridge 把无关形状连起来 #5056 的一部分,没有在本 PR 里改 —— 那是别的批次的文件,且修法已在door-reachability.testkit.ts里给出。packages/lint的runtime-lazy-deps.test.ts在本容器里会因 5000ms 超时而红(多 agent 共享容器);把 timeout 放宽到 60s 后 4/4 通过,契约断言本身(typescript / sucrase 都没被加载)成立,所以不是本批引起的。⛔ 草稿,不合并、不置 ready。
content/docs/releases/零触碰。Generated by Claude Code