fix(core,app-shell): resolve a relationship target from a reference STRING only — the carrier axis - #6718
Merged
Conversation
… STRING only
`resolveReferenceTo` (dataset designer) and its sibling
`resolveRelationshipTarget` (`chart-series.ts`) each branched on three CARRIERS
for the canonical `reference` key: a bare string, a one-element array (taking
element zero), and `{ object }`. objectui#6528 narrowed the SPELLING axis and
left the carrier axis for its own census; that census is now run.
`FieldSchema.reference` is a plain `z.string()`. `ObjectSchema.safeParse`
(spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
`['crm_account']` (`expected string, received array`) and
`{ object: 'crm_account' }` (`expected string, received object`).
The carrier census walked STRUCTURE, not text — JSON/YAML parsed and walked,
TS/TSX through the TypeScript compiler API — recording each hit's ancestor
property chain and its enclosing object's sibling keys, so a FIELD DEF is
separated from the other tiers that also spell `reference`. Across both trees:
607 hits at the field-def key position, 587 of them the bare-string carrier,
and ZERO array or `{ object }` carriers from any producer. Every dynamic
initializer at that position resolved to a string-typed source, and every
`reference` TYPE declaration in either tree declares `string`. The detector
demonstrably fires on the shape it hunted: the only array / `{ object }`
carriers found were the two green pins asserting this tolerance, plus one
framework lint fixture whose own rule (`refOf`) already reads string-only.
The array branch additionally made a silent PRODUCT decision — element zero
returned, the rest discarded. Nothing declares a multi-target value:
polymorphic lookup is an open Tier-3 gap in the spec's own audit report
("Current `reference` only supports a single target"), and the platform's one
polymorphic reference (ADR-0018 `xRef`) is a string with a sibling
discriminator, never a list.
Removed in BOTH files in one pass so the two canonicalizations cannot diverge.
The two carrier assertions were green pins on the tolerant behaviour — they
were the tolerance written down, not evidence for it — so they are converted to
refusal pins extending objectui#6528's `refuses the legacy spelling ...`
pattern, and re-widening the carrier turns them red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Collaborator
Author
|
The two census by-catch items the body refers to, now filed (neither is ridden along here):
Session for this work: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 Generated by Claude Code Generated by Claude Code |
Contributor
✅ Console Performance Budget
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
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6648
objectui#6528 narrowed both relationship-target resolvers to the single spec
SPELLING
referenceand deliberately left the CARRIER — the shape the value maytake — for its own census, because cutting on an unfinished census is exactly
what #6528 was filed to stop. That census is now run, to #6528's standard, and
it forces the removal.
The census — method first
Not a grep. A bare grep for
referenceconflates tiers (PR #6670'soptionsrow: 71 bare hits, zero on a
ListColumn), so this walked STRUCTURE:ancestor property chain.
each hit's ancestor property chain, the classification of its initializer by
AST node kind (string literal / array literal / object literal / dynamic),
and the sibling keys of its enclosing object literal.
Sibling keys are what make it key-position aware without depending on how a
literal is nested. Tier A = the key position these two resolvers actually
read — a FIELD DEF: a sibling
typenaming a relationship, or an ancestor chainrunning through
fields. Tier B = every other place the wordreferenceisa key.
Trees:
objectui@98188c284andobjectstack@8cb96ec(bothorigin/main;the framework tree was checked out fresh for this, not read off a stale
working copy). 10,439 files parsed. 16 parse failures, every one a
tsconfig*.jsonwritten as JSONC — enumerated and none of them a metadata document.
The census — population and result
referenceKEY hits, all tiers{ object }carrierThe 449 in the framework tree corroborates #6528's independently-taken count
(445 of 565 lookup / master_detail defs).
Every non-string Tier-A hit was run down individually.
chart-series.test.ts:202— the green pin thatasserted this very tolerance.
{ object }hits: 2 are the other green pins; 4 are translationentries for a metadata-form field literally NAMED
reference; 1 is aJSON-Schema property descriptor that itself says
type: 'string'; 1 is afield named
referenceoftype: 'text'; and 1 isobjectstack packages/lint/src/runtime-gate.object-writes.test.ts:358, a lintfixture — whose own rule's reader (
refOf) is already string-only, so thecarrier resolves to
undefinedthere and the fixture's assertion (aboutfinding paths) is unaffected. Filed separately, see below.
PARENT/ACCOUNTstring consts,foreignKey.referencedTable(declaredstring),OUTPUT_LOOKUP_OBJECTS(Record(string, string)), the two designer writers'referenceTo(declaredstring), andnullin blueprint tests whose schemais
z.string().nullable().referenceTYPE declaration in either tree declaresstring(28 of them), except threeunknownat tolerant read sites and onestring | string[]inauditHistoryDisplay.tswhose runtime reader refusesnon-strings anyway.
Producers of an array or
{ object }carrier at the field-def key position:ZERO.
The zero is not a blind query
Per this seat's rule, a zero does not exist until a positive control fires in
the same query shape — and here there are two:
{ object }detector itself demonstrably fires — it reportedarray and
{ object }carriers, at exactly the field-def key position. It isjust that every one it found was a test asserting the tolerance, or the one
lint fixture above. The detector can see the shape; there is nothing else
emitting it.
The open question the card named, run down
The card asked specifically whether any polymorphic / multi-target lookup is
intended to carry a list. It is not:
Field.referenceistype: stringin the spec's own JSON Schema, describedas "Target object name (snake_case)". Singular.
report: "Salesforce's WhoId/WhatId can point to multiple objects. Current
referenceonly supports a single target" — listed under Tier 3(T3-2), i.e. future work, not a shipped shape.
multiplekey means multiple values of the SAME target, notmultiple targets;
referencestays one string.xRef,approval approver values) is a string with a sibling discriminator, never
a list.
auditHistoryDisplay.lookupTarget, commented "skip polymorphic" — chose torefuse the non-string carrier rather than take element zero.
If multi-target lookups ever land, they land as a declared spec shape. Nothing
today declares one, and
ObjectSchema.safeParserefuses one.What changed
FieldSchema.referenceis a plainz.string(). Re-measured here on spec17.2.0, one lookup field:
reference: 'crm_account'reference: ['crm_account']invalid_type: expected string, received arrayreference: ['crm_account','crm_lead']reference: { object: 'crm_account' }expected string, received objectBoth resolvers branched on all three. Both are narrowed to the string carrier,
in one pass, in both files, so the two canonicalizations cannot diverge:
packages/core/src/utils/chart-series.ts—resolveRelationshipTargetpackages/app-shell/src/views/metadata-admin/inspectors/useDatasetFields.ts—resolveReferenceToThe array branch was a silent product decision, not a compatibility shim.
Handed a multi-target value it returned
raw[0]and discarded the rest — nowarning, no error, a plausible-looking answer. That is data loss, and it is the
precise shape AGENTS.md #0.1 forbids: the lenient consumer is where the wrong
producer hides.
The two existing tests were cover, not evidence
The carrier assertions in
useDatasetFields.test.tsandchart-series.test.tsread
.toBe('account')/.toBe('crm_account')— they were the tolerancewritten down. They are converted to refusal pins extending #6528's existing
refuses the legacy spelling ...shape (sameit.each, same"a producer emitting it is the bug" wording, same
toBeUndefined()), with athird case added per file for the multi-element array — the discarded-rest case
the old branch was silently answering.
Verification
Ablation, run after the fix was committed so the restore leg had a real
reference point. Precondition checked and stated: both test files import the
source relatively (
../chart-series,./useDatasetFields), not through thepackage
exports, so nodistis in the resolution path and no rebuild leg isrequired. Mutation =
git checkout origin/main --the two resolvers; proved ondisk by anchored counts, not by an editor exit code (
Array.isArray(raw)reintroduced 1/1, narrowed return gone 0/0, plus a non-empty
git diff HEAD --stat).The script carried
trap ... EXIT INT TERM.Direction predicted before running: RED, and specifically red on the six new
carrier pins only.
Exactly the six new pins moved; the other 56 stayed green. Restore proved by
observing state, not exit codes:
git hash-objectof each path equals its HEADblob,
git diff HEADempty,git status --porcelainempty.Green union, re-run after the final commit, at
adb1c9ec8:pnpm exec vitest runon both pinned files —Test Files 2 passed (2),Tests 62 passed (62)pnpm --filter @object-ui/core --filter @object-ui/app-shell type-check(
tsc --noEmit && tsc -p tsconfig.test.json) —Donefor both.--listFilesconfirms both edited test files are inside the test program, so "typecheck
clean" actually covers them.
pnpm --filter '@object-ui/core^...' --filter '@object-ui/app-shell^...' build— dependency closures built before any of the above.
pnpm check:control-bytes— its own verdict line:check-control-bytes: OK (scanned 5570 tracked text file(s); skipped 85 binary),plus a direct control-byte scan of the four edited files.
Lint was narrowed, and the narrowing is measured rather than assumed:
(1) the population comes from eslint's own config resolution, not a guess about
which files count; (2)
--format jsonreports 4 files linted, 0 errors, 4warnings, all four warnings pre-existing in
useDatasetFields.tsat lines193/277/324, outside this diff; (3) the root config extends
tseslint.configs.recommendedwith noparserOptions.projectand noprojectService— type-aware linting is off — so every file's verdict is afunction of its own text plus the shared config, and a diff touching four
source files and no config file cannot move any untouched file's verdict. CI
runs the full farm regardless.
Changeset
patchfor@object-ui/app-shelland@object-ui/core. Reasoning: thisremoves branches that no producer can reach on any document the spec accepts, so
no user-visible behaviour changes for any valid input — the census is the
evidence for that claim, not an assumption. It is not
minor(no newcapability, nothing additive) and not
major(the only inputs whose resultchanges are ones
ObjectSchemaalready refuses, i.e. producer-side defects thatshould surface rather than be absorbed). This matches the score #6528 took for
the same class of narrowing on the spelling axis.
Filed separately, not fixed here
objectstack— the lint fixture atpackages/lint/src/runtime-gate.object-writes.test.ts:358spells a carrier thespec refuses; its own rule already reads string-only.
objectui—packages/app-shell/src/utils/auditHistoryDisplay.tsdeclaresreference?: string | string[]andreference_to?: string | string[]andreads
reference_to ?? reference: a third site carrying both a widened carriertype and finding(app-shell):
resolveReferenceTosreferenceTobranch is unreachable now that the read door strips the key #6528's legacy-spelling chain. Its runtime read already refusesnon-strings, so there is no live defect — hygiene on both axes, and a
different axis from this card, so it is filed rather than ridden along.
Generated by Claude Code
Generated by Claude Code