Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): audit history reads the relationship target as reference, a bare string - #6836
Merged
Merged
Conversation
…ence`, a bare string
`AuditFieldDef` was the third reader of a relationship field's target object,
missed by objectui#6528 (which narrowed two resolvers to the spec spelling) and
objectui#6648 (which narrowed the same two to the string carrier). It declared
`reference_to?: string | string[]` / `reference?: string | string[]` and read
`def?.reference_to ?? def?.reference` — the legacy spelling first.
Both widenings are unfounded on the documents this reader is handed. Its one
caller chain is `RecordDetailView`'s History effect, which passes
`objectDef.fields` from `useMetadata().objects` — object metadata documents,
never ObjectUI's own view/field contract. `ObjectSchema.safeParse` (spec 17.2.0)
accepts `reference: '<name>'`, refuses `reference_to` by name, and refuses both
the array and `{ object }` carriers; a structure-walking census of both trees
found zero producers of either carrier at the field-def key position against 599
bare-string carriers, and `AuditFieldDef` held the only `string | string[]`
declaration of either key in either tree against 43 declaring `string`.
Observation-class, not a live defect: the runtime `typeof` narrowing already
resolved a non-string carrier to `null`, and `normalizeSchemaReferenceKeys`
stamps both snake_case spellings at the ingestion choke point — which is also
why dropping the `reference_to` arm loses nothing. That choke point exists so
per-consumer dual-key fallbacks cannot drift; this was one of them.
Both axes are pinned in the shape objectui#6528 / objectui#6648 established,
plus a `@ts-expect-error` pin for the carrier, which only `tsconfig.test.json`
can measure. The `predecessors` fixture moved from `reference_to` to
`reference`: the old spelling is refused by name, so it was never a document
this reader could legally be handed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
This was referenced Aug 29, 2026
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
|
os-sam
marked this pull request as ready for review
August 30, 2026 03:35
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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#6719
packages/app-shell/src/utils/auditHistoryDisplay.tswas a third reader of arelationship field's target object, missed by objectui#6528 (which narrowed two
resolvers to the spec spelling
reference) and objectui#6648 (which narrowed thesame two to the string carrier). Both narrowings now reach it:
Observation-class, not a bug. The runtime
typeofnarrowing already resolved anon-string carrier to
null, so nothing produced a wrong answer. What is removed is adeclared surface wider than anything that can reach it, plus a spelling preference
inverted relative to the contract.
The card's one open question, settled by measurement
It is fed object metadata documents. The arm is dropped.
lookupTargethas exactlyone caller chain:
RecordDetailView's History effect(
packages/app-shell/src/views/RecordDetailView.tsx), which passesobjectDef.fields,where
objectDefis an entry ofuseMetadata().objects. That list has one writer —MetadataProvider's type cache for metadata typeobject(TYPE_BY_STATE_KEY.objects).The two passes that decorate it on the way out,
mergeViewsIntoObjectsandattachInlineSubforms, writelistViews/formViews/form, neverfields; and thesession cache that could bypass the fetch path is registered for type
apponly. So thefieldsrecord reaching this helper is an object metadata document on every path.ObjectUI's own view/field contract does carry
reference_to—DetailViewFieldSchemain@object-ui/typesviews.zod.ts— butplugin-detailtranslates INTO it fromreference(RecordDetailDrawer,RecordMetaFooter), and none of that flows back intoobjectDef.fields. Note that even there the carrier isz.string(), never a list.Dropping the arm loses nothing even for a def that arrives spelling only the legacy key:
normalizeSchemaReferenceKeys(@object-ui/core) runs over everyobjectitem at theingestion choke point and stamps both snake_case keys from whichever spelling arrived
(pinned in
packages/core/src/utils/__tests__/reference-keys.test.ts). Its own docs givethe reason this reader must not keep a second copy of that tolerance — the choke point
exists "so per-consumer dual-key fallbacks can't drift" (AGENTS.md #0.1).
Re-derived census (against
origin/main, not the card's numbers)Structure-walked, not text-matched: JSON/YAML parsed and walked, TS/TSX/JS through the
TypeScript compiler API, recording each hit's ancestor property chain, its enclosing
object's sibling keys, and the carrier its value takes — so a field def is separated from
the other tiers that also spell
reference. Trees: this repo at26896c689, frameworkat
7404925. 2209 candidate files, 903 hits, 815 at the field-def key position.Spec probe (
ObjectSchema.safeParse,@objectstack/spec17.2.0).FieldSchema.referenceintrospects as
optional -> string.fields.planreference: 'crm_account'reference: 'crm_account', multiple: true(the fixture shape)reference: ['crm_account']invalid_type: expected string, received arrayreference: ['a','b','c']reference: { object: 'crm_account' }expected string, received objectreference_to: 'crm_account'unrecognized_keys— "Did you meanreference_totoreference?"referenceTo: 'crm_account'unrecognized_keysCarrier census at the field-def key position — every zero measured against a control
in the same query that hits:
referencereference{ object }onreferenceThe detector is not blind to the shapes it hunted: it did report 8
objectcarriersand 3
arraycarriers, and every one is another tier — a JSON-Schema property descriptor(
{ type: 'string', title: 'Reference' }), a form field literally namedreference,four generated i18n translation entries (
{ label: "Reference", helpText: ... }), twopackages/spec/liveness/field.jsonledger rows, and a memory-driver$nottest documentwhose own data column is called
reference. Zero of them is a producer of a relationshiptarget.
Type-declaration census —
AuditFieldDefheld the onlystring | string[]declaration of either key in either tree:
reference/reference_to:string | string[]reference/reference_to/referenceTo:stringunknown(structural pass-throughs)referenceTo: never(a retirement tombstone)Out of structural scope, stated rather than assumed:
.md/.mdxwere swepttextually only. Seven hits, all documentation prose of the form
"Spec reference: Kernel". None is a carrier.
Pins and ablations
Both axes are pinned in the shape objectui#6528 and objectui#6648 established. Ablations
were run from the committed implementation, each mutation confirmed on disk by counting
the injected and the removed text (not by an editor's exit code), each restored with
git checkout HEAD --on absolute paths under anEXIT INT TERMtrap and verified by anempty
git diff HEAD. Nodistis involved: the suite imports the source relativelyinside its own package, so no rebuild gates these readings.
Baseline
24 passed (24); post-restore re-measurement24 passed (24).def?.reference_to ?? def?.referencerefuses the legacy spelling reference_to,prefers reference on a partially-migrated def carrying both,does not resolve a display label through a refused spellingtypeof target === 'string'narrowingstring | string[]auditHistoryDisplay.test.ts(176,5): error TS2578: Unused '@ts-expect-error' directive,tsc exit=2Ablation C is the only thing that can measure a type-only narrowing, and it doubles as
proof the file is genuinely in the
tsconfig.test.jsonprogram rather than silentlyexcluded.
referenceToandreference_to_objectare pinned alongsidereference_toto match the sibling pins, but they stay green under ablation A — they were never read
here, so they document the refusal rather than carry it.
What moved
One existing fixture.
predecessorsspelled its targetreference_to; that is not merelynon-canonical on this surface, it is refused by name by
ObjectSchema.safeParse, sothe fixture was never a document this reader could legally be handed. Re-spelled to
reference, which the same parse accepts withmultiple: truealongside it — a fixturetriage decision, not a search-and-replace. No other test in either tree reads these
helpers (
collectAuditChanges/collectLookupIds/formatAuditValueappear in exactlythree files, all in
app-shell), and no test drivesRecordDetailView's history diff.Verification
All runs from the repo root, on the final commit
634719ffa:pnpm exec vitest run packages/app-shell/src/utils/__tests__/auditHistoryDisplay.test.ts—
Test Files 1 passed (1)/Tests 24 passed (24), executed file echoed aspackages/app-shell/src/utils/__tests__/auditHistoryDisplay.test.ts.pnpm --filter @object-ui/app-shell run type-check—tsc --noEmit && tsc -p tsconfig.test.json,both green. The second is what measures the
@ts-expect-errorpin, and what the CIType Checkjob runs.pnpm --filter '@object-ui/app-shell^...' build(the dependency closure first, so nothingbelow reads a stale
dist) — green.check-changeset-presence"2 sourcefile(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ...
Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the
explicit exemption and a complete answer to this gate";
check-changeset-no-major"Nochangeset declares a
majorbump";check-changeset-fixed"All workspace packages are inthe changeset fixed group";
check-changeset-overwrite"No pre-existing changeset wasmodified or deleted".
check-control-bytes— "OK (scanned 5647 tracked text file(s); skipped 85 binary)", plusa direct control-character sweep of the three changed files (no hits).
Lint was narrowed, and here is the measurement that says the narrowing excluded nothing.
Ran
eslint .insidepackages/app-shell(the repo-root flat config resolves upward)rather than
turbo run lintacross all 39 packages. (1) The receiving population iseslint's own selection from its own config, not my guess about which files count.
(2)
--format jsonreports 1016 files linted, 0 errors (2790 pre-existing warnings,which
eslint .does not gate on), and both changed files appear in that file list.(3) Invariance for untouched files:
eslint.config.jsextendstseslint.configs.recommended,not
recommendedTypeChecked, and itslanguageOptionsdeclares onlyecmaVersionandglobals— noparserOptions.project, noprojectService. Type-aware linting is off, soevery rule is single-file and AST-local; a diff that touches no config file cannot move the
verdict of any file it does not contain.
No changeset content, deliberately
Empty frontmatter, which the gate names as a complete answer.
AuditFieldDefandauditHistoryDisplayare package-internal: neither is re-exported frompackages/app-shell/src/index.ts, and the package'sexportsmap exposes only.and./styles.css, so no consumer can deep-import them. Runtime behaviour is unchanged onevery document that can reach the helper, for the normalizer reason above.
No
packages/specchange of any kind, and nocontent/docs/releases/edit.Generated by Claude Code
Generated by Claude Code