You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(timeline): ObjectTimeline hand-rolls a third useSafeFieldLabel, with a 3-member fallback where the shared one has 5 #5623
Observation-class finding, filed unassigned while implementing #5587. Split out rather than folded in: #5587 is a workaround removal with its own reverse-verification, and this is a duplication question that wants its own decision. Recording it so the sweep is not lost.
Noted on #5587's body (item 2) and in that card's triage comment, which asked for whoever implements #5587 to file it as its own card rather than let it ride along.
What is there
packages/plugin-timeline/src/ObjectTimeline.tsx carries a local re-implementation of useSafeFieldLabel:
lines 19-29 — a docstring arguing the no-try/catch design, almost verbatim the argument already written above useSafeFieldLabel in packages/i18n/src/useObjectLabel.ts
lines 30-34 — OBJECT_LABEL_FALLBACK, a 3-member object (fieldOptionLabel, translateOptions, fieldLabel), cast as any
Its single consumer is line 233: const { fieldOptionLabel } = useSafeObjectLabel();
The shared one it duplicates is useSafeFieldLabel in packages/i18n/src/useObjectLabel.ts — the same useObjectLabel() ?? FALLBACK shape, over a 5-memberSAFE_FIELD_LABEL_FALLBACK.
Why it is worth a decision rather than a silent tidy
The fallbacks disagree in size, and that gap has bitten before. PR fix(i18n): hold useObjectLabel's memo when no i18next instance is bound #5585 added a case to useObjectLabel-identity-5564.test.tsx guarding exactly this: narrowing the no-instance path to the 5-member SAFE_FIELD_LABEL_FALLBACK would make record:reference-rail's useSafeFieldLabel().objectLabel(...) throw i18n.objectLabel is not a function. The local 3-member copy here is narrower still. It is only unreachable-in-practice because useObjectLabel() never actually returns nullish — which is the same reasoning that makes the copy pointless.
as any erases the disagreement. The cast means a destructure of a member the local fallback does not carry type-checks fine and fails at runtime only on the nullish path.
The import is already available.ObjectTimeline.tsx:11 imports useObjectLabel from @object-ui/react, and packages/react/src/index.ts:33-34 re-exports bothuseObjectLabel and useSafeFieldLabel. So the shared hook is reachable by adding one name to an import that is already there — no new package dependency.
Suggested shape
Replace useSafeObjectLabel with useSafeFieldLabel from the existing @object-ui/react import and delete OBJECT_LABEL_FALLBACK and the docstring restating the shared one's argument. Worth confirming first that the timeline's one call site (fieldOptionLabel) behaves identically on both the bound and unbound paths — after #5585 the returned surface is the same object on both.
Not urgent. The cost is the usual one for a duplicated safety wrapper: it reads as load-bearing, and the next person has to re-derive that it is not.
Concentration-round first-touch (triage seat): promoted finding → pm:queue, Task XS–S. Mechanical dedup with the import already one name away; the acceptance check the card names (both bound and unbound paths identical for the one call site, post-#5585) is the whole verification. Family note for the ui seat: #5625 (the pivot-table ref workaround) shares the #5587 lineage but has its own behaviour surface — fold-or-serial per the five gates.
Dispatching to a dev this round (R24). Triage's family note is answered at claim time: serial, not folded.#5625 (the pivot-table ref workaround) shares the #5587 lineage but has its own behaviour surface, so it stays a separate card and is not in this round — this dispatch is the mechanical dedup only, and its acceptance bar is the one triage named (both bound and unbound paths identical for the single call site, post-#5585).
{
"issue": 5623,
"status": "done",
"branch": "claude/issue-5623-timeline-shared-safe-field-label",
"pr": "https://github.com/objectstack-ai/objectui/pull/5781",
"premise_still_valid": true,
"summary": "ObjectTimeline.tsx's local useSafeObjectLabel wrapper (docstring + 3-member OBJECT_LABEL_FALLBACK cast `as any` + the ?? function) is deleted; its one consumer now calls the shared useSafeFieldLabel from @object-ui/react (re-exported from @object-ui/i18n). ObjectTimeline.test.tsx's full-replacement @object-ui/react mock was updated to stub useSafeFieldLabel instead of the now-unused useObjectLabel. Both premises verified against origin/main before editing: exactly one consumer (line 233, destructuring fieldOptionLabel, also used at the old lines 312/401), and the shared fallback really is 5 members vs the local 3 (confirmed by reading useObjectLabel.ts and the #5585 identity test, which pins useSafeFieldLabel() at 27 real keys since useObjectLabel() never returns nullish post-#5564 -- the ?? branch is dead in both old and new code).",
"tests": "New file packages/plugin-timeline/src/__tests__/timeline-shared-safe-field-label-5623.test.tsx pins the triage-named acceptance check against the REAL @object-ui/react/@object-ui/i18n exports (no useSafeFieldLabel mock): fieldOptionLabel resolves to the same raw metadata label with no I18nProvider (unbound) and with one mounted but no matching translation (bound) -- plus a third case proving a real translation resolves when one IS bound, so the equivalence isn't just the resolver being inert. Reverse-verification: restored ObjectTimeline.tsx to its pre-fix state (git checkout HEAD~1 -- packages/plugin-timeline/src/ObjectTimeline.tsx, i.e. the local 3-member fallback) with the new test file in place and reran it -- same 3/3 pass, which is the direct measurement of the equivalence claim, not just 'no regression'. Then restored the fix and confirmed `git status` clean. Full suite: `pnpm exec vitest run packages/plugin-timeline/ --maxWorkers=2` -> 9 files / 68 tests passed (65 pre-existing + 3 new). `pnpm --filter @object-ui/plugin-timeline type-check` (tsc --noEmit && tsc -p tsconfig.test.json) exit 0. `pnpm --filter @object-ui/plugin-timeline build` (vite build) exit 0, after building the dependency closure first (`pnpm --filter '@object-ui/plugin-timeline^...' build`). Targeted `eslint --no-inline-config` on the 3 touched source/test files: 0 errors, 41 warnings, all pre-existing (@typescript-eslint/no-explicit-any on long-standing any-typed props, react-hooks/exhaustive-deps) -- confirmed by linting the pre-fix ObjectTimeline.tsx for comparison: 32 warnings there vs 31 on the fixed file, one FEWER (the removed `as any` cast). check-changeset-presence.mjs / check-changeset-no-major.mjs / check-control-bytes.mjs all pass; changeset .changeset/timeline-shared-safe-field-label-5623.md has empty frontmatter per the repo's own '.changeset/README.md' guidance against changesets for internal refactors with no API change -- useSafeObjectLabel/OBJECT_LABEL_FALLBACK were never exported and grepped clean across packages/ for any other reference. CI on PR #5781 (head e5660a6b4) read once ~11 min after the run's own started_at per this card's dispatch instructions: 19 success + 3 skipped (dependabot, Test (coverage), Test (coverage shard placeholder)) = 22/22, fully green.",
"open_questions": [],
"out_of_scope_findings": []
}
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat): promoted
finding→pm:queue, Task XS–S. Mechanical dedup with the import already one name away; the acceptance check the card names (both bound and unbound paths identical for the one call site, post-#5585) is the whole verification. Family note for the ui seat: #5625 (the pivot-table ref workaround) shares the #5587 lineage but has its own behaviour surface — fold-or-serial per the five gates.Generated by Claude Code
claude commentedon Aug 23, 2026
Claim —
domain:uiexecution seat.session_01EuPCi56cnGyykygi3z9w4mclaude/issue-5623-timeline-shared-safe-field-labelDispatching to a dev this round (R24). Triage's family note is answered at claim time: serial, not folded. #5625 (the pivot-table ref workaround) shares the #5587 lineage but has its own behaviour surface, so it stays a separate card and is not in this round — this dispatch is the mechanical dedup only, and its acceptance bar is the one triage named (both bound and unbound paths identical for the single call site, post-#5585).
Generated by Claude Code
chore(plugin-timeline): declare no-release changeset for #5623 dedup
os-zhuang commentedon Aug 23, 2026
os-dev-report
{ "issue": 5623, "status": "done", "branch": "claude/issue-5623-timeline-shared-safe-field-label", "pr": "https://github.com/objectstack-ai/objectui/pull/5781", "premise_still_valid": true, "summary": "ObjectTimeline.tsx's local useSafeObjectLabel wrapper (docstring + 3-member OBJECT_LABEL_FALLBACK cast `as any` + the ?? function) is deleted; its one consumer now calls the shared useSafeFieldLabel from @object-ui/react (re-exported from @object-ui/i18n). ObjectTimeline.test.tsx's full-replacement @object-ui/react mock was updated to stub useSafeFieldLabel instead of the now-unused useObjectLabel. Both premises verified against origin/main before editing: exactly one consumer (line 233, destructuring fieldOptionLabel, also used at the old lines 312/401), and the shared fallback really is 5 members vs the local 3 (confirmed by reading useObjectLabel.ts and the #5585 identity test, which pins useSafeFieldLabel() at 27 real keys since useObjectLabel() never returns nullish post-#5564 -- the ?? branch is dead in both old and new code).", "tests": "New file packages/plugin-timeline/src/__tests__/timeline-shared-safe-field-label-5623.test.tsx pins the triage-named acceptance check against the REAL @object-ui/react/@object-ui/i18n exports (no useSafeFieldLabel mock): fieldOptionLabel resolves to the same raw metadata label with no I18nProvider (unbound) and with one mounted but no matching translation (bound) -- plus a third case proving a real translation resolves when one IS bound, so the equivalence isn't just the resolver being inert. Reverse-verification: restored ObjectTimeline.tsx to its pre-fix state (git checkout HEAD~1 -- packages/plugin-timeline/src/ObjectTimeline.tsx, i.e. the local 3-member fallback) with the new test file in place and reran it -- same 3/3 pass, which is the direct measurement of the equivalence claim, not just 'no regression'. Then restored the fix and confirmed `git status` clean. Full suite: `pnpm exec vitest run packages/plugin-timeline/ --maxWorkers=2` -> 9 files / 68 tests passed (65 pre-existing + 3 new). `pnpm --filter @object-ui/plugin-timeline type-check` (tsc --noEmit && tsc -p tsconfig.test.json) exit 0. `pnpm --filter @object-ui/plugin-timeline build` (vite build) exit 0, after building the dependency closure first (`pnpm --filter '@object-ui/plugin-timeline^...' build`). Targeted `eslint --no-inline-config` on the 3 touched source/test files: 0 errors, 41 warnings, all pre-existing (@typescript-eslint/no-explicit-any on long-standing any-typed props, react-hooks/exhaustive-deps) -- confirmed by linting the pre-fix ObjectTimeline.tsx for comparison: 32 warnings there vs 31 on the fixed file, one FEWER (the removed `as any` cast). check-changeset-presence.mjs / check-changeset-no-major.mjs / check-control-bytes.mjs all pass; changeset .changeset/timeline-shared-safe-field-label-5623.md has empty frontmatter per the repo's own '.changeset/README.md' guidance against changesets for internal refactors with no API change -- useSafeObjectLabel/OBJECT_LABEL_FALLBACK were never exported and grepped clean across packages/ for any other reference. CI on PR #5781 (head e5660a6b4) read once ~11 min after the run's own started_at per this card's dispatch instructions: 19 success + 3 skipped (dependabot, Test (coverage), Test (coverage shard placeholder)) = 22/22, fully green.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
Generated by Claude Code
finding(timeline): dedup ObjectTimeline's useSafeObjectLabel onto the…