Repository navigation
fix(types,components): calendar date-value keys cross the JSON/TS boundary once (objectui#10293) - #10302
Conversation
…ndary once (objectui#10293) CalendarSchema.defaultValue / .value: the declaration becomes Date | string, the set the mirror already accepts (ISO 8601 string for JSON, Date for in-process callers, as on DatePickerSchema). The ui:calendar renderer coerces a string through toDisplayDate, so a date-only string selects the day it names west of UTC. Ledger rows removed from KnownDrift, WiderThanDeclared and WIDER_ARMS; header figures re-derived. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: ① Derived judgmentsStatic review of Ruling. D1-(iii) as adopted in 5617465269: 「镜像声明 spec 的 authoring 类型(ISO 字符串),渲染器补 string → Date 的 coercion…TS 声明记录实现( Claims verified:
Open question 1 (dropping Open question 2 (no ISO refinement on the string arm): CONSISTENT. No sibling date key carries a format refinement. Adding one would be a narrowing beyond the ruling's remit. Behaviour for currently rendering values:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI blocker, not this PR's:
Generated by Claude Code |
… navigation item type objectstack#19789 widened the spec's navigation item union with `doc`, and `NAV_TYPE_META` is a Record keyed by that spec-derived union, so objectui stopped compiling against @objectstack/spec built from objectstack main (the Spec Main Shape Gate, TS2741 on NavigationDesigner). - `doc` entry: `appDesigner.navTypeDoc`, `bg-blue-100 text-blue-700`, `BookOpen`. - The map is typed `Record<NavigationItemType | 'doc', ...>`: the pinned spec 17.4.0 has no `doc`, so a plain `doc:` key is an excess property there. The `| 'doc'` goes at the pin bump that ships `doc`. - `appDesigner.navTypeDoc` fallback in useDesignerTranslation and all ten locale packs. - Guard test: every discriminant of the installed spec's NavigationItemSchema renders a row with a resolved type badge (spec subset of map, not equality). - `doc` is deliberately NOT in QUICK_ADD_TYPES: an empty doc item fails the spec's book-or-doc requirement; authoring it is objectui#10188. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC (cherry picked from commit 6f970da)
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Resolves zod-mirror-parity.test.ts: keeps main's ledger moves (objectui#7759 group E, objectui#10280) and this branch's CalendarSchema row removals, and re-derives the KnownDrift and WiderThanDeclared header figures from the merged ledger. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3S
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…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>
Clause-②: yes
Closes #10293
Part of #7759 (group D, ruling D1-(iii): comments 5617221514, 5617465269 and 5617614225)
What changed
form.tsCalendarSchema.defaultValue/.value(TS declaration)Date | Date[]Date | stringform.zod.tsCalendarSchema.defaultValue/.value(mirror)z.union([z.string(), z.date()]).describe()text now names the ISO 8601 authoring typeui:calendarrenderer (packages/components/src/renderers/form/calendar.tsx)schema.value || schema.defaultValuetoDayPickeruntouchedtoDisplayDate(@object-ui/core, the objectui#10183 helper). ADateor nothing passes through unchangedzod-mirror-parity.test.ts)KnownDrift,WiderThanDeclaredandWIDER_ARMSThe docs page (
content/docs/components/form/calendar.mdx) documents the ISO string.Per-key judgement (ruling 5617465269 asks for it)
@objectstack/spec17.4.0 has noui:calendarprimitive. Itscalendarhits are all the list-view type. So this is an objectui-owned key, and the read site governs.z.date()arm stay? Yes, following in-tree precedent. Every settled date-value key inform.zod.tskeeps it:DatePickerSchema.defaultValue/.value(TSDate | string, mirrorstring | Date) and this schema's ownminDate/maxDate. JSON can never carry aDate. In-process callers thatsafeParseaDatekeep working.z.string(). No ISO-format refinement was added. No sibling date key has one, and it would narrow the mirror beyond what the ledger measured. This is listed as an open question in the report.The read site, measured before editing (base
5ea623eab)This used a probe render of
ui:calendaron a frozen clock2026-09-03T10:35Z, in forked vitest children:new Date(2026, 8, 15)"2026-09-15"(value or defaultValue)"2026-09-15T12:00:00Z""not a date"So a string already reached
DayPicker: date-fns parses it with the engine's ownDateparse. What was missing was the date-only convention. A date-only string is UTC midnight, read back in local time. After the change, the date-only row selects the 15th in both zones. Every other row is unchanged.Pins, and proof that they redden on revert
packages/types/src/__tests__/calendar-date-value-boundary-10293.test.ts: type-level equality of both faces withDate | string | undefined, plus a@ts-expect-erroronDate[]. At runtime it checks that the mirror accepts date-only, date-time andDatevalues and refuses a list.packages/components/src/renderers/form/__tests__/calendar.dateValueZone-10293.test.tsx: an ISO string selects the same day as the equivalentDate. The file has UTC cases, driven west cases (LA, with a rig case and a fixture-validity case) and driven east control cases (Shanghai). It is registered as a fifth pin inscripts/__tests__/date-only-zone-pins-10183.test.ts, the forks-pool driver that PR fix(components,plugin-detail,app-shell,fields): a date-only value renders the day it names west of UTC at four more sites (objectui#10183) #10266 built.ablation-replace.mjsin WRAP mode, anchorselected={toSelected(...)}replaced by the raw read). The anchor went from 1 hit to 0, and the blob went from6e729e18d454to3323137ca363. The driver went red:Tests 2 failed | 4 passed (6). The calendar pin failed on exactly one case, the west date-only case. Restore: the blob equals HEAD andgit diff HEADis empty.value?: Date | stringback toDate | Date[]).tsc -p tsconfig.test.jsonexited 2 with 7 errors: 4 in the new pin and 3 inzod-mirror-parity.test.ts, whereform.zod.ts#CalendarSchemabecame "not assignable to type 'never'" in the ledger assertions. Restore: the blob equals HEAD andgit diff HEADis empty.Local gates, at
248f23de5pnpm --filter @object-ui/types type-check: VERDICT command-exit 0.vitest run packages/types/plus the calendar pin plus the zone driver:Test Files 217 passed (217),Tests 4939 passed | 7 skipped. The 7 skipped are the driven zone cases in the normal run. The driver itself passed, so they ran in the forked child.pnpm --workspace-concurrency=2 --filter '@object-ui/components^...' build(8 of 47 projects): exit 0. Thenpnpm --filter @object-ui/components type-check, andvitest run packages/components/src/renderers/form/:62 passed,427 passed | 7 skipped.eslint --no-inline-config --format jsonon the 7 changed lintable files: 7 files, 0 errors, 15 warnings. All 15 are existingno-explicit-anyhits (calendar.tsxhad 3 onorigin/mainand still has 3). Type-aware linting is not configured (eslint.config.jshas noparserOptions.project), so this diff cannot change the verdict for any untouched file.check-changeset-presence,check:spec-symbols,check:new-line-citations(0 new),check:doc-types,check:doc-fences,check:doc-example-ids,check:changeset-claims,check:pending-changeset-literals,check:control-bytes,check:component-surface-parity,check:test-path-roots,check-changeset-no-major: all exit 0.check:doc-snippetsandcheck:doc-examples, which exit 2 with PRECONDITION NOT MET. They need a build of 34 packages. They are left to CI.Acceptance notes
Date[]. The issue said the declaration wasDate. Onmainit wasDate | Date[]. The ruling text and the dispatch both namestring | Date, soDate[]left. The effect is TypeScript-only. The mirror never accepted a list, no in-tree caller passes one, and the renderer still forwards an array unchanged, somultiplemode with an in-processDate[]still renders. The changeset states this. If the list arm should stay, the fix is to keepDate[]in the declaration and keep aKnownDriftrow for it. The report carries this as an open question.defaultMonth. That was true before this change and is unchanged, so the pins freeze the clock inside the month under test.valueundermode: 'multiple'throws at render, and every value the mirror accepts undermode: 'range'selects nothing.248f23de5carries a harness-shapedCo-Authored-Bytrailer that names the model. The os-dev contract asks for model-free trailer pairs. History is not rewritten; this is reported only.Session:
https://claude.ai/code/session_01877XiBYSaRCk2CU7cMSg3SGenerated by Claude Code