Skip to content

metadata-admin: FormFieldSpec declares dependsOn, one declaration for both halves - #5537

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-5040-formfieldspec-dependson
Aug 21, 2026
Merged

metadata-admin: FormFieldSpec declares dependsOn, one declaration for both halves#5537
qq9340100 merged 2 commits into
mainfrom
claude/issue-5040-formfieldspec-dependson

Conversation

@claude

@claudeclaudeBot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fixes#5040

FormFieldSpec is the authoring surface of a metadata-admin form layout — the
element type of FormSectionSpec.fields[], what an author actually writes. It
did not declare dependsOn. widgets.tsx held a second, inline description
of the same object
as WidgetProps.fieldSpec, and that one did — because two
registered widgets read it as their primary configuration:

widgethow it reads dependsOnwhat it decides
field-selectordependsOn || reference || 'objectName'which object's field catalog is offered
dynamic-configformData[dependsOn]context.dynamicSchemas[value]which sub-schema renders

One value, one channel (MetadataField hands the same object down), two
descriptions — disagreeing on the one key that decides what those widgets show.
So the only configuration that makes field-selector work,

{field: 'fields',widget: 'field-selector',dependsOn: 'objectName'}

was TS2353 for anyone who typed their spec. Nothing was broken at runtime, and
that is exactly why it survived: in-repo specs reach the form through as any /
loose types, so the authoring type was never asked the question it answered
wrongly.

What this PR does

  1. Declares dependsOn?: string | string[] on FormFieldSpec.
  2. Converges the two descriptions into one: WidgetProps.fieldSpecis
    FormFieldSpec now, not a structural copy of it.
  3. Moves the declaration (with VisibilityPredicate, which it depends on) into
    a leaf module views/metadata-admin/form-spec.ts. SchemaForm.tsx imports
    ./widgets.js, so a back-edge from widgets.tsx would close a cycle; a
    module that imports nothing is importable from both sides. SchemaForm.tsx
    re-exports both names, so no importer moves.
  4. Updates the now-stale comment at SchemaForm.widgetLabelling.test.tsx, which
    named this gap as "filed separately rather than papered over here" — its
    cases are typed Partial<FormFieldSpec> and its as FormFieldSpec cast is
    gone, so that file is now an incidental second pin.

The moved text is byte-identical apart from the one new key — verified by
reconstructing the extracted block minus the insertion and comparing it to the
pre-edit bytes.

The measurement this card actually turned on

Pointing WidgetProps.fieldSpec at FormFieldSpecnarrows two keys:
colSpan (number1|2|3|4) and fields (any[]Array<string | FormFieldSpec>). Whether widgets.tsx still compiled under that narrowing had
never been measured.

It compiles, with zero call-site changes. Enumerated first — widgets.tsx
reads exactly seven members off fieldSpec (options, dependsOn, language,
reference, multiple, label, field) and neithercolSpannor
fields is among them — then confirmed by running it:

> @object-ui/app-shell@17.6.0 type-check
> tsc --noEmit && tsc -p tsconfig.test.json
(exit 0)

No widening of FormFieldSpec was needed and none was made.

Reverse verification — prediction written before the run

Ablation: delete only dependsOn from FormFieldSpec, keep everything else.
No rebuild was needed or performed — tsc reads app-shell src/ directly, and
the only thing tsconfig.test.json resolves through built .d.ts is other
packages, which the ablation does not touch. The mutation was confirmed on disk
against the exact text removed (declaration lines 1 → 0) before anything was
read.

Predicted, then observed:

#predictedobserved
1source project goes red — widgets.tsx now reads dependsOnoff FormFieldSpec; TS2339, ≥3 sites✅ 4 × TS2339 at widgets.tsx:670, :2411 (×3)
2test project goes red with the card's exact error on the pinsTS2353 on PIN A/B and the three typed runtime specs; SchemaForm.widgetLabelling.test.tsx ×3
3both @ts-expect-error negative controls stay satisfied — no TS2578✅ no TS2578 anywhere
4vitest stays green — the widgets read the key at runtime regardless✅ 10/10 passed with the type removed

Two honest deltas from the prediction:

  • Unpredicted, same direction:DynamicConfigWidget.test.tsx also went red
    (5 × TS2353). It passes fieldSpec={{ …, dependsOn: 'driver' }} as an object
    literal, so the convergence pulled a pre-existing test under the authoring
    type's checking. More diagnostics, not fewer — evidence the convergence
    extended type coverage rather than just adding a key.
  • PIN E (the shape assert) failed as TS2339 on the indexed access rather than
    the TS2344 I guessed. Same direction, different code.

