feat(spec)!: 退役 ui/ 五个没有承载键的交互配置文件 —— touch/dnd/keyboard/animation/offline (#4988) - #5321
Conversation
touch / dnd / keyboard / animation / offline 五个模块声明了完整的交互配置词表
(22 个 z.object 站点、32 个 def、64 个导出名),而协议里**没有任何承载键** ——
没有 schema 声明过 `touch:` / `dnd:` / `keyboard:` / `animation:` / `offline:`
槽位,所以没有元数据文档能到达这些形状,也从来没有 parse。
三条独立测量在 origin/main 上重跑,每条的阳性对照都在同一次运行内通过:
静态(除 ui/index.ts barrel 外零 importer)、图可达(25 roots / 4742 nodes,
21 个具名对象形状全不可达,Page/Webhook/StateMachine 全部 direct,注入合成
承载键后 21 个全部翻转)、调用点(三仓零 parse)。
真正的缺陷在**文档侧**:authorable-surface.json 躺着 109 个键,
content/docs/references/ui/{touch,dnd,keyboard,animation,offline}.mdx 把它们
渲染成授权表 —— AI 作者照 dnd.mdx 往页面组件写 dnd: 块,会被
PageComponentSchema 以未知键拒绝。这是 PD#10 的宣传运行时不提供的能力,
不是 strictness 缺口:收紧只会精确校验一个没人够得到的槽位(#4583)。
按 spec-property-retirement 路线 3(nothing parses it → 既不墓碑也不 D2),
与 #4834 / #4938 同形;注册 ADR-0087 D3 SemanticMigration
`ui-interaction-config-family-retired`,并扩写 protocol-17 步骤的 rationale。
⚠️ ui/animation.zod.ts 与 #5021 退役的 theme animation 块是两张面:不同文件、
不同 def、不同 manifest 条目。那一张有承载键、走墓碑;这一张没有、走删除。
Fixes #4988
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
按 .gitattributes 的 os-regen 四步互保执行:merge origin/main → 把全部 os-regen 路径 checkout 回 origin/main → 整体重算 → 断言兄弟条目仍在。 互保断言(重算后的基线上实测): - 本单 #4988:32 个 ui/ 交互 def 不在 manifest,109 个 authorable 键为 0; - 兄弟 #4938 / PR #5293:system/HttpServerConfig 三张表全部仍为删除态; - 兄弟 #5021 / PR #5289:6 个 theme 族 [RETIRED] 标记全部仍在 (ui/Theme:animation、ui/Theme:zIndex、ui/Typography:fontSize/fontWeight/ letterSpacing/lineHeight)—— 与本单删除的 ui/ComponentAnimation 一族是 两张互不相干的面,名字撞车但条目分离,已逐条核对; - 阳性对照:ui/Page 的 authorable 键仍在,ui/ResponsiveConfig 仍被 emit。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 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:
|
三处冲突全部语义合并,无机械取边: - `ui/index.ts` barrel:保留 #5300 为 notification.zod / sharing.zod 写的两段 「幸存导出」说明,同时落实本单五个退役模块的 export 删除。 - `migrations/registry.ts`:step17 rationale 与 semantic[] 两处冲突都是**双方各自 追加**,两边全留。rationale 里把 #5015 的段落排在前、本单排在后,并把本单开头 从「最后一个不需要改源码的退役」改写为「同类的第二个」—— #5015 落地后那句 独占措辞已不成立。semantic[] 两个条目并存。 - 台账:双方各删各的行,**两边的删除都生效**(#5300 删 sharing/notification 两行, 本单删五行交互行);`no door` 小结改写为同时反映两次退役,并记下这一类九个条目 里已有七个在同一个发布窗口内由「退役」而非「收紧」闭合。 按 AGENTS.md §9 刷新构建状态(frozen-lockfile install + 重建),再按 os-regen 四步 把九条生成路径整体重算。⛔ 未手改 #5304 新引入的 authorable-surface.base.json 锚点 —— 该文件只由 gen:schema 从 git 解析出的基线写入。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
#5304 引入的树内锚点。keys 因兄弟 PR 落地而漂移,gen:schema 据此把 baseRev 从 26e1029(#5300)推进到当前 merge base 1c3da1f,并整体重写 keys。 ⛔ 未手改一个字节:该文件只由 gen:schema 从 git 解析出的基线写入,手改它就是 #4650 要防的那种攻击本身。authenticity 两条均已核:baseRev 是 origin/main 的 祖先(此处即等于 origin/main),且 keys 与该 commit 的 surface 逐行一致 —— 以 check:authorable-surface 转绿为准,而不是要求 baseRev == 任何特定值。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
接力同步(第 2 轮):合入
|
| 文件 | 冲突性质 | 处置 |
|---|---|---|
ui/index.ts |
#5300 在同一段加了两条「幸存导出」说明,我删了五行 export | 两边都要:保留 #5300 的注释,落实我的删除 |
migrations/registry.ts ×2 |
step17 rationale 与 semantic[] 双方各自追加 |
两边全留。rationale 里 #5015 段在前、本单在后,并把本单开头由「最后一个不需要改源码的退役」改写为「同类的第二个」—— #5015 落地后那句独占措辞已不成立 |
| 台账 | 双方各删各的行 | 两边删除都生效(#5300 删 sharing/notification,本单删五行交互行);no door 小结改写为同时反映两次退役 |
semantic[] 那处值得单记:两个条目起始于同一个 {,机械取任一边都会静默丢掉另一个迁移记录且语法照样合法 —— 在接缝处补 },{ 才是正确解。同理 rationale 那处:#5015 段落原本以 ', 结尾(属性终止符),直接拼接会造成语法错误,改成 \n\n' 续接。
#5319 / #5304 两处交互,已按你点名的方式核过
- feat(spec)!: ViewItemSchema 拆成授权门 + wire 变体,并让 wire 的开放递归生效 (#5074) #5319 的新 view 面:对 32 个退役名做精确扫描,
view.zod.ts与view-authoring-wire-split.test.ts零命中 —— 无残留,以 main 的新结构为准。 - fix(spec): #4650 删除闸门改用树内基线锚点,按 SHA 钉住的离线消费者构建不再硬失败 (#5235) #5304 的树内锚点:
authorable-surface.base.json已进 os-regen 清单,四步法照做。⛔ 一个字节都没手改 —— 该文件只由gen:schema从 git 解析出的基线写入,手改它就是 authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650 要防的攻击本身。gen:schema因兄弟落地导致 keys 漂移,自行把baseRev由26e1029f5(feat(spec)!: 退役NotificationActionSchema/EmbedConfigSchema—— 两个没有授权门的 ui 词汇表形状 (#5015) #5300)推进到1c3da1f6f089,单独一个 commit 留痕。按你的修正版措辞验收:check:authorable-surface绿,且baseRev是origin/main的祖先 —— 没有去要求它等于 merge-base。
增量恰为自身(你点名的信号确实出现过)
第一次跑 gen:schema 报的是 23 条,不是 21 —— 因为当时合并尚未 commit,merge-base 仍解析到 175d789,多出的 2 条正是 #5300 的 ui/EmbedConfig / ui/NotificationAction。commit 合并后重跑,merge-base 变为 1c3da1f6f089,读数即为恰好 21 个 def / 109 键,全部是本 PR 自己的,无一条兄弟移除被算到本单头上。
兄弟断言(合并后的源码树 + 重算后的基线)
#5021 theme retiredKey 墓碑 11 处仍在
#4938 HttpServerConfigSchema 仍为删除态
#5015 NotificationAction / EmbedConfig 仍为删除态
#5319 ViewItemWireSchema 在册
#5308 email 'smtp' 在册
#5306 FilterArray 在册
验证(本轮实测)
typecheck exit=0
check:generated ✓ All 9 generated artifacts are up to date
check:liveness PASS check:empty-state PASS
check:exported-any PASS check:dual-source-exports PASS
check:skill-examples PASS check:skill-refs PASS
check:skill-docs PASS check:variant-docs PASS
check:strictness-ledger PASS check:react-declaration-parity PASS
spec test: 308 files / 7885 tests passed
lint test: 57 files / 1198 tests passed (消费半径)
cli test: 70 files / 633 tests passed (消费半径)
check-nul-bytes: OK (5364 files) + 对 origin/main...HEAD 全部改动文件逐个 grep -naP 控制字符,全净
台账重算(整体重算,零手改):454 sites / strip 195 / 携带站点文件 29 —— 与第 1 轮的 199 / 31 之差来自 #5319、#5318 等兄弟,不是本单又动了什么。
packages/lint 首轮红过 10 个文件,不是本 PR 的问题:错误是 Failed to resolve entry for package "@objectstack/formula" / "@objectstack/sdui-parser",即合并多个 spec PR 后依赖链未重建(AGENTS.md §9 的老陷阱,PR #5300 记过同一条)。按 pnpm --filter "@objectstack/lint^..." --filter "@objectstack/cli^..." build 重建后全绿,如上。
check:i18n 仍为 EXIT=1,与第 1 轮同因、同 9 个包,已在 #5217 留过对照证据(未改动检出上同样复现);本 PR 未改该门任何输入。
auto-merge 未挂,等你复核。
Generated by Claude Code
合并 origin/main 后重跑 gen:schema,锚点 baseRev 由 1c3da1f0899 前进到 merge base 28ad90e(7907 keys,与该 commit 的 authorable-surface.json 逐行一致)。生成器写入,非手改;lag 本可保留,但 gen:schema 在 keys 漂移 (#5321 退役 109 键)时按设计刷新。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Fixes #4988
ADR-0049 enforce-or-remove,按 2026-08-04 维护者裁决 A —— 退役 执行。走
spec-property-retirement套件的整文件删除路线(route 3)。先复核前提(rule 6)—— 三条测量在
origin/mainf8cfbb4 上全部重跑issue 的证据是 2026-08-03 的,不能直接当施工依据。三条独立测量重跑,每条都带同一次运行内通过的对照:
packages/spec/src下的非测试 importer 只有ui/index.ts;协议里不存在touch:/dnd:/keyboard:/animation:/offline:槽位PageSchema/WebhookSchema/StateMachineSchema全部direct;注入合成承载键后 21 个全部翻成direct.parse()/.safeParse()站点数复核:
touch7 +animation4 +dnd4 +keyboard4 +offline3 = 22,与 issue 一致。其中 21 个是具名对象 def,第 22 个是TouchTargetConfig.hitSlop的内联对象(不产生独立 def)—— 这解释了 21 与 22 的差。存量实测:
examples/**与apps/**里 authored 实例 = 0,而且是必然为 0:键根本写不出来。这就是不注册 D2 的依据。authorable-surface.json里是 58 个键,实测是 109 个。 我按实测走。改了什么
五个文件整体删除(
touch/dnd/keyboard/animation/offline的.zod.ts与.test.ts),32 个 def、64 个导出名、109 个 authorable 键、5 张参考文档随之退役。整文件删除是逐文件核过的,不是假设(#4938 的教训)
套件明写「按 SCHEMA 退役,不是按文件」:文件里若还有活的兄弟导出,就只退形状、留文件。逐个核对结果是五个文件的全部 64 个导出名都在族内且都不可达,所以五个文件才都删。
ui/notification.zod.ts(PR #5300 只摘走两个形状、留下三个活枚举)是反例,本次不碰。为什么没有墓碑、也没有 D2
没有承载键 → 没有任何 shape 能承载
retiredKey()墓碑,也没有任何sys_metadata行或源码可供 D2 改写。这是 route 3(nothing parses it → 既不墓碑也不 D2),与 #4834 / PR #4878(kernel plugin-runtime 族)、#4938 / PR #5293(HttpServerConfig)同形。注册的是 ADR-0087 D3SemanticMigrationui-interaction-config-family-retired,并扩写 protocol-17 步骤的rationale。animation是两张面,已逐条核分离ui/animation.zod.ts的ComponentAnimation/MotionConfig/PageTransition/AnimationTrigger族,与 #5021(PR #5289,已在本分支 base 里)退役的 themeanimation块,是不同文件、不同 def、不同 manifest 条目。末段同步后在重算的基线上逐条断言过:#5021 的 6 个 theme 族[RETIRED]标记(ui/Theme:animation、ui/Theme:zIndex、ui/Typography:fontSize/fontWeight/letterSpacing/lineHeight)全部仍在,而本单删的是ui/ComponentAnimation等 def。两者互不覆盖。路线自证:四张 ratchet 的读数(整 def 删除,必须变化)
套件明写枚举值收窄与整 def 删除的 ratchet 可见性相反。本次是整 def 删除,实测:
json-schema.manifest.json−32authorable-surface.json−109api-surface.json−64(32 type + 32 const)而且这串输出本身就是路线的自证,原样留档 —— #2978 ratchet 先开火,要求有意删 manifest key:
删完重跑,merge-base gate(对
origin/main,本 PR 改不动它)自行判为 #4650 路径 3:21 个 def × 各自键数 = 109,与上面的 −109 对得上。
新 pin:缺席与存活双向,两边都承重
packages/spec/src/ui/interaction-config-retirement.test.ts,照 #4834 / PR #5300 的编译器 API 形制:按 resolved symbol identity 遍历package.jsonexports map 的每个 public entry,带反空转守卫(#4642:本包 tsconfig 排除*.test.ts,类型级断言是装饰品)。存活侧和缺席侧一样承重:一次删过头的「退役」能满足全部缺席断言。存活清单里第一个就是
ResponsiveConfigSchema—— 它是同一批 13 的第六个文件,实测可达(page.components[].responsive),当时是被收紧而不是退役的,是最可能被顺手带走的那个。反向验证:方向先预判后运行,两次都如预期,且落在不同的半边
ui/offline.zod.ts连同 barrel 导出加回去OfflineStrategy must have zero holders after #4988: expected [ './ui' ] to deeply equal []、ui/offline.zod.ts must be deleted: expected true to be false,以及sync-retirement.test.ts第 3 节ConflictResolution ... expected [ './ui (src/ui/offline.zod.ts:72)' ] to deeply equal []。存活断言一条没红export * from './responsive.zod'摘掉ResponsiveConfigSchema must SURVIVE this retirement/must SURVIVE at runtime两次破坏红在不同半边,这才是双向 pin 的意义 —— 单向 pin 对第二种错误是全绿的。
存量 pin 的三分法处置(不是批量重拼)
automation/sync-retirement.test.ts(#4738)—— 断言方向被反转,这是本 PR 最需要人看的一处#4738 之所以把 connector 侧改名
ConnectorConflictResolution,正是因为裸名ConflictResolution归ui/offline.zod.ts所有。那个 owner 现在没了。原断言是
holders.map(h => h.sub)).toEqual(['./ui'])。照原样留着它会为正确的理由变红,然后被人用错误的方式「修好」 —— 把它指向任何一个还有ConflictResolution的 entry,而那恰恰是该文件 C14 注释点名的「re-export 会对域说谎」。所以第 3 节整体改写为:裸名现在没有任何 owner,且任何域都不得悄悄收编它。#4738 的改名不撤销(把一个空出来的词要回来是第二次破坏性变更,收益为零)。第 5 节同样改写:原写法用
holdersOf('ConflictResolution')[0].origin当基准来证明ConflictResolutionStrategy是不同声明 —— owner 没了之后[0]直接抛异常,断言根本跑不到。改为断言这位「第四号亲戚」保住了自己的名字和src/api/出身。ui/i18n.test.ts—— 整体替换,不是删断言原有
does NOT ride .merge() into the four no-door files断言五个被删形状仍接受未声明键。这五个模块是全包仅有的.merge(AriaPropsSchema.partial())站点。只删断言会留下一个「因为什么都没产出而绿」的空测试(PR #5046 记录的陷阱),所以改为在其真正生效的层面重新承重:全包.merge(AriaPropsSchema…)站点数为 0,带双向反空转(walk 真的扫到了 100+ 个 schema 文件;匹配器对合成源真的会命中),外加一条.optional()承载者仍在的正向断言。存量留痕的一并改写
renamed-defs.ts(表项保留,源仍未 emit、目标仍 emit,只改注释)、automation/index.ts、integration/connector.zod.ts、api/router.zod.ts、theme.zod.ts与theme.test.ts(那条「绝不处方一个没有承载键的词表」的断言因本次退役而更强,不是变没用)、chart.zod.ts(两个被当阴性对照引用的形状已不存在,注明重跑需自备对照)、SYNC_ARCHITECTURE.md、content/docs/protocol/objectql/index.mdx。.changeset/datasync-conflict-dual-source-c13-c15.md(#4738)原话是「若你要客户端离线冲突策略:import { ConflictResolution } from '@objectstack/spec/ui'(本次未动)」。两份 changeset 在同一个 release 里发布,不改的话 CHANGELOG 会叫升级者去 import 一个已不存在的符号。已就地标注为「同窗口内已在 #4988 一并退役」。这是套件 §4「更正必须传播到每一行」的形状。台账(#5220 格式)
数字一律
gen:strictness-ledger整体重算,零手改。 476 → 454 sites,strip 221 → 199,no door38 → 16,ui/192 → 170(strip 75 → 53),携带站点的文件 36 → 31。strict列 249 不变(ui/112 不变) —— 这是预期读数,不是漏做:退役移走的是strip站点,关闭的门一个没碰,所以它不可能在任一方向上动这条 ratchet 的开口债。反向 pin 要求闭合行必须删,五行 remaining-strip 行 + 一行 triage 行按 gate 逐条处置,去向另记在表下 —— 因为反向 pin 对零命中一视同仁,分不清「有意的地板」和「被退役掉的地板」(PR #5300 为
sharing.zod.ts记过同一件事)。末段同步(os-regen 四步互保)
跑到一半
main前进到 175d789(#5293 落地)。按.gitattributes四步执行:git merge origin/main(无 rebase / 无 force-push)→ 把全部 os-regen 路径checkout origin/main --→ 整体重算 → 逐条断言兄弟条目仍在:os-regen merge driver 自身也确认:
all deferred artifacts are current — marker cleared。验证
pnpm check:i18n在本容器里是红的,但与本 PR 无因果关系,已带对照证明:同一命令在未改动的共享主检出(1b9a53b,与本分支无任何共同改动)上给出一模一样的EXIT=1与同样 9 个包;建过@objectstack/cli依赖后仍复现。本 PR 也没有改动该门的任何输入(零.form.ts、零 translation bundle)。已归到既有的 #5217 并补了对照数据,未在本 PR 修。⛔ 不在本 PR 范围
objectui 不碰(派单要求)。精确测量:38 个文件提到这些名字,其中真正跨仓耦合到 spec 的是 5 个:
packages/react/src/hooks/useOffline.tsOfflineStrategy/ConflictResolution/PersistStorage/EvictionPolicy及两个 Schema 的typeof)packages/types/src/index.tsexport type { … } from '@objectstack/spec/ui'packages/mobile/src/__tests__/gesture-spec-parity.test.tsxGestureTypeSchemapackages/react/src/hooks/__tests__/animation-notification-spec-parity.test.tsxTransitionPresetSchema/EasingFunctionSchemapackages/react/src/hooks/__tests__/offline-nav-performance-spec-parity.test.tspackages/core/src/protocols/DndProtocol.ts/KeyboardProtocol.ts等是本地同名声明,不 import spec,不受影响 —— 名字撞车不等于耦合。一条给 objectui 适配方的线索:
packages/types/src/__tests__/page-nav-misc-spec-parity.test.ts的 ratchet 就是为这一刻设计的,注释原话是 "if the spec RETIRES one of these, the local dialect can take the natural name back. A workaround should not outlive its reason" —— 所以TouchGestureType/TouchGestureConfig/PWAOfflineConfig现在可以改回GestureType/GestureConfig/OfflineConfig。那不是要糊过去的红,是本次退役解锁的一次正向清理。content/docs/releases/未改动。🤖 Generated with Claude Code
https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB