Repository navigation
Commit 544aca2
fix(core,fields): refuse an impossible calendar day in date-times and in the sub-grid date columns (objectui#10301) (#10459)
Fixes #10301
Clause-②: no
The shared date path now refuses a **date-time** written on a calendar
day that does not exist (`2026-02-30T10:00:00Z`). It used to render as
`Mar 2, 2026, 10:00 AM`. The sub-grid's `date` and `datetime` columns
now reach that refusal too. This is the date-time half of the
objectui#10026 ruling (`5749269396`, direction A: "a silently wrong day
is the failure North Star rule 4 forbids"). Triage graded it an
inherited branch (`5819858578`), so no new decision was needed.
Implemented by the `os-dev` agent that the `domain:ui#4` seat
dispatched, session
`https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C`.
## What changed
- **`toDisplayDate` (`@object-ui/core`) gains one arm.** A string
matching `ISO_DATETIME_RE` whose first ten characters fail
`isRealCalendarDate` becomes an Invalid Date, the same answer
objectui#10026 gave the date-only spelling. The regex is spelled exactly
like `dataset-format.ts`'s `ISO_DATETIME_RE`, for the reason the file
already gives for `ISO_DATE_ONLY_RE`: one spelling per convention.
`isRealCalendarDate` is still called only from `date-display.ts` and the
filter builder. No consumer has a second realness check.
- **`GridField`'s `temporalText`:**
- The `date` arm no longer builds `new Date(y, m - 1, d)` by hand. It
passes the verbatim `YYYY-MM-DD` to `toDisplayDate`.
- The `datetime` arm takes its validity from `toDisplayDate`. It still
formats the engine's `new Date(raw)`, the same split
`DateTimeCellRenderer` uses.
- A refused value shows the raw stored string, which is this surface's
existing face for a value it cannot parse (objectui#3569).
- **`DateTimeCellRenderer` needed no code change.** Since objectui#10299
its validity guard already reads `toDisplayDate`, so the new arm reaches
it through that guard. Only its comment changed: it now names the
date-time case, and it warns that the guard is the only thing standing
between this cell and a rolled `Date`. Ablation 2 below proves that.
- One changeset, `.changeset/10301-refuse-impossible-datetime-day.md`
(`@object-ui/core` patch, `@object-ui/fields` patch).
### Two files outside the claimed surface (comment and prose only)
Both edits correct a sentence that this change makes false.
- `packages/core/src/utils/dataset-format.ts`: the `ISO_DATETIME_RE`
docblock said `2026-02-30T09:30` "renders rolled over" and that refusing
it was an open question. The measure's datetime arm now renders the
refusal dash. A new pin covers this (`a dataset measure agrees with the
list cell on the refusal`).
- `.changeset/10026-refuse-impossible-calendar-day.md` (pending): its
last sentence said "A value that carries a time … still rolls". It now
points at this entry, the same way PR objectui#10299 corrected the
objectui#10110 changeset. Only the prose changed; the front matter is
untouched. `check-changeset-overwrite` reports this edit as its case 2,
"correcting a declaration on purpose".
## Mechanism assumptions (from the dispatch), measured
- **A1: timezone edge. Holds.**
- The arm reads `value.slice(0, 10)` of the stored string, and no
surface converts a string before the check:
- the `datetime` cell passes `coerceToSafeValue(value)`, which leaves a
string a string;
- the sub-grid `date` arm uses `toDateInputValue`, which keeps a
string's leading day verbatim;
- the sub-grid `datetime` arm passes the raw string.
- Only a `Date` or number input is converted, and neither can carry a
nonexistent day.
- A conversion before the check could only hide a refusal, never cause a
false one: a parsed `Date` always names a real day. So the refusal pins
also cover the direction A1 worries about.
- Pinned: `2026-02-28T23:30:00-05:00` (March 1st 04:30 in UTC) is
**not** refused, at three places: `toDisplayDate` (same instant as
`Date.parse`), the `datetime` cell, and the sub-grid `datetime` column.
- **A2: one parse step. Holds, and one premise is stale.** The card says
`DateTimeCellRenderer` "hands its formatters an already-parsed `Date`".
That is true, but its guard already reads `toDisplayDate`, so no
rerouting was needed there. `temporalText` now reaches `toDisplayDate`
on both arms. No consumer calls `isRealCalendarDate` a second time.
- **A3: other date-time display paths in `@object-ui/fields` /
`@object-ui/core`.** Grep for `new Date(` and for `toLocale*` /
`Date.parse`, outside tests:
- **Reach the refusal with no change:**
- `DateField` and `FormulaField` (validity through `toDisplayDate`);
- `DateTimeField` readonly (`formatDateTime` on the raw value);
- `lookupColumnDisplay`'s `$date` fallback (`formatDate`);
- `DateCellRenderer`, whose `title` uses `new Date(safe)` only after the
guard;
- the dataset measure's datetime arm.
- **Not display paths:** `FormulaFunctions`, `validation-engine`,
`date-macros`, `dashboard-filters`.
- **Still rolls:** the editor adapter `toDateTimeInputValue` in
`nativeDateValue.ts`. Measured: the editable `DateTimeField` shows
`2026-03-02T10:00` for a stored `2026-02-30T10:00:00Z`, and so does the
sub-grid's editable `datetime` cell. That is out of scope and goes to
the report as a finding.
- **A4: the objectui#10026 pins stay green.**
`date-display.impossibleDay-10026.test.ts` and
`date-carriers.impossibleDay-10026.test.tsx` are unchanged by this diff.
They are green in every run below, including the ablation that deletes
the new arm.
## Tests
New pins:
-
`packages/core/src/utils/__tests__/date-display.impossibleDateTime-10301.test.ts`:
`toDisplayDate`, the four formatters and the dataset measure, with the
offset control and the positive control.
- Positive control: `2026-02-28T10:00:00Z` renders `Feb 28, 2026, 10:00
AM`, and its compact parts are `2/28/2026` and `10:00 am`.
-
`packages/fields/src/__tests__/datetime-carriers.impossibleDay-10301.test.tsx`:
five faces, each compared byte for byte with its own `not-a-date`
output. The faces are the `datetime` cell, the `date` cell, readonly
`DateTimeField`, readonly `DateField` and `FormulaField` with
`return_type: date`. The file also covers the affordance, the offset
control and the positive control.
- `packages/fields/src/widgets/GridField.impossibleDay-10301.test.tsx`:
the read-only table and list mode, `date` and `datetime` columns, plus
controls.
- An impossible day shows the stored string.
- Two further `date`-column side effects of reading the shared step,
measured on base sources before they were pinned: `2026-13-45` used to
render `Feb 14, 2027` and now shows as stored; `0026-08-01` used to
render `Aug 1, 1926` and now renders year 26, as the `date` cell does.
Runs. All go through the shared verify lock and run in UTC, which
`vitest.config.mts` pins.
| run | tree | result |
| --- | --- | --- |
| premise | base sources, with a pre-commit draft of the new pins (one
sub-grid face, 43 cases) | 29 failed, 14 passed (the controls). Rendered
`Mar 2, 2026, 10:00 AM`, cell `3/2/2026 10:00 am`, sub-grid `Mar 2` |
| fix | the same pre-commit draft of the pins, on the fixed sources | 3
files, 43 passed. Not re-run on the committed pin files, which hold 51
cases at the first commit (the second sub-grid face) and 55 at head (the
out-of-range month and year-below-100 cases). The ablation rows below
were run on the committed tree |
| ablation 1: new arm deleted from `toDisplayDate` | committed tree | 27
failed: 5 core, 18 fields faces, 4 sub-grid `datetime`. Both
objectui#10026 files green, and the sub-grid `date` cases green |
| ablation 2: `datetime` cell guard back to `new Date(safe)` | committed
tree | 6 failed, exactly the `datetime` cell cases. Core pins green |
| ablation 3: sub-grid `date` arm back to `new Date(y, m - 1, d)` |
committed tree | 12 failed, exactly the `date`-column cases in both
faces |
| ablation 4: sub-grid `datetime` guard back to `new Date(raw)` |
committed tree | 4 failed, exactly the `datetime`-column cases |
| union of 57 suites naming `toDisplayDate` / `isRealCalendarDate` /
`DateTimeCellRenderer` / `GridField` / `temporalText`, plus the
`date-display` / `dataset-format` suites | HEAD `436888655` | 53 files
passed, 4 skipped; 1171 tests passed, 40 skipped |
- Each ablation went through `ablation-replace.mjs`. The tool checked on
disk that the anchor count went 1 to 0 and the blob changed, and after
each run it restored the file to its HEAD blob with an empty `git diff
HEAD`.
- No build was involved: vitest aliases `@object-ui/core` to
`packages/core/src`.
- The 4 skipped files in the union run are the date-only zone pins. They
skip in a normal run by design, and
`scripts/__tests__/date-only-zone-pins-10183.test.ts`, which is in the
union, drives them in a forked child, where they passed.
## Gates
- **Type-check.** First the closure build, `pnpm --filter
'@object-ui/fields^...' build` (11 projects, exit 0). Then `type-check`
for `@object-ui/core` and `@object-ui/fields` (`tsc --noEmit && tsc -p
tsconfig.test.json`), exit 0 for both. `--listFilesOnly` confirms that
all three new test files are in the test programs.
- **Lint, narrowed to the touched files.**
- Population: each package's `lint` script is `eslint .` under the root
`eslint.config.js`. The touched files are 3 in core and 4 in fields, a
count taken from `--format json`.
- Result: 0 errors. The warnings are the existing debt; CI sets no
`--max-warnings`.
- Why the narrowing excludes nothing: the config sets neither
`parserOptions.project` nor `projectService`, so linting is not
type-aware. The custom rules in `eslint-rules/` read no other file. So
this diff cannot change the verdict on any untouched file.
- **Changeset and text gates:**
- `check-changeset-presence`, `check-changeset-no-major`,
`check-changeset-fixed`, `check:pending-changeset-literals`,
`check:control-bytes` and `check:test-path-roots`: exit 0.
- `check:new-line-citations`: 0 new citations.
- `check-changeset-overwrite`: report-only; it reports the
objectui#10026 correction above.
- `check:changeset-claims`: report-only. I read all six paragraphs it
flagged (objectui#10183, #6625, #6694, #6837, #7166, #7178), and each is
still true. I also searched the pending changesets for the changed
symbols, and none makes a claim this change falsifies other than the
objectui#10026 one.
- **Governed surface.** `check-governed-queue-guard.mjs --test` over all
9 paths: NOT GOVERNED.
- **Source-text anchor pins.** No test quotes a line this diff changes.
## Acceptance notes
- **Boundary:**
- A lowercase `t` separator (`2026-02-30t10:00:00z`) parses, but
`ISO_DATETIME_RE` does not match it, so it still rolls. The regex is
kept identical to the measure's. The measure's arm does not match it
either, so it falls through to the raw string.
- `2026-02-30T10`, with the hour only, is already rejected by the
engine's own parse.
- **Option C**, refusing at the producing boundary in objectstack:
carrier: none.
- `.changeset/dataset-format-8263-impossible-date-prose.md` names a test
title that PR objectui#10299 renamed. It went stale with that PR, not
this one. It has empty front matter and declares no release. I left it
untouched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a12f533 commit 544aca2
9 files changed
Lines changed: 471 additions & 29 deletions
File tree
- .changeset
- packages
- core/src/utils
- __tests__
- fields/src
- __tests__
- widgets
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
153 | 153 | | |
154 | 154 | | |
155 | 155 | | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
108 | 120 | | |
109 | 121 | | |
110 | 122 | | |
| |||
177 | 189 | | |
178 | 190 | | |
179 | 191 | | |
180 | | - | |
| 192 | + | |
| 193 | + | |
181 | 194 | | |
182 | | - | |
| 195 | + | |
183 | 196 | | |
184 | 197 | | |
185 | 198 | | |
| |||
204 | 217 | | |
205 | 218 | | |
206 | 219 | | |
207 | | - | |
208 | | - | |
209 | | - | |
210 | | - | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
211 | 229 | | |
212 | 230 | | |
213 | 231 | | |
| |||
227 | 245 | | |
228 | 246 | | |
229 | 247 | | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
230 | 253 | | |
231 | 254 | | |
232 | 255 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
0 commit comments