Skip to content

fix(app-shell): Recently Accessed stores identity and labels each item at render; an unchanged list writes nothing (objectui#11678) - #11713

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11678-recent-items-identity
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11678-recent-items-identity

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11678
Clause-②: yes (narrowing)

"Recently Accessed" stored a label the route tracker made from the route's machine name at visit time (titleize: the Delivery Operations dashboard was listed as "Showcase Ops Dashboard"), froze it in the visit's language, and saved the list again on every visit to the item already at its head. This PR makes a recent entry store identity, labels it when it is rendered, and makes "no change, no write" a property of the store.

What changed

Identity, not text. useTrackRouteAsRecent records an object, dashboard, page or report as { id, type, name, href }; titleize is gone. RecentItem is now RecentNamedItem | RecentTextItem: the four named kinds carry name and no label; record and metadata entries keep a label (a record's title, which is data and has no metadata label to resolve; a Studio metadata item's name). The kinds are declared once as RecentItemType.

Labelled at render, by the resolvers the console already has. New useRecentItemLabel() holds no labelling rule of its own: an object or a dashboard asks useNavTargetLabel (what the sidebar shows for an unlabelled nav entry opening the same target); a page or a report reads its cached metadata label through useObjectLabel's pageLabel / reportLabel, using the same two helpers useNavTargetLabel uses (metadataText, findByName, now exported from that module). No label anywhere: the machine name, which is the navigation renderer's own backstop. Every surface that rendered the stored label now asks it: the /home rail (HomeContinue, which HomePage hands the resolver as labelOf, the way it hands t), the RecentApps cards, the sidebar's Recent group, Studio home, and the command palette's recent records. useRecentItemLabel is exported from the entry from its own module; the useRecentItems shim stays hooks and types only.

No change, no write, at the store. RecentItemsProvider decides writes from a ref outside any state updater (the old code wrote inside a setState updater, which React may run more than once), and commit refuses a list equal to the one it holds: no state change, no localStorage write, no adapter.save(), so no sys_user_preference PATCH. addRecentItem is a no-op when the head entry already has the same content, so a revisit no longer stamps a fresher visitedAt (the head's visitedAt is when it became the most recent entry). A head whose content changed, such as a record title that resolved after the first add, is still written.

Lists already stored. Legacy entries carry their identity in id (TYPE:NAME, the only id the tracker ever wrote for the four named kinds), so the reader takes the name from there and drops the minted label; it is not used as a fallback. Reading a legacy list writes nothing back; the next real change writes the identity shape.

Gate entry repointed. check:i18n-keys read the home.recentApps.itemType.* members off the type field of interface RecentItem, which is now a union it cannot parse; its family entry in scripts/check-i18n-call-site-keys.mjs now names the RecentItemType union (114 member keys checked exactly, the same six recent kinds as before).

Measured before building (the order's Zone 2)

  1. Confirmed on da35453: the tracker minted the label with titleize at visit time, the provider persisted it, RecentApps rendered the stored string. Probe reading of a dashboard visit's saved row at the base: "label":"Showcase Ops Dashboard"; at this head: "type":"dashboard","name":"showcase_ops_dashboard" and no label.
  2. The double write. A jsdom probe (real RecentItemsProvider, real tracker, real createObjectStackUserStateAdapter over a counting data source; not committed) counted update calls, base vs head:
scenario base head
one visit 1 1
leave, then come back to the item at the head 2 1
an object's list route, then one of its view routes 2 1
the tracker remounted on the same route 2 1
one visit under StrictMode 1 1
appName resolving after the pathname 1 1

StrictMode is not the source. Every re-report of the identity already at the head wrote again at the base, and the store now refuses all of them. NOT MEASURED: which navigation produced the second PATCH in the triage's live session — this container has no backend to run (the sibling objectstack checkout is neither installed nor built), so no browser reading was taken.

  1. Legacy entries: the order assumed the reader would fall back to the stored text; measured instead that the identity is in id, so no fallback to the minted text is needed (pinned).
  2. Resolvers reused, as above; no second labelling rule.
  3. Built declarations (packages/app-shell/dist/context/RecentItemsProvider.d.ts): RecentItem went from one interface with a required label to the union above; addRecentItem takes RecentItemInput. Six new entry exports: useRecentItemLabel, RecentItemLabelResolver, RecentItemInput, RecentItemType, RecentNamedItem, RecentTextItem. Measured arm: yes (narrowing) — the entry widens, and addRecentItem refuses the label-carrying shape it took for the four named kinds. Reverse check against the built .d.ts: a probe compiled with tsc refuses a label on a dashboard input (TS2353), refuses the old input with no name (TS2353), and refuses item.label on an unnarrowed entry (TS2339); the identity-shape and record controls compile.

Files beyond the claim's surface

The claim named RecentApps as the renderer; /home actually renders HomeContinue, and RecentApps is rendered by no page today. Four more render sites read the stored label and had to move with the shape, or they would have rendered blank: console/home/HomeRail.tsx, layout/UnifiedSidebar.tsx, views/metadata-admin/StudioHomePage.tsx, chrome/CommandPalette.tsx. Also: hooks/useRecentItemLabel.ts (new), hooks/useNavTargetLabel.ts (two helpers exported), index.ts (exports), console/home/HomePage.tsx (passes the resolver to the rail; patch round 1), the package README and content/docs/guide/user-state-persistence.md (AGENTS.md rule 2, docs-driven), scripts/check-i18n-call-site-keys.mjs (the family entry above), and two existing test fixtures whose entries were label-carrying (HomeItemTypeLabel.parity.test.tsx, StudioHomePage.displayLocale-10232.test.tsx). No serial-constraint check was run on these by this dev; that read is the seat's.

Behaviour to know

  • Home now reads the dashboard, page and report metadata lists lazily when the recent list holds such an entry: one cached list request per type, as the sidebar already does. Before a type's list arrives the entry shows its machine name.
  • In the showcase today, the dashboard and the two pages carry no zh-CN label translation (the showcase bundle translates their nav entries, not the items), so under 中文 they show their own English labels, which is what their own pages show.

Patch round 1: the import graph (CI run 37455366245 on ec51ed9)

Five RecordDetailView.* suites failed with ReferenceError: Cannot access 'authFetchSpy' before initialization. The useRecentItems shim re-exported useRecentItemLabel, which reaches MetadataProvider and through it metadataClientFactory, whose module load calls createAuthenticatedFetch(); so RecordDetailView, which only wants the list, newly loaded that call before those suites' @object-ui/auth mock spy was initialised. Reproduced locally at ec51ed9: 5 files failed, the same ReferenceError five times.

  • The shim is hooks and types again; the entry exports useRecentItemLabel and RecentItemLabelResolver from ./hooks/useRecentItemLabel.js.
  • HomeContinue takes the resolver as a labelOf prop from HomePage, so HomeRail.tsx, which also holds the action center and the activity card, does not load the metadata cache either. HomePage already loaded it on main.
  • Measured with a static runtime-import walk over packages/app-shell/src (type-only imports and dynamic import() excluded), main (abd374b) against the merged head: RecordDetailView, ObjectView, the five suites and HomeRail.tsx reached MetadataProvider before the fix and do not after it. What reaches it newly is only the resolver itself, the two surfaces that render with it (RecentApps, StudioHomePage, the latter already loading metadataClientFactory on main), Studio's lazily imported barrel, and their own tests. Controls: UnifiedSidebar and HomePage reach it in every tree; RecordDetailView does not on main and did before the fix, which is the CI reading.
  • ⛔ No suite or mock was edited to get green, none is skipped.
  • main (abd374b) is merged in with a merge commit; no rebase, no force-push.

Tests

At head 0c8198b, through the shared verify lock:

  • The five suites: red at ec51ed9 (5 files failed, ReferenceError ×5), green at 0c8198b (5 files, 24 tests passed).
  • The whole app-shell suite, pnpm exec vitest run packages/app-shell/: exit 0, 1031 files passed and 1 skipped (1032), 10165 tests passed and 9 skipped. It covers every file that references authFetchSpy or mocks @object-ui/auth there (216) and round one's 34-file union.
  • New pins: RecentApps.labelAtRender-11678.test.tsx (the three measured pages in en, then zh-CN over the same stored entries, on both the /home rail wired as HomePage wires it and the RecentApps cards — served dashboard label, client bundle page label, and an untranslated page showing its own label; a legacy list shows current labels, never the minted text; an unloaded type shows the machine name); useTrackRouteAsRecent.test.tsx "revisiting the most recent item makes no write", list route then view route saved once, and a control that does write; RecentItemsProvider.test.tsx head re-add writes nothing, clearing an empty list writes nothing, a changed head is written, and the legacy read from localStorage and from the adapter.
  • Ablations (round one, at ec51ed9; the store and resolver code they mutate did not change this round), each with the fix committed first, the mutation proven on disk and restored to the HEAD blob with an empty git diff HEAD (ablation-replace.mjs): removing the head no-op turned exactly the three head-revisit pins red; removing the store's equality guard turned exactly the empty-clear pin red (the head pins stay green, held by the head no-op, as predicted); short-circuiting the page label to the machine name turned both label pins red.
  • pnpm --filter @object-ui/app-shell type-check (src and tsconfig.test.json, which includes every test file), build (dist completeness verified) and lint (0 errors): exit 0 each. The built entry exports useRecentItemLabel from ./hooks/useRecentItemLabel.js, and the shim's declaration no longer names it. Reverse type check against the rebuilt .d.ts: the same three refusals (TS2353, TS2353, TS2339), controls compile.
  • The edited gate script's own suites (round one, at ec51ed9; the script is unchanged since): 8 files, 493 tests passed.

Gates (not under the lock, head 0c8198b)

check-i18n-call-site-keys 0 · check-readme-exports 0 · check-doc-snippet-types 0 (after building its --build-filter closure) · check-changeset-presence 0 · check-changeset-no-major 0 · check-changeset-overwrite 0 · check-changeset-claims 0 (report-only; it names .changeset/11659-console-record-ux.md, whose RecoveryPasswordReminder paragraph names HomePage.tsx — still true against this diff, which leaves the reminder's import and its two render sites untouched) · check-pending-changeset-literals 0 · check-new-cross-file-line-citations 0 (0 new) · check-control-bytes 0 · check-doc-links 0 · check-doc-component-types 0 · check-doc-fence-languages 0 · check-doc-example-ids 0 · check-vi-mock-specifiers 0 · check-vi-mock-inherit 0 · check-unreferenced-sources 0 · check-test-path-roots 0 · check-spec-symbol-derivation 0 · check-package-self-import 0 · check-phantom-dependencies 0. Repo-wide lint is CI's. @object-ui/console type-check not run: no console source names a recent-items symbol.

Acceptance notes

  • Cold-landing race, untouched here: a visit recorded before the adapter's load() resolves is overwritten by hydration (probe: 0 writes, entry gone, at the base and at this head alike). Measured only in jsdom, not at a console door; carrier: none.
  • Dashboard label precedence: useNavTargetLabel (and so this list and the sidebar) runs a served dashboard label through the client bundle, while the dashboard page header draws the served label as given (objectui#11295). They differ only when a dashboard has both a published label edit and a bundle key. Read-only inference, not measured; carrier: none.
  • RecentItemsProvider still keys its debounced flusher effect on a useMemo identity (AGENTS.md rule 10); pre-existing and not changed here.

Generated by Claude Code

claude added 3 commits October 6, 2026 10:37
…r; an unchanged list is not rewritten (objectui#11678)

An object, dashboard, page or report entry in "Recently Accessed" now
stores its type and name only. The route tracker no longer mints a label
with titleize ("Showcase Ops Dashboard" for a dashboard labelled
"Delivery Operations"); every surface that renders an entry (Home cards,
Home rail, sidebar Recent, Studio home, the command palette) asks
useRecentItemLabel, which reuses the resolver the console already has for
that kind, in the current language.

The provider decides writes outside state updaters and refuses to write a
list equal to the one it holds: re-adding the entry already at the head is
a no-op (no fresher visitedAt), so revisiting the most recent item no
longer PATCHes sys_user_preference. Lists stored in the old shape are read
by identity (the name is in the id) and their minted label is dropped.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
… narrow contentOf by switch (objectui#11678)

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
…amily stays checked (objectui#11678)

check:i18n-keys reads the home.recentApps.itemType.* members off the
`type` field of `interface RecentItem`; RecentItem is now a union of two
entry shapes, which that reader cannot parse. The kinds are declared once
as RecentItemType, both shapes take their arms from it, and the family
entry points at it.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests package: app-shell labels Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 332 chunks) 3323.9 KB 3330.4 KB
Main entry chunk (gzip) 153.5 KB 350 KB
Entry file index-CY5H_WZE.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.24KB 6.38KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.83KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ec51ed9d6850daaecc96d0a64e882f1759b9c671
Local-runs: none

Inputs: card #11678 (body, claim 6013942564, os-dev-report 6015145317, claim amendment 6015203262, ACCEPT 6015212592), PR #11713 (body, 21-file list, net diff against main at da35453), and the 42 check-runs on the head. Unchanged lines around the diff were read from the fetched objects (git show on a pinned ref); nothing was checked out, built, run or re-run.

① Derived judgments

Public surface of @object-ui/app-shell is exports["."] → dist/index.d.ts ← src/index.ts; the recent-items names reach it through hooks/useRecentItems.ts.

  1. RecentItem becomes RecentNamedItem | RecentTextItem — right, and a narrowing. The four named kinds (object, dashboard, page, report) carry name and no label; record and metadata keep label. A consumer reading item.label off an unnarrowed entry no longer compiles (TS2339, the dev's reverse check against the built declarations). The changeset's Breaking paragraph says so.
  2. addRecentItem takes RecentItemInput — right, a narrowing of the accept set: a label-carrying input for a named kind, and a named input without name, are refused at compile time (TS2353). The two in-repo callers compile: the tracker (identity shape) and views/RecordDetailView.tsx :667 (type: 'record' with label: recordTitle, a text kind).
  3. Six new entry exports — right, a widening: useRecentItemLabel (value) and the types RecentItemLabelResolver, RecentItemInput, RecentItemType, RecentNamedItem, RecentTextItem, all re-exported through hooks/useRecentItems.ts into src/index.ts :48 and :66–:71. The changeset lists all six.
  4. metadataText, findByName, LabelledRecord exported from hooks/useNavTargetLabel.ts — right that this is NOT a public-surface change: src/index.ts does not reach useNavTargetLabel.ts, so the three exports are module-internal; the two function bodies are unchanged.
  5. UseTrackRouteAsRecentOptions unchanged — right: label stays on the ObjectLike pick so the options type does not move; useTrackRouteAsRecent is not reached from the entry in any case. Its behaviour moves: titleize deleted, dashboard/page/report recorded as { id: 'kind:name', type, name, href } with name decoded by safeDecode and href built from the raw segment, so a revisit of the same route yields equal content.
  6. Persisted shape (ui.recent and localStorage objectui-recent-items:u:ID) — right, and both arms are present. Written shape for named kinds moves from { id, label, href, type, visitedAt } to { id, type, name, href, visitedAt }. Read-back is one reader, readEntry/readList, applied to localStorage, adapter.load(), cross-tab sync and addRecentItem's argument; a legacy named entry takes its name from the type: prefix of id and its minted label is dropped (changeset paragraph 2; pinned from localStorage and from the adapter, and the adapter read writes nothing back). The read accept set NARROWS against the base reader, which kept any entry with a string id: an entry with an unknown type, a missing href, a record/metadata entry without a string label, and a named-kind entry whose id has no type: prefix and no name are now dropped. The only writer of the last case is a host calling addRecentItem with a hand-made id, a call the Breaking paragraph already refuses at compile time; the drop is stated in the PR body and pinned ({ id: 'my-page', type: 'page' } in RecentItemsProvider.test.tsx), and the changeset covers it only implicitly ("read by identity, which was always in the entry's id" — true of the tracker's writes). Named as the one under-spelled corner; not a failing gap.
  7. Published components whose rendered behaviour moves — right: UnifiedSidebar (index.ts :144) and CommandPalette (:157) directly; Home (RecentApps, HomeContinue), Studio home and the console shell through internal modules. Every surface that rendered the stored label now calls useRecentItemLabel(); git grep at the head finds no remaining item.label read on a recent entry in non-test source. The changeset names the five surfaces. useMetadata() returns a frozen no-op outside its provider (packages/react/src/context/AppShellContext.tsx :107–:113), so a host mounting CommandPalette outside MetadataProvider does not crash — and its recent group lists record entries only, which carry their own text. No new provider requirement is added.
  8. useRecentItemLabel holds no rule of its own — right: object/dashboard go through useNavTargetLabel (the sidebar's resolver, objectui#9868); page/report compose the same metadataText + findByName pair over useObjectLabel().pageLabel / reportLabel (packages/i18n/src/useObjectLabel.ts :466, :473); machine name is the backstop; text kinds return their stored label. The useMetadata() getters for dashboards, reports, pages call ensureType when idle (MetadataProvider.tsx :1078–:1086), which is the lazy per-type list read the PR body's "Behaviour to know" describes — right.
  9. "No change, no write" — right at both levels. commit refuses a list equal to itemsRef.current (no state, no localStorage, no adapter.save()); addRecentItem is a no-op when the head's contentOf (id, type, href, name-or-label) is unchanged, so no fresher visitedAt is written; decisions are taken outside state updaters. The list→view route pair for an object produces one entry with one href, so it is one visit (pinned). A record revisit: recordTitle starts undefined (RecordDetailView.tsx :428) and the add fires only once it resolves, so a revisit with the same title is a head no-op; a title that resolves differently is a changed head and is written, as the changeset says. Adapter hydration still writes localStorage (saveRecent(sane)) on every load — pre-existing, localStorage only, no PATCH.
  10. Done-when against the diff — identity stored, label at render in the current locale: yes (RecentApps.labelAtRender-11678 pins the three measured items in en and zh-CN over one stored list, with no minted string). A visit that does not change the list writes nothing: yes (store guard and head no-op, each ablated red by the dev). Audit question untouched: yes. The named pin "revisiting the most recent item makes no write" is held in jsdom by the tracker suite; the live console reading remains unmeasured (③ item 4).
  11. Gate script — right: scripts/check-i18n-call-site-keys.mjs :1090 repoints the home.recentApps.itemType. family at the RecentItemType union; the script's reader supports kind: 'union' (:1177), and the family stays six members. ci.yml :517 runs pnpm check:i18n-keys in the Lint job, which concluded success on this head.

② Semver level

  • Changeset .changeset/11678-recent-items-identity.md: @object-ui/app-shell: minor, with a Breaking (types) paragraph naming the union, the RecentItemInput narrowing, the item.label read and the six new exports. That is objectui's declared shape for a breaking change (AGENTS.md: no major in a changeset; the break spelled out in the body). Matches what the diff publishes (① 1–3, 7). Check-runs Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check and Changeset Overwrite Report: success.
  • Clause-②: yes (narrowing) — right. The entry widens (six names) and addRecentItem's accept set and RecentItem's readable members narrow; both directions in one diff. The PR body's declaration line reads Clause-②: yes (narrowing); the claim was amended to it (6015203262); the changeset's prose carries the same arm.
  • One under-spelled corner, named in ① 6: the dropped non-prefixed legacy entry is in the PR body and the tests, implicit in the changeset. Not a mismatch of level or arm.

③ Boundary flags

Dev deviations (os-dev-report 6015145317), each:

  1. File-surface breach (four render sites, the new hook, two helpers, entry, README, guide, gate script, two fixtures) — declared; answered by the seat's claim amendment 6015203262, which widened the surface and read the serial constraints (objectui#11711 shares src/index.ts and README.md; merge-tree clean). Closed.
  2. Clause-② arm measured as yes (narrowing) — verified from the diff (②). Closed.
  3. Zone 2 item 3 falsified (identity is in id) — verified in readEntry; pinned. The caveat for ids without the type: prefix is ① 6. Closed.
  4. Live second-PATCH source NOT MEASURED (no backend in the container) — the fix is at the store and refuses an equal list whoever reports it, and the jsdom probe shows every same-identity re-report at the base wrote again; the live reading of the three measured pages is the next console dogfood sweep's, not this review's. Accepted as a declared residual.
  5. Gate script edited — verified (① 11); Lint success. Closed.
  6. Commit trailers per objectui AGENTS.md — repo convention, not a contract question. Noted.
  7. First check-readme-exports run was a could-not-run (unbuilt closure); re-run exit 0, and README Export Check success on the head. Closed.
  8. No labels written — the labeler added documentation, tests, package: app-shell; the seat hung needs:contract-review. Closed.

open_questions: none declared.

out_of_scope_findings (4, none filed, carrier none): cold-landing hydration race (pre-existing, base and head alike; the new tracker suite's mount helper documents it); dashboard label precedence against objectui#11295 (inference; the recent list now matches the sidebar, not the dashboard header); flusher effect keyed on a useMemo identity (pre-existing); Starred items store a label at favorite time — the same defect class as this card, and the seat's filing decision, escalated to the seat as a follow-up card candidate.

Check-runs on the head at this read: 30 success, 3 skipped, 0 failure, 9 in progress — Spec Main Shape Gate, Type Check, Test (shard 1/8) through Test (shard 7/8). Their convergence is the seat's landing check.

Implemented-by: claude/issue-11678-recent-items-identity
Reviewed-by: session_01FngvPpdrnhHMdHHq6vwwju

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red on ec51ed9: five test shards, one cause in this PR's import graph. A patch round is queued for the original dev

domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T11:33Z.

  • Failing: Test (shard 1/8), (2/8), (4/8), (5/8) and (6/8) in run 37455366245. Each fails one RecordDetailView.*.test.tsx suite (modalDispatch, refusedLaunch-9973, approvalRecallGate, recordIdParamSeed, headerRecordIdLoadWindow) with ReferenceError: Cannot access 'authFetchSpy' before initialization. The stack is metadataClientFactory.ts:41 ← MetadataProvider.tsx:16 ← useRecentItemLabel.ts:42.
  • Cause (this PR's): RecordDetailView imports useRecentItems from hooks/useRecentItems.ts, which now also re-exports useRecentItemLabel. That pulls MetadataProvider, and with it metadataClientFactory's module-load createAuthenticatedFetch(), into a graph that never loaded them. These suites' @object-ui/auth mock then runs before its spy is initialised.
  • Not a flake, not the base's: the five suites pass on main, and every other file in each shard passed.
  • Next: a patch round on this branch keeps the hook shim free of MetadataProvider (⛔ no test edits, ⛔ no skips), then the seat re-reviews. The contract review 6015334185 covers ec51ed9 only, so a new at-tier record is owed on the new head before enqueue.

Generated by Claude Code

claude added 2 commits October 6, 2026 11:36
…raphs (objectui#11678)

The useRecentItems shim re-exported useRecentItemLabel, so every module that
only wanted the list (RecordDetailView among them) loaded MetadataProvider and
metadataClientFactory's module-load createAuthenticatedFetch(); five
RecordDetailView suites whose @object-ui/auth mock closes over a top-level spy
then hit it in its TDZ. The shim is hooks and types again and the entry
exports useRecentItemLabel from its own module. HomeContinue takes the
resolver as a prop from HomePage, as it takes t, so HomeRail.tsx (which also
holds the action center and activity card) does not load the cache either; a
pin on that /home rail joins the label-at-render suite.

Claude-Session: https://claude.ai/code/session_01FngvPpdrnhHMdHHq6vwwju
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/11659-console-record-ux.md

  • names HomePage.tsx → packages/app-shell/src/console/home/HomePage.tsx — edited by this change

    The backup-password reminder no longer appears in the user's first session. 「建议设置一个备用密码」 showed on the environment home right after a new user built their first app: the reminder was gated on a home-visit count (quiet on the first home mount, shown on the second), and coming back to home after building is the second mount inside the first sitting. It now stays quiet for 12 hours after the first home visit on the device, so it first shows on a later day's visit. The first-visit record (os:recovery-pw-first-seen) now holds a timestamp; a device that holds the old '1' flag starts the 12 hours over rather than reading it as long ago. When storage is unavailable it stays quiet. RecoveryPasswordReminder moves out of HomePage.tsx into its own module; it is not exported from the package entry, and its other conditions (dismissed, SSO-enforced, has a local password) are unchanged.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with abd374bb2 (merge-base with origin/main): 21 file(s) changed outside .changeset/, read against 62 pending declaration(s) that publish a body (64 pending in total). · run

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3323.6 KB 3330.4 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-_uVqcwva.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 574.72KB 137.94KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.23KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.83KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0c8198b2bff23dc1e286ee8891c7a66537f1fc80
Local-runs: none

Inputs: card #11678 (body; claim 6013942564; os-dev-report 6015145317; claim amendment 6015203262; ACCEPT 6015212592; round-1 os-dev-report 6016245224; ACCEPT 6016299959), PR #11713 (body, 22-file list, net diff against main at the merge-base abd374bb22a5a256b2aff93c5a089dda795b26b3, and its comments: CI-red note 6015367531, the previous record 6015334185 on ec51ed9, the changeset-claim-re-read bot 6015468157), and the 43 check-runs on the head. Unchanged lines around the diff, and the runtime import specifiers of the modules named below, were read off the fetched objects (git show on a pinned ref, at main, ec51ed9 and the head); nothing was checked out, built, run or re-run. The previous record covers ec51ed9 only; this one judges the whole net diff at the head, and the round's change in particular.

① Derived judgments

Public surface of @object-ui/app-shell: exports is . (dist/index.d.ts from src/index.ts) and ./styles.css only. The persisted ui.recent list and the rendered behaviour of the entry-exported UnifiedSidebar, CommandPalette and HomePage are part of what it publishes.

The round's change (CI red on ec51ed9):

  1. Cause and fix at the root — right. Read off the pinned objects: at main, RecordDetailView.tsx (63 modules reached), the five RecordDetailView.* suites (64 each), HomeRail.tsx (10), the hooks/useRecentItems.ts shim (3) and the three tests that import only HomeActionCenter / HomeActivity from HomeRail reach neither providers/MetadataProvider.tsx nor views/metadata-admin/metadataClientFactory.ts. At ec51ed9 every one of them did, by one edge: hooks/useRecentItems.ts → hooks/useRecentItemLabel.ts → providers/MetadataProvider.tsx (:16) → metadataClientFactory.ts, whose :41 const consoleApiFetch = createAuthenticatedFetch() runs at module load — the call the suites' @object-ui/auth mock answers with a spy still in its TDZ. At the head none of them reaches either module, and their reach counts are main's again (63 / 64 / 10 / 3). The edge is removed where it was made: the shim re-exports hooks and types only (:14–:21, with the ⛔ comment), and HomeRail.tsx :23 takes the resolver as import type, erased at compile. Nothing is papered over: RecordDetailView.tsx and the five suites are byte-identical to main; no test file in the net diff adds .skip, .only or .todo; the one vi.mock the net diff adds is in the PR's own new suite (RecentApps.labelAtRender-11678.test.tsx, a useAuth stub over importOriginal). What newly reaches MetadataProvider at the head is only what renders with the resolver: RecentApps (imported by no source module but the console/home/index.ts barrel, which no module imports and the entry does not reach, plus its two tests) and StudioHomePage (which already reached metadataClientFactory on main through views/metadata-admin/useMetadata.ts :23, so the module-load call was already in its graph); CommandPalette reached MetadataProvider on main already through useNavTargetLabel. The dev's walk (332 / 433 / 340) and this read agree. Check-runs Test (shard 1/8) through (8/8), the five that were red, are success on the head.
  2. Entry's exported set unchanged from round one — right. Round one's delta on src/index.ts (da35453 → ec51ed9) added six names: useRecentItemLabel, RecentItemInput, RecentItemType, RecentNamedItem, RecentTextItem, RecentItemLabelResolver. The net delta (abd374b → head) adds the same six, with useRecentItemLabel (:51) and RecentItemLabelResolver (:75) now from ./hooks/useRecentItemLabel.js instead of through the shim. The source module of a re-export is not part of the published surface; the built dist/index.d.ts carries the same names (dev's build reading; Type Check, Test (dist pins) and README Export Check success). The signUpOffer / bootstrapStatus lines that also appear between ec51ed9 and the head are main's (objectui#11705), not this PR's.
  3. HomeContinue's new required labelOf: RecentItemLabelResolver prop — right that it touches no published surface. HomeRail.tsx is not reached from src/index.ts (no HomeRail, no console/home/index import there); HomePage is entry-exported (HomePage / DefaultHomePage) but is export function HomePage() with no props, so no type reaches HomeContinue's props through it; the exports map has no subpath for console/home. All three JSX usages (HomePage.tsx :600 and the two tests) pass it; Type Check success. The published HomePage's rendered behaviour is the same as at ec51ed9: the same resolver, now handed in by the page the way t is, so HomeRail.tsx (which also holds the action center and the activity card) no longer loads the metadata cache.
  4. The merge and the net diff — right. c2e625f is a two-parent merge of abd374b (no rebase, no force-push); the merge-base is abd374b; the net diff is 22 files, +1105/−117: round one's 21 plus console/home/HomePage.tsx, which changes by one import, one hook call with its comment, and labelOf={recentLabel} on the HomeContinue element. The README (+58) and the changesets 11679-* and 11705-* in the ec51ed9..head range are main's; the PR's own README and guide hunks are byte-identical to round one's. The round-one source files the round did not touch (RecentItemsProvider.tsx, useRecentItemLabel.ts, useTrackRouteAsRecent.ts, useNavTargetLabel.ts, CommandPalette.tsx, UnifiedSidebar.tsx, StudioHomePage.tsx, RecentApps.tsx, scripts/check-i18n-call-site-keys.mjs, the changeset, the four round-one test files) are byte-identical to ec51ed9, so the judgments below carry to this head unchanged.
  5. HomePage.tsx against the pending .changeset/11659-console-record-ux.md (flagged report-only by Changeset Claim Re-read) — right that the paragraph is still true: at the head HomePage.tsx :32 imports RecoveryPasswordReminder from ./RecoveryPasswordReminder.js, renders it at :339 and :425, and defines none; the net diff's five HomePage.tsx lines touch none of those. No correction owed.

The whole net diff (round one's judgments, re-read at this head):

  1. RecentItem becomes RecentNamedItem | RecentTextItem — right, a narrowing of what a consumer may read: the four named kinds carry name and no label; record and metadata keep label; item.label off an unnarrowed entry no longer compiles (TS2339 in the dev's reverse check). The changeset's Breaking paragraph says so.
  2. addRecentItem takes RecentItemInput — right, a narrowing of the accept set (a label-carrying named input, or a named input without name: TS2353). The two in-repo callers compile: the tracker (identity shape) and RecordDetailView.tsx (type: 'record' with label, a text kind; the file is untouched).
  3. Six new entry exports — right, a widening (item 2), all named in the changeset.
  4. metadataText, findByName, LabelledRecord exported from hooks/useNavTargetLabel.ts — right that this is not a public-surface change: the entry re-exports nothing from that module, and no entry-exported name reaches those three through its props, params or returns; the two function bodies are unchanged.
  5. UseTrackRouteAsRecentOptions unchanged — right (label stays on the ObjectLike pick); the hook is not entry-exported in any case. Behaviour moves: titleize is deleted; dashboard, page and report are recorded as { id: 'kind:name', type, name, href } with name from safeDecode(seg3) and href from the raw segment, so a revisit of the same route yields equal content.
  6. Persisted shape (ui.recent and localStorage objectui-recent-items:u:ID) — right, both arms present. Written shape for named kinds moves from { id, label, href, type, visitedAt } to { id, type, name, href, visitedAt }. One reader, readEntry / readList, for localStorage, adapter.load(), cross-tab sync and addRecentItem's argument; a legacy named entry takes name from the TYPE: prefix of its id and its minted label is dropped (pinned from localStorage and from the adapter; the adapter read writes nothing back). The read accept set narrows against the base reader (which kept any entry with a string id): an unknown type, a missing href, a text kind without a string label, and a named kind with neither name nor the prefix are dropped. The last case's only writer is a host calling addRecentItem with a hand-made id, which the Breaking paragraph refuses at compile time; it is pinned ({ id: 'my-page', type: 'page' }) and stated in the PR body, implicit in the changeset ("read by identity, which was always in the entry's id"). The one under-spelled corner; not a failing gap.
  7. Published components whose rendered behaviour moves — right: UnifiedSidebar and CommandPalette directly, HomePage through HomeContinue, Studio home and RecentApps through internal modules. All five ask useRecentItemLabel() (or receive its resolver); no item.label read on a recent entry remains in non-test source. useMetadata() returns a frozen no-op outside its provider, so a host mounting CommandPalette outside MetadataProvider does not crash, and its recent group lists record entries only. No new provider requirement.
  8. useRecentItemLabel holds no rule of its own — right: object and dashboard through useNavTargetLabel; page and report through metadataText + findByName over useObjectLabel().pageLabel / reportLabel; machine name as the backstop; text kinds return their stored label. The useMetadata() getters for dashboards / reports / pages do the lazy per-type list read the PR body's "Behaviour to know" describes.
  9. "No change, no write" — right at both levels: commit refuses a list equal to itemsRef.current (no state, no localStorage, no adapter.save()); addRecentItem is a no-op when the head's contentOf (id, type, href, name-or-label) is unchanged, so no fresher visitedAt; decisions are taken outside state updaters. A record title that resolves differently is a changed head and is written (pinned as the control). Adapter hydration still writes localStorage on every load: pre-existing, localStorage only, no PATCH.
  10. Done-when against the diff — identity stored, label at render in the current locale: yes, now pinned on both the /home rail (HomeContinue wired as HomePage wires it) and the RecentApps cards, in en then zh-CN over one stored list with no minted string. A visit that does not change the list writes nothing: yes (store guard and head no-op, each ablated red in round one on code unchanged since). The audit question stays objectstack's. The live console reading of the three measured pages remains unmeasured (③).
  11. Gate script — right: the home.recentApps.itemType. family reads the RecentItemType union (kind: 'union'), six members as before; Lint, which runs check:i18n-keys, is success on the head.

② Semver level

  • .changeset/11678-recent-items-identity.md is byte-identical to round one's: @object-ui/app-shell: minor, with a Breaking (types) paragraph naming the union, the RecentItemInput narrowing, the item.label read and the six new exports. That is objectui's declared shape for a breaking change (no major; the break spelled out). It matches what the diff publishes (① 6–8, 12). This round publishes nothing new: an internal component's prop and a re-export's source module need no changeset prose. Check-runs Changeset Bump Policy, Changeset Claim Re-read, Changeset Declaration, Changeset Fixed Group Check and Changeset Overwrite Report: success.
  • Clause-②: yes (narrowing) — right. The entry widens by six names; addRecentItem's accept set and RecentItem's readable members narrow; both directions in one diff. The PR body carries the line at the start of its second line, the claim was amended to it (6015203262), and the changeset's last paragraph says the same.
  • The under-spelled legacy corner (① 11) is unchanged from the previous record: in the PR body and the tests, implicit in the changeset; not a mismatch of level or arm.

③ Boundary flags

Round-1 os-dev-report 6016245224, deviations, each:

  1. HomePage.tsx added beyond the amended surface — declared; the seat's ACCEPT 6016299959 adds it to the claim's surface. It is three lines plus a comment (① 4). Any serial-constraint read on HomePage.tsx against other open PRs is the seat's enqueue read and outside this record's inputs. Closed for the contract.
  2. Ablations and the gate script's 8 suites not re-run at 0c8198b — the code they mutate or exercise (RecentItemsProvider.tsx, useRecentItemLabel.ts, scripts/check-i18n-call-site-keys.mjs) is byte-identical to ec51ed9 (① 4), so round one's readings carry; Lint and all eight Test shards are success on the head. Closed.
  3. RecentApps is rendered by no page — verified: its importers are the console/home/index.ts barrel (which nothing imports) and two tests. The round adds the /home rail pin so the live surface is pinned. Whether the dead component is removed is housekeeping, carrier none; not this card's. Closed for this record.
  4. The API 529 mid-turn; the full app-shell run completed under the lock and was not re-run — CI's shards on the head are the gate verdict: all success. Closed.

Round-one deviations (os-dev-report 6015145317, nine items): closed by the previous record 6015334185; every file they concern is unchanged since, and the claim amendment 6015203262 still covers the widened surface. Carried.

open_questions: none in either report.

out_of_scope_findings: round 1 adds the unused RecentApps (above). Round one's four stand (cold-landing hydration race, pre-existing at base and head; dashboard label precedence against objectui#11295, an inference; the flusher effect keyed on a useMemo identity, pre-existing; Starred items store a label at favorite time). The last is the same defect class as this card and stays escalated to the seat as a follow-up card candidate; no card for it is among the inputs.

Check-runs on the head at this read: 43, all completed — 40 success, 3 skipped (Test (coverage), Test (coverage shard), dependabot: skipped by design on a PR), 0 failure, 0 in progress. The five shards red at ec51ed9 are success. Nothing is left to converge.

Implemented-by: claude/issue-11678-recent-items-identity
Reviewed-by: session_01FngvPpdrnhHMdHHq6vwwju

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 12:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 12:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 48c82c9 Oct 6, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11678-recent-items-identity branch October 6, 2026 12:59
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 package: app-shell tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: "Recently Accessed" stores a humanised machine name frozen at visit time, and rewrites the preference (audited) on every visit

2 participants