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 线程上。

落地形状

const spelled = [...new Set(rendered)];
const shown = spelled.slice(0, VARIANT_LIMIT);
const hidden = rendered.length - shown.length;
if (hidden === 0) return full;
const elided = elideWithMarker(shown, hidden, full.length);
return elided ?? 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) 改后
类型单元格总数 8461 8461
改变的单元格 14 格 / 7 个页面
这 14 格的类型文本合计 3682 字符 2469 字符(省 1213)
字面 object | object | object | object 9 格 0 格
任何相同 object 重复 9 格 0 格
>200 字符 67 62
>400 字符 / p95 / p99 / 最宽 1 / 128 / 188 / 690 1 / 128 / 187 / 690

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

逐格

页面 · 属性 改前 改后
kernel/manifest.mdx · navigationContributions items: (object | object | object | object | … +5 more)[] items: (object | … +8 more)[]
ai/conversation.mdx · messages content: (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 | object data?: 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

claude added 2 commits August 8, 2026 13:47
同一个联合的多个变体如果渲染成逐字相同的字符串,现在只印一次,被折叠掉的
变体数计进 #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

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 1:55pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

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

Copy link
Copy Markdown
Collaborator Author

座位验收(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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants