fix(spec): 内联形状摘要只展开一层,四条下钻路径共用同一条深度预算 (#6374) - #6572
Conversation
`format-type.ts` 一直只展开一层 `{ … }` 形状,但这条预算写在键循环的三元
表达式里,于是只有直接对象子节点受它约束。数组元素、`Record` 的值、联合的
变体这三条路径都会重新进入对象分支而预算不在作用域内,单元格宽度于是等于
每层键数 × 变体数 × 每个形状宽度,一层一层乘上去。`ui/page.mdx` 的
`Page.slots` 因此是 1538 字符 —— 而且是在 #5340 的枚举省略已在该格生效
8 次之后的宽度。
新常量 `SHAPE_DEPTH_LIMIT` 把同一条预算移到对象分支本身,四条路径都要过它。
阈值 1 不是选出来的,是取回来的:它就是直接子节点路径上一直生效的那个值。
全语料实测(215 页 / 8499 个类型单元格),任何大于 1 的取值都比不改还差,
因为提高上限必然放松那条本来就是 1 的路径。
深度上限 | 改前 | 1 | 2 | 3 | 4
>200 字符 | 121 | 42 | 173 | 191 | 191
>400 字符 | 9 | 1 | 19 | 35 | 35
>900 字符 | 1 | 0 | 1 | 1 | 1
最宽 | 1538 | 656 | 1538 | 1538 | 1538
改后仅剩的 >400 单元格是 `PageComponent.type`(656),落在联合变体里的顶层
词表,#6225 有意不收,且不含任何嵌套形状 —— 形状深度带来的宽度已经从语料
里消失。
`content/docs/references/**` 55 页由 `gen:schema && gen:docs` 重生成,
无一处手改。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckNo hand-written docs reference the 0 changed package(s). ✅ |
|
PM 验收:通过,转 ready 并开启自动合并。( 边界:验收依据是 diff + 报告 + 已完成的检查;若 派单的裁决是「先量后裁」,三条方向都量了 —— 满足。 逐项复核: 1. 采纳的那条,阈值是「找回来」的而非选出来的。 渲染器一直有深度预算 = 1(键循环里那个三元式),但只作用在四条下钻路径的一条上;数组元素 / 2. 否决 D2 的理由是本轮最硬的一条推理:D2 的下界就是 D1。 预算 150/200 时它的宽度剖面与深度预算逐项相同(>200=42、>400=1、>900=0、max 656),因为每个超预算的格本来就回落到深度 1;再往上只会更宽。所以 D2 在目标上不可能赢过 D1,多出的字符只买到「在本来就有余量的格里多展开一层」。而代价是实测出来的:预算 150 时同一个 3. 我给的试金石按预言成立。 D3 对 4 个无联合的格逐字节未动(598→598、595→595、583→583、450→450)。更致命的是它按迭代顺序渲染: 4. 反向验证的形态是本轮最值得学的一条。 它没有硬套「全红」模板,而是先说明本单不该全红:这个修法是把既有规则统一化,所以钉「原本就存在的那条腿」的用例必须保持绿,否则它们钉的就不是统一性。预言 6 红 / 4 绿,实测恰好,且四条绿正是预言的四条。第 7 条红在块外、预言没点到 —— 它照实报告而不是塞进预言里。这与 #6497 的 dev 拒绝把误报下限凑进红集是同一种品质。 5. 两条既有用例的整体替换有据:#5340 那条钉的正是被移除的那条腿(261 成员枚举在两层形状之下),而 #5340 真正主张的是「省略沿包装器组合」—— 这一点仍然成立(预算挡的是重入对象分支,不是把 6. 上游未被关掉:深度预算位于两个既有省略之上,吸收了部分出现次数而非停用它们(枚举标记 178→156,变体标记 16→9),#6226 的旗舰 关于 #6569 的处理方式,我认可并想点名表扬:深度预算使全 object 的联合变体渲染成同一串(11 格,此前 0),它没有自行裁定,而是发了最保守的一支(保留 arity)、把它钉成一条测试、另立 #6569 交裁决 —— 于是改判 B/C 的代价恰好是一条测试。理由也是证据而非口味:仓库已经有一条 #6226 的活钉,断言 ㉕ 申报:报告明写「无扩面」,与 diff 一致(只动 Generated by Claude Code |
…ine-shape-depth-budget
`origin/main` 上有三个提交改了 schema 并各自重生成了 `content/docs/references/**`(#6512 i18n 标签契约、#6540 capability 注册、 #6526 ADR-0049 退役 sweep),与本分支的重生成在 13 个文件上相交。 `.gitattributes` 的 `merge=os-regen` 驱动按设计**没有做文本合并**,而是把这 13 个文件标记为「必须在合并后的树上重生成」—— 否则会落地 #6224 那种「零冲突 却陈旧」的组合。 本提交就是那次重生成:`gen:schema && gen:docs` 跑在合并后的树上,12 个文件 被修正,`check-regen-pending` 标记已清除。无一处手改 `.mdx`; `packages/spec/scripts/lib/format-type.ts` 与其测试**逐字未动**(main 上没有 任何提交碰过这两个文件)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
|
PM 追加验收(解冲突轮):通过,自动合并已重开。 放行条件是我在派单里设的那条 —— PR 正文断言的每个数字必须在合并树上重核,因为这单的全部论证就是它的测量。已满足,且做法正确: 冲突性质:纯机械,不是设计问题。 main 上碰过 一处值得全仓记住的现场:文本合并 exit 0、零冲突标记,而 守住的数字: 移动并已改正文的数字:语料 8499 → 8432 格(退役 sweep 删了 schema);改前 >400:9 → 13 —— 在飞期间 main 上新长出 4 个宽格( 它自己抓到的测量错误值得单独表扬:重扫时它发现自己的脚手架有个假象 —— 把深度覆盖设成 99 会连原有的三元式一起去掉,那不等于「不修」,于是基线被读成 >400=33 而真值是 13。它改用真正的 merge-base 文件重测方向三,并在全文改用真基线。它主动点名的理由是「未改正的那一行会夸大本修法的效果」 —— 一个只会让自己好看的错误,由它自己找出来并说破,这是本会话最硬的一次自查。 测试口径的变化也如实交代:全量 343/8832 → 342/8796,原因是退役 sweep 在 main 上删掉了 etl 的 spec 测试,不是本单丢了用例。 Generated by Claude Code |
Fixes #6374
content/docs/references/**里最宽的类型单元格是ui/page.mdx的Page.slots,1738 字符 —— 而且是在 #5340 的枚举省略已经在这一格生效 8 次之后的宽度。
本次修完,这一格是 122 字符。
机制:预算一直存在,只是只作用在四条下钻路径中的一条
format-type.ts一直只展开一层{ … }形状,再往下的对象打印object。但这条预算写在键循环的三元表达式里:
于是只有直接对象子节点受它约束。另外三条路径 —— 数组元素(
{ … }[])、Record的值(Record< string, { … } >)、联合的变体({ … } | { … }[])——都会重新进入对象分支,而预算不在作用域内。单元格宽度于是等于
「每层键数 × 变体数 × 每个形状宽度」,一层一层乘上去。
结果是:同一个形状在同一个阅读深度上,印全还是印
object,取决于作者有没有把它包在数组里 —— 这是关于 Zod 写法的事实,不是关于读者怎么读的事实,和 #6225
拆掉的那种不对称完全同类。
新常量
SHAPE_DEPTH_LIMIT把同一条预算移到对象分支本身,四条路径都要过它;depth走的是函数参数而不是TypeContext字段,因为深度是递归的事实、不是页面的事实(被替换的三元表达式也不需要
ctx)。派单要求的三向实测(合并后全语料 214 页 / 8432 个类型单元格)
三条候选方向各自重生成全语料再测量,不是纸上比较。基线(不改)是
>200 = 129,>400 = 13,>600 = 3,>900 = 1,p95 145,p99 233,最宽 1738,
总字符 274084。
方向一:深度预算(采用)
阈值 1 不是选出来的,是取回来的。 它就是直接子节点路径上一直生效的那个值;
任何大于 1 的取值都比不改还差 —— 提高上限必然放松那条本来就是 1 的路径,
所以 >200 的单元格从 129 涨到 176+,而旗舰样本一动不动。这里没有分布可扫、
没有阈值可调:1 是把现状统一化,2 及以上是打扮成预算的回退。
方向二:整格字符预算(否)
方向二的下界就是方向一。 预算收到 150/200 时,宽度剖面与深度预算逐项相同
(>200 = 51,>400 = 1,>900 = 0,最宽 656)—— 因为超预算的格最终都回落到深度 1。
预算放宽则严格更宽(总字符 257642 / 263431,对比方向一的 252087)。
也就是说:方向二在目标上赢不了方向一,多花的字符全部买在「还有余量的格里
多展开一层」上,而代价是渲染不再由节点本身决定。
实测到的代价,同一个
Expression形状在预算 150 下有三种拼法:{ dialect: …; source?: string; ast?: any; meta?: { rationale?: string; generatedBy?: string } }{ dialect: …; source?: string; ast?: any; meta?: object }object——Object.userActions决定它的是同一格里其它兄弟键有多宽。 给某个无关的兄弟键加一个字段,就能让三层
之外的另一个形状悄悄从「印全」变成
object。方向一下,渲染只是(节点,结构深度)的函数 —— 本地、确定、与邻居无关。
顺带:#6226 的维护者裁决已经否过「整格字符预算退化为
object」这一条,理由是信息损失更大。本 PR 的实测与那条裁决同向。
方向三:同形去重(否)
全语料:>200 从 129 只降到 123,>400 从 13 只降到 9,总字符
274084 → 270411(-1.3%)。对比方向一的 51 / 1 / 252087。
座位预判的「4 个无联合的单元格是试金石」实测成立 —— 四格逐字未动:
Manifest.capabilitiesPluginRegistryEntry.capabilitiesGetTranslationsResponse.translationsPluginSecurityManifest.permissions更要命的是它按遍历顺序说谎。
Page.slots在方向三下印成:header和actions是同一个类型,页面却把它们印成不同的东西 —— 只因为header先被遍历到。Object.userActions(edit印全、delete印object)、StateMachine.states(entry印全、exit印object)同样如此。对「AI 作者的权威输入」(ADR-0033)来说,这比宽单元格严重得多。
全部 13 个宽单元格逐格对照(合并后)
原立单时是 9 个;main 上的 schema 变更在本 PR 在飞期间又添了 4 个(表中标 🆕)。
方向一把除刻意排除的那一个之外的全部 12 个收进 200 字符以内。
Page.slotsPageComponent.typeStateMachine.statesManifest.capabilitiesPluginRegistryEntry.capabilitiesGetTranslationsResponse.translationsObject.userActionsConversationSession.messagesPluginSecurityManifest.permissionsManifest.navigationContributionsListView.userFiltersInterfacePageConfig.userFiltersPage.interfaceConfigPageComponent.type(656)按派单要求刻意不碰:它是落在联合变体里的顶层词表,#6225 有意只匹配「属性自己的类型节点就是词表」,且它不含任何嵌套形状。
改后仅剩的这一个 >400 单元格因此不是形状宽度 —— 形状深度带来的宽度已经从语料
里消失了。
三条不可让的约束
object不是截断:它对键什么都不声称,所以不像前缀那样会被误读成完整列表。它也不是这些表格里的新省略风格 —— 嵌套
形状本来就一直印
object(改前的Manifest.capabilities里就有protocol: object)。完整形状仍在原处:生成器为它出页时是它自己的
## Schema一节,任何情况下都在json-schema/里。保留,并在深度预算下继续正确工作(见下面 gen:docs 深度预算落地后,11 个单元格出现
object | object | object | object—— 同形变体是否该去重,落在 #6226 的裁决面上 #6569)。598/595/583/450 降到 98/98/145/106;方向三对它们完全无效。
#5340 / #6226 的标记都还活着
深度预算在它们上游,所以会吸收掉一部分出现位置,但两条都没有被关掉:
#6226 的旗舰样本
App.navigation在深度 0,逐字未变(377 字符)。测试
新增
formatType — one shape level, whichever way down (#6374),10 条:四条下钻路径各一条(旗舰
Page.slots真实节点、数组元素、Record值、联合变体)、「直接对象子节点本来就不透明」(被统一的那条既有规则)、「包装器不花预算,所以
格子顶层的
{ … }[]与Record< string, { … } >仍开一层」、「无键的Record不是形状」、「无
ctx也生效」、「本来就只有一层的格逐字未变」,以及同形变体的元数 pin(见下)。
空洞性守卫:旗舰那条断言
rendered.length === 122且not.toContain('Enum<')—— 预算一撤,这个节点会把同一份
PageComponent摘要印 8 遍,两条断言同时以一个数量级的差距失败。(该夹具是自带的真实节点快照,所以合并后依旧是 122 —— 而活的
Page.slots也仍是 122:深度预算把第一层以下全收掉了,main 新加的嵌套键因此够不到这一格的宽度。这正是这条修法的稳健性。)
反向验证 —— 方向在跑之前就写死了
撤销 = 删掉
depth >= SHAPE_DEPTH_LIMIT守卫并把三元表达式放回去。方向不是全红,而这正是本块的要点:本次修的是让一条既有规则统一,所以那些钉住
「本来就存在的那条限制」的用例必须在撤销下保持绿,否则它们钉的就不是一致性。
预测(写在运行之前,已落在测试块注释里):6 红 / 4 绿
object经由数组 /Record值 / 联合变体到达的每一条,加上 no-ctx那条。Record不是形状」。
实测:逐条吻合。 该块 6 红 4 绿,4 条绿的正是预测的那 4 条。
块外另有 1 红,是被重新指向的 #6226 夹具(见下),预测未涵盖它 —— 如实记录。
夹具分诊(2 条既有测试)
elides through array-of-object nesting:它的夹具ERROR_RESPONSE把 261 成员的词表放在两层形状之下,而那条路径正是本次关掉的。
按「整体替换」处置:gen:docs 内联形状里的长枚举不省略,单个类型单元格可达约 900 字符(BulkActionDef.params 实例) #5340 真正主张的是「省略能穿过摘要与词表之间的包装器」,
这一条仍然成立且仍值得钉 —— 预算挡的是重新进入对象分支,并没有切断
inShapeSummary与数组/记录的联系。改用「数组套枚举」的夹具;ERROR_RESPONSE本体则移到 gen:docs 第三种残留宽度:嵌套形状深度 ——
INLINE_KEY_LIMIT只管一层,摘要沿数组/Record/联合无预算下钻(Page.slots1538 字符) #6374 块里,现在钉的是预算本身。PageSlots.slots1538 字符,枚举已省略后仍如此) #6226 的caps a union nested inside a shape summary:原夹具是键下的 6 个对象变体,现在 6 个都印
object,而 6 个六字符拼写比替换其中两个的标记还窄,于是共享守卫拒绝加标记 —— 拒绝是对的,但这条用例就变成在断言守卫而不是
断言上限了。换成语料里仅剩的那个「嵌套联合仍宽到值得上限」的真实节点
(
Manifest.navigationContributions),断言… +5 more与 4 + 5 = 9。深度预算的后果之一:一个联合的多个变体如果都是对象,现在会渲染成同一个字符串,
于是出现
object | object | object | object。全语料 11 格,改前 0 格。本 PR 不折叠它们,三条理由:
PageSlots.slots1538 字符,枚举已省略后仍如此) #6226 的维护者裁决正是「省略必须自报数量」;
format-type.test.ts里 gen:docs 联合类型的每个对象变体都印一遍完整摘要,一个单元格里出现近乎相同的形状 N 次(PageSlots.slots1538 字符,枚举已省略后仍如此) #6226 的现存 pinstring | string | string | string | string逐字保留,理由就是「标记比省下的还长」。
object | object | object | object只是同一条既有行为遇上新拼写;PageSlots.slots1538 字符,枚举已省略后仍如此) #6226 对Manifest.navigationContributions那一格的渲染裁决 ——在维护者刚裁决过的面上单方面改口。
宽度上它无关紧要。行为已明确钉在
prints an identical variant once per variant一条里并注明是待裁决项;#6569 请维护者在 A(现状)/ B(折叠)/ C(折叠+自报元数)之间裁决,
裁决若选 B/C,改的是那一条 pin。
合并与数字复核
GitHub 报
mergeable_state: dirty。冲突是纯机械的,不涉及本 PR 的源改动。origin/main上有三个提交改了 schema 并各自重生成了content/docs/references/**,与本分支的重生成在 13 个
.mdx上相交:a36db28b7feat(spec,cli): i18n 标签契约 —— 内联 locale map 授权化 + filter-only tab 的翻译槽 #6512 i18n 标签契约(18 个.mdx)ae31a1912fix(spec,metadata-protocol): capability 补齐三处注册 —— 授权面不再接受任意 JSON #6540 capability 补齐三处注册(3 个.mdx)f549a0d4arefactor(spec)!: ADR-0049 退役 sweep —— server 运行期词表 / ViewProtocol / L2 ETL(#5295 #6239 #6414) #6526 ADR-0049 退役 sweep(7 个.mdx,并删除了automation/etl.mdx)main 上碰过
packages/spec/scripts/lib/format-type.ts的提交数:0。碰过
format-type.test.ts的:0。碰过本 PR changeset 的:0。所以这不是设计问题,是这条车道本就要串行的那一类碰撞。
.gitattributes的merge=os-regen驱动按设计没有做文本合并,而是把这 13 个文件标记为待重生成 —— 正是为了避免 #6224 那种「零冲突却落地陈旧组合」。
check-regen-pending随即报了这 13 个文件;在合并后的树上跑gen:schema && gen:docs修正了其中 12 个(第 13 个恰好已正确),标记清除。⛔ 无一处手改
.mdx。复核结论:修法对每一格的效果逐格未变,动的是问题本身的规模。
Page.slots被 #6512 加宽Page.slots改后PageComponent.type原 9 格的改后值全部逐字未变(122 / 656 / 197 / 98 / 98 / 145 / 93 / 153 / 106)。
三条方向的结论在合并后的树上全部复现,只有绝对数字随语料移动;上文所有表格
都已换成合并后的数字。
门禁(合并后的树上实测)
pnpm --filter @objectstack/spec check:generated—— 10 / 10 全绿pnpm --filter @objectstack/spec test—— 342 files / 8796 tests passedscripts/format-type.test.ts单跑 —— 78 passedtypecheck/check:scripts-typecheck—— 通过pnpm --filter @objectstack/docs build—— 全语料 MDX 站点构建通过eslint改动文件 —— exit 0;check:nul-bytes—— 通过check-regen-pending—— 标记已清除⛔
content/docs/references/**全部由gen:schema && gen:docs重生成,无一处手改;未触碰content/docs/releases/。🤖 Generated with Claude Code
https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o