Line 1 of that table is the card in one line: those reads compiled before this
PR, because they were checked against widgets.tsx's own copy. Line 4 is why a
runtime-only verification of this card would have proved nothing.

The negative controls, and why they are not decoration

A pin that only showed dependsOn compiling would also pass if the type had
merely become looser. Both controls are @ts-expect-error, which is itself an
error (TS2578) once the line below it starts compiling — so a loosening
turns the file red:

  • PIN C — an undeclared key is still rejected, so the fix is not an index
    signature or any.
  • PIN DdependsOn: [{ field, param }] is still rejected. @object-ui/types
    already defines a canonical DependsOnInput (packages/types/src/form.ts)
    that admits that object arm. It is the obvious thing to reach for and it is
    wrong here: widgets.tsx:670 and :2411 both index [0] and use the
    result as a field name to look up in formData, so an object would arrive
    where a string is required and break both widgets silently. string | string[]
    is the shape this surface can consume. Converging the two is its own decision;
    this PR pins the refusal rather than taking it quietly.

Scope

This card lands the app-shell half only. There is a third inline copy of
this contract at apps/console/src/components/FormPage.tsx:278, which the card
and triage both frame as two descriptions converging. apps/console is held
by in-flight #5504 and is out of this card's file surface, so it is untouched
here and filed as an unassigned follow-up. #5504 is not addressed by this PR.

