Skip to content

fix(plugin-detail): derive ReferenceRailEntry from @objectstack/spec and retire the icon key - #5794

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-5494-reference-rail-entry-derive
Aug 23, 2026
Merged

fix(plugin-detail): derive ReferenceRailEntry from @objectstack/spec and retire the icon key#5794
os-sam merged 2 commits into
mainfrom
claude/issue-5494-reference-rail-entry-derive

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#5494

Implements the maintainer's Option B ruling (2026-08-22, decision-inbox digest, 「接受所有」): objectui drops icon and derives ReferenceRailEntry from the spec. Option C stayed rejected; the ruling's flip condition (real existing usage of icon) did not trigger — stock scan below.

Stock scan (ran first — it decides the direction)

Result: no reachable authored usage of icon on reference-rail entries. The ruling does not flip; Option B proceeds.

Populations swept, each with a positive control proving the scanner arrived:

  • objectui @ cd4961a68 — all 31 files matching reference_rail|reference-rail|ReferenceRail, plus examples/ (456 authored files incl. the schema-catalog corpus; control: objectName|displayField hits 4 files). Zero record:reference_rail schemas exist in examples at all. The only icon writers found are objectui's own: the local interface declaration and buildDefaultPageSchema's synthesized entries (both retired here). The icon: 'check' in strictPayload.test.ts is a related option feeding record:related_list tabs, not a rail entry.
  • objectstack @ 764dbbc — repo-wide git grep for the same terms (control: relationshipField hits 3 example files): hits only in content docs (component-type lists, no icon advertising), ADR/audit docs, packages/lint, and packages/spec. Zero rails in examples/, skills/, or any template.
  • Stored customer metadata — NOT REACHABLE from this seat (Carrier for Phase 2 of the row-predicate deprecation: the stored-metadata survey that must size the removal window before any spelling retires #5741 ran five independent probes; gap open since objectstack#7917). A clean scan here is "no reachable usage", not "no usage". This does not change the direction on its own: the spec side already refuses icon at save and nothing ever rendered it (below), so unreachable stock cannot be depending on a render effect that does not exist.

The premise correction worth reading

The card says the renderer "reads and honours" icon. Measured false — and false at filing: no render path in record-reference-rail.tsx has ever read the key (verified back to the file's first commit, 66751896c). The spec's own v18 migration entry ui-reference-rail-unknown-keys-refused measured the same, verbatim: "the interface's icon is read by nothing and is refused, not declared". So no functioning affordance retires; what retires is a lying TS declaration plus a synth emission of a key the spec refuses at save. This makes Option B strictly cheaper than the ruling assumed. The runtime accept set does not move — ReferenceRailEntrySchema was $strict without icon before and after; validation was already tight, the declaration was lying.

Changes

  • packages/plugin-detail/src/renderers/record-reference-rail.tsx — the hand-written interface ReferenceRailEntry is replaced by export type { ReferenceRailEntry } from '@objectstack/spec/ui' (the gate's sanctioned re-export form). The icon?: string member and the header comment's icon claim go with it.
  • scripts/check-spec-symbol-derivation.mjs — the @object-ui/plugin-detail: ["ReferenceRailEntry"]DEBT entry and its comment block are burned. The ledger is shrink-only; --ledger regeneration reproduces the remaining block byte-for-byte.
  • .changeset/reference-rail-entry-spec-derived.mdminor (breaking-as-minor per check-changeset-no-major; @object-ui/* fixed group) with the loud migration note the ruling requires: authored icon stops being suggested by the type, never had a render effect, and was already refused at save. Release notes compile from changesets — no releases/ page touched.
  • packages/plugin-detail/src/synth/buildDefaultPageSchema.tsdeclared file-surface amendment (bounded in-place fix, all four conditions held): the synthesized reference-rail entries emitted icon: rel.icon, i.e. objectui's own synth was the one live producer of the exact key this ruling retires — same defect class; the correct shape is pinned by the landed ruling and the spec's $strict schema; packages/plugin-detail carried no other claim (dispatch: lane in-flight zero); covered by the same gate family (plugin-detail tests/type-check, no new verification face). Boundary scan: repo-wide grep shows no other writer of icon onto reference-rail entries, and no synth test pins the key (assertions check entries length/objectName only). Without this line the "retired" key kept being written into every synthesized rail page — a save-refusal trap for any customize-and-persist flow. Sibling defect at the region level (aside.className, Synthesized aside region carries className, which PageRegionSchema rejects — a Reference Rail page has no persistable spelling #4286) is a different key and remains open — not addressed here.

Verification (at 9a13a78f2 unless noted)

  • check:spec-symbols — ✅ 1297 files scanned against 4966 spec export names; 16 declared dialects, 3 untriaged collisions in 1 packages (the 3 are the pre-existing @object-ui/types trio).
  • Reverse verification, both directions, mutation confirmed on disk by anchored grep before each reading, restored from the committed state:
    • resurrect the hand-written interface (file from cd4961a68), ledger stays burned → gate exit 1: interface ReferenceRailEntry …record-reference-rail.tsx:33 (exported by @objectstack/spec/ui);
    • keep the fix, re-add the DEBT entry → ratchet exit 1: @object-ui/plugin-detail lists 1 symbol in DEBT that no longer collide — ReferenceRailEntry.
  • Type-face probe against the installed spec dist (proves the derived type refuses the key post-rebuild): red leg error TS2353 … 'icon' does not exist in type '{ objectName; relationshipField; title?; limit?; displayField? }'; green leg (declared keys only) exit 0.
  • Tests/type-check at 4be78c5d0 + the changeset (the only later commit is the changeset .md, outside every test/tsc input): dependency closure built, then vitest run packages/plugin-detail/ — ✅ Test Files 93 passed (93) · Tests 866 passed (866); pnpm --filter @object-ui/plugin-detail type-check — ✅ (lock VERDICT command-exit 0). Gate pin test check-spec-symbol-derivation.test.ts — ✅ 34/34; type-check:scripts — ✅.
  • Light gates at 9a13a78f2: check:control-bytes ✅ · check:phantom-deps ✅ · check:self-import ✅ · check:esm-specifiers (specifiers leg) ✅ · check-changeset-fixed ✅ · check-changeset-no-major ✅ · check-changeset-presence ✅ · check:action-forward-parity ✅ · check:i18n-keys ✅ · check:i18n-drift ✅.
  • Declared narrowings (CI runs the full farm regardless):
    • lint: ran pnpm --filter @object-ui/plugin-detail lint + eslint --no-inline-config scripts/check-spec-symbol-derivation.mjs — 0 errors. Invariance for untouched files: eslint.config.js contains no projectService/parserOptions.project (no type-aware linting), so this diff cannot move any unlinted file's judgment.
    • check:published-dist: the gate hardcodes a full-workspace turbo build (its own header: "why this gate costs a build"). Ran the targeted equivalent on the one published package this diff touches: plugin-detail's fresh dist (101 files) has 0 test/bench/stories artifacts and 0 vitest imports; every other package's dist is a function of sources this diff does not touch.
    • Downstream consumer sweep (prefix direction, ...@object-ui/plugin-detail = dependents): ReferenceRailEntry has zero importers outside the declaring file in the whole tree (git grep at HEAD; the package's exports map exposes only ., and dist/index.d.ts never re-exported it), and no typed caller anywhere writes icon on entries — so no dependent's type-check judgment can move on this narrowing.
  • Known broken gauges not exercised: Build Docs red on main (Build Docs is red on main: fs reaches the browser bundle via @objectstack/specpg-connection-string #5668, inherited); check-doc-snippet-types / check-eager-closure-budget require built consoles.

Out-of-scope findings filed: #5792 (stale parity-test prose citing this card as unsettled), #5793 (spec dependency floors vs 17.1.0-only symbols).


Generated by Claude Code

… retire the icon key
ReferenceRailEntry is spec-owned as of @objectstack/spec 17.1.0. The local
interface re-declared it with an extra optional icon key that no render path
ever read and the spec's $strict ReferenceRailEntrySchema refuses at save.
Replace the declaration with a type re-export from @objectstack/spec/ui, stop
emitting icon on synthesized reference-rail entries in buildDefaultPageSchema,
and burn the collision's DEBT ledger entry in check-spec-symbol-derivation.mjs
(regenerated with --ledger).
Part of #5494
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
…rement
Part of #5494
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E7snar5mwF7qoXJazqKhys
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3918.3 KB3990.2 KB
Main entry chunk (gzip)152.5 KB350 KB
Entry fileindex-DmEHgkGj.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)12.80KB4.47KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)33.99KB8.57KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)503.77KB113.91KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.65KB18.32KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.41KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.33KB60.97KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.80KB27.20KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.61KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)44.39KB14.99KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)3.40KB1.68KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-samClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — verified independently, not from the report

I re-derived the load-bearing claims against the tree rather than reading them off the PR body. All four hold.

1. check:spec-symbols passes because the collision is gone, not because the ledger excuses it. This was the one thing I would not take on trust, since the card is this gate. Read the scanner: check-spec-symbol-derivation.mjs:907-926 records export type { X } from '…' as "re-export" with derived = isSpecModule(fromModule) — so the new form binds the local name to the spec's own definition and satisfies the rule by derivation, not by exemption. And the DEBT burn was mandatory, not cosmetic: line 1066 is a staleness ratchet (… lists N symbols in DEBT that no longer collide), so keeping the entry would have failed the gate on the fix itself. That makes the two reverse-verification legs the right pair, and both messages quoted in the PR body are the script's own strings.

2. The buildDefaultPageSchema amendment is correctly bounded — I checked the boundary myself rather than accepting "no other writer". A repo-wide scan (control: relationshipField, 230 hits — scanner demonstrably arrived) surfaces three icon: rel.icon writers in that file, not one. The other two are not rail entries and are correctly left alone:

  • :689 writes icon onto componentNode('record:related_list', …) — a different component;
  • :694 writes it onto a tab item in asOwnTab — not a component payload at all.

Only :939 fed record:reference_rail. The narrow fix is the right cut.

3. The type genuinely was not published. Verified rather than assumed: packages/plugin-detail has no src/index.ts — the entry is src/index.tsx, which imports only RecordReferenceRailRenderer (:29) and carries no export * from that module; the exports map exposes "." alone. So ReferenceRailEntry was never on the published .d.ts surface, which corroborates the zero-importer sweep.

4. The premise correction is the valuable part of this PR. The card and triage's four-facet block both asserted the renderer "reads and honours" icon. Measured false, and false at filing. That is worth stating plainly: what retired was a lying TS declaration plus a synth emission of a key the spec refuses at save — not a user-facing affordance. The runtime accept set does not move in either direction. Reporting this rather than quietly implementing the ruling as written is exactly right.

One accuracy caveat, recorded not demanded

The changeset's third bullet is headed "The published TS surface narrows." Per finding 3 that heading overstates reach — the symbol was not in the published surface; only in-repo source importers could ever have hit the error. The sentence under it is properly qualified ("from @object-ui/plugin-detailinternals") and the migration instruction is correct, so the guidance a reader acts on is sound. I am not asking for a re-push: this repo's CI is cancel-in-progress, so a one-word prose fix would discard and re-run the entire farm, and the note errs in the safe direction. Recorded here so the release-notes compile step has the correction if that line is ever quoted.

Gate reading

Green on every gate that is this PR's own. Read by name, never off a rollup: Type Check (the job that runs pnpm check:spec-symbols, ci.yml:238) is the decisive one and the merge is held on it.

Build Docs reports success here in 10 seconds — that is a turbo cache hit, not a build. Per this seat's standing platform fact, a green whose duration is implausible for the work is not evidence, so I am neither crediting nor debiting it; the gate is red-on-main (#5668) and inherited either way.


Generated by Claude Code

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReferenceRailEntry is owned by @objectstack/spec as of 17.1.0 and objectui's local copy declares an extra icon key the spec's strict schema refuses

2 participants

@os-sam@claude