Repository navigation
Commit 9f39094
feat(core): import reads dotted and compact year-first days and matches option values in any case (#25596)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuDWJsBGQpadCH8JR2xZQ5re)_
Part of #25582
Clause-②: yes (widening: two date-cell spellings and an option value in
another case, each refused before with `invalid_date` /
`invalid_option`, are now read; nothing read before is refused or stored
differently)
Metadata-protocol: none
## What
Ruling 6377844879 (on #25493, letter A), proposals **1** and **2**, in
`packages/core/src/utils/import-coerce.ts`:
1. **`parseDateCell` reads `YYYY.MM.DD` and `YYYYMMDD`.** A four-digit
year, a two-digit month and day, nothing else in the cell. `2011.07.01`
is stored as `2011-07-01`, `20090615` as `2009-06-15`. The day must
exist (`namesRealCalendarDay`, the same arithmetic as the other shapes):
`20090230` and `2009.02.30` stay `invalid_date`. No other form is
widened: `2011.7.1`, `2011.07.01 9:00`, `11.07.01`, `01.07.2011`,
`2011.07-01`, `200961`, `2009615`, `200906150` and `20090615 9:00` stay
refused.
- **Field kinds.** Read for a `date` and a `datetime` (midnight UTC, the
reading a bare `2026/7/15` already gets on a `datetime`), never for a
`time`: both spellings name a day and no clock. The `time` branch keeps
its year-first-only fallback unchanged.
- **A number is not a compact day.** The compact form is read only from
a text cell, so a JSON number `20090615` stays refused. The existing
reader treats numbers as never temporal (`outsideYears` docblock: an
Excel serial, epoch ms). An xlsx cell holding that number reaches the
reader as its text (`xlsxCellToString` in `import-prepare.ts`), so it is
read. That path is read from the code, not measured.
- An eight-digit cell on a `number` or `text` column is unaffected
(`20090615` → the number `20090615` / the text `'20090615'`). This is
pinned.
2. **Option values match case-insensitively.** `matchOption` now has
three passes: exact value, then label case-insensitively (as before),
then value case-insensitively. It stores the declared value, never the
cell's spelling: `CN` stores `cn`. The value pass runs last, so a cell
that matched before stores what it stored (pinned: a cell that is one
option's label and, by case, another option's value keeps the label
match). Single select, `multiple: true` select and multiselect parts all
go through this one function.
- **Ambiguity, measured.** The spec cannot declare two values that
differ only by case: `FieldSchema.safeParse` on
`[{value:'cn'},{value:'CN'}]` fails at `options.1.value` ("System
identifier must be lowercase…"). A programmatic
`engine.registry.registerObject` accepts and keeps the pair unparsed.
Measured with a throwaway test against `@objectstack/objectql`, then
deleted. So the pass refuses to guess. A cell naming two such values by
case alone (`Cn`) gets the no-match answer it got before
(`invalid_option`, or kept under `createMissingOptions`). An exact `cn`
/ `CN` still matches exactly. The same value declared twice counts as
one value.
#25582 remains open: proposal 4 (every cell error of a row) is not in
this PR. It needs an `errors` member on `ImportRowResultSchema` first
(the `domain:spec` seat's; the note is on the card), and
`import-runner.ts` still keeps `errors[0]`. Proposal 3 (`aliases`) was
not ruled in.
## Tests
- `packages/core/src/utils/import-coerce.test.ts`: a `[#25582]` block
covering the new forms on `date` and `datetime`, the invalid-calendar
controls, the not-widened controls, `time` refusal, number refusal, the
8-digit number/text control, and the row-level `invalid_date`.
`matchOption` gets value-case match storing the canonical value,
label-ahead-of-value, and the ambiguous pair. `coerceRow` gets select,
multiselect and `multiple: true` select parts stored canonical, with
`keptOptionValues` empty and an unmatched part named.
- `packages/rest/src/import-year-first-day-option-case.test.ts` (new):
through the real `POST /api/v1/data/:object/import` over `SqlDriver`
(better-sqlite3), a CSV row `2011.07.01,20090615,CN` imports and stores
`2011-07-01` / `2009-06-15` / `cn`. The JSON dry run of the same row is
clean and persists nothing. `20090230` and `DE` are still that row's
`invalid_date` / `invalid_option`.
**Ablation, once per proposal.** Each leg went through
`scripts/ablation-replace.mjs` (anchor hit 1 → 0, blob moved). Then
`@objectstack/core` was rebuilt, because the rest test reads core's
`dist/`, and `ablation-dist-preflight.mjs` saw the marker in
`dist/index.js` and `dist/index.cjs`.
- **P1** (the `readYearFirstDay` call removed): core 9 failed / 167
passed of 176. Route 3 failed of 3 (`ok: 0, errors: 1` on the ledger
row).
- **P2** (the value pass returns nothing): core 3 failed / 173 passed.
Route 2 failed / 1 passed (the refusal case stays green, as expected).
- **Restore.** Blob == HEAD (`60af90a93041`), `git diff HEAD` empty.
Core rebuilt, and `--absent` passed for both markers on all 14 built
files with a clean tree.
## Gates
All readings below were taken at head `c630eacca0` (after merging
`origin/main`), unless a line names another commit.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 70 commands. All 70 ran and exited
0. `--ran` gives: `✓ dispatch-gates --ran: 70 derived famil(ies)
accounted for — 70 run, 0 NOT-MEASURED`.
- `pnpm lint`, through the verify lock: `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/core test`: 118 files / 2757 tests passed.
`pnpm --filter @objectstack/core typecheck` and `pnpm --filter
@objectstack/rest typecheck` passed (each package's test-layer
`check:test-typecheck: OK`). Both through the lock, `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/rest exec vitest run` over the new file
plus `import-date-cell-iso-real-day`, `import-dryrun-parity` and
`import-integration`: 4 files / 119 tests passed.
- After the merge: the `@objectstack/rest` closure was rebuilt, and
`pnpm --filter @objectstack/spec check:generated` gives `✓ All 14
generated artifacts are up to date`.
- NOT MEASURED locally: the whole `@objectstack/rest` suite (only a test
file is added there; CI `Test Core` runs it) and `test:repo` (no
repo-project test is touched).
## Acceptance notes
- The xlsx template's instructions sheet (`TEMPLATE_READER_CLAIMS` in
`packages/rest/src/import-template.ts`) lists example date spellings and
does not mention the two new ones. Every claim it makes is still true
(none of its refused examples is now read). Adding them is outside this
slice's file surface. carrier: none.
- `2011.7.1` (dotted, one-digit month/day) stays refused, per the
ruling's 「不放宽其它形式」. Widening it would be a new decision.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01CxBKycaiUpA1yGx1XyoGqX
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a57643d commit 9f39094
4 files changed
Lines changed: 319 additions & 7 deletions
File tree
- .changeset
- packages
- core/src/utils
- rest/src
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
212 | 212 | | |
213 | 213 | | |
214 | 214 | | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
215 | 273 | | |
216 | 274 | | |
217 | 275 | | |
| |||
224 | 282 | | |
225 | 283 | | |
226 | 284 | | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
227 | 310 | | |
228 | 311 | | |
229 | 312 | | |
| |||
401 | 484 | | |
402 | 485 | | |
403 | 486 | | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
404 | 509 | | |
405 | 510 | | |
406 | 511 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
335 | 335 | | |
336 | 336 | | |
337 | 337 | | |
338 | | - | |
339 | | - | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
340 | 341 | | |
341 | 342 | | |
342 | 343 | | |
| |||
450 | 451 | | |
451 | 452 | | |
452 | 453 | | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
453 | 492 | | |
454 | 493 | | |
455 | 494 | | |
| |||
472 | 511 | | |
473 | 512 | | |
474 | 513 | | |
475 | | - | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
476 | 517 | | |
477 | 518 | | |
478 | 519 | | |
| |||
561 | 602 | | |
562 | 603 | | |
563 | 604 | | |
564 | | - | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
565 | 609 | | |
566 | 610 | | |
567 | 611 | | |
| |||
608 | 652 | | |
609 | 653 | | |
610 | 654 | | |
611 | | - | |
612 | | - | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
613 | 668 | | |
614 | 669 | | |
615 | 670 | | |
| |||
626 | 681 | | |
627 | 682 | | |
628 | 683 | | |
629 | | - | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
| 688 | + | |
| 689 | + | |
| 690 | + | |
| 691 | + | |
630 | 692 | | |
631 | 693 | | |
632 | 694 | | |
| |||
0 commit comments