test(console,layout): parity 门按 ADR-0087 墓碑收窄接受集,识别做成共享 judge (#3809) - #4967
Conversation
`retiredKey()` 退役一个键不是删条目 —— 它把成员换成 `z.never().optional()`, 所以退役键仍留在 `.shape` 里。任何从 `Object.keys(shape)` 推导「spec 接受的键」 的门,回答的都不是它名字宣称的那个问题。 `registry-inputs-spec-parity.test.ts` 的两个方向读同一个集合,于是在墓碑上朝 相反方向失效:正方向(不得声明 spec 拒绝的键)把墓碑算作「接受」→ 假绿; 反方向(spec 声明的键必须可发现)把它算作「声明」→ 假红,要求本仓去发布一个 契约按名拒绝的键。一处收窄 `specTopLevelKeys` 同时修掉两向。 卡里说「今天休眠」已经过期:它写于 rc.5(`ComponentPropsMap` 零墓碑),而 rc.6 pin(#4167)带来八个 —— page:header.icon、page:card.actions、 page:card.body、page:tabs.type、record:details.layout,加 element:record_picker 的 displayField / searchFields / multiple。反方向的红从那次 pin 起就是活的,由 八条显式豁免逐键吸收,每条都写明只有本卡能解。八条一并删除:不是手挑,而是收窄 后原有的 dangling + stale 两道检查各自点名了全部八条。此后自清 —— 上游之后退役 的键一进 shape 就离开接受集,顺带带走覆盖它的豁免,不再需要后续 issue。 判据收进 `@object-ui/test-support`(私有、不发布):同一判断此前在四处手写,副本 已经漂移(两处只有结构通道,一处压根没有 —— 就是本卡的假绿)。共享 judge 把结构 通道与 `[REMOVED]` 描述通道 OR 起来,任一条被改坏都不会静默变宽;校准只做一次, 且是把结构判据钉在契约自身 `safeParse` 的实际拒绝上。`packages/layout` 的 page-header 门按它自己注释的要求换用共享判据。 两处消费者都断言了推导的前提(退役保留成员),所以上游哪天改成真删键,这些过滤器 会被判成死码而不是静默过滤零个。plugin-detail 与 app-shell 还各有一份本地副本, 今天都是对的,#4947 跟踪转换。 Fixes #3809 Co-authored-by: Claude <noreply@anthropic.com>
ℹ️ Console Performance Budget — not measuredThis run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget. This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.
See the workflow run for details. No package size report: it is only generated from a complete package build, so a partial one is never shown. |
…bstone-aware-parity
合并 origin/main 带进了 @objectstack/spec 17.0.0 GA pin(#4636 / PR #4639), 本分支的基线还在 rc.6 上,于是 CI 的 merge 结果里 pnpm-lock.yaml 半更新、 frozen install 报 ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY。合并后完整重解析, `pnpm install --frozen-lockfile` 本地复现通过。 test-support 的 spec devDependency 对齐仓库的 ^17.0.0。 GA 上重测墓碑集:仍是同样八个(`ComponentPropsMap` 条目从 31 增到 37,墓碑数不变), 两条识别通道在 GA 上依然同时成立,所以收割钉子的八个键名与 premise 断言都无需改动 —— 只把散文里的「实测于 rc.6」更正为两个 pin 都测过。`targetVariable` 那两个在 GA 上 仍是活键(上游 main 已退役),下一次 pin 升级仍会按自清机制点名它们。 Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
…bstone-aware-parity
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
1 similar comment
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
两件事,都是给自己的门补上和它给别人补的同一种保障: 1. 批量函数原来在 `.filter()` 谓词里逐键重解析 shape —— 对 `lazySchema()` 条目 等于每个键重跑一次 thunk。正是 AGENTS.md §测试纪律 记下的那条(`keysOf(x)` 写在谓词里,7.51s → 25ms)。改成一次解析、一趟 partition。 2. 判据(两条通道的 OR)原来在单键谓词与批量 partition 里各写了一遍。变异测试 立刻暴露了代价:把单键谓词改成恒 false,批量那份仍在收窄,于是消费门的 premise 断言照绿 —— 半个 judge 被静默关掉而没人红。现在两处都走同一个 `isTombstone`,再跑同一变异:premise 断言与反方向五个 block 一起红(15 红), 与重构前的方向一致。 Co-authored-by: Claude <noreply@anthropic.com>
|
【CI 说明:红着的 3 个 test shard 不是本 PR 的】
在纯净 已由 #4968 跟踪( 本 PR 自己覆盖的三个套件在这次 CI 里都在通过的 shard 内,且本地在 GA pin 上全绿(31 files / 384 tests)。同一轮 CI 里 Type Check(全仓)、Lint、Build & E2E、Bundle Analysis、Control Byte Scan、三条 changeset 门 全绿。 Generated by Claude Code |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
PM 验收 ✅ ACCEPT(#3809,批次 19) 实物核验(按真实 merge-base 对账):11 files,+936/−183 —— CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。首轮 premise 复核:卡按 rc.5 断「今天不成立」,dev 实测 rc.6/GA 各八墓碑、三个变异在两个 pin 上双跑逐字相同 —— 前提验证先行的正确用法;变异①按真实红方向改写派发单措辞并如实记录(把墓碑当接受只会更绿,让正方向红的是「有 block 声明墓碑键」),不套模板。 #4947(plugin-detail/app-shell 两份本地墓碑判据待转换)finding 入池已确认;GA-pending 五条豁免散文过期的信号归 #4668 范围,本 PR 未动它们,边界正确。undraft + auto-merge(squash)。 Generated by Claude Code |
Resolve the one header-narrative conflict in registry-inputs-spec-parity.test.ts: keep this branch's rewritten types bullet (expressiveness closed by #3832, the comparison half tracked as #4971) and drop this branch's pre-#3809 tombstone bullet — main already records that limit as closed (#3809 landed via #4967), so the LIMIT list stays at exactly the two items its preamble announces. Co-authored-by: Claude <noreply@anthropic.com>
Fixes #3809
retiredKey()退役一个键不是删条目 —— 它把成员换成z.never().optional()(故意的:删掉的键会被非 strict 解析静默剥掉,而never成员在编写处就让tsc失败、在解析时抛出升级处方)。所以退役键仍然留在.shape里,任何从Object.keys(shape)推导「spec 接受的键」的门,回答的都不是它名字宣称的那个问题。一处收窄,两个方向
apps/console/src/__tests__/registry-inputs-spec-parity.test.ts的两个方向读同一个集合,于是在墓碑上朝相反方向失效:declared − acceptedaccepted − declaredspecTopLevelKeys现在减掉墓碑 —— 这一处同时修掉两向,正是卡里点名的落点。premise 已过期:不是休眠,是活的
卡写于
@objectstack/spec@17.0.0-rc.5(ComponentPropsMap零墓碑,所以卡断定「今天不成立」)。实测:@objectstackfamily at17.0.0-rc.6— the adaptation surface beyond what objectstack#7100 could carry #4167,本分支起点)带来八个:page:header.icon、page:card.actions、page:card.body、page:tabs.type、record:details.layout,加element:record_picker的displayField/searchFields/multiple;@objectstack/specpin from^17.0.0-rc.6to 17.0.0 GA (lockfile resolution + green-keeping adaptations) #4636 / PR chore(deps): raise the @objectstack/* pin from ^17.0.0-rc.6 to 17.0.0 GA #4639,在本 PR 在途期间合并进 main,已 merge 上来)carries 同样那八个 ——ComponentPropsMap条目从 31 增到 37,墓碑数不变,两条识别通道在两个 pin 上都同时成立。两个 pin 都测过;origin/main(@23abe2782)有十个 —— 多出的两个是element:text_input.targetVariable与element:record_picker.targetVariable,GA 上仍是活键。所以反方向的假红从 rc.6 起就是活的,由八条显式豁免逐键吸收(每条都写明「只有 #3809 能解」)。正方向的假绿今天没有实例:没有任何 block 声明这八个键中的任何一个 —— 它是可达但未被触发的,所以用变异证明(见下)。
八条豁免一并收割
不是手挑:收窄之后原有的两道检查各自点名了全部八条 ——
every unpublished-key exemption names a key the spec really declares判为 dangling,carries no stale unpublished-key exemption判为 stale。删除是唯一的绿法。此后自清:上游之后退役的键一进 shape 就离开接受集,顺带带走覆盖它的豁免,不再需要后续 issue。已经排在后面的就是上面那两个
targetVariable—— 下一次 pin 升级会点名它们,处置是删条目(#3834 那个「要不要发布 intent-only 键」的问题届时由上游给出否定答案)。page:tabs.type的解法与它自己条目的预测不同,值得一读:那条把它读作「spec 的活键,但扁平 SDUI 载体表达不了」,并把收敛推给上游;上游收敛了 —— 直接退役了type拼法,保留本仓已发布的tabStyle。载体冲突不是被容忍,而是没了。判据收进共享 judge
同一判断此前在四处手写,而且副本已经漂移:两处只有结构通道,一处压根没有(就是本卡的假绿)。现在落在
packages/test-support/src/spec-tombstones.ts(私有、永不发布,正是该包 README 写明的存在理由):.optional()后是never)与描述(retiredKey()盖的[REMOVED]前缀)。实测八个墓碑两条都成立,所以 OR 不是为覆盖率,而是为了任一条被改坏时不会静默变宽;要不要求两条一致是消费者的 pin,识别本身从不要求;safeParse,必须在该键路径上报出 issue(按路径读而非看success,因为这些 schema 有自己的必填成员);活键作对照必须干净。校准是派生的,不钉键名,所以 pin 一动不需要改名单;packages/layout的 page-header 门按它自己注释的要求(「when parity 门只比键名,retiredKey()墓碑会被判成「spec 接受」—— 今天 pin 版零墓碑所以休眠,spec pin 一升就是门里的假绿(page:card.body是现成标本) #3809 lands, this local helper is what it replaces」)换用共享判据。isDormantOnThisPin反而保留读原始 listed 键,并写明为什么:dormancy 问的是「这个 release 认不认识这个键」,墓碑答是,所以覆盖退役键的豁免必须保持活着并被判 stale —— 用收窄集会给退役键的豁免一个永久藏身处,那是同一个盲点下沉一层。premise 断言(两向共钉)
以上全部成立的前提只有一条:退役保留成员。加了
the tombstone premise still holds — a retired key stays IN the shape:某个 covered block 仍然 listed 一个墓碑键;逐 block 断言 listed 严格多于 accepted;活键page:card.title作反向对照。上游哪天改成真删键,这条红,处置是删掉过滤器而不是继续过滤零个 —— 死码被判死,而不是静默失效。the eight tombstoned keys are recognised, not exempted — and not published either是收割留下的钉子:八个键各若干事实(仍 listed / 仍判为墓碑 / 反方向不再索要 / 无豁免 / 也没被发布),外加一条派生的完备性(covered block 上任何墓碑键都不得带豁免)。没有它,「重新写一条豁免」和「收窄失效」两种回退都不会在别处红。反向验证(先预判,后跑)
三个变异在两个 pin 上各跑一遍(rc.6 上 merge 前、GA 上 merge 后),结果逐字相同。变异均在 commit 之后进行、
git checkout还原(⛔ 未用 stash),还原后重跑全绿。① 把
bodyinput 重新声明回page:card(#4027 的历史状态)预判:正方向红。实测:
①b 保留该声明,同时把收窄退回原始 listed
预判:正方向转绿(假绿现场)。实测:该断言从失败列表消失 = 绿;同时 premise 断言红、反方向在五个 block 上红(GA 上 7 failed / 57 passed)。这是本卡假绿的活体标本。
② 把一个真墓碑(
record:details.layout)从识别里排除 —— pin 版有墓碑,所以走「实测路径」,不需要注入 fixture预判:反方向红索要它。实测:
③ 识别谓词恒 false
预判:premise 断言红。实测:
连带 15 红:layout 的探针 pin、judge 校准的两条单通道 fixture、反方向五个 block。
①的措辞值得记一笔:派发单把①写成「伪造墓碑键进接受集 → 正方向钉红」,但把墓碑当成接受只会让接受集变大,正方向只可能更绿。让正方向红的是「有 block 声明了墓碑键」,所以①按后者做;而「识别失效」这一支(=③)的真实方向是反方向与 premise 断言红,不是正方向。两个方向都如实记在这里,而不是把结果套进模板。
一次 CI 红及其原因(已修)
首轮 CI 全线在 15 秒内失败于
ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY。不是本改动的问题,而是基线过期:本分支起于 GA pin 之前,pull_request事件跑的是与新 main 的 merge 结果,而 pnpm-lock.yaml 的自动合并落在了一个 broken 状态(pnpm 的报错自己就点名了这个可能)。处置:merge origin/main(两次,期间又落了一个 dev-deps bump)+ 完整重解析,本地pnpm install --frozen-lockfile复现通过;packages/test-support的 spec devDependency 同时对齐仓库的^17.0.0。明确不在本卡
page:card.body→children的迁移(声明面已在 objectui 跟进(#5775):page:card 的 registry inputs 仍把已退役的body发布为设计器槽;page:section/footer/sidebar 三个注册项没有 inputs,设计器无法授权 children #4027 撤掉,渲染器的 back-compat 读故意保留 —— 存量文档要等 ADR-0087 D2 conversion 在装载时改写;back-compat 读不是编写面,所以不进inputs);packages/plugin-detail与packages/app-shell还各有一份本地墓碑判据,今天都是对的、都只有结构通道 —— 立 finding: 墓碑判据还剩两份本地副本(plugin-detail / app-shell),都只有结构通道,共享 judge 已就位 #4947 跟踪转换(observation-class,finding,未排队)。顺带一个给 triage 的信号(不改动、不立卡):GA pin 已经落地,而
UNPUBLISHED_EXEMPTIONS里那五条 GA-pending 条目的散文仍写着「the pinned 17.0.0-rc.6 does not declare it」,而 GA 已经声明了它们 —— 那正是 #4668 的范围(它自己写明「Declared by objectui#4668 on the GA pin」),现在已解阻。本 PR 未动它们:那五条在 GA 上仍是有效豁免(spec 声明、inputs 未发布),两道检查都绿。验证(GA pin 上)
changeset 按
ban-package-self-import.md的先例写成空 frontmatter(test/tooling-only,不发版)。🤖 Generated with Claude Code
https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt