Skip to content

fix(types,core): adopt the spec's date-default refinement; { preset } becomes an ADR-0089 legacy alias - #4246

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4165-preset-alias-migration
Aug 11, 2026
Merged

fix(types,core): adopt the spec's date-default refinement; { preset } becomes an ADR-0089 legacy alias#4246
yinlianghui merged 1 commit into
mainfrom
claude/issue-4165-preset-alias-migration

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4165

Executes the maintainer ruling. Quoted verbatim from the ruling comment:

Ruling: spec stays strict — no widening. rc.6's refinement stands; the bare preset NAME is the single canonical spelling. objectui's side of the contract: change normalizeDateDefault to emit the bare name, and treat stored { preset } as a documented legacy alias per the ADR-0089 alias-retirement pattern — lift on read, rewrite on next save, retirement window recorded at the read site. No spec change, no hard data migration; stored dashboards stay safe through the alias window.

Premise check first — one sub-claim did not survive, the ruling did

The isolated probe reproduces exactly as the card describes on this tip (@objectstack/spec 17.0.0-rc.6):

WIDENED + {preset}:        REFUSED — "…is not a value a `type: 'date'` filter can resolve…"
WIDENED + bare name:       ACCEPTED
SPEC(bare) + bare name:    ACCEPTED
SPEC(bare) + {preset}:     REFUSED (invalid_union — the field type, not the refinement)

What did not survive is the issue's supporting claim that "dashboard-filters.ts produces it, and it is what is on disk" — inherited from the divergence prose in complex.zod.ts. Measured:

  1. normalizeDateDefault is called only from resolveDashboardFilterDefs, whose only non-test callers are DashboardRenderer and DashboardWidgetInspector — both read-side. Nothing writes a resolved DashboardFilterDef back into a dashboard document.
  2. Its output is the runtime value (DateRangeValue, read by DateRangeFilter via .preset/.from/.to and by buildFilterCondition), not the stored declaration.
  3. No stored artifact in the repo carries the object form: every examples/schema-catalog dashboard uses a bare name or no date default.
  4. framework#4475's own regression test says so in prose: "a globalFilters entry of type: 'date' declares its default as a bare preset NAME … so there is no object spelling to write".

So { preset } is objectui's value shape, never its stored declaration shape; the prose conflated the two spaces, and that conflation is what #4165 was filed about.

This changes one clause of the ruling and nothing else. "normalizeDateDefault emits the bare name" was aimed at a producer-of-stored-metadata that does not exist; executing it literally would hand DateRangeFilter a string it cannot read and buildFilterCondition a value it warns-and-skips — i.e. re-open framework#4475 exactly, a user-visible regression the ruling plainly does not intend ("stored dashboards stay safe"). normalizeDateDefault therefore keeps converting declaration to value, and its doc block now draws the distinction explicitly. The ruling's contract half — spec stays strict, bare name canonical, refinement adopted, divergence retired, alias lifted and rewritten — is executed in full.

What changed

@object-ui/types — the defaultValue divergence is retired and the spec's refinement is carried. Composing that needed a new spelling, because zod 4 closes every structural door on a refined object schema (all measured on rc.6 + zod 4.4.3):

door result
.extend() throws at module load
.omit() / .pick() throws — ".omit() cannot be used on object schemas containing refinements"
.safeExtend() runs, but types overrides never — still TS2322 on both remaining overrides, so retiring the third divergence did not re-open this door

The schema now spreads the spec's .shape and re-attaches the spec's object-level rules by delegating: it re-parses the spec-owned keys through SpecGlobalFilterSchema itself and forwards its issues. Nothing restates the spec's grammar, the rejection message an author sees is the spec's own, and a refinement the spec adds later flows in with no change here. options / optionsFrom are withheld from the delegated parse — the whole and only exemption.

The lift's home: a new zod-free module packages/types/src/dashboard-filter-alias.ts, exporting liftLegacyGlobalFilterDefault (per filter) and liftLegacyDashboardFilterDefaults (per document). Zod-free and off zod/complex.zod.ts deliberately: the main @object-ui/types entry re-exports runtime helpers, and pulling this from a *.zod.ts module would drag the whole zod graph into every consumer of that entry. It lifts only the exact single-key { preset: string } form — { preset, from, to } has no canonical spelling to lift to, and dropping bounds to manufacture one would lose data silently, so it falls through to the spec's named rejection.

Read site@object-ui/core's resolveDashboardFilterDefs lifts each stored entry before anything reads it, and warns when it does (ADR-0078: a silent lift can never be retired). The retirement window is recorded here, per the ruling.

Rewrite on save, proved not asserted@object-ui/plugin-designer's DashboardDesignPage is the console's /design/dashboard/:name route and the only place in objectui that both reads a stored dashboard and writes one back (dataSource.update('sys_dashboard', …)). It lifts at load (draft becomes canonical) and at handleChange, the single write funnel — which also covers handleImport, whose document comes from outside the load path. The pin asserts the real dataSource.update payload after an ordinary edit, not component state, because the payload is what reaches storage. Reverse-verified: neutralising both lifts turns two of those four pins red.

Retirement window (at the read site, and on the lift): opened 2026-08-11 at @object-ui/types 17.x; removable at 18.0.0, by which point every dashboard opened in the designer has been rewritten; a document still carrying the object form then gets the spec's named rejection, which is the intended end state.

The tripwire, and a second gate that fell out

#4169's standstill tripwire — "does NOT carry the spec rc.6 refinement…"went red exactly as designed. That red is the confirmation, so it is deleted with its note rather than repaired, and replaced by a block pinning the resolution in both directions.

check:spec-symbols then failed on its ratchet, not on a new fork: the @object-ui/types:GlobalFilterSchema ALLOW entry granted by #4167 was stale. That exemption existed because the spread hid the derivation inside an object literal, which referencesSpec deliberately does not descend into. The delegation puts SpecGlobalFilterSchema back in the initializer's AST outside any literal, so the guard sees the derivation again and the exemption is burned down (22 lines deleted, deletion-only).

Reverse verification — three fixes, three predictions, one inverted

removed predicted observed
the delegated refinement carries the REFINEMENT red red — and REFUSES the legacy { preset } stayed green
the read-site lift convergence + render pins red only the warning pin red
the two designer lifts designer pins red 2 of 4 red

Both surprises are reported rather than papered over, because each is a fact about what the tests can actually detect.

The inversion. #4165's body warned that probing the spec with { preset } measures nothing about the refinement — the narrow defaultValue field type rejects first and the refinement never runs. Retiring objectui's z.any() gives this schema that same narrow field, so the shadowing now happens here too:

{"preset":"last_7_days"}  -> invalid_union@defaultValue: "Invalid input"
"last_5_fortnights"       -> custom@defaultValue: "…is not a value a `type: 'date'` filter can resolve…"

So the two facts need two probes. That { preset } is refused is pinned on the outcome and issue path, deliberately not on the refinement's message — demanding that message would pin a rule that never ran. That the refinement is carried is isolated with a bogus preset string, which clears the field type and can only be refused by the cross-field rule; that assertion is the only one in the file that goes red if the delegation is dropped.

The weak read-site pins. A legacy declaration already rendered correctly before this change and still does with the lift removed, because { preset } also happens to be the runtime value shape normalizeDateDefault produces, and it passes non-strings straight through. That coincidence is why the object form looked harmless for so long, and how the prose drifted into calling it "the on-disk form". The convergence and bounds pins are therefore labelled in-file as regression guards, not evidence; the warning pin is the one that detects the lift's absence, and the behavioural teeth are the schema refusal and the rewrite-on-save. Claiming otherwise would be claiming coverage these tests do not have.

Consumer set, measured

GlobalFilterSchema is exported from no barrel (zod/index.zod.ts and index.ts both checked); it reaches the outside only through DashboardComponentSchema.globalFilters, which is used at type level only — there is no runtime .parse of a dashboard document anywhere in the repo. Runtime consumers of the new lift are @object-ui/core and @object-ui/plugin-designer; runtime consumers of resolveDashboardFilterDefs are @object-ui/plugin-dashboard and @object-ui/app-shell. Verified packages, using the downstream/prefix filter direction where a closure was needed:

  • build closure: pnpm --filter '@object-ui/{types,core,plugin-designer,plugin-dashboard,app-shell}^...' build — Done
  • type-check on all five — Done
  • vitest run packages/types/ packages/core/ packages/plugin-designer/ packages/plugin-dashboard/143 files, 2358 tests passed
  • packages/app-shell/src/views/metadata-admin/inspectors/ (the resolveDashboardFilterDefs consumer) — passed in an 80-file / 829-test run
  • eslint on all changed files — 0 errors (25 warnings, all pre-existing style; --max-warnings is deliberately unset repo-wide). One react-hooks/refs report on DashboardDesignPage was checked against the pristine origin/main file and is pre-existing.
  • check:control-bytes — OK (3948 files); plus a targeted grep -naP self-scan of every file touched — clean
  • check:spec-symbols — OK after the ratchet burn-down

Changesets: @object-ui/types / @object-ui/core / @object-ui/plugin-designer, all patch. Not skip-changeset.


Generated by Claude Code

…}` becomes an ADR-0089 legacy alias

Executes the maintainer ruling on objectui#4165: the spec stays strict, the
bare preset NAME is the single canonical spelling of a `type: 'date'` global
filter's `defaultValue`, and the stored `{ preset }` object form becomes a
documented legacy alias — lifted on read, rewritten on next save, with the
retirement window recorded at the read site.

- `@object-ui/types`: the `defaultValue` divergence is retired and the spec's
  rc.6 refinement is carried. A refined object schema in zod 4 closes
  `.extend()`, `.omit()` and (by typing overrides `never`) `.safeExtend()`, so
  the schema spreads the spec's `.shape` and re-attaches the spec's
  object-level rules by delegating to the spec schema itself — no local
  restatement of the grammar. `options`/`optionsFrom` divergences unaffected.
- New zod-free module `dashboard-filter-alias.ts` exports the lift, carrying
  the ADR-0089 window.
- `@object-ui/core`: `resolveDashboardFilterDefs` lifts each stored entry and
  warns, so a surviving legacy document is visible rather than silently
  tolerated.
- `@object-ui/plugin-designer`: `DashboardDesignPage` lifts into the editable
  draft and at the save funnel, so the next save persists the canonical name.
- #4169's standstill tripwire went red exactly as designed and is deleted with
  its note; the `check:spec-symbols` ALLOW entry for `GlobalFilterSchema` is
  burned down, because the delegation made the derivation visible to that
  guard again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 11, 2026 7:14am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.3 KB 350 KB
Entry file index-CZqODzPO.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.88KB 3.25KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 488.60KB 108.25KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 144.34KB 37.61KB
fields (index.js) 228.33KB 56.58KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.52KB 17.49KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.58KB 30.71KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 238.53KB 59.65KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.97KB 49.90KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.31KB 26.76KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 07:23
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit abb0f81 Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4165-preset-alias-migration branch August 11, 2026 07:24
Sign up for free to 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.

spec rc.6 refines a date GlobalFilter.defaultValue to reject { preset } — the exact shape objectui's normalizer produces and stored dashboards carry

2 participants