… preset label half
Two tie-restorations on the date-range preset vocabulary. Both copies were
byte-faithful to `@objectstack/spec` 17 today — which is when this is cheapest,
and when a pin is easiest to write badly, because everything passes either way.
1. `DashboardComponentSchema.dateRange.defaultRange` (packages/types) was a
hand-written 14-member union restating `DATE_RANGE_DEFAULT_RANGES`. It is now
`DateRangeDefaultRange` imported from `@objectstack/spec/ui`. That union is
what typechecks every TS-constructed dashboard through
`resolveDashboardFilterDefs`, so a preset the spec ADDS would have been a
legal document objectui's own types said could not exist — the same
"narrower than the contract it implements" shape as objectui#4163's `label`.
No gate could see it: `check:spec-symbols` rule 1 matches by NAME and an
inline union on an interface member has no symbol to collide with, and rule
2's claim heuristic was waved through by the `SpecGlobalFilter` reference a
few lines up.
2. `packages/i18n/src/__tests__/dashboard-range-preset-labels-4984.test.ts` pins
the LABEL half of the same fan-out, mirroring the BOUNDS half already pinned
in `packages/core/src/utils/__tests__/dashboard-filters.test.ts`.
`DashboardFilterBar` builds its key as a template literal, so
`check:i18n-keys` cannot see it and a new preset would render as its own
member name with underscores swapped for spaces, in ten locales, with every
gate green.
The published type surface does not move. Measured, not assumed: `dist/` and the
out-of-tree `packages/types/tsconfig.tsbuildinfo` were both cleared before each
build (composite `tsc` skips emit outright if the build info survives), the
emitted declarations were compared by hash (1 of 55 files moved, `complex.d.ts`,
the inline union collapsing to the alias), and reachability was measured with
the TypeScript checker rather than grepped, since `export *` propagates a symbol
without naming it: 679 reachable exports from `dist/index.d.ts`, 22 from
`dist/complex.d.ts`, and `defaultRange` resolving to the identical 14
string-literal members on both sides.
Out of scope, per the card's own boundary: the deliberately-looser zod twin at
`packages/types/src/zod/complex.zod.ts` (ledgered under `SpecDashboardFields`;
belongs to #2231's pile) and `FilterBuilderDateRangePreset`, a different
vocabulary rather than a copy of this one.
Fixes #4984
Co-authored-by: Claude <noreply@anthropic.com>
Fixes #4984
Two tie-restorations on the date-range preset vocabulary. Triage's framing is the
one worth keeping in view: both copies are byte-faithful to
@objectstack/spectoday — which is when this is cheapest, and also when a pin is easiest to write
badly, because everything passes either way. The evidence that the pins
discriminate is therefore the point of this PR, not a footnote; it is in
Measurements below.
All gate/test results below were produced on this branch at
22b883bc7, with aclean working tree.
1 —
dateRange.defaultRangeis bound to the spec, not restatedpackages/types/src/complex.tscarried a hand-written 14-member union under acomment claiming "Aligned with @objectstack/spec DashboardSchema.dateRange". It is
now
DateRangeDefaultRangeimported from@objectstack/spec/ui— one line plusthe import — which makes that claim structural instead of prose.
The union was byte-faithful (all 14 members, same order), so nothing a user hits
changes today. What was missing is the tie.
resolveDashboardFilterDefstakesPick<DashboardComponentSchema, 'globalFilters' | 'dateRange'>, so this union iswhat typechecks every TS-constructed dashboard: a preset the spec ADDS would have
been a legal document that objectui's own types said could not exist — the same
"narrower than the contract it implements" shape as objectui#4163's
label, whoseconsequence was that the bad reads were invisible to
tsc.No gate could report it, and that is worth recording as the objectui#4607
asymmetry in the wild:
check:spec-symbolsrule 1 matches by NAME, and an inlineunion on an interface member has no colliding symbol to match; rule 2's claim
heuristic fires only when the declaration references nothing spec-bound, and
DashboardComponentSchemareferencesSpecGlobalFiltera few lines up, so thetie test waved the whole interface through.
2 — the LABEL half of the fan-out is pinned
New:
packages/i18n/src/__tests__/dashboard-range-preset-labels-4984.test.ts.objectui#4167 made
@object-ui/core'sDATE_RANGE_PRESETSthe spec's own arrayby reference, so a preset the spec adds reaches the dashboard dropdown for free.
Two things have to follow it, and only one was pinned:
packages/core/src/utils/__tests__/dashboard-filters.test.ts("every offered preset resolves to date-macro bounds") already covers it.
DashboardFilterBar.tsx:105builds the key as atemplate literal, so
check:i18n-keyscannot see it, and the call site degradessilently: a new preset renders as its own member name with underscores swapped
for spaces —
next_weekas "next week", in Chinese, Japanese and Arabic alike —with every gate green.
The new file is deliberately the mirror of the bounds-half test: same source of
truth (
DATE_RANGE_PRESETS, never a copy of it), same "every offered preset …"shape, three assertions — a non-vacuity guard, the forward tie (every preset has
an
enlabel), and the reverse tie (no orphan label left by a preset the specremoved).
Two placement decisions, both deliberate:
@object-ui/i18n, not in core. The claim spans the vocabularyand the locale packs.
@object-ui/i18ndepends on@object-ui/core, so itreaches both; core cannot reach the packs without inverting that edge.
@objectstack/specis deliberately not imported there — it is not adependency of that package (
check:phantom-depswould be right to object), andreading the vocabulary through
@object-ui/coreis the same reference anyway,already pinned by
toBein the bounds-half file.enonly, on purpose.enis where the vocabulary/label tie lives and whatfallbackLng: 'en'degrades to. The 10-locale spread is the i18n channel'sexisting business:
en-zh-key-parity.test.tscarrieszhoff this pin for free,and the remaining eight packs are tracked under objectui#2872 part a.
Measurements
The parity test's own mutation legs — measured RED, direction predicted first
Predicted before running: each leg turns exactly one assertion red, and the other
two stay green. Both legs restored under
trap … EXIT INT TERM; the anchor'spristine occurrence count was asserted before mutating and re-read after, and
the restore was confirmed by hash.
last_90_daysenkeyenlabel" RED,missing=['last_90_days']expected [ 'last_90_days' ] to deeply equal []— Tests 1 failed | 2 passednext_week: 'Next week',(a label with no preset)orphans=['next_week']expected [ 'next_week' ] to deeply equal []— Tests 1 failed | 2 passedBoth legs restored: file hash back to
a16f3e78…and the anchor counts back totheir pristine values (1 and 0).
No rebuild step is owed by these legs, and that is measured rather than
asserted. The root vitest config aliases
@object-ui/coretopackages/core/src, and../localesis a relative source import — nothing here isserved from a
dist/that a stale build could hold green. A RED leg is itself theproof: had the test been reading built output, mutating
packages/i18n/src/locales/en.tswould have left it green.
Clause ② — the published type surface does not move
The
tsbuildinfotrap was real here:packages/typesiscomposite: truewith notsBuildInfoFile, so its build info lands atpackages/types/tsconfig.tsbuildinfo— outside
dist/. Both baseline and after-build cleareddist/and that file(verified empty before each
tsc); had it survived,tscwould have skipped emitand "no movement" would have been an artefact of the measurement.
Bytes — 1 of 55
.d.tsfiles moved:The other 54 are hash-identical. The movement is exactly the expected shape: the
inline 14-member union collapses to
defaultRange?: SpecDateRangeDefaultRange;,plus the added named import and the doc comment.
Surface — no movement, measured with the TypeScript checker, not grepped
(
export *propagates a symbol without naming it, so a grep over the emitted textcannot see reachability). Resolving the emitted declarations through the checker
gives byte-identical JSON before and after:
dist/index.d.tsdist/complex.d.tsDashboardComponentSchema.dateRange.defaultRangemembersDateRangeDefaultRangedoes not become a new export of@object-ui/types— itis an
import typeconsumed inline, and the reachable-export count is unchanged.Reverse verification against the rebuilt
.d.ts(two legs, distinguishableexit codes, so the instrument is falsifiable): a consumer assigning all 14 members
compiles (
exit 0); one assigning'next_week'fails (exit 2) withTS2322: Type '"next_week"' is not assignable to type '"today" | … | "custom" | undefined'.The error enumerating the full union is the evidence that the fresh emit was read
rather than a cache.
Gates and tests — all at
22b883bc7Exit codes captured before any pipe; each verdict quoted from the gate's own output.
check-changeset-presence✅ 2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)changeset:check✅ All workspace packages are in the changeset fixed group./✅ No changeset declares a major bump.check:spec-symbols✅ spec symbol derivation: 1291 files scanned…/✅ spec alignment claims: …check:control-bytes✅ check-control-bytes: OK (scanned 4719 tracked text file(s)…)check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.check:self-import✅ No package names itself inside its own src/.check:i18n-keysEvery in-scope call-site key resolves against the en pack (2919 keys)…check:i18n-driftNo en value changed in this range.(0 en values changed — the locale packs are untouched)check:esm-specifiersno un-ledgered package emits an extensionless relative specifiercheck:action-forward-paritycheck-lint-coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errorscheck-type-check-coverage✅ type-check coverage: 45/46…/✅ test type-check coverage: 41/41…check:i18n-dead-keysis report-only by its own header, and ran clean of anythingattributable here.
Type-checks (script name echoed in each, so none was a zero-match no-op):
@object-ui/types,@object-ui/i18n, and the three packages that actuallyreference
defaultRange—@object-ui/core,@object-ui/plugin-dashboard,@object-ui/app-shell— alltype-check: Done, exit 0, after building eachdependency closure first.
Tests:
vitest run packages/i18n packages/core/src/utils/__tests__/dashboard-filters.test.ts→ 52 files, 925 tests, all passed, including both halves of the fan-out (the
existing bounds-half pins and the three new label-half assertions).
Lint: the repo gate form
eslint .for both touched packages exits 0 (warningsonly, all pre-existing). A full-tree
eslint . --no-inline-configrun alsocompleted locally in 342 s: the population is 3524 files read from eslint's own
--format jsonoutput, both changed files are in it, and neither carries anerror —
complex.tshas 7 pre-existingno-explicit-anywarnings at lines 94, 169,264, 338, 421, 718, 723, none inside the edited ranges (line 20 and 787-809),
and the new test file has zero findings.
Out of scope — decided, not missed
Per the card body's own boundary:
packages/types/src/zod/complex.zod.ts(
defaultRange: z.string().optional()), whose ledger entrySpecDashboardFieldssays so in as many words — it belongs to Unify hand-written @object-ui/types zod with @objectstack/spec/ui (ListViewSchema drift) #2231's pile, and converging the TS
union does not require touching it.
FilterBuilderDateRangePresetinpackages/types/src/data-protocol.ts— adifferent vocabulary for the filter builder, already renamed off the spec's
symbol name with the reason at the declaration. Not a copy of this one.
Neither is addressed here; both remain open questions for their own owners.
Generated by Claude Code
Generated by Claude Code