fix(types,core): adopt the spec's date-default refinement; { preset } becomes an ADR-0089 legacy alias - #4246
Merged
Conversation
…}` 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
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 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 #4165
Executes the maintainer ruling. Quoted verbatim from the ruling comment:
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/spec17.0.0-rc.6):What did not survive is the issue's supporting claim that "
dashboard-filters.tsproduces it, and it is what is on disk" — inherited from the divergence prose incomplex.zod.ts. Measured:normalizeDateDefaultis called only fromresolveDashboardFilterDefs, whose only non-test callers areDashboardRendererandDashboardWidgetInspector— both read-side. Nothing writes a resolvedDashboardFilterDefback into a dashboard document.DateRangeValue, read byDateRangeFiltervia.preset/.from/.toand bybuildFilterCondition), not the stored declaration.examples/schema-catalogdashboard uses a bare name or no date default.globalFiltersentry oftype: '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. "
normalizeDateDefaultemits the bare name" was aimed at a producer-of-stored-metadata that does not exist; executing it literally would handDateRangeFiltera string it cannot read andbuildFilterConditiona 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").normalizeDateDefaulttherefore 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— thedefaultValuedivergence 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):.extend().omit()/.pick().safeExtend()never— still TS2322 on both remaining overrides, so retiring the third divergence did not re-open this doorThe schema now spreads the spec's
.shapeand re-attaches the spec's object-level rules by delegating: it re-parses the spec-owned keys throughSpecGlobalFilterSchemaitself 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/optionsFromare 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, exportingliftLegacyGlobalFilterDefault(per filter) andliftLegacyDashboardFilterDefaults(per document). Zod-free and offzod/complex.zod.tsdeliberately: the main@object-ui/typesentry re-exports runtime helpers, and pulling this from a*.zod.tsmodule 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'sresolveDashboardFilterDefslifts 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'sDashboardDesignPageis the console's/design/dashboard/:nameroute 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 athandleChange, the single write funnel — which also covershandleImport, whose document comes from outside the load path. The pin asserts the realdataSource.updatepayload 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/types17.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-symbolsthen failed on its ratchet, not on a new fork: the@object-ui/types:GlobalFilterSchemaALLOW entry granted by #4167 was stale. That exemption existed because the spread hid the derivation inside an object literal, whichreferencesSpecdeliberately does not descend into. The delegation putsSpecGlobalFilterSchemaback 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
carries the REFINEMENTredREFUSES the legacy { preset }stayed greenBoth 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 narrowdefaultValuefield type rejects first and the refinement never runs. Retiring objectui'sz.any()gives this schema that same narrow field, so the shadowing now happens here too: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 shapenormalizeDateDefaultproduces, 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
GlobalFilterSchemais exported from no barrel (zod/index.zod.tsandindex.tsboth checked); it reaches the outside only throughDashboardComponentSchema.globalFilters, which is used at type level only — there is no runtime.parseof a dashboard document anywhere in the repo. Runtime consumers of the new lift are@object-ui/coreand@object-ui/plugin-designer; runtime consumers ofresolveDashboardFilterDefsare@object-ui/plugin-dashboardand@object-ui/app-shell. Verified packages, using the downstream/prefix filter direction where a closure was needed:pnpm --filter '@object-ui/{types,core,plugin-designer,plugin-dashboard,app-shell}^...' build— Donetype-checkon all five — Donevitest run packages/types/ packages/core/ packages/plugin-designer/ packages/plugin-dashboard/— 143 files, 2358 tests passedpackages/app-shell/src/views/metadata-admin/inspectors/(theresolveDashboardFilterDefsconsumer) — passed in an 80-file / 829-test runeslinton all changed files — 0 errors (25 warnings, all pre-existing style;--max-warningsis deliberately unset repo-wide). Onereact-hooks/refsreport onDashboardDesignPagewas checked against the pristineorigin/mainfile and is pre-existing.check:control-bytes— OK (3948 files); plus a targetedgrep -naPself-scan of every file touched — cleancheck:spec-symbols— OK after the ratchet burn-downChangesets:
@object-ui/types/@object-ui/core/@object-ui/plugin-designer, all patch. Notskip-changeset.Generated by Claude Code