A producer/consumer contract conflict surfaced by the @objectstack/spec 17.0.0-rc.6 bump in objectstack#7100. Filed as a decision, not implemented — the deciding half belongs to the spec owner, and encoding either answer consumer-side would preempt it.
What changed
rc.6 added a cross-field refinement to GlobalFilterSchema.
Measuring it correctly matters, and the obvious probe is misleading — parsing a { preset } default against the bare spec schema DOES fail, but on the OLD field type (defaultValue is string | number | boolean, invalid_union, message Invalid input), and the refinement never runs. Isolate it by widening the field first — which is exactly the shape objectui's dialect has:
const widened = SpecGlobalFilterSchema.safeExtend({ defaultValue: z.any().optional() });
widened.safeParse({ field: 'created_at', type: 'date', defaultValue: { preset: 'last_7_days' } })
That is refused, and the message names the rule:
{"preset":"last_7_days"} is not a value a type: 'date' filter can resolve. Use one of three spellings: a preset name (today, yesterday, this_week, last_week, this_month, last_month, this_quarter, last_quarter, this_year, last_year, last_7_days, last_30_days, last_90_days); an ISO date such as 2026-01-15 or 2026-01-15T08:30:00Z, meaning that day exactly; or a date-macro token such as {today} or {30_days_ago} (the full vocabulary is DATE_MACRO_TOKENS in @objectstack/spec/data). custom is not among them — it is a dateRange.defaultRange sentinel that carries no bounds of its own.
The bare preset NAME (defaultValue: 'last_7_days') is accepted by both sides.
Why it bites objectui specifically
@object-ui/types' GlobalFilterSchema derives from the spec's and pins three deliberate divergences (packages/types/src/zod/complex.zod.ts) — the widened keys are options, optionsFrom and defaultValue. The third is this one:
defaultValue stays z.any() — normalizeDateDefault (framework#4475) lifts a date preset NAME into { preset }, and stored dashboards carry that object form, which the spec's string | number | boolean rejects.
So objectui does not merely tolerate { preset }: @object-ui/core's dashboard-filters.ts produces it, and it is what is on disk.
The evidence chain, end to end:
- framework#4475 introduced
normalizeDateDefault, which lifts a preset name into { preset }.
@object-ui/core/src/utils/dashboard-filters.ts runs it, so the object form is what objectui writes.
packages/types/src/zod/complex.zod.ts widened defaultValue to z.any() for exactly that reason, and said so in prose.
packages/types/src/__tests__/report-chart-query-spec-parity.test.ts → accepts the normalized date-preset default object has pinned it since.
Widening the FIELD used to be enough, because nothing else in the spec had an opinion. A refinement is not a field type, so it no longer is: any derivation that carries the refinement refuses the object form however defaultValue is typed.
The mechanical consequence, and what objectstack#7100's PR does
rc.6 closed both extension doors on this schema:
.extend() — what the code used to be — now throws at module load: "Cannot overwrite keys on object schemas containing refinements. Use .safeExtend() instead." It took six @object-ui/types suites down before any of them ran a test.
.safeExtend() — zod's own suggested replacement — runs, but is "safe" precisely in that it will not let you REPLACE an existing key's type. It types every incompatible override as never, so all three divergences stop compiling (TS2322 … is not assignable to type 'never').
Some spelling therefore had to change. PR #4169 composes by spreading SpecGlobalFilterSchema.shape — the spec's fields still flow in BY REFERENCE, the three keys are replaced, and the refinement is not carried. That is byte-for-byte the behaviour this schema had at rc.5, so the bump changes nothing here and neither answer below is encoded. A tripwire pins the standstill and goes red from either side (objectui stops accepting the object form, or the refinement is withdrawn/reworded upstream).
The options
A. Upstream relaxes in rc.7 — the refinement was not meant to exclude the stored { preset } form.
B. Upstream confirms intent — { preset } is deliberately not a resolvable date default.
- Then objectui migrates: change
normalizeDateDefault to keep the bare preset name, drop the defaultValue divergence (the other two are unaffected), adopt the refinement, and handle stored dashboards that already hold the object form.
- Scope estimate: one function in
@object-ui/core/src/utils/dashboard-filters.ts, the divergence and its prose in packages/types/src/zod/complex.zod.ts, the two pins in report-chart-query-spec-parity.test.ts, plus a read-path migration for stored dashboards (the part with real risk — until it runs, an existing dashboard fails validation). Small code, non-trivial data.
- Expressively this costs nothing: the preset NAME is already one of the three legal spellings, so it is a rewrite, not a capability loss.
C. Keep the divergence permanently. Rejected on principle, and named only so it is visibly rejected: objectui would accept a dashboard the platform refuses — the designer stays green and the save fails server-side. That is the tolerant-consumer failure AGENTS.md #0.1 exists to prevent, one level up.
Recommendation: ask upstream first (A vs B), never C. A and B differ only in who moves, and the answer is a fact about the spec owner's intent that we do not hold. Consumer-side we take neither until then; the standstill costs nothing while it lasts, because the spread preserves rc.5 behaviour exactly.
Blocking
PR #4169 stays draft until this is ruled. It carries the global_nav retirement (objectstack#7100) plus the rc.6 bump, which cannot compile apart.
A producer/consumer contract conflict surfaced by the
@objectstack/spec17.0.0-rc.6bump in objectstack#7100. Filed as a decision, not implemented — the deciding half belongs to the spec owner, and encoding either answer consumer-side would preempt it.What changed
rc.6 added a cross-field refinement to
GlobalFilterSchema.Measuring it correctly matters, and the obvious probe is misleading — parsing a
{ preset }default against the bare spec schema DOES fail, but on the OLD field type (defaultValueisstring | number | boolean,invalid_union, messageInvalid input), and the refinement never runs. Isolate it by widening the field first — which is exactly the shape objectui's dialect has:That is refused, and the message names the rule:
The bare preset NAME (
defaultValue: 'last_7_days') is accepted by both sides.Why it bites objectui specifically
@object-ui/types'GlobalFilterSchemaderives from the spec's and pins three deliberate divergences (packages/types/src/zod/complex.zod.ts) — the widened keys areoptions,optionsFromanddefaultValue. The third is this one:So objectui does not merely tolerate
{ preset }:@object-ui/core'sdashboard-filters.tsproduces it, and it is what is on disk.The evidence chain, end to end:
normalizeDateDefault, which lifts a preset name into{ preset }.@object-ui/core/src/utils/dashboard-filters.tsruns it, so the object form is what objectui writes.packages/types/src/zod/complex.zod.tswideneddefaultValuetoz.any()for exactly that reason, and said so in prose.packages/types/src/__tests__/report-chart-query-spec-parity.test.ts→accepts the normalized date-preset default objecthas pinned it since.Widening the FIELD used to be enough, because nothing else in the spec had an opinion. A refinement is not a field type, so it no longer is: any derivation that carries the refinement refuses the object form however
defaultValueis typed.The mechanical consequence, and what objectstack#7100's PR does
rc.6 closed both extension doors on this schema:
.extend()— what the code used to be — now throws at module load: "Cannot overwrite keys on object schemas containing refinements. Use.safeExtend()instead." It took six@object-ui/typessuites down before any of them ran a test..safeExtend()— zod's own suggested replacement — runs, but is "safe" precisely in that it will not let you REPLACE an existing key's type. It types every incompatible override asnever, so all three divergences stop compiling (TS2322 … is not assignable to type 'never').Some spelling therefore had to change. PR #4169 composes by spreading
SpecGlobalFilterSchema.shape— the spec's fields still flow in BY REFERENCE, the three keys are replaced, and the refinement is not carried. That is byte-for-byte the behaviour this schema had at rc.5, so the bump changes nothing here and neither answer below is encoded. A tripwire pins the standstill and goes red from either side (objectui stops accepting the object form, or the refinement is withdrawn/reworded upstream).The options
A. Upstream relaxes in rc.7 — the refinement was not meant to exclude the stored
{ preset }form.{ preset }considered when the three spellings were enumerated?B. Upstream confirms intent —
{ preset }is deliberately not a resolvable date default.normalizeDateDefaultto keep the bare preset name, drop thedefaultValuedivergence (the other two are unaffected), adopt the refinement, and handle stored dashboards that already hold the object form.@object-ui/core/src/utils/dashboard-filters.ts, the divergence and its prose inpackages/types/src/zod/complex.zod.ts, the two pins inreport-chart-query-spec-parity.test.ts, plus a read-path migration for stored dashboards (the part with real risk — until it runs, an existing dashboard fails validation). Small code, non-trivial data.C. Keep the divergence permanently. Rejected on principle, and named only so it is visibly rejected: objectui would accept a dashboard the platform refuses — the designer stays green and the save fails server-side. That is the tolerant-consumer failure AGENTS.md #0.1 exists to prevent, one level up.
Recommendation: ask upstream first (A vs B), never C. A and B differ only in who moves, and the answer is a fact about the spec owner's intent that we do not hold. Consumer-side we take neither until then; the standstill costs nothing while it lasts, because the spread preserves rc.5 behaviour exactly.
Blocking
PR #4169 stays draft until this is ruled. It carries the
global_navretirement (objectstack#7100) plus the rc.6 bump, which cannot compile apart.