…th faces (objectui#10334) (objectstack-ai#10338)
Closes objectstack-ai#10334
Part of objectstack-ai#7759
Clause-②: yes
## What
`DashboardComponentSchema.dateRange` now takes `@objectstack/spec`'s
`DashboardSchema.dateRange` **authoring** member by reference, on both
faces. This settles the objectui#7759 group F residue row
`complex.zod.ts#DashboardComponentSchema::dateRange`.
- `dateRange` left `DASHBOARD_SPEC_EXCLUDED`. Both the Zod mirror
(`specFieldsExcept(stripImportedDefaults(SpecDashboardSchema).shape,
…)`) and the TypeScript twin (`Omit` over the spec `Dashboard` input
type) read that one list, so both now project the spec member.
- The hand-written mirror element (a stripping `z.object` with
`defaultRange: z.string()`) and the hand-written TS member were deleted.
- Ledger: `'dateRange'` left
`WiderThanDeclared['complex.zod.ts#DashboardComponentSchema']`, and the
`WIDER_ARMS` row was removed. The WIDER header figures were re-derived
to 8 / 10 / 11 and 5 / 5 / 0 / 1 (after merging main at `2d76f4e67`;
main read 8 / 11 / 12 and 5 / 6 / 0 / 1), with a history sentence. The
pin 'the WIDER ledger's header figures are derived from its ARMS' is
green on them.
- `page-app-dashboard-spec-parity.test.ts`: `dateRange` was removed from
the Dashboard `omitted` list.
- Docs: `content/docs/guide/dashboard-filters.md` now says the shape is
the spec's and that unknown presets and keys are refused.
## Measurement (step 1): why the mirror was wider
I ran a tsc probe of the old mirror input against the old TS member, key
by key. It gave `Type '"defaultRange"' is not assignable to type
'never'`, so **only `defaultRange`** was wider: a bare `string` against
the 14-member `DateRangeDefaultRange`. `field` and `allowCustomRange`
agreed. The mirror also stripped unknown keys, which is not
type-visible, whereas the spec object is strict.
## Rule applied: rule 1 (spec-declared) and F1
The spec declares the key in both places I checked:
- installed 17.4.0 (`dist/ui/index.d.ts`): `dateRange: ZodOptional of a
strict ZodObject`, with `field?` string, `defaultRange` a `ZodDefault`
enum of the 14 names (`DATE_RANGE_DEFAULT_RANGES`, the presets plus
`custom`), and `allowCustomRange` a `ZodDefault` boolean;
- objectstack `main` `b81da66d`
(`packages/spec/src/ui/dashboard.zod.ts`): the same key set,
`strictObject` with named alias refusals, and
`z.enum(DATE_RANGE_DEFAULT_RANGES).default('this_month')`.
The read site, `resolveDashboardFilterDefs` in `@object-ui/core`,
already implements every arm: each preset lifts to `{ preset }` through
`PRESET_RANGES`, `custom` starts empty, `field` falls back to
`created_at`, and `allowCustomRange` is read with `!== false`. So no
renderer change was needed, and I judge the protocol right. The mirror
still authors no default, because every crossing goes through
`stripImportedDefaults`.
## Evidence (HEAD `2d76f4e67`, after merging `origin/main` past
objectui#10300 `a05c3506`)
- `pnpm --filter @object-ui/types type-check` (tsc, examples, test
config): exit 0, run under the verify lock.
- `pnpm exec vitest run packages/types/`: `Test Files 228 passed (228)`,
`Tests 5099 passed (5099)`. `core` and `plugin-dashboard` type-check,
and the three `dateRange` consumer suites pass (84 tests). Spec Main
Shape Gate: a spec built from objectstack main `9d81af71` was injected,
and `types` type-checks with 0 errors.
- New pin
`packages/types/src/__tests__/dashboard-daterange-spec-10334.test.ts`, 7
tests:
- type equality of the twin and of the mirror input with
`Dashboard['dateRange']`, plus a negative control;
- `invalid_value` at `dateRange.defaultRange` for `last_7_dayz`;
- `unrecognized_keys` at `dateRange`;
- all 14 names accepted;
- no default injected;
- verdict parity with the spec schema over 8 samples.
- **Reverse verification**, with the fix committed first and a trap
restore that uses absolute paths: I wrote the base-commit
`complex.zod.ts` and `complex.ts` into place. The on-disk markers
confirmed the mutation (bare-string `defaultRange` 1, `'dateRange'` in
the exclusion list 1, old TS member 1).
- The pin gave `Tests 4 failed | 3 passed (7)`.
- `tsc -p tsconfig.test.json` exited 2 with 3 errors: the pin's mirror
equality, plus `WiderLedgerMismatch` on
`complex.zod.ts#DashboardComponentSchema` and on `dateRange`.
- Restore used `git checkout HEAD -- FILE`. Afterwards `git diff HEAD`
was 0 bytes, and the HEAD and disk blob hashes matched for both files.
- **Forward compatibility (Spec Main Shape Gate reproduced locally).** I
built `@objectstack/spec` from objectstack `main` `b81da66d`, ran `npm
pack`, then `node scripts/spec-main-shape-gate.mjs inject`, which exited
0 and re-pointed zod 4.6.1. Against that spec:
- types type-check exit 0;
- the pin, the parity ledger, the Dashboard spec parity and twins-9736
gave `72 passed`.
- The pinned 17.4.0 install was restored afterwards with `pnpm install
--force`, verified by the spec 17.4.0 manifest declaring zod `^4.4.3`.
- Downstream: turbo build of the `@object-ui/plugin-dashboard^...`
closure (`12 successful, 12 total`), then `pnpm --filter @object-ui/core
type-check` and `pnpm --filter @object-ui/plugin-dashboard type-check`,
all exit 0. The three dateRange consumer suites
(DashboardRenderer.filters, dashboardAuthoredInputs, core
dashboard-filters) gave `80 passed`.
- eslint `--no-inline-config --format json` over the 5 changed TS files:
5 files, 0 errors. There are 13 `no-explicit-any` warnings, all on
untouched pre-existing lines. Type-aware linting is not configured (no
`parserOptions.project` in `eslint.config.js`), so the diff cannot move
any untouched file's verdict.
- These gates exited 0: `check-changeset-presence`,
`check:spec-symbols`, `check:new-line-citations` (`0 new citation(s)`),
`check-changeset-no-major`, `check:control-bytes`,
`check:changeset-claims`, `check:pending-changeset-literals`,
`check:installed-pin-claims`, `check:doc-fences`, `check-doc-links`.
- NOT MEASURED: `check:doc-snippets`. Reason: it exited 2 with
PREREQUISITE (it needs a scoped workspace build), so it is left to CI.
The doc edit adds no code block.
## Changeset
`.changeset/10334-dashboard-daterange-spec-authoring.md` is `minor` on
`@object-ui/types`, and it states the breaking change to the validator
only. An unknown `defaultRange` and unknown keys inside `dateRange` are
now refused where before they were accepted or stripped. The TS type is
unchanged in effect.
## Acceptance notes
- Shared ledger: objectui#10294, objectstack-ai#10302, objectstack-ai#10307 and objectstack-ai#10300 have all
landed. This head re-derives the WIDER header figures from the merged
`WIDER_ARMS`, and the header pin is green. The next ledger PR in the
chain, objectui#10387, re-derives after this one lands.
- Observation, left to the seat: the spec declares `defaultRange` with
`.default('this_month')`, while an authored `dateRange` that omits
`defaultRange` starts with no window at the read site. This PR does not
change that.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S)_
Co-authored-by: Claude <noreply@anthropic.com>
Closes #10286
Part of #7759
Clause-②: yes
This PR settles all four keys the card lists that could be decided (the card named five candidates; see the per-key sections). It settles them by the objectui#7759 ruling (comments 5617465269 and 5617614225). Where the spec declares a key, both faces align to the spec. For an objectui-own key, the read site is the truth. The fourth key,
FormSchema.mode, was first stopped because measuring it falsified the premise it was dispatched on. The seat then decided it under ruling D1-(ii), and it is retired in commitf31d5c2(see below).Per-key decisions
FilterField.operatorsand, through its element,FilterBuilderSchema.fieldsVIEW_FILTER_OPERATORSin@objectstack/spec/ui, 20 members, identical in installed 17.4.0 and on objectstackorigin/main67ebc84typealone (operatorsForFieldType); nothing readsfield.operatorsoperators?: ViewFilterOperator[], taken from the spec by type reference. Mirror: the 20 members spelled out.ContainerSchema.maxWidthcontainer.tsxmapsfalsetomax-w-noneand each size word to itsmax-w-*.truematches no branch.z.boolean()narrowed toz.literal(false), which matches the declarationHeaderBarSchema.variantheader-barhits in spec src and dist; the control grep hits)header-barrenderer readsactions,crumbs,rightContentandsearchoffschemaand takes no spread propsvariant?: never, mirrorretirementTombstone(...). The two faces used to offerfloatingandtransparentrespectively.Runtime reading, not a grep, as AGENTS.md requires before calling a declared key inert. I ran a one-off probe through the real
SchemaRendererand the real registry, varying only the one key, and deleted it afterwards:The TS face imports the spec type. The mirror spells the list out instead of importing
VIEW_FILTER_OPERATORS, because a raw spec value read inside a mirror must pass the objectui#8317 import boundary (imported-defaults-8317.test.ts). That boundary has no arm for a bare array, and adding one would widen this card's surface. The copy cannot drift silently: the parity ledger compares it with the TS face, which is the spec type, and the new pin compares it with the spec's array at runtime.FilterOperatorSchemaandFilterBuilderOperatorare unchanged. They still type a condition'soperator, which is outside this card's regions (see Acceptance notes).FormSchema.mode: retired under D1-(ii) (commit
f31d5c2)The dispatch premise was that the spec declares
create | edit | viewfor this key. It does not declare it for the plainformnode.mode: z.enum(['create','edit','view'])in the spec'scomponent.zod.tsbelongs toObjectFormPropsSchema, which is theobject-formblock. The spec has noformcomponent:element:formis retired whole at element grain, and'form'sits inPAGE_TYPE_ROADMAP. So this is an objectui-own key, and rule 2 applies.The read site: nothing reads
FormSchema.mode.ObjectForm,ModalFormandDrawerFormall consume their ownmode. TheFormSchemaobjects they build carry nomode. Theformrenderer reads none either. The same probe renderedmodeabsent / edit / read / disabled / create / view. The only difference in the HTML is a leakedmode="..."attribute on the DOMformelement. Theformrenderer's strip list does not namemode, so it rides...formPropsonto the element.So no live reader depends on
readordisabled, and none depends oncreateorvieweither. Ruling D(ii) (both faces dead, no spec declaration) points at retiring the key, not at the TS alignment the card asked for. That changes the direction the dispatch set. (Updated by the PM seat.) The seat decided it under the ruling's own D1-(ii): 「两面都死且 spec 无声明 ⇒ 退役」, whose table namesForm.modeas 零读点. Commitf31d5c2retires it on both faces:mode?: never.retirementTombstonewhose refusal namesobject-formfor create / edit / view, anddisabled: truefor a non-editable plain form. It was measured to disable the inputs and the submit button.The KnownDrift and WIDER rows are removed (KnownDrift 44/81, WIDER 16/24/30). No example, doc, skill or app authors
modeon a plainform.ObjectForm/ModalForm/DrawerFormare untouched. The DOMmodeleak remains only for unvalidated documents; the changeset says so.Ledger movement
KnownDrift:complex.zod.ts#FilterFieldSchemaandnavigation.zod.ts#HeaderBarSchemaleave whole, andcomplex.zod.ts#FilterBuilderSchemashrinks toonChange. The header now reads 44 entries and 82 keys, plus the history sentence.WiderThanDeclaredandWIDER_ARMS: four keys and five arms leave.HeaderBarSchemastays onlogo. The header now reads 16 / 25 / 31, split 5 / 20 / 0 / 6, plus the history sentence.zod-mirror-parityper UNION ARM — a per-key verdict over per-arm facts letDashboardComponentSchema.widgets' concrete gap hide behind a schema-node sibling (census ordered by the #7952 ruling) #8252) read the header's own spelling and are green.Pins and reverse verification
New file:
packages/types/src/__tests__/mirror-groups-cd-10286.test.ts, 12 tests. Each refusal has a lit control on the same schema (an undeclared key stays green, becauseBaseSchemais.passthrough()). Each face's TS half is a@ts-expect-errorchecked bytsc -p tsconfig.test.json.Reverse verification ran once per key, from the committed state (
cb0cca085), throughablation-replace.mjs: the anchor must hit, and the blob hash must move and then return to HEAD. Each leg was restored withgit checkout HEAD -- PATH, andgit diff HEADwas proved empty.z.array(FilterOperatorSchema)restoredz.boolean()restoredz.enum([...,'transparent'])restoredNo rebuild was needed: the pin imports the mirrors by relative path, so it reads
src.Verification on the final head
1ff5f45cf(merge oforigin/main8b1f06619)pnpm --filter '@object-ui/components^...' build(the dependency closure, 8 projects): exit 0.pnpm --filter @object-ui/types type-check(all three tsc projects): exit 0.pnpm --filter @object-ui/components type-check: exit 0. This is the one downstream consumer ofHeaderBarSchema/ContainerSchema.plugin-formandapp-shelldo not import the changed types (theirFilterField/ContainerSchemahits are other same-named symbols), so they were not run.vitest run packages/types/ examples/schema-catalog/plus the header-bar and container renderer tests: 255 files / 7307 tests passed.check-changeset-presence,check-changeset-no-major,check:spec-symbols,check:new-line-citations(0 new),check:control-bytes,check:doc-types: all exit 0..tsfiles (--no-inline-config --format json): 0 errors, 13 warnings, all pre-existingno-explicit-anyon untouched lines. The population isfiles: ['**/*.{ts,tsx}']ineslint.config.js. The config enables no type-aware linting (noproject/projectService), so this diff cannot move any untouched file's verdict.Acceptance notes (not filed)
FilterBuilderOperatorhasis_empty/is_not_emptyandFilterOperatorSchemahasis_null/is_not_null. That isFilterBuilderCondition.operator, recorded already in the parity file's objectui#7760 note, and outside this card's regions.FilterField.operatorshas no read site. The vocabulary is settled here; whether to honour the key or retire it is a separate question.header-bardeclares further keys its renderer does not read (title,logo,nav,left,center,right,sticky,height). They are listed from source reading only. Onlyvariantgot the runtime probe, and onlyvariantis this card's.Changeset
.changeset/10286-mirror-groups-cd-settled.md:minoron@object-ui/types. The breaking notes are in its body.Implemented by the os-dev agent dispatched from PM seat
session_01877XiBYSaRCk2CU7cMSg3S(domain:spec#1).Generated by Claude Code