Repository navigation
Commit f6ccca4
Fixes #20846
Clause-②: yes (widening)
The claim on #20846 declared `Clause-②: no`. This diff adds one export
to `@objectstack/core`'s root, `SUPPORTED_TEMPORAL_YEARS`, so the public
surface widens. It is declared `yes (widening)` here and in the
changeset (`@objectstack/core` minor), and the report says so. No
value's verdict moves, and no wire key moves.
## What changes
A `date` or `datetime` value refused for its year now gets a message
that names the kind's years. A readable ISO 8601 value such as
`0500-07-15T10:00:00Z`, `+010000-01-01` or `0000-06-15`, or a `Date`,
used to get "must be a valid datetime (ISO-8601)", which is false for
that value. The field code stays `invalid_date`, and its `constraint`
stays `{ type }`.
| door | value | before | after |
|:--|:--|:--|:--|
| `POST /api/v1/data/:object` | `opened_at: "0500-07-15T10:00:00Z"` |
Opened must be a valid datetime (ISO-8601) | Opened must be a datetime
whose UTC year falls in the years 1000 to 9999 |
| `POST /api/v1/data/:object` | `placed_on: "+010000-01-01"` | Placed
must be a valid date (ISO-8601) | Placed must be a date in the years
0001 to 9999 |
| import row, refused by the write door | cell `0500-07-15T10:00:00Z`
(`datetime`) | Opened must be a valid datetime (ISO-8601) | Opened must
be a datetime whose UTC year falls in the years 1000 to 9999 |
| import row, refused by the import's reader | cell `+010000-01-01`
(`date`) | Placed: "+010000-01-01" is not a valid date | Placed must be
a date in the years 0001 to 9999 |
| both (control) | `not-a-date` | unchanged | unchanged |
- **`packages/spec/src/system/validation-message.ts`**: adds two
`invalid_date` sentences, `invalid_date_range` and
`invalid_datetime_range`, in `en`, `zh-CN`, `ja-JP` and `es-ES`. The
years come from the `{{firstYear}}` / `{{lastYear}}` parameters. No
template spells a year, and a spec test pins that.
- **`packages/core/src/utils/temporal-storage-form.ts`**: this landing
site is outside the claim's file surface. The bounds were two private
constants beside `isOutsideTemporalYearRange`. They are now one
exported, frozen `SUPPORTED_TEMPORAL_YEARS`, and the predicate reads it.
Triage's direction requires the sentence's years to come "from the same
range function the doors use", with no literal copy, and the dispatch
forbids a second copy of the bounds. Exporting them from their one
producer was the only way to meet both. Nothing else in core changes.
- **`packages/objectql/src/validation/record-validator.ts`**: the date
arm picks the range sentence when the value is `readable` and
`isOutsideTemporalYearRange(value, t)` holds. That is the same year
class that `yearClassOf` in the temporal-comparand door names by its
years. `buildFieldError` gains an internal `messageParams` channel,
which is interpolated into the message and never shipped. The years
reach the sentence without a new key on `constraint`. `buildFieldError`
is not exported from the package entry.
- **`packages/rest/src/import-coerce.ts`**: a `date` / `datetime` cell
that the reader does not take, but whose year `Date.parse` reads outside
the range (more than four year digits, such as `+010000-01-01`), gets
the write door's range sentence with the write door's code. Both refusal
paths of one import now give one sentence per kind. A number keeps the
import's own sentence, as the write door keeps its sentence for a
number.
## Premise check (measured on `origin/main` `bee75cebe`)
- **H1**: one `return fail('invalid_date', …)` served both the malformed
and the out-of-range value. The validator can tell them apart with
core's existing `isOutsideTemporalYearRange` (already exported), so it
needs no constraint key and no wire code. The new branch adds no second
`fail('invalid_date', …)` call site, and
`check:dispatcher-error-vocabulary` is green.
- **H2**: both doors refuse import rows. A cell with a four-digit year
(`0500-07-15T10:00:00Z`, `0000-06-15`) passes `import-coerce.ts` and the
record validator refuses it. A cell with more than four year digits
(`+010000-01-01`, `10000-01-01`) matches neither of the reader's shapes,
so `import-coerce.ts` refuses it with `import_invalid_date`, the same
false claim. The fix covers both. No other lane file refuses such a row.
- **H3**: `{{…}}` placeholders were filled only from `constraint`,
`value`, `label` and `field` at the write door, and only from `value` at
the import. The bounds were private (`FIRST_SUPPORTED_YEAR` /
`LAST_SUPPORTED_YEAR`), which is the core change above.
- **H4**: no translation bundle or gate learns the keys. The catalog
ships as constants outside the extract-and-gate bundle pipeline (its own
module note). `check:i18n` reads none of it, and the catalog's own
completeness test (`every locale defines every message key`) covers the
four locales.
## Tests
Full package suites, run on this branch under the shared verify lock.
The runs differ from the head `6adb0dbbf` only in the objectql test file
that the last commit restructured, and that file was re-run at the head.
- `@objectstack/spec`: 584 files, 17183 passed, 1 todo. `typecheck` exit
0. `build`, then `check:generated`: all 15 generated artifacts up to
date.
- `@objectstack/core`: 61 files, 1798 passed. `typecheck` exit 0.
- `@objectstack/objectql`: 347 files, 6813 passed. `typecheck` exit 0 at
`6adb0dbbf`. `src/validation/record-validator.test.ts` at `6adb0dbbf`:
126 passed.
- `@objectstack/rest`: 243 files, 4868 passed, 114 skipped. `typecheck`
exit 0.
New pins:
- `packages/rest/src/data-temporal-year-range-message.test.ts` goes
through `POST /api/v1/data/:object` and `POST
/api/v1/data/:object/import` over a real `SqlDriver` on SQLite. It
asserts `400`, `code: VALIDATION_FAILED`, the finding's `code:
invalid_date` and `constraint` equal to `{ type }` exactly, the
finding's message, and the first sentence of the envelope's `error`. It
covers `datetime` years 500, 999, 10000 (as UTC) and `+010000`, and
`date` years `+010000` and 0. The malformed controls keep the ISO
sentence. On the import, each refused row carries the same code and
sentence whichever reader refused it, the malformed cells keep the
import's own sentence, and the years' edges are stored.
- The spec catalog test checks that the placeholders are present in
every locale and that no range template contains a digit.
- The record-validator unit pins include a `Date` in year 500, the
control sentences, the edges, and the `zh-CN` / `ja-JP` renderings.
- The `import-coerce` unit pins are paired with controls.
- A core test checks that the edges of `SUPPORTED_TEMPORAL_YEARS` are
inside `isOutsideTemporalYearRange` and the years next to them are
outside, and that the object is frozen.
**Ablation.** Both selections were disabled at once through
`scripts/ablation-replace.mjs`: in the record validator, the `const
years = readable && isOutsideTemporalYearRange(…)` line was replaced by
a `globalThis.__ablation20846` guard, and in `import-coerce.ts` the
`outsideYears(raw, t)` line likewise. The anchors hit 1 then 0, and the
blobs moved. objectql was rebuilt, and `ablation-dist-preflight` found
the marker in 4 built files.
- REST pins: 10 failed, 143 passed. The received messages were the base
sentences: "Opened must be a valid datetime (ISO-8601)", "Placed must be
a valid date (ISO-8601)", and `Due: "+010000-01-01" is not a valid
date`.
- objectql `record-validator.test.ts`: 11 failed, 115 passed.
- Every control stayed green, and the failures went red in the expected
direction.
- Restore: each blob equals HEAD, `git diff HEAD` is empty, and `git
status --porcelain` is empty. objectql was rebuilt, preflight `--absent`
passed, and the pins are green again (rest 203 passed, 8 skipped;
objectql 136 passed).
**Type reverse check.** In `import-coerce.ts`,
`SUPPORTED_TEMPORAL_YEARS[kind]` was replaced by
`SUPPORTED_TEMPORAL_YEARS['time']`, and `tsc --noEmit` in
`packages/rest` failed with `TS7053`, naming `Readonly…Record…"datetime"
| "date"…`. That shows the consumer reads the rebuilt core `.d.ts`. The
file was then restored, and its blob equals HEAD.
## Gates
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` over this diff (merge base `bee75cebe`)
derives 88 commands. All 88 were run, with exit codes recorded, and
`--ran` reconciles them: "88 derived famil(ies) accounted for — 88 run,
0 NOT-MEASURED (a DERIVED zero …)".
- Four gates first exited non-zero. None of these was a pass. Their
final runs:
- `check-engine-split-ratio --days 90` and `check-plugin-teardown-shape
--self-test` refused on a shallow clone. After `git fetch
--shallow-since=2026-06-25 origin main`, both exit 0.
- `check:dual-build-cjs-loads` and `check:type-check-debt` hit
PREREQUISITE NOT MET (the whole tree was not built). After building the
43 missing packages, both exit 0.
- `check:error-code-casing` was a real red: four `toMatchObject({ code:
'invalid_date', message })` literals in the new objectql test. I
restructured them, and the gate exits 0.
- The roster gates located in the touched directories also exit 0:
`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`,
`check:filter-alias-parity` and `check-changeset-fixed`.
**Lint: a proven narrowing, not a repo-wide run.** At `6adb0dbbf`,
`eslint --no-inline-config --format json` over the 9 touched `.ts` files
gives 9 results, 0 errors, 0 warnings and 0 ignored. `--print-config`
shows each file in eslint's own population (4 to 6 active rules each).
This repo's `eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, per its own note), so this diff cannot move the
verdict on a file it does not touch. The repo-wide `pnpm lint` is left
to CI.
## Acceptance notes
- **The comparand door still spells its years as literals.** Its
sentences in `packages/objectql/src/temporal-comparand-door.ts` read
"the years 0001 to 9999" and "the years 1000 to 9999". They are correct
today, and they could now read `SUPPORTED_TEMPORAL_YEARS`. This is drift
risk only, outside this card, and nobody holds it.
- **Non-ISO spellings are read in the host zone.**
`isOutsideTemporalYearRange` reads such a spelling (`10000-01-01`,
`10000/01/01`) through `Date.parse`, in the host zone. On a UTC+ host
that instant falls in year 9999 UTC, so such a value gets the ISO
sentence rather than the range sentence. The ISO sentence is true for
that spelling, and the verdict does not move on any host. The pins use
the ISO 8601 expanded spelling `+010000-01-01`, which is read the same
on every host.
- **A value both malformed and out of range gets the range sentence.**
An example is `07/15/0500 10:00` on a `datetime`. The comparand door
picks its year class the same way, and after the year is fixed, the ISO
sentence follows.
- **Overrides of the old keys no longer cover these values.** A
deployment that overrides `validation.field.invalid_date` /
`invalid_datetime` does not cover the range values, because they use
their own keys now. The changeset says this.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017VaLJnYwhPsanVCe9dMCJU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3693a1b commit f6ccca4
10 files changed
Lines changed: 529 additions & 24 deletions
File tree
- .changeset
- packages
- core/src/utils
- objectql/src/validation
- rest/src
- spec/src/system
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
274 | 274 | | |
275 | 275 | | |
276 | 276 | | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
166 | 172 | | |
167 | | - | |
168 | | - | |
169 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
170 | 179 | | |
171 | 180 | | |
172 | 181 | | |
| |||
203 | 212 | | |
204 | 213 | | |
205 | 214 | | |
206 | | - | |
| 215 | + | |
| 216 | + | |
207 | 217 | | |
208 | 218 | | |
209 | 219 | | |
| |||
227 | 237 | | |
228 | 238 | | |
229 | 239 | | |
230 | | - | |
| 240 | + | |
231 | 241 | | |
232 | 242 | | |
233 | 243 | | |
| |||
Lines changed: 79 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1360 | 1360 | | |
1361 | 1361 | | |
1362 | 1362 | | |
| 1363 | + | |
| 1364 | + | |
| 1365 | + | |
| 1366 | + | |
| 1367 | + | |
| 1368 | + | |
| 1369 | + | |
| 1370 | + | |
| 1371 | + | |
| 1372 | + | |
| 1373 | + | |
| 1374 | + | |
| 1375 | + | |
| 1376 | + | |
| 1377 | + | |
| 1378 | + | |
| 1379 | + | |
| 1380 | + | |
| 1381 | + | |
| 1382 | + | |
| 1383 | + | |
| 1384 | + | |
| 1385 | + | |
| 1386 | + | |
| 1387 | + | |
| 1388 | + | |
| 1389 | + | |
| 1390 | + | |
| 1391 | + | |
| 1392 | + | |
| 1393 | + | |
| 1394 | + | |
| 1395 | + | |
| 1396 | + | |
| 1397 | + | |
| 1398 | + | |
| 1399 | + | |
| 1400 | + | |
| 1401 | + | |
| 1402 | + | |
| 1403 | + | |
| 1404 | + | |
| 1405 | + | |
| 1406 | + | |
| 1407 | + | |
| 1408 | + | |
| 1409 | + | |
| 1410 | + | |
| 1411 | + | |
| 1412 | + | |
| 1413 | + | |
| 1414 | + | |
| 1415 | + | |
| 1416 | + | |
| 1417 | + | |
| 1418 | + | |
| 1419 | + | |
| 1420 | + | |
| 1421 | + | |
| 1422 | + | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
| 1426 | + | |
| 1427 | + | |
| 1428 | + | |
| 1429 | + | |
| 1430 | + | |
| 1431 | + | |
| 1432 | + | |
| 1433 | + | |
| 1434 | + | |
| 1435 | + | |
| 1436 | + | |
| 1437 | + | |
| 1438 | + | |
| 1439 | + | |
| 1440 | + | |
| 1441 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
62 | 64 | | |
63 | 65 | | |
64 | 66 | | |
| |||
93 | 95 | | |
94 | 96 | | |
95 | 97 | | |
96 | | - | |
| 98 | + | |
97 | 99 | | |
98 | 100 | | |
99 | 101 | | |
| |||
434 | 436 | | |
435 | 437 | | |
436 | 438 | | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
437 | 445 | | |
438 | 446 | | |
439 | 447 | | |
| |||
451 | 459 | | |
452 | 460 | | |
453 | 461 | | |
| 462 | + | |
| 463 | + | |
454 | 464 | | |
455 | 465 | | |
456 | 466 | | |
| |||
462 | 472 | | |
463 | 473 | | |
464 | 474 | | |
465 | | - | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
466 | 480 | | |
467 | 481 | | |
468 | 482 | | |
| |||
845 | 859 | | |
846 | 860 | | |
847 | 861 | | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
848 | 870 | | |
849 | 871 | | |
850 | 872 | | |
| |||
876 | 898 | | |
877 | 899 | | |
878 | 900 | | |
879 | | - | |
| 901 | + | |
| 902 | + | |
880 | 903 | | |
881 | 904 | | |
882 | 905 | | |
| |||
1282 | 1305 | | |
1283 | 1306 | | |
1284 | 1307 | | |
1285 | | - | |
1286 | | - | |
| 1308 | + | |
| 1309 | + | |
| 1310 | + | |
| 1311 | + | |
| 1312 | + | |
| 1313 | + | |
| 1314 | + | |
| 1315 | + | |
| 1316 | + | |
| 1317 | + | |
| 1318 | + | |
| 1319 | + | |
| 1320 | + | |
| 1321 | + | |
| 1322 | + | |
| 1323 | + | |
| 1324 | + | |
| 1325 | + | |
| 1326 | + | |
| 1327 | + | |
| 1328 | + | |
| 1329 | + | |
| 1330 | + | |
| 1331 | + | |
| 1332 | + | |
| 1333 | + | |
1287 | 1334 | | |
1288 | 1335 | | |
1289 | 1336 | | |
| |||
0 commit comments