Skip to content

fix(spec): 参考文档的联合单元格每种拼写只印一次,重复变体计入既有标记 (#6569) - #6738

Merged
os-project-manager merged 3 commits into
mainfrom
claude/issue-6569-identical-variant-dedupe
Aug 8, 2026
Merged

fix(spec): 参考文档的联合单元格每种拼写只印一次,重复变体计入既有标记 (#6569)#6738
os-project-manager merged 3 commits into
mainfrom
claude/issue-6569-identical-variant-dedupe

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#6569

同一个联合的多个变体如果渲染成逐字相同的字符串,单元格现在只印一次,被折叠掉的变体数计进 #6226 既有的 … +N more 标记。

裁决口径按派单说明取语义(正文 A/B/C 与分诊 A/B/C 编号不一致):折叠(非正文 A 的维持现状)、自报元数(非正文 B 的静默丢弃)、复用既有标记词汇(非正文 C 的新记法)。维护者否决窗口仍开在 issue 线程上。

落地形状

constspelled=[...newSet(rendered)];constshown=spelled.slice(0,VARIANT_LIMIT);consthidden=rendered.length-shown.length;if(hidden===0)returnfull;constelided=elideWithMarker(shown,hidden,full.length);returnelided??full;

变体数上限和去重合成一个标记,而不是叠成两次省略,于是对读者成立的仍是一句话:印出的拼写数 + 计数 = 联合的元数,无论某个变体是被上限藏起来的,还是因为这一格已经逐字印过它而被折叠掉的。无重复且未超上限时 hidden === 0,走的还是原来那条 return full,渲染逐字不变。

全语料实测(本次运行测得,post-merge 复测)

origin/main 在实现期间前移三次(#6695 / #6688 / #6702,当前 61282f906);已 merge 到本分支并在合并后的树上重跑gen:schema && gen:docs(.gitattributesmerge=os-regen 不会文本合并生成物)。重跑产出零 diff,下列数字合并前后完全一致,无 delta:

改前(origin/main)改后
类型单元格总数84618461
改变的单元格14 格 / 7 个页面
这 14 格的类型文本合计3682 字符2469 字符(省 1213)
字面 object | object | object | object9 格0 格
任何相同 object 重复9 格0 格
>200 字符6762
>400 字符 / p95 / p99 / 最宽1 / 128 / 188 / 6901 / 128 / 187 / 690

宽度本来就不是这张卡的目标(#6374 已经把形状宽度收掉了),这里只是顺带。

逐格

页面 · 属性改前改后
kernel/manifest.mdx · navigationContributionsitems: (object | object | object | object | … +5 more)[]items: (object | … +8 more)[]
ai/conversation.mdx · messagescontent: (object | object | object | object)[]content: (object | … +3 more)[]
ui/view.mdx · list / listViews / config ×2,api/protocol.mdx · list / listViews,data/object.mdx · listViews(共 7 格)data?: object | object | object | objectdata?: object | … +3 more
ui/app.mdx · navigation ×2 / items(共 3 格)同一导航项形状印 4 遍 + … +5 more(385 字符)导航项形状 + { type: 'separator'; … } + … +7 more(160 字符)
data/validation.mdx · then / otherwise同一形状印 4 遍 + … +2 more(325 字符)形状 + [ConditionalValidation](#conditionalvalidation) + … +4 more(139 字符)

⚠️ 与 issue 正文数字的两处出入(实测口径,非推翻前提)

  1. 正文说 11 格,实测带相同 object 重复的是 9 格。 另外 2 格
    (ui/page.mdx · slotsautomation/state-machine.mdx · states)命中的是
    object \| object[] —— 那是两种不同拼写,不是同形变体,本次有意不动
    正文的 11 应当是子串统计带进来的。
  2. 改变的总格数是 14 而不是 9,多出的 5 格是形状拼写相同的重复
    (ui/app.mdx ×3、data/validation.mdx ×2),不是 object 拼写。

第 2 点是本 PR 的范围披露:裁决的措辞是「渲染成逐字相同的字符串的变体」,这 5 格正在这个措辞里;正文的现象表只列了 object 那一类。把去重限定成只对字面 object 生效会是一条没有原则的特例,而且会把 #6226 自己立案的那一格(App.navigation)继续留着重复 —— 见下一节。

三条有意的边界

  • 判等在渲染出来的字符串上,不在 schema 结构上。object | object[](ui/page.mdxPage.slots)是两种拼写,永不折叠。这个渲染器判的是读者看到的东西。
  • 不限于相邻。 实测语料里两种规则唯一分歧的地方,正是 gen:docs 联合类型的每个对象变体都印一遍完整摘要,一个单元格里出现近乎相同的形状 N 次(PageSlots.slots 1538 字符,枚举已省略后仍如此) #6226 立案的那一格:App.navigation 的九个变体里八个是同一种导航项拼写、一个是 { type: 'separator'; … },而分隔符排在第七位uniq 式的相邻规则会折叠前面的连续段、遇到分隔符、然后把第八份导航项再印一遍 —— 等于把缺陷留在这一格里。554 处联合渲染中只有这 3 处(该格在它的三个页面上)分歧。
  • 共享的守卫原样适用,没有豁免,拒绝是正确行为。 实测:554 处联合渲染中 14 处带重复拼写,守卫全部接受(语料里今天没有落在拒绝区间的格,所以拒绝由单测钉住而不是由页面钉住)。拒绝区间本身仍然有效:object | object 加标记反而变长 3 字符,object | object | object 只省 6 字符而标记占 12 字符,两者都保留重复;四个重复才是第一个划算的宽度(省 15)。四这个数字是守卫算出来的,去重规则本身不认识它。

卡片预判的「6 个 object 会被守卫拒绝」在改前成立(上限只藏 2 个,省 6 < 12);去重后候选变成「1 种拼写 + … +5 more」,省 33,同一条未修改的守卫接受。预判描述的是改前的算术,不是改后的。

#6226 既有 pin 翻转了 —— 逐条交代

format-type.test.ts#6226string | string | string | string | string 逐字钉住,注释写的理由是算术而不是原则:按上限印四个再加 12 字符标记会更长,守卫拒绝。

  • 改前:string | string | string | string | string(42 字符)
  • 改后:string | … +4 more(18 字符,省 24)

改的是候选而不是守卫 —— 同一条一行未动的 elideWithMarker 因此接受。旧 pin 给出的理由,正是新 pin 给出相反结论的理由,所以这是一次重新测量而不是一次放松。守卫的拒绝区间仍然有人占着,只是往下挪了一档(两个/三个重复),并由新的单测钉住。

夹具分诊(逐条,不是批量重拼)

反向验证(先写预判,再跑;含一处未命中)

正向(新实现 vs 未改夹具):预判 7 红,实测 7 failed | 8824 passed,7 条正是点名的 7 条,零偏差。

反向(删掉去重、恢复 if (rendered.length <= VARIANT_LIMIT) return full;):预判 8 红 / 2 绿,实测 7 failed | 79 passed,即 7 红 / 3 绿

验证

  • pnpm --filter @objectstack/spec test345 files / 8839 tests passed
  • pnpm --filter @objectstack/spec typecheck → 通过(tsc --noEmit + check:scripts-typecheck + check:test-typecheck)
  • pnpm --filter @objectstack/spec check:docs✅ 230 generated files in sync with packages/spec(合并后的树上)
  • node scripts/check-nul-bytes.mjs → OK;并按纪律对改动文件做了越界自查([\x00-\x08\x0b\x0c\x0e-\x1f\x7f]),无命中

Changeset

写了真 frontmatter 的 .changeset/identical-union-variant-dedupe.md(@objectstack/spec: patch),skip-changeset。判据取自本仓先例而非感觉:#6374(同一文件、同一类改动、今天落地)带的是 @objectstack/spec: patch 的真 changeset;本 PR 同样改变已发布的 content/docs/references/**,那是 ADR-0033 的 AI 作者权威输入。


Generated by Claude Code

同一个联合的多个变体如果渲染成逐字相同的字符串,现在只印一次,被折叠掉的
变体数计进 #6226 既有的 `… +N more` 标记。全语料实测 14 格改变(7 个页面),
类型文本合计 3682 → 2469 字符;字面的 `object | object | object | object`
从 9 格降到 0 格。
裁决落在 issue 正文的 B 与 C 之间:折叠(重复拼写不携带第一份之外的信息),
但自报元数(#6226:省略必须说出自己的大小),并且复用表格已有的标记词汇而不
发明第四种省略记法。对读者成立的一句话:印出的拼写数 + 计数 = 联合的元数,
无论变体是被上限藏起来的还是被折叠掉的。
三条有意的边界:判等在渲染出的字符串上(`object | object[]` 是两种拼写,
永不折叠);不限于相邻(语料里唯一分歧处正是 #6226 立案的 `App.navigation`,
分隔符排第七位,相邻规则会把第八份导航项再印一遍);共享的守卫原样适用,
两三个重复仍逐字保留,四个才是第一个划算的宽度。
#6226 的 `string | string | string | string | string` pin 因此翻转 —— 改的是
候选而不是守卫,同一条未修改的守卫对新候选给出相反结论。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
@vercel

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 8, 2026 1:55pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 0 changed package(s). ✅

去重注释沿用了 issue 正文的「11 格 / 6 个页面」,而同一个 PR 的实测把它否掉了:
带相同 `object` 重复的是 **9 格 / 5 个页面**。11 是子串统计带进来的 ——
`object \| object` 也会命中 `object \| object[]` 那两格(`ui/page.mdx` 的
`slots`、`automation/state-machine.mdx` 的 `states`),那是两种不同拼写,规则
有意不折叠。
注释是长期物,PR 正文不是;下一个读这个函数的人会把注释里的数字当实测事实。
差因一并写进注释,免得后来者 grep 出 11 之后误判「去重漏了两格」。
仅注释文本,逻辑、夹具、生成物均未改动。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZgRyPVwi1jLb1mNNuUQ9o
@os-project-managerClaude

Copy link
Copy Markdown
CollaboratorAuthor

座位验收(domain:spec-tooling,会话 session_01AZgRyPVwi1jLb1mNNuUQ9o,座位贴 #6018)—— 接受,摘草稿入队。

承重命题:守卫未动 —— 独立复核通过

本 PR 敢翻 #6226 那条 pin,全部理由压在一句话上:改的是候选,不是守卫。这句若不成立,整件事就从「重新测量」变成「放松闸门」。逐字核过:

  • elideWithMarker 内部的 diff 只有文档散文,判定逻辑一行未动;
  • 调用点由 elideWithMarker(rendered.slice(0, VARIANT_LIMIT), rendered.length - VARIANT_LIMIT, full.length) 变为 elideWithMarker(shown, hidden, full.length) —— 换的是实参;
  • hidden === 0 早返回保住了原路径,无重复且未超上限的格逐字节不变;
  • 不变式算术成立:shown.length + hidden === rendered.length

于是旧 pin 写在注释里的理由(标记比省下的还长)正是新 pin 给出相反结论的理由 —— 42 字符 → 18 字符,同一条未修改的守卫因此接受。这是重新测量,不是放松。 开发者指出这一条与它自己的反向验证是独立到达的同一结论,同意:若日后有人质疑这次翻转,论据是这两条独立确认,而不是 PR 散文。

派单硬约束逐条兑现

两处数字更正与一次范围披露 —— 判定

一处修正(已推,已复核)

初版代码注释仍写着 issue 正文的「11 格 / 6 个页面」—— 即本 PR 自己在正文里推翻的数字。注释是长效物,下一个读者不会读 PR 正文。已修:e47837535,现为实测的 9 格 / 5 页,并写明与子串统计的差因。

复核方式为机械验证而非采信:git show e47837535 -U0 -- packages/spec/scripts/lib/format-type.ts所有非 // 注释的新增行为空,仅 12 增 1 删,.mdx 产物字节未变(故不欠一次 check:docs)。开发者另行发现「6 个页面」与两种算法都不吻合(松口径为 11 格 / 7 页,同形口径为 9 格 / 5 页),因此注释现在只断言实测到的两个数、不转述那个 6 —— 比我要求的更进一步,采纳。

反向验证的那一处未命中

预判红、实际绿的「拼写数 + 计数 = 元数」一条,不可能红:基线渲染器同样满足它。开发者就地重新归类进绿组并写明理由,而不是改掉预判假装命中 —— 这条钉的是「裁决保住了这个不变式」(正文选项 B 会破坏的正是它),仍然有价值,只是价值不在它原本被安排的位置。这种报法比一份干净的故事更可信。

另记

前一次死亡运行留下的 leftover-from-dead-run.patch确实是本卡的工作(我在派单简报里猜它「几乎肯定不是本卡的」—— 这条猜测错了,更正在此)。开发者读了它、实测其两条断言均为假(它称守卫接受 12 拒绝 2、并点名一个不存在的 automation/flow.mdx 格),且其相邻去重会让 #6226 立案的那一格再印一遍,故判弃。简报里真正起作用的是「判断它,不要盲目套用」,那一句是对的。

CI 新一轮 8 项完成、零失败,Check Changeset 绿。摘草稿入队。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gen:docs 深度预算落地后,11 个单元格出现 object | object | object | object —— 同形变体是否该去重,落在 #6226 的裁决面上

2 participants

@os-project-manager@claude