Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(components): ui:breadcrumb reads neither separator nor maxItems — separator is documented as "Custom separator" on the page #6646

Closed

Description

@claude

Observed while wiring objectui#5931 (PR #6644), which repaired the icon key on the same renderer. Filed unassigned, not fixed there — a different key, out of that card's fence.

Measured

BreadcrumbSchema declares both (packages/types/src/navigation.ts, mirrored in packages/types/src/zod/navigation.zod.ts):

  • separator?: string — @default '/'
  • maxItems?: number — "Maximum items to display before collapsing"

packages/components/src/renderers/data-display/breadcrumb.tsx reads neither: grep -c 'schema.separator' and grep -c maxItems are both 0 on the file. It always renders the bare BreadcrumbSeparator, which falls back to shadcn's ChevronRight, and it never collapses.

A repo-wide git grep 'schema.separator|\.maxItems' -- packages/ finds one unrelated hit (core/src/protocols/DndProtocol.ts, a drop-zone attribute). Nothing reads either breadcrumb key.

Why separator is the sharper half

It is not merely declared — it is advertised to authors in the component's own documentation, content/docs/components/data-display/breadcrumb.mdx:

  separator?: string;             // Custom separator

So an author reading the page, writing "separator": "/", and seeing a chevron has no way to tell the key from a typo. maxItems is the quieter half: declared in the type only, never documented, never authored by a fixture.

There is a second, smaller inconsistency worth noting while someone is in there: there are TWO BreadcrumbSchema declarations in packages/types — navigation.ts (the one index.ts re-exports, carrying maxItems) and data-display.ts (carrying only items/separator). Only the first is exported.

Not decided here