Every edit is inside packages/app-shell/src/views/metadata-admin/**.

Verification

All evidence below was re-run against 4660062e2, confirmed to be the head
this PR is testing
(git rev-parse HEAD == git ls-remote origin <branch>,
working tree clean). Every verdict is the line the gate itself printed.

Correction — an earlier revision of this description was wrong, and the way
it was wrong is worth recording.
It claimed the same gate results "against
4660062e2, the final commit on this branch". That commit existed locally
and was never pushed: I opened the PR at 66ca55b0e, then committed a fix and
did not push it. So the green was real and taken on a real tree — just not on
the tree CI was testing. Type Check correctly went red at 66ca55b0e with
TS2493 on line 186 of the new test file, which is precisely the defect the
unpushed commit fixed. The missing commit is now pushed and every gate re-run
at the confirmed head. A verified tree that is not the pushed tree is not
evidence, and the report should never have said otherwise.

On that TS2493, since it landed inside a pin file: it was not a pin,
and nothing was silenced to clear it. Line 186 is the runtime assertion
expect(fetchMock.mock.calls.map((c) => String(c[0]))). The mock was declared
vi.fn(async () => …) — zero parameters — so vitest typed mock.calls as an
array of empty tuples and c[0] was genuinely unreachable. The fix gives the
mock the parameter the thing it stubs actually has
(async (input: RequestInfo | URL) => …), so the call tuple carries the
argument the assertion reads. No cast, no widened type, no pin touched — and the
assertion still discriminates: formData carries a decoy under the widget's
default key (objectName: 'the_fallback_object') and the assertion demands the
exact URL built from the dependsOn-named field.

pnpm --filter '@object-ui/app-shell^...' build exit 0 (lock: VERDICT command-exit 0 · held 132s · waited 0s)
pnpm --filter @object-ui/app-shell type-check exit 0 tsc --noEmit && tsc -p tsconfig.test.json
pnpm exec vitest run --maxWorkers=1 <6 files> exit 0 Test Files 6 passed (6) · Tests 142 passed (142)
pnpm exec vitest run packages/app-shell/src/views/metadata-admin/
exit 0 Test Files 190 passed (190) · Tests 1955 passed | 1 skipped (1956)
(lock: VERDICT command-exit 0 · held 581s · waited 40s)
pnpm check:control-bytes ✅ OK (scanned 4585 tracked text file(s); skipped 85 binary)
pnpm check:esm-specifiers Specifier leg: no un-ledgered package emits an extensionless relative specifier
pnpm check:self-import ✅ No package names itself inside its own src/
pnpm check:phantom-deps ✅ Every in-scope import is declared by the package that publishes it
pnpm check:spec-symbols ✅ spec symbol derivation: 1285 files scanned against 4912 spec export names
pnpm --filter @object-ui/app-shell lint exit 0 ✖ 2508 problems (0 errors, 2508 warnings)

The 190-file metadata-admin suite is the complete test family for this PR's
entire file surface — every edit is inside that directory. It took two attempts:
the first hit the container's shared verify lock (exit 99, never acquired
after 9m00s) and was reported as not-run rather than claimed; the retry acquired
it and passed.

check:esm-specifiers is the family this diff most directly implicates — it
added two ./form-spec.js specifiers. check:published-dist and the full
check:node-esm-load load leg are not owed by this diff: both workflows
trigger only nightly, on workflow_dispatch, or on a push touching the gate's
own script — measured from their on: blocks, not assumed.

Lint scope, stated so it is checkable.pnpm lint is turbo run lint, i.e.
each package's own eslint .; the job for the only package this PR edits was run
in full. Population read from eslint's own config rather than guessed: 898 files
(--format json count), 0 errors, and all five changed files are provably in
that population. No type-aware linting is configured (no project /
projectService in eslint.config.js), so an app-shell edit cannot move the
verdict on any file in a package this PR does not touch.

dispatch-gates.mjs could not be used to derive the family list: it resolves its
repo root from its own location, so run from the objectui worktree it derived
against the objectstack tree and refused (exit 2) rather than answering
wrongly. The list above was derived by hand from objectui's own
.github/workflows/*.yml.


Generated by Claude Code


Generated by Claude Code

…or both halves (objectui#5040)
`FormFieldSpec` is the authoring surface of a metadata-admin form layout, and
it did not declare `dependsOn`. `widgets.tsx` held a SECOND inline description
of the same object as `WidgetProps.fieldSpec`, and that one did — because
`field-selector` (`dependsOn || reference || 'objectName'`) and
`dynamic-config` (`formData[dependsOn]` → `context.dynamicSchemas`) both read
it as their primary configuration. One value, one channel, two descriptions,
disagreeing on the key that decides what those widgets offer: so
{ field: 'fields', widget: 'field-selector', dependsOn: 'objectName' }
— the one configuration that makes that widget work — was `TS2353` for anyone
who typed their spec. It survived because in-repo specs reach the form through
`as any`, so the authoring type was never asked the question it answered wrong.
The declaration moves to a leaf module both halves import (`form-spec.ts`);
`SchemaForm.tsx` imports `./widgets.js`, so a back-edge would close a cycle.
The moved text is byte-identical apart from the new key, and `SchemaForm.tsx`
re-exports `FormFieldSpec` / `VisibilityPredicate` so no importer moves.
`string | string[]`, NOT `@object-ui/types`' wider `DependsOnInput`: both
readers index `[0]` and use it as a field name, so the object arm would arrive
where a string is required. Pinned as a refusal, not left to a later widening.
No runtime behaviour changes — the runtime already passed the key through.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014zHsbJoTkTZeJQ5DLbRXrE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.5 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-BtQHdgLg.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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)506.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.14KB59.49KB
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.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.94KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.75KB54.24KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
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.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
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)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
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)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.5 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-DRirhiYa.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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)506.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.14KB59.49KB
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.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.94KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.75KB54.24KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
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.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
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)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
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)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

Merged via the queue into main with commit eba3a6eAug 21, 2026
23 checks passed
os-sales pushed a commit that referenced this pull request Aug 21, 2026
…it (#5542)
objectui#5040 was not a missing key — it was that two hand-written descriptions
of one contract drifted, and nothing could notice, because each was only ever
checked against itself. PR #5537 converged the two app-shell descriptions into
views/metadata-admin/form-spec.ts. A third survived: apps/console FormPage.tsx
declared its own nine-key `interface FormFieldSpec`, under the same name, in a
different package, leaving that failure mode fully available.
Measured key by key before picking a route. The console's copy was a strict
subset — 9 of the shared type's 26 keys, every one identical in type, none
console-only — sitting in a position that describes an AUTHORED DOCUMENT:
`FormSectionSpec.fields`, read straight off the /meta/view/:name payload, the
same spec FormView metadata-admin renders (both files spell the same six-member
`type` union and call the element type `FormFieldSpec`). The narrow,
renderer-honoured shape is a different type that already exists in that file,
`RenderableField`. So this was one contract described twice, and the console's
description was wrong about the document: `visibleWhen`, `dependsOn`, `type`,
`options`, `immutable`, the recursive `fields` and ten more legal keys were
undeclared there — #5040's own symptom, "the type rejects the configuration the
runtime accepts".
Route: import, not rename. @object-ui/app-shell re-exports FormFieldSpec from
its package root (type-only, erased at build), FormPage.tsx imports it and
deletes the local declaration. Reachability is load-bearing: a type that cannot
be imported is a type that gets retyped. form-spec.ts itself is untouched.
FormPage.fieldSpec.test.ts is the pin. It reads the field-spec type back out of
the exported buildSections signature rather than naming it, so re-inlining a
local copy fails type-check even if the copy agrees on every key the day it is
written. Liveness controls keep it from being a phantom check: the removed
nine-key shape is pinned NOT equal to the shared type, RenderableField is pinned
not equal either, and an undeclared key is still rejected.
Fixes#5542
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant

@claude