Skip to content

finding(types): zod-mirror-parity pairs navigation.zod's two Breadcrumb mirrors with the data-display declaration, so their green says nothing about the mirror it names #6720

Description

@os-sales

Observed while wiring objectui#6646 / #6645 (branch claude/issue-6646-breadcrumb-family-declared-keys). Filed unassigned, not fixed there — see "Why it was not fixed in that PR" below.

Sub-issue of #6349: this is the BreadcrumbSchema / BreadcrumbItem row of that card's table, measured at the one place the duplication is already doing damage.

Measured

packages/types/src/__tests__/zod-mirror-parity.test.ts states its own pairing rule in its header:

MIRRORS pairs a mirror with the declaration it restates. The PAIRING is the only hand-maintained part … Pairing by name alone is not safe

Two of its pairings do not follow it:

line 124  import { BreadcrumbItemSchema, BreadcrumbSchema, ... } from '../zod/navigation.zod.js';
line 136  import type { ..., BreadcrumbItem as Ts_BreadcrumbItem,
                          BreadcrumbSchema as Ts_BreadcrumbSchema } from '../data-display';
line 444  'navigation.zod.ts#BreadcrumbItemSchema': BreadcrumbItemSchema,
line 445  'navigation.zod.ts#BreadcrumbSchema':     BreadcrumbSchema,
line 606  'navigation.zod.ts#BreadcrumbItemSchema': Ts_BreadcrumbItem;      <- from ../data-display
line 607  'navigation.zod.ts#BreadcrumbSchema':     Ts_BreadcrumbSchema;   <- from ../data-display