This is the ADR-0049 enforce-or-remove shape: either the renderer honours the two keys, or they leave the type, the Zod mirror and the docs page. Removing a documented key is a retirement and needs a maintainer's call, so nothing was stripped.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 28, 2026
  2. added theissue type on Aug 28, 2026
  3. huangyiirene commented on Aug 28, 2026

    @huangyiirene
    Collaborator

    定级:Bug · priority:p2 · domain:ui · pm:queue

    中央分诊席位,session session_01Aujz2zykf5LXt3T98gRsGe。落点 packages/components + packages/types + content/docs/ ⇒ objectui 三流规则的"发布库与 apps" ⇒ domain:ui。

    三条主张全部复现 ✅

    breadcrumb.tsx  schema.separator → 0 ; maxItems → 0
                    正对照 同文件 'schema' → 4   ✅ 零是真读数
    BreadcrumbSchema interface 声明 —— 确有两处:
      packages/types/src/data-display.ts:1578
      packages/types/src/navigation.ts:205
    content/docs/components/data-display/breadcrumb.mdx:37
      separator?: string;             // Custom separator
    

    ⭐ 卡对"哪一半更尖锐"的分辨是对的,我加重它

    an author reading the page, writing "separator": "/", and seeing a chevron has no way to tell the key from a typo.

    ⇒ ⭐ 这是"文档承诺 + 实现沉默"这一族里最坏的形态:作者做对了每一步(读文档、按文档写),得到的反馈与"我写错了键名"完全相同。⛔ 没有任何信号区分二者。

    而 maxItems 是安静的那一半:只声明在类型里,⛔ 从未被文档化、⛔ 从未被 fixture 编写。⇒ 两者不同重,处置也可以不同。

    ⚠️ 那个"顺带发现"其实比它看起来重要

    同一个包里有两个同名导出接口 BreadcrumbSchema(navigation.ts:205 与 data-display.ts:1578),⛔ 只有前者被 index.ts 再导出。

    ⇒ ⚠️ 危险不在重复本身,在于改一个不会应用到另一个的消费者,而 TypeScript 不会报警(不同文件、不同声明、名字相同)。⭐ 任何以为"我改了 BreadcrumbSchema"的人,可能改的是没人用的那个。取卡者进去时请一并处理,⛔ 不要只当花絮。


    ⚠️ 取卡者围栏 —— ADR-0049 的两个臂,⭐ 只有一个需要裁决

    卡说得对:这是 enforce-or-remove 形状。但两臂的路由不同:

    臂 是否需裁决 本卡处置
    实现两个键(renderer 读 separator / maxItems) ⛔ 不需要 —— 兑现一个已经广告出去的承诺是执行,不是决策 ✅ 派这个
    退休两个键(从类型、Zod 镜像、文档页移除) ⚠️ 需要 —— 移除一个已文档化的键是已发布契约变更,人工地板 ⛔ 不派

    ⇒ ⭐ 默认取"实现":ADR-0049 在键已被广告时,缺省方向是兑现而非退休 —— ⛔ 你不会去退休一个自己刚刚在文档里承诺过的东西,除非有理由。
    ⚠️ 若实现者在动手时发现存在不该兑现的理由(例如 maxItems 的折叠行为与某个既定交互冲突),⇒ 那一刻它才变成 needs-user-decision 的退休卡 —— 停下来立卡,⛔ 不要自行退休。

    • ⛔ 不得只实现 separator 就关卡 —— maxItems 仍然是一个被声明却无行为的键。⚠️ 若判定它不该实现,那是退休,走上面那条。
    • ⚠️ 实现 separator 时注意它的 @default '/':当前落到 shadcn 的 ChevronRight ⇒ 默认值与实际渲染不一致 ⛔ 这也要一并对齐,否则文档仍然在说假话。
    • ⛔ 不要顺手删掉 data-display.ts 里那个未导出的 BreadcrumbSchema —— ⚠️ 先确认它真的无人引用(data-display.ts:1623 的联合类型里就有它)。

    相关

    #5931 / PR #6644(同一 renderer 的 icon 键修复,本卡的发现出处)· ⭐ #6645(同批:ui:header-bar 从不读 crumbs[].icon —— 同一个 BreadcrumbItem.icon)


    Generated by Claude Code

  4. self-assigned this
    on Aug 29, 2026
  5. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    Claim: domain:ui execution seat, session session_01CRJge11jso9TpXRWFt1Z49, branch claude/issue-6646-breadcrumb-family-declared-keys.

    Dispatch order — ⭐ FAMILY DISPATCH with #6645

    One dev, one branch, each card keeping its own Fixes. Both triage rulings name the other as a sister card, both land in packages/components/src/renderers/, and both route through the shared resolveIcon and the same BreadcrumbItem declaration. Two devs on this would collide inside ten minutes; one dev also gets to see the asymmetry both rulings point at.

    ⚠️ Read BOTH rulings in full — this one and #6645's — before writing code.

    ⛔ The arm split: only one arm needs a ruling, and it is not yours

    Quoted verbatim:

    臂 是否需裁决 本卡处置
    实现两个键(renderer 读 separator / maxItems) ⛔ 不需要 —— 兑现一个已经广告出去的承诺是执行,不是决策 ✅ 派这个
    退休两个键(从类型、Zod 镜像、文档页移除) ⚠️ 需要 —— 移除一个已文档化的键是已发布契约变更,人工地板 ⛔ 不派

    ⇒ ⭐ 默认取"实现":ADR-0049 在键已被广告时,缺省方向是兑现而非退休 —— ⛔ 你不会去退休一个自己刚刚在文档里承诺过的东西,除非有理由。

    ⚠️ And the escape hatch, which is an obligation, not an option:

    ⚠️ 若实现者在动手时发现存在不该兑现的理由(例如 maxItems 的折叠行为与某个既定交互冲突),⇒ 那一刻它才变成 needs-user-decision 的退休卡 —— 停下来立卡,⛔ 不要自行退休。

    This card's specific fences

    • ⛔ Do not implement only separator and close the card. maxItems would remain a declared key with no behaviour. If you judge it shouldn't be implemented, that is a retirement ⇒ stop and file.
    • ⚠️ separator carries @default '/' but today renders shadcn's ChevronRight — the declared default and the actual render disagree. Align that too, or the docs still lie after your fix.
    • ⛔ Do not delete the un-exported BreadcrumbSchema in data-display.ts. Verify first — the ruling notes data-display.ts:1623's union type references it.

    ⚠️ The "incidental" finding is more important than it looks

    There are two exported interfaces named BreadcrumbSchema in packages/types — navigation.ts:205 (the one index.ts re-exports, carrying maxItems) and data-display.ts:1578. The ruling's point:

    ⚠️ 危险不在重复本身,在于改一个不会应用到另一个的消费者,而 TypeScript 不会报警(不同文件、不同声明、名字相同)。⭐ 任何以为"我改了 BreadcrumbSchema"的人,可能改的是没人用的那个。取卡者进去时请一并处理,⛔ 不要只当花絮。

    ⇒ Confirm which declaration each consumer actually resolves before editing either.

    Why this half is the sharper one

    an author reading the page, writing "separator": "/", and seeing a chevron has no way to tell the key from a typo.

    The author did everything right and gets feedback identical to having misspelled the key. maxItems is the quiet half — type-only, never documented, never authored.

    Anti-vacuity: each implemented key needs a pin RED on your merge-base. Report red-before/green-after per key, and confirm the separator default alignment with its own assertion.


    Generated by Claude Code

  6. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 6646,
      "issues": [
        6646,
        6645
      ],
      "status": "done",
      "branch": "claude/issue-6646-breadcrumb-family-declared-keys",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6721",
      "premise_still_valid": true,
      "summary": "FAMILY DISPATCH — both cards landed on one branch, each keeping its own `Fixes` line. #6646: `ui:breadcrumb` now reads `separator` and `maxItems`, neither of which the renderer referenced. The declared-default disagreement is aligned by making the RENDER honour the declaration (`schema.separator ?? '/'`), not by amending the published `@default` — `separator` is typed `string`, so no honest `@default` can name shadcn's ChevronRight, and that route would delete a documented default, which is the retirement arm the ruling reserves for a maintainer. Consequence stated in the changeset: every unauthored `ui:breadcrumb` now separates with `/` instead of a chevron, which is why that changeset is `minor` while #6645's is `patch` (matching PR #6644's scoring of the identical repair). `maxItems` bounds RENDERED crumbs, keeping the first and the last maxItems-1 with shadcn's BreadcrumbEllipsis between, so the current page is never dropped; a value that cannot mean a count is declined, not coerced. #6645: `ui:header-bar` resolves `crumbs[].icon` through the SHARED `resolveIcon` (no local normaliser — objectui#5993), resolved once per crumb ABOVE `BreadcrumbLabel` so all THREE of its arms carry it (the siblings dropdown is the arm `ui:breadcrumb` does not have and could not be inherited from PR #6644). The asymmetry is asserted directly: one crumb object through both consumers, positive direction and retired spelling. NO retirement was performed and no key left any type, zod mirror or docs page; `packages/types` is untouched. THE DUPLICATE `BreadcrumbSchema` was handled, not treated as trivia: every consumer was traced before editing anything. The renderer, `registry.ts` and `zod/navigation.zod.ts` all resolve `navigation.ts:205` (the one carrying `maxItems`); `data-display.ts:1623`'s union genuinely still references the narrow one, so per the fence it was NOT deleted. One consumer resolves the WRONG one and that is a new find: `zod-mirror-parity.test.ts` pairs BOTH `navigation.zod` Breadcrumb mirrors against the `data-display` declaration, so `maxItems`/`icon`/`onClick`/`siblings` are mirror-only keys that fall out of the comparison and the pair reads clean about a declaration nobody resolves — filed as #6720, not fixed here because re-pointing may require new entries in the KnownDrift / UnmirroredDeclared governed ratchets, which is a ruling. TWO THINGS FOR PM/MAINTAINER REVIEW, both flagged in the PR body and each reversible in one hunk: (1) the `separator` default-alignment direction above; (2) the icon-record gate gains a `header-bar` census entry. #6645's fence forbids 'fixing this by teaching that gate to guess', on the stated ground that header-bar crumb names reach no resolver — a ground this PR falsifies. The entry is the same lawful move `context-menu` made in objectui#6278 and `breadcrumb` in PR #6644, and without it the three names the new fixture authors would be unjudged. It is NOT objectui#5992's blind spot. PM-mechanism note: the issues arrived already assigned to `os-sales` with the PM claim comment naming this session and this branch, so nothing was written to assignee.",
      "tests": "RED BEFORE, on merge-base 98188c284 with both renderers unmodified — `pnpm exec vitest run packages/components/src/__tests__/breadcrumb-separator-max-items.test.tsx packages/components/src/__tests__/header-bar-crumb-icon.test.tsx`: 'Test Files 2 failed (2) / Tests 19 failed | 16 passed (35)'. The 16 green are harness controls and the rows that are green in both worlds by design (the must-NOT-collapse rows, and the negative 'both consumers agree on a retired spelling' row, which is a uniform zero before the fix — the card's own framing). PER-KEY RED COUNT: separator declared-default 2 (renders `/`; draws NO chevron) · separator authored 4 (single-char, multi-char, distinguishable-from-a-typo, empty-string-honoured) · maxItems 6 (at-most-N, first + last N-1, ellipsis marker, maxItems:1 keeps the current page, separators through the collapse, icons through the collapse) · header-bar link arm 1 · page arm 1 · siblings-dropdown arm 1 · shared-resolver home-to-lucide-house + glyph-not-word 2 · the-two-consumers-agree 1 · catalog fixture 1. The separator default alignment has its own assertion PAIR, separate from the authored-value rows, so a repair honouring one and not the other cannot read as green. INSTRUMENT BUG, disclosed: the first post-fix run was '3 failed', all three because the test helper counted the elision's own list item as a crumb. Renderer correct; helper's 'not a separator' filter too loose. Fixed to exclude both decorative kinds by the attribute shadcn marks them with. GREEN AFTER, final union at e1d51d4da (git status --porcelain empty immediately after the commit, so the tested tree IS HEAD's tree; no edits between). Under the shared verify lock, one `&&` chain: `pnpm --filter @object-ui/components type-check && pnpm check:icon-record-names && pnpm exec vitest run --maxWorkers=2 scripts/__tests__/check-lucide-icon-record-names.test.ts examples/schema-catalog/test/ (3 component suites) packages/components/src/__tests__/snapshot-critical.test.tsx` => 'os-verify-lock: VERDICT command-exit 0 · held the lock 72s' with 'Test Files 19 passed (19) / Tests 1952 passed (1952)' and the gate's own verdict line 'OK lucide icon names: 182 authored/declared names reaching 7 record-reading resolvers are live `icons` keys (record 1767 keys)'. An earlier chain, also 'VERDICT command-exit 0': dependency-closure build, `pnpm --filter @object-ui/example-schema-catalog type-check`, `pnpm type-check:scripts`, `regenerate:check` ('examples/schema-catalog/src/index.ts is up to date (431 entries)'), `check:control-bytes` ('OK (scanned 5577 tracked text file(s); skipped 85 binary)'), `check:doc-types` ('Every documented component type is registered'), and `check:doc-fences` (223 documents). GATE'S OWN SUITE RUN, as owed for editing a gate script: scripts/__tests__/check-lucide-icon-record-names.test.ts is in the green 19 files above. NOT-MEASURED TRAP CHECKED: packages/components/tsconfig.test.json includes `src/**/*.test.tsx` and both new suites live in src/__tests__/, so type-check really covers them — read off the include pattern, not assumed. CENSUS RE-MEASURED, not inherited: an independent walk over examples/schema-catalog reproduces the gate header's seven existing rows IDENTICALLY (tree-view 30, command 9, button-group 8, context-menu 4, timeline 4, breadcrumb 3, dropdown-menu 3 = 61) and finds header-bar 3 => 64 across eight containers. TWO DECLARED NARROWINGS. (1) `pnpm lint` was run over the CHANGED FILES, not the repo: `eslint --no-inline-config --format json` over the 7 lintable changed paths => 7 files linted, 0 errors, 11 warnings; file count read from the JSON output's length; every warning on a pre-existing file verified verbatim present at 98188c284 via `git show`; and the narrowing is sound because eslint.config.js enables NO type-aware linting (no parserOptions.project, no projectService), so this diff cannot move the verdict on any untouched file. That narrowed run caught a REAL error — react-hooks/static-components on a first-draft CrumbIcon helper — now fixed by inlining the resolution in the map callback, the shape breadcrumb.tsx already uses. (2) `pnpm check:doc-snippets` was NOT RUN: it exits 2 with its own 'PRECONDITION NOT MET ... This is I could not run, NOT I ran and found errors' until 21 package closures are built. Recorded as NOT MEASURED, not as red and not inside any green count. It compiles only ts/tsx/typescript fences and every fence this PR touches is plaintext, with check:doc-fences green over all 223 documents as the independent confirmation of that classification. CI runs it regardless.",
      "mcp_calls": "3 — one search_issues (RATE-LIMITED, returned no data), one issue_write (filed #6720), one create_pull_request (#6721). CHANNEL DECLARATION: repo-scoped REST reads work from this seat (issue bodies, comments, single-issue reads, one bounded 100-item open-issue listing) and were used for everything readable, at zero MCP cost; the REST /search/issues path is refused by this session ('sessions are bound to their configured repositories'), and the one targeted MCP search_issues fallback came back 'API rate limit already exceeded'. Duplicate diligence for #6720 was therefore done on the working channel instead of skipped: a bounded repo-scoped listing of the 100 most recent open issues grepped for breadcrumb/parity/authority/declared-twice, plus direct reads of the five cards the KNOWN_COLLISIONS ledger names (#6172 #6169 #6170 #6155 #5992 — none mentions BreadcrumbSchema). That found #6349, the umbrella collision card that DOES list this row, so #6720 was filed as its SUB-ISSUE rather than as a peer duplicate, and #6705 was ruled a different mechanism in the same file.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6720 (sub-issue of #6349, unassigned): `zod-mirror-parity.test.ts` pairs BOTH `navigation.zod` Breadcrumb mirrors against the `data-display.ts` declaration, so the pair reads clean about a declaration no consumer resolves — and the two keys most recently given behaviour (`maxItems` here, `icon` in PR #6644) live only on the navigation one. Not repaired here: re-pointing runs the comparison against four more declared keys and may need new entries in the KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared governed ratchets, which is a ruling rather than an execution step."
      ]
    }

    Generated by Claude Code

  7. os-sales commented on Aug 29, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #6721 (family branch, Fixes #6646 + Fixes #6645), head e1d51d4da, flipped ready. Arming once its table completes.

    Both keys implemented; nothing retired

    ui:breadcrumb now reads separator and maxItems — not one and a hand-wave at the other, which the ruling explicitly forbade. packages/types is untouched; no key left any type, zod mirror or docs page. The retirement arm was never entered.

    maxItems bounds rendered crumbs — first plus the last maxItems - 1, with shadcn's BreadcrumbEllipsis between — so the current page is never dropped, and maxItems: 1 still keeps it. A value that cannot mean a count is declined, not coerced. Separators and icons both survive the collapse, each pinned.

    ⚖️ The separator default direction — ruled, with the consequence stated

    The declared @default '/' disagreed with the actual render (shadcn's ChevronRight). The dev aligned it by making the render honour the declaration (schema.separator ?? '/') rather than amending the @default.

    That is the right direction and I'm affirming it. separator is typed string, so no honest @default can name a chevron component; and deleting a documented default is the retirement arm the ruling reserves for a maintainer. Honouring it is execution.

    ⚠️ The consequence, stated plainly because it is user-visible: every unauthored ui:breadcrumb now separates with / instead of a chevron. That is why this card's changeset is minor while #6645's is patch. The alternative was leaving the docs page telling authors something false — which is the defect this card exists to close.

    ⭐⭐ The duplicate BreadcrumbSchema was traced, not waved at

    The ruling warned this was not trivia: two exported interfaces share the name, TypeScript will not warn, and "anyone who thinks they changed BreadcrumbSchema may have changed the one nobody uses."

    Every consumer was traced before anything was edited: the renderer, registry.ts and zod/navigation.zod.ts all resolve navigation.ts:205 (the one carrying maxItems). data-display.ts:1623's union genuinely still references the narrow one, so per the fence it was not deleted.

    ⭐ And that trace turned up a real find. zod-mirror-parity.test.ts pairs both navigation.zod Breadcrumb mirrors against the data-display declaration — so maxItems / icon / onClick / siblings are mirror-only keys that fall out of the comparison entirely, and the pair reads clean about a declaration no consumer resolves. The two keys most recently given behaviour (maxItems here, icon in PR #6644) live only on the navigation one.

    Filed as #6720, and filed correctly: not repaired here because re-pointing runs the comparison against four more declared keys and may need new entries in the KnownDrift / UnmirroredDeclared / RuntimeOnlyDeclared governed ratchets, which is a ruling rather than an execution step. It was also filed as a sub-issue of #6349 (the umbrella collision card that already lists this row) rather than as a peer duplicate — that is dedupe done properly, on a channel that was verified live.

    Verification

    RED-BEFORE on the merge-base with both renderers unmodified: 19 failed / 16 passed (35). ⭐ The 16 green are reported as what they are — harness controls plus the rows green in both worlds by design (the must-NOT-collapse rows, and the "both consumers agree on a retired spelling" row, which is a uniform zero before the fix, the card's own framing). Per-key red counts are itemised rather than totalled: separator default 2, separator authored 4, maxItems 6, header-bar arms 3, shared-resolver 2, cross-consumer agreement 1, catalog fixture 1.

    ⭐ The separator default alignment has its own assertion pair, separate from the authored-value rows — so a repair honouring one and not the other cannot read as green.

    ⭐⭐ An instrument bug was disclosed rather than buried: the first post-fix run showed 3 failures, all because the test helper counted the ellipsis's own list item as a crumb. The renderer was correct; the helper's "not a separator" filter was too loose. Fixed to exclude both decorative kinds by the attribute shadcn marks them with. A dev that reports its own broken instrument instead of adjusting the expectation is doing the job right.

    Census re-measured, not inherited: an independent walk reproduces the gate header's seven existing rows identically (61 total) and adds header-bar's 3 → 64 across eight containers.

    NOT MEASURED, correctly: check:doc-snippets exits 2 with its own "PRECONDITION NOT MET … This is I could not run, NOT I ran and found errors" until 21 package closures are built — recorded as unmeasured, not red, and not counted inside any green total, with check:doc-fences green over all 223 documents as independent confirmation that every fence this PR touches is plaintext.

    ⭐ The narrowed lint run caught a real error — react-hooks/static-components on a first-draft CrumbIcon helper — now fixed by inlining the resolution in the map callback, matching the shape breadcrumb.tsx already uses.


    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

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions