Skip to content

fix(core,app-shell): resolve a relationship target from reference only - #6649

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-6528-resolve-reference-to-census
Aug 28, 2026
Merged

fix(core,app-shell): resolve a relationship target from reference only#6649
os-sales merged 1 commit into
mainfrom
claude/issue-6528-resolve-reference-to-census

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6528

resolveReferenceTo (dataset designer) and its sibling resolveRelationshipTarget (packages/core/src/utils/chart-series.ts) each read a relationship field's target through a four-spelling tolerant chain:

reference??reference_to??referenceTo??reference_to_object

The card asked for the dead referenceTo branch. Following the triage charter, the spellings were measured first and three of the four turn out to be unfounded, so the chain is narrowed to reference in both places in one pass — they must not diverge, since a fix leaving them disagreeing recreates the defect one file over.

1. Census — which spellings can a served document actually carry

Every zero carries a positive control in the same query shape, so it is a measurement rather than a broken pattern. reference is that control throughout.

spellingObjectSchema.safeParse (spec 17.2.0)producers on the object-metadata surfaceverdict
referenceACCEPTED — parses green, value survives the parselive — both designer writers emit it (MetadataService.toFieldPayload, MetadataFieldsPage.fromDesignerField, each reference: target); 445 of 565 lookup / master_detail defs in the framework treeKEEP
reference_toREFUSED BY NAME — unrecognized_keys, "Did you mean reference_to -> reference?"0DROP
referenceToREFUSED BY NAME — unrecognized_keys, "Did you mean referenceTo -> reference?"0DROP
reference_to_objectREFUSED — unrecognized_keys, and not even a recognised alias0DROP

Population census behind the producer column — every lookup / master_detail field def in each tree, and which spelling names its target:

treedefs foundreferencereference_toreferenceToreference_to_object
framework (objectstack)565445660
this repo (objectui)360148109506

Both trees' non-reference hits were read individually rather than counted and trusted:

  • framework: all 12 are test fixtures, one doc comment, and the spec's own alias table (referenceTo: 'reference', the machinery that PRODUCES the refusal hint). Zero production emit sites. driver-sql states it outright — "reference_to is a REJECTED ALIAS, not a normalised one".
  • this repo: the reference_to hits are almost entirely a different surface. @object-ui/types' views.zod.ts declares reference_to on ObjectUI's own view/field schema, and plugin-detail translates INTO it from the served reference (RecordDetailDrawer.tsx, RelatedList.tsx, buildDefaultPageSchema.ts). That is a separate contract; an object metadata document never carries it. The referenceTo hits are ObjectUI's own TS property names (DatasetRelationship.referenceTo, the resolver's OUTPUT), not wire keys.
  • reference_to_object occurs nowhere in either tree outside the two chains and the two unit tests that called them. It was only ever produced by its own test.

2. Premise verified on the merged ref

The cut rests on "the referenceTo branch is unreachable since #6519 strips the key". Confirmed on origin/main at 5361083, not taken from the card:

  • packages/app-shell/src/views/metadata-admin/previews/object-fields-io.tsRETIRED_FIELD_KEYS = ['indexed', 'referenceTo', 'isSystem'], stripped by readFields.
  • normalizeObject builds every def through readFields, and it is resolveReferenceTo's only production caller.

So referenceTo could not reach that helper even from a stored pre-#6041 row. Premise holds.

3. The fork did NOT fire

No real producer emits a legacy spelling onto a document either consumer reads. The two historical referenceTo producers were already retired at the producer by #6041 — which is the remedy the fork prescribes, applied before this card existed.

One producer-side finding did turn up and is filed rather than fixed here: #6647 — the console object preview sample spells a lookup target reference_to, a second off-spec key its KNOWN_STALE reason does not record. It is dev-only and unreachable from both consumers, so it neither blocks nor is blocked by this cut.

4. One real behaviour change, and it is deliberate

The two consumers are not symmetric, and the difference is load-bearing:

  • resolveReferenceTo sits behind the read door — the strip makes its narrowing cosmetic.
  • resolveRelationshipTarget reads GET /meta/object/:namedirectly (ObjectChart.tsx, useDatasetDimensionLabels.ts both fetch and pass the raw doc). No door strips retired keys on that path.

So a stored pre-#6041 row spelling the target referenceTo no longer resolves in the chart path. The walk is best-effort by construction — no entry is yielded and the caller keeps the raw value — so such a row degrades visibly instead of being silently absorbed. Per AGENTS.md #0.1 that row is a producer-side defect, and a lenient consumer is exactly where it would have stayed hidden. reference was already head of the old chain, so any document carrying both is unaffected (pinned by a new test).

5. Tests — updated with the helpers, not preserved as cover

The dead-branch pins asserted the removed branches. They are rewritten, and the refusal is now pinned in both files so a silent re-widening turns red:

refuses the legacy spelling `reference_to` — a producer emitting it is the bug
refuses the legacy spelling `referenceTo` — a producer emitting it is the bug
refuses the legacy spelling `reference_to_object` — a producer emitting it is the bug
prefers `reference` on a partially-migrated def carrying both

Fixture triage across the rule's consumption radius, not just the edited packages: fixtures feeding normalizeObject / useDatasetFieldCatalog were re-spelled to reference (they used an alias). Every dimension-walk exerciser in plugin-dashboard, plugin-report, react and plugin-charts was checked and carries zero legacy spellings, with live reference controls proving the query. column-sortability.test.ts carries a reference_to fixture but its module reads no reference key at all — out of radius, left alone.

Verification

All readings below are on the final commit, 4ef4cc7c.

  • Touched suites, from the repo root (the package-scoped form is refused by this repo's guard): pnpm exec vitest run over packages/core/src/utils/__tests__/, packages/app-shell/src/views/metadata-admin/inspectors/, and the dataset widget / report renderer walk exercisers — Test Files 97 passed (97), Tests 1334 passed | 1 skipped (1335).
  • Reverse verification — predicted direction RED, and measured RED. Restoring the four-spelling chain (mutation confirmed on disk by grepping for both the removed text and the injected text, under an EXIT/INT/TERM trap with absolute paths) gives Tests 6 failed | 51 passed — exactly the 6 refusal pins, 3 per file, nothing else. The restore leg is proven by git diff HEAD empty and blob-hash equality against HEAD (which carries the fix, since the fix was committed first). No rebuild step is involved: this repo resolves both vitest and tsc to src, so there is no dist for a stale artifact to hide in.
  • Type-check, whole downstream closure of @object-ui/core (--filter '...@object-ui/core', prefix form = consumers): 32 Done, 1 Failed, the failure being apps/site unable to resolve an unbuilt @object-ui/example-schema-catalog. Building that example and re-running apps/site alone passes, so the closure is 33/33 clean. Every consumer of the two changed functions is green: core, app-shell, plugin-charts, plugin-dashboard, plugin-report, react.
  • pnpm lint (eslint . over the whole repo, no narrowing): 47 successful, 47 total, 0 errors (2773 pre-existing no-explicit-any warnings, unchanged in kind; this diff removes two as any casts).
  • Changeset gates: check-changeset-presence.mjs — "5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"; check-changeset-no-major.mjs — "No changeset declares a major bump." The bump is an honest patch on both packages; the gate ruled it and was not pre-empted.
  • Control-byte self-scan clean across all changed files.

Scope

Confined to packages/core and packages/app-shell. MetadataService was read but never touched (#6490 is live in app-shell on saveObject); nothing here reaches it.

#6597 is not addressed here and nothing in this PR settles it. It concerns FieldMeta.referenceTo in plugin-dashboard — same word, different surface, still awaiting a ruling. plugin-dashboard and FieldMeta are untouched. One census result is input to that pending ruling and no more: spec 17.2.0 refuses referenceToby name on a FieldSchema with an explicit rename hint to reference, and the referenceTo occurrences in this repo's own TS interfaces are property names rather than wire keys.

The string / array / { object } carriers are untouched — a separate axis from the spelling, whose own census is not finished. Filed as #6648.


Generated by Claude Code

`resolveReferenceTo` (dataset designer) and its sibling
`resolveRelationshipTarget` (`chart-series.ts`) each read a relationship
field's target through a four-spelling tolerant chain:
reference ?? reference_to ?? referenceTo ?? reference_to_object
Censused against every producer that can reach them, with `reference` as the
positive control: `ObjectSchema.safeParse` (spec 17.2.0) ACCEPTS `reference`
and REFUSES the other three BY NAME, and none of the three has a producer on
the object-metadata surface. `reference_to` is live only on ObjectUI's own
view/field schema (a different contract, translated INTO from `reference`);
`referenceTo`'s two producers were retired by objectui#6041 and it is stripped
by the read door since objectui#6519; `reference_to_object` occurs nowhere in
either tree outside the chain and the test that called it.
Narrowed in BOTH places in one pass so the two canonicalizations cannot
diverge. The dead-branch unit tests are updated with the helpers and gain
refusal pins, so re-widening the chain turns red.
`chart-series.ts` reads `GET /meta/object/:name` directly, with no read door,
so this is a real behaviour change there: a stored pre-objectui#6041 row
spelling the target `referenceTo` no longer resolves. The walk is best-effort
by construction, so it degrades visibly rather than being absorbed — per
AGENTS.md #0.1 such a row is a producer-side defect, and a lenient consumer is
where it would have stayed hidden. `reference` was already head of the old
chain, so a document carrying both is unaffected.
Carriers (string / array / `{ object }`) are untouched — a separate axis.
Part of #6528
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3237.2 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-RfUwy7de.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)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
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)40.18KB10.59KB
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)507.69KB114.99KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)238.89KB60.02KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
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.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.41KB34.47KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.66KB54.57KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
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)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
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)9.30KB3.22KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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 (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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)6.28KB2.87KB
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-sales
os-sales marked this pull request as ready for review August 28, 2026 08:51
@os-sales
os-sales added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit d0889e2Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6528-resolve-reference-to-census branch August 28, 2026 09:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(app-shell): resolveReferenceTos referenceTo branch is unreachable now that the read door strips the key

2 participants

@os-sales@claude