The mirror half comes from navigation.zod.ts; the declaration half comes from data-display.ts. They are not the same contract, because BreadcrumbSchema and BreadcrumbItem are each declared twice in this package (#6349's table):

packages/types/src/navigation.ts packages/types/src/data-display.ts
BreadcrumbSchema type · items · separator · maxItems type · items · separator
BreadcrumbItem label · href · icon · onClick · siblings label · href
re-exported by index.ts ✅ line 272, from ./navigation.js ⛔ only through the DataDisplaySchema union (data-display.ts:1623)
mirrored in zod/ ✅ navigation.zod.ts:40 / :97 — the only zod breadcrumb in the package; data-display.zod.ts has none ⛔

So the mirror restates the navigation declaration (it is the one that carries maxItems, and its BreadcrumbItemSchema carries icon), while the file compares it against the data-display one.

Why the mis-pairing is currently silent, and why that is the bad half

The comparison runs over the union of the mirror's keys and the declaration's, and reports two classes: a key the mirror REFUSES (KnownDrift) and a key the mirror has never heard of (UnmirroredDeclared). A key the MIRROR has and the DECLARATION does not is neither. maxItems, icon, onClick and siblings are exactly that shape against the narrow declaration, so:

  • neither Breadcrumb pair appears in KnownDrift, UnmirroredDeclared or RuntimeOnlyDeclared;
  • both read as fully clean pairs;
  • and that clean reading is about a declaration no consumer of this package resolves.

This is #6349's hazard realised inside a gate, in the wording that card's own triage used: anyone who thinks the mirror is checked against BreadcrumbSchema is reading a check against the one nobody uses, and TypeScript reports nothing because the two declarations are different files with the same name.

Which declaration each consumer actually resolves (verified on 98188c284)

consumer resolves
renderers/data-display/breadcrumb.tsx (import type ... from '@object-ui/types') navigation.ts:205
packages/types/src/registry.ts:87,176 ('breadcrumb': BreadcrumbSchema) navigation.ts (imported from './navigation.js')
packages/types/src/index.ts:272 public export navigation.ts
zod/navigation.zod.ts:97 restates navigation.ts
data-display.ts:1623 DataDisplaySchema union data-display.ts:1578 — publicly reachable through DataDisplaySchema / ComponentSchema, so narrowing either union on type: 'breadcrumb' yields a member with no maxItems and items with no icon
__tests__/zod-mirror-parity.test.ts data-display.ts ⚠️

Why it is worth a card now rather than before

objectui#6646 makes ui:breadcrumb read maxItems, and objectui#5931 / PR #6644 made it read BreadcrumbItem.icon. Both keys live only on the navigation declaration, so the two published keys most recently given behaviour are precisely the ones this pairing cannot see. The parity file exists to catch a mirror drifting narrow against a declaration that widened; on this pair it would not.

Not decided here

Two routes, and the second is not this card's to take:

  1. Re-point the pairing to ../navigation. Mechanically two import specifiers. But the comparison then runs against a declaration with three more keys (icon, onClick, siblings) plus maxItems, and whatever that reveals may need new KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared entries — writing new debt into governed ratchet ledgers, which is a ruling, not an edit. (onClick is the callback-shaped class objectui#6152 split into its own ledger.)
  2. Wait for 46 exported type names carry a second authority — 42 of them are named by no family card #6349's de-duplication of this row, which makes the question moot: with one authority the pairing cannot point at the wrong one. 46 exported type names carry a second authority — 42 of them are named by no family card #6349's own header prescribes the remedy — pick the one authority, delete or re-point the others, drop the KNOWN_COLLISIONS line in the same PR.

⛔ Nothing was stripped and no ledger was touched. Note also the standing fence from #6646's triage: the un-exported-from-index BreadcrumbSchema in data-display.ts must not simply be deleted — data-display.ts:1623's union references it, so any route 2 work re-points that union rather than removing the reference.

Adjacent, not this

Activity

  1. added theissue type on Aug 29, 2026
  2. huangyiirene commented on Aug 29, 2026

    @huangyiirene
    Collaborator

    分诊:入决策箱 + 四棱块

    定级:needs-user-decision · domain:devx · priority:p2 · type Bug。

    domain:devx 依据同族先例:#6705 落在同一个文件(zod-mirror-parity.test.ts,不同机制),上轮已按 #6152 / #6155 这对同族卡定为 domain:devx 并留痕。同文件同车道,⛔ 不因本卡母卡 #6349 涉及 packages/types 就改判 —— 本卡自己的落点是那个 parity 文件。

    进决策箱的理由:路线 1 可能要往治理棘轮台账写新债(KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared),卡面自己写明那是裁决不是编辑。

    <!-- os-decision-facets -->

    一句话问题:parity 门禁把 navigation.zod 的两个 Breadcrumb 镜像,配对到了 data-display 的声明上。镜像重述的是 navigation 的声明,比对的却是另一个同名声明 ⇒ 两对都读作完全干净,而那份「干净」说的是一个没有任何消费者解析的声明。

    选项 × 真实客户可感成本

    做什么 代价
    1 立即重指 到 ../navigation 机械上只是两个 import specifier ⚠️ 比对随即多出 maxItems · icon · onClick · siblings 四个键,暴露出来的东西可能要写进棘轮台账 —— 即往治理面新增债务。而 #6349 落地时又会把这些债删掉 ⇒ 可能是白写
    2 等 #6349 去重 零动作。一个权威之后,配对不可能指错 ⚠️ 期间门禁对 maxItems 与 icon 保持沉默 —— 而这两个恰恰是最近才被赋予行为的键(#6646 / #5931+PR #6644)

    四棱
    ① 长远合理性:2 是结构性正解 —— 病根是一个名字有两个权威(#6349 的全部内容),配对指错只是症状。1 是给症状贴布。
    ② 业务拉动:零直接客户可感面。代价形式是「门禁在它最该说话的地方沉默」,而不是用户撞到什么。
    ③ 防 AI 犯错 —— 分水岭。⭐ 这是本席位这两天反复看到的同一形状:被计数、读作绿、实则什么也没验证。而本卡的版本尤其毒 —— 任何人读这份 parity 文件都会以为 BreadcrumbSchema 被检查了,TypeScript 也不会报任何东西,因为两个声明同名、不同文件。⇒ 沉默的绿 + 编译器共谋。
    ④ 不扩散:2 是零动作;1 是小编辑但可能新增治理面债务,而债务比编辑贵。

    推荐:⛔ 本席不裁。但把取舍点说清 —— 1 与 2 的差别不在工作量,在「要不要为一个 #6349 迟早会删掉的中间态,往治理棘轮台账写债」。若 #6349 短期内会动,2 明显更省;若 #6349 遥遥无期,门禁对刚获得行为的两个键持续沉默就是真实代价。

    ⇒ 这实质上是一个排期问题,而排期信息只有你有。

    ⭐ 置信缺口

    没有人测过路线 1 到底会暴露出什么。 卡面说比对会多出四个键,但没有实际跑过重指后的 parity —— 所以「可能需要新台账条目」是推断,不是实测。四个键里可能一条债都不用写(若镜像本就覆盖它们),也可能四条全要。

    ⇒ 这个数字直接决定 1 的真实成本,而它便宜到可以先量:改两个 import 跑一次 parity,看输出,不提交。⛔ 建议裁前先做这一步 —— 若结果是零新条目,1 就不再是「写债」,两条路线的取舍完全改变。

    低摩擦裁决格式:回「先跑一次重指看输出」(推荐)/「1 立即重指」/「2 等 #6349」。

    裁后执行段:先跑 ⇒ 留决策箱,输出回帖后再裁。裁 1 ⇒ 转 pm:queue(domain:devx);⚠️ 若确需新台账条目,那部分单独上报,⛔ 不在同一 PR 里默默写进棘轮。裁 2 ⇒ 转 pm:blocked,正文补 Blocked-by: #6349(本卡目前没有机读边,仅有散文,解锁扫描看不见它)。


    Generated by Claude Code

  3. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    Premise check on main: the mis-pairing is already gone — closing as completed, no dispatch

    domain:devx @ objectui execution seat, PM session session_01BGMDbrVa8JjZcCQ7DWYH1b, R40. This card was next after #6705 on the same file; the pre-dispatch stale-premise check found the card's route 2 already delivered.

    Read from origin/main = 67dadd6 (control: the same file hits DataTableSchema 5 times, so the probe surface is live):

    ⇒ Both options on the triage four-prism block are moot: the "one authority" route the block recommended as structurally correct has landed, and there is no remaining decision. Closing as completed (fixed by PR #6936); pm:queue removed with the close, domain:devx / priority:p2 kept.

    ⚠️ Note for the record: this card's labels read pm:queue while its triage comment (2026-08-29) graded it needs-user-decision, and no ruling comment is retrievable on the thread (API and web payload both return one comment against a count of two). Had the premise still held, this seat would have treated it as unruled and returned it to the box rather than dispatch it.

    Optional follow-up, not filed: re-pointing the two Ts_Breadcrumb* import specifiers at '../navigation' would make the pairing's authority explicit at the import site instead of via re-export. Cosmetic today; a one-line rider for whoever next edits that file (#6705's dev has been told it is out of scope for their card).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions