fix(rest)!: /import reads a date, datetime or time cell only in ISO 8601, the export shape or a year-first date, on a real day, with a four-digit year (#20534) - #20601
Conversation
…601 or the export shape, on a real day (#20534) parseDateCell refuses an impossible day on every branch instead of rolling it over, refuses every text cell that is neither ISO 8601 nor the export's YYYY-MM-DD HH:mm:ss instead of reading it in the server process's zone and month-first, and pads a date's year to four digits. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
… a real day, under two host zones (#20534) Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…ort shape (#20534) Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…rse-date-cell-family
Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4766625cdaea351988c0e3d20f4f01cb0cd1a72a && git checkout 4766625cdaea351988c0e3d20f4f01cb0cd1a72a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c96beb2707e9d12fbc8abcf985c6dc93f957ef2c 3b2e47349e98346c72d3f3cf9dfda3ed1c6eb8bf && git checkout -B drift-repro c96beb2707e9d12fbc8abcf985c6dc93f957ef2c && git merge --no-ff 3b2e47349e98346c72d3f3cf9dfda3ed1c6eb8bf
node scripts/docs-audit/affected-docs.mjs --json c96beb2707e9d12fbc8abcf985c6dc93f957ef2c |
Contract reviewServed-tier: PR #20601 for card #20534, judged against triage's binding answer A ( Check-runs on this head at the read: 34 runs, one per name. 30 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flagsEvery dev flag (deviations, acceptance notes, out-of-scope findings) and the open question, answered:
Implemented-by: VERDICT: PASS |
…rse-date-cell-family
…real day, stored padded (#20534) Per the maintainer ruling on the card: YYYY/M/D and YYYY-M-D with the same separator, optionally a space and a zone-naive H:MM[:SS], are read beside ISO 8601 and the export shape. The day must exist, the clock is 0..23 / 00..59, the day is stored as the padded ISO day and a clock is a wall clock read as the export shape's is. No zone, fraction or T in this form. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…es and the real /import door (#20534) Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…fused edges (#20534) Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…rse-date-cell-family
Contract reviewServed-tier: Delta review of PR #20601 for card #20534 at the patch-round head, after the full PASS at Check-runs on this head at the read: 39 runs over 32 names (7 names ran twice: a suite at 07:03Z on the push and a suite at 07:09Z after the body edit; no completed duplicate disagrees). Newest per name: 21 ① Derived judgmentsThe brief's six questions, answered on the diff: Q1. The admitted year-first set equals the ruled set exactly — yes. Q2. Beyond the padding, the 1900s fix and the named Q3. A year-first date-time is read through the same business-zone wall-clock path as the export shape — yes. Both readers return the one Q4. The refusals reuse the existing keys, with no new code and no date-format option — yes. Q5. The changeset is right, and the FROM Q6. The restored fixtures and assertions match Every accept-set and public-surface change the diff implies:
② Semver level
③ Boundary flagsThe newest
Implemented-by: VERDICT: PASS |
… to the commits that decided them (objectstack-ai#20632) Part of objectstack-ai#20594 Clause-②: no ## What changed This is stage 2 of the `domain:cli` lane of the dead-citation sweep: `packages/rest/src/**`. Every comment or docblock site in scope that cited a tracker number answering 404 now cites, in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), the commit in this repository's history that decided what the line describes, and says in its own words what that commit decided. PR objectstack-ai#20533 is the method and PR objectstack-ai#20624 (stage 1, `packages/runtime`) the precedent this follows line for line. Later stages cover `cli`, `types` and the rest of the lane, so this PR says `Part of` and the card stays open. That is **457 comment sites on 445 lines in 85 files, covering 74 numbers**: the census's 191 sites, 256 more in test comments (which the census defers), and 10 sites whose dead number is the second half of a slash-joined pair the citation grammar does not read (`objectstack-ai#3984/objectstack-ai#6241`, `objectstack-ai#9901/objectstack-ai#10255` four times, `objectstack-ai#10993/objectstack-ai#11235/objectstack-ai#11292`, `objectstack-ai#11235/objectstack-ai#11242` twice, `objectstack-ai#10993/objectstack-ai#11242`, `objectstack-ai#7543/objectstack-ai#15071`). Each rewritten line cites one of **70 distinct commits**. ADR-0076 D11 is the only ADR that records any of these numbers, and it records objectstack-ai#8850 only as the extraction it names as landed in `8664a2c99`, so that commit is the anchor there. No other ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/` records the decision behind any of these numbers, so every anchor is a commit. The anchors the landed stages already gave the same numbers are reused where the rest sites describe the same decision (30 numbers, for example `79c46da90` for objectstack-ai#9934, `7986d973f` / `311433f6b` for the compound-name retirement, `6a180e42d` for objectstack-ai#13279 and `cf6e0a193` for objectstack-ai#15071), so each number carries one anchor across the tree. Only comments changed. Every touched file keeps its line count (451 lines out, 451 in, over 85 files), so no line citation into these files moves. Six of the 451 lines held no dead site; each is the other half of a sentence that had to change: - `discovery-schema-conformance.test.ts:343` (「(reaffirmed by」 to 「(which commits」, because line 344 now names the two commits that landed the ruling), - `package-door-16019-raw-statement-fault-code.test.ts:51` and `error-response.ts:1485` (a trailing 「PR」 whose number wrapped onto the next line), - `error-response-structured-arm-door-parity.test.ts:463` (「That card added the limb」 to 「That commit」, because line 459's tag now names the commit), - `rest-hook-script-fault-envelope.test.ts:331` (「both sides of that card」 to 「that fix」), - `rest-server.ts:908` (「(objectstack-ai#14409, landed」 to 「(landed as commit」, the sha `3ecb7dc1a` already standing on line 909). **No citation number is added.** Every tracker number on an added line was already on the line it replaces. No PR number stands on an added line. One of the 70 shas is on a removed line, and it was there before: `rest-14078-invalid-date-total-arm.test.ts:19` read 「PR objectstack-ai#14409 (landed `3ecb7dc1a`)」 and now reads 「Commit 3ecb7dc drove」. No code token moves (see the guard below). Three dead comment sites are left on purpose, listed under "The sites left". One more file: a `patch` changeset for `@objectstack/rest`, because the rewritten docblocks ship (see Changeset below). ## Census: `packages/rest`, before and after **Instrument.** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged, run with the fleet token. Its surface is comment prose in `packages/**/src/**/*.ts` with string literals blanked, and it defers `*.test.ts`. The count is its `allocated-but-absent` findings under `packages/rest/`. Both runs enumerated the whole board (185 pages), so neither read a truncated board. | reading | tree | board | whole-repo `allocated-but-absent` | rest sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `a186aea996`, run 2026-09-29T10:28:18Z to 10:36:06Z | enumerated, 185 pages, frontier objectstack-ai#20628, 18,455 numbers | 2,015 | **191** | 186 | 14 | 51 | | after | head `93e4d69ba6`, run 11:11:30Z to 11:17:37Z | enumerated, 185 pages, frontier objectstack-ai#20630, 18,457 numbers | 1,764 | **0** | 0 | 0 | 0 | The before count equals the card's 191 at `f11b5f20a2`. The whole-repo drop is 251: this diff's 191, plus the 60 of PR objectstack-ai#20626 (`packages/plugins/plugin-sharing`, 63 to 3), which landed on `main` in between and came in with the merge. No other package moved. **Supplementary instrument, the whole scope.** The census does not read test files or strings, and this stage's scope includes test comments. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `classifyCitation` over every `.ts` file under `packages/rest/src` (256 files), against the board enumerated through the gate's own `enumerateBoard`. The lit controls objectstack-ai#20594, objectstack-ai#19123 and objectstack-ai#20624 answered 200 and are on both boards; the dead controls objectstack-ai#13214, objectstack-ai#14541 and objectstack-ai#15071 answered 404 and are on neither. | reading | tree | board | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---|---|---| | before, 10:29Z | `a186aea996` | 185 pages, frontier objectstack-ai#20628 | 4,620 | **577** | 191 | 259 | 1 | 126 | | after, 11:21Z | `93e4d69ba6` | 185 pages, frontier objectstack-ai#20631 | 4,174 | **130** | 0 | 3 | 1 | 126 | Its src-comment column equals the census's 191 and 0, which is the control on the second instrument, and a site-by-site comparison of the two before-readings is identical. Resolving comment citations move by one (1,364 to 1,365 in src): `(objectstack-ai#10993/objectstack-ai#11235/objectstack-ai#11292)` became `(objectstack-ai#10993, commit 376c70f, objectstack-ai#11292)`, so the grammar now reads the live `objectstack-ai#11292` that the slash hid. The drop is 447 grammar-read sites; the other 10 rewritten sites are the slash-joined ones the grammar never read. Separately, every one of the 77 numbers was probed on its web endpoint: 76 answer 404 (deleted) and one, #14026, answers 302 to objectstack-ai/objectui#10102 (transferred), which is why it is left (see below). ## Per-number table Sites and files are the dead comment sites in scope at the base, tests and slash-joined halves included. `left` is a site with no deciding commit (see below). `strings kept` counts string-literal sites, which are tokens and stay as they were. Every anchor was read in its message or its diff, not only in its subject: it is the commit that made the change the line describes, and its own message or diff names the number it replaces or adds the citation the line carries. | number | comment sites / files | rewritten | left | strings kept | anchor | |---|---|---|---|---|---| | `objectstack-ai#6037` | 5/3 | 5 | 0 | 0 | `18189983d` | | `objectstack-ai#6122` | 2/2 | 2 | 0 | 0 | `64cd01082` | | `objectstack-ai#6206` | 1/1 | 1 | 0 | 0 | `8e13ca876` | | `objectstack-ai#6216` | 6/2 | 6 | 0 | 2 | `f586f1a89` | | `objectstack-ai#6241` | 10/3 (1 slash-joined) | 10 | 0 | 1 | `83a3b1f2e` | | `objectstack-ai#6259` | 2/1 | 2 | 0 | 0 | `6968885ef` | | `objectstack-ai#6303` | 1/1 | 1 | 0 | 0 | `465c5fc14` | | `objectstack-ai#6306` | 9/5 | 9 | 0 | 3 | `fec784863` | | `objectstack-ai#6307` | 4/2 | 4 | 0 | 0 | `293476148` | | `objectstack-ai#6349` | 4/2 | 4 | 0 | 4 | `2443bb4c4` | | `objectstack-ai#6474` | 1/1 | 1 | 0 | 0 | `18189983d` | | `objectstack-ai#6535` | 3/2 | 3 | 0 | 0 | `a92b1793c` | | `objectstack-ai#6640` | 1/1 | 1 | 0 | 1 | `2ab1257c9` | | `objectstack-ai#6704` | 5/1 | 5 | 0 | 1 | `c3f491626` | | `objectstack-ai#8641` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#8850` | 3/3 | 3 | 0 | 0 | `8664a2c99` | | `objectstack-ai#8885` | 6/3 | 6 | 0 | 3 | `30b1c636a` | | `objectstack-ai#8919` | 7/3 | 7 | 0 | 7 | `b5378550e` | | `objectstack-ai#9741` | 12/1 | 12 | 0 | 0 | `2a29caa53` | | `objectstack-ai#9805` | 1/1 | 1 | 0 | 0 | `45862a53d` | | `objectstack-ai#9934` | 19/10 | 19 | 0 | 4 | `79c46da90` | | `objectstack-ai#9967` | 2/2 | 2 | 0 | 4 | `8f266f1cd` | | `objectstack-ai#10063` | 2/2 | 2 | 0 | 1 | `9e04c3e35` | | `objectstack-ai#10178` | 1/1 | 1 | 0 | 0 | `38cf397ea` | | `objectstack-ai#10179` | 0/0 | 0 | 0 | 1 | | | `objectstack-ai#10255` | 18/4 (4 slash-joined) | 18 | 0 | 2 | `6ce58a735` | | `objectstack-ai#10340` | 13/3 | 13 | 0 | 2 | `26f3588fb` | | `objectstack-ai#10345` | 13/6 | 13 | 0 | 6 | `cad8b42f0` | | `objectstack-ai#10350` | 1/1 | 1 | 0 | 0 | `490879ad0` | | `objectstack-ai#10485` | 2/1 | 2 | 0 | 1 | `35ad101bc` | | `objectstack-ai#10537` | 9/3 | 9 | 0 | 1 | `e634ecf6a` | | `objectstack-ai#10888` | 2/2 | 2 | 0 | 0 | `d806081dd` | | `objectstack-ai#11006` | 3/1 | 3 | 0 | 0 | `cccbe51bf` | | `objectstack-ai#11130` | 1/1 | 1 | 0 | 0 | `851909530` | | `objectstack-ai#11235` | 4/2 (1 slash-joined) | 4 | 0 | 0 | `376c70f98` | | `objectstack-ai#11242` | 3/2 (3 slash-joined) | 3 | 0 | 0 | `98ea3443f` | | `objectstack-ai#12144` | 1/1 | 1 | 0 | 0 | `3a04b0125` | | `objectstack-ai#12176` | 11/7 | 11 | 0 | 2 | `7986d973f` | | `objectstack-ai#12194` | 15/5 | 15 | 0 | 4 | `311433f6b` | | `objectstack-ai#12195` | 35/16 | 35 | 0 | 7 | `7986d973f` | | `objectstack-ai#13182` | 2/2 | 2 | 0 | 0 | `5b3ff63cc` | | `objectstack-ai#13197` | 1/1 | 1 | 0 | 0 | `56c093c4d` | | `objectstack-ai#13213` | 2/1 | 2 | 0 | 0 | `4801296e7` | | `objectstack-ai#13214` | 18/6 | 18 | 0 | 14 | `cc837dbfe`, `889ec5b42`, `3d10755f0` | | `objectstack-ai#13244` | 5/2 | 5 | 0 | 1 | `889ec5b42` | | `objectstack-ai#13255` | 4/1 | 4 | 0 | 6 | `43028a8f8` | | `objectstack-ai#13258` | 1/1 | 1 | 0 | 0 | `3d10755f0` | | `objectstack-ai#13279` | 23/5 | 23 | 0 | 5 | `6a180e42d` | | `objectstack-ai#13280` | 13/4 | 13 | 0 | 2 | `add6a1b1c` | | `objectstack-ai#13282` | 1/1 | 1 | 0 | 0 | `43028a8f8` | | `objectstack-ai#13377` | 3/2 | 3 | 0 | 0 | `e10cf3444` | | `objectstack-ai#13378` | 2/1 | 2 | 0 | 0 | `82faea03f` | | `objectstack-ai#13454` | 1/1 | 1 | 0 | 0 | `7ad57e17a` | | `#14026` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#14365` | 1/1 | 0 | 1 | 0 | — | | `objectstack-ai#14366` | 14/4 | 14 | 0 | 2 | `53cbad9f7` | | `objectstack-ai#14369` | 3/2 | 3 | 0 | 0 | `a3d5724c8`, `53cbad9f7` | | `objectstack-ai#14389` | 7/3 | 7 | 0 | 7 | `10220a7bf` | | `objectstack-ai#14390` | 1/1 | 1 | 0 | 0 | `9d7f7259f` | | `objectstack-ai#14409` | 2/2 | 2 | 0 | 0 | `3ecb7dc1a` | | `objectstack-ai#14541` | 27/4 | 27 | 0 | 5 | `6d178a408` | | `objectstack-ai#14613` | 2/2 | 2 | 0 | 0 | `81208086a` | | `objectstack-ai#14677` | 1/1 | 1 | 0 | 0 | `a4e4d2d78` | | `objectstack-ai#14683` | 8/2 | 8 | 0 | 0 | `96326040f` | | `objectstack-ai#14691` | 15/2 | 15 | 0 | 2 | `b3a63d32c` | | `objectstack-ai#14704` | 9/3 | 9 | 0 | 2 | `1c7adc73d` | | `objectstack-ai#14723` | 7/4 | 7 | 0 | 4 | `65846bc46` | | `objectstack-ai#14725` | 3/3 | 3 | 0 | 2 | `f5cc78b63` | | `objectstack-ai#14849` | 3/1 | 3 | 0 | 0 | `226e72443` | | `objectstack-ai#14907` | 1/1 | 1 | 0 | 0 | `e1d4f9e3f` | | `objectstack-ai#14908` | 1/1 | 1 | 0 | 0 | `d5cbb44f3` | | `objectstack-ai#15021` | 2/1 | 2 | 0 | 8 | `cc238db8b` | | `objectstack-ai#15034` | 6/2 | 6 | 0 | 0 | `abf9101f1` | | `objectstack-ai#15065` | 1/1 | 1 | 0 | 0 | `1c7adc73d` | | `objectstack-ai#15071` | 23/4 (1 slash-joined) | 23 | 0 | 3 | `cf6e0a193` | | `objectstack-ai#16650` | 1/1 | 1 | 0 | 0 | `001a83b04` | | `objectstack-ai#17058` | 3/1 | 3 | 0 | 4 | `94c930248` | | `objectstack-ai#18546` | 3/2 | 3 | 0 | 3 | `58f60e37e` | | **total** | **460** | **457** | **3** | **127** | **70 distinct commits** | Every cited sha matches exactly one object (`git rev-parse --disambiguate`, count 1 for each of the 70), is a commit, has one parent, and is an ancestor of the base (`merge-base --is-ancestor`, exit 0 for all 70). The checkout is not shallow (`--is-shallow-repository` false); the control leg `13a6cb4ad` exits 0 and the negative control (this branch's first WIP commit, not on `main`) exits 1. Several numbers are the PR number of their own anchor commit (objectstack-ai#6122, objectstack-ai#6303, objectstack-ai#6474, objectstack-ai#11242, objectstack-ai#13213, objectstack-ai#13244, objectstack-ai#13258, objectstack-ai#13282, objectstack-ai#14409, objectstack-ai#14677, objectstack-ai#14908, objectstack-ai#15065, objectstack-ai#16650), so the sha is the same object the number named. **Numbers with more than one anchor, by site:** - `objectstack-ai#13214` (18 sites) was one card with three commits. `cc837dbfe` (the ownership gate, the 2026-08-30 ruling) for the 11 sites that describe the gate; `889ec5b42` for the 5 in `ui-view-route-identity.measurement.test.ts`, the identity measurement it created; `3d10755f0` for the tenancy file's header, the measurement it created; and `rest-server.ts:2247`, 「Driven and reported on objectstack-ai#13214 (PRs objectstack-ai#13244, objectstack-ai#13258)」, now reads 「Measured in commits 889ec5b (identity) and 3d10755 (tenancy)」: those PRs are exactly those two commits. - `objectstack-ai#14369` (3 sites): `a3d5724c8` (the liveness census it recorded) for `rest-server.ts:1172` and `rest-sub-config-parse-not-cast.test.ts:48`. `rest-server.ts:4092` said the zero read sites of `api.documentation` / `api.responseFormat` came from 「the objectstack-ai#14369 census」, but `a3d5724c8` explicitly left `api` out of that census; the zero was measured by `53cbad9f7` (its changeset: no other read site for either key), which is the anchor there. - `objectstack-ai#11235` / `objectstack-ai#11242` / `objectstack-ai#10993`: `376c70f98` derives the discovery `version` in metadata-protocol (objectstack-ai#11235), and `98ea3443f` is objectstack-ai#11242's own squash, which landed the objectstack-ai#10993 ruling on `/health` and the dispatcher's `/discovery`. So 「the objectstack-ai#10993 ruling … reaffirmed by objectstack-ai#11235/objectstack-ai#11242」 now reads 「the objectstack-ai#10993 ruling, landed by commits 98ea344 and 376c70f」 (`rest-server.ts:4528`, `discovery-schema-conformance.test.ts:343-344`). `objectstack-ai#10993`, `objectstack-ai#11292` and `objectstack-ai#11297` answer 200 and stay. - `objectstack-ai#6037` / `objectstack-ai#6474`: one commit, `18189983d` (objectstack-ai#6474 is its PR number), so 「(objectstack-ai#6037 / PR objectstack-ai#6474)」 became 「(commit 1818998)」. **Wordings to check, each true of its commit:** - A commit does not rule. Where a line said a number ruled, it now says what the commit did with the ruling: 「the ruling commit 79c46da landed says it does」, 「the ruling commit cf6e0a1 implemented fences it」, 「the ruling commit 10220a7 implemented」, 「the 2026-08-20 ruling, landed as commit 6ce58a7」, 「recorded in commit 6ce58a7's message (option A)」 (its message reads 「Ruled on objectstack-ai#10255 (2026-08-20, option A)」), and 「question was ruled on 2026-08-20 and landed as commit 6ce58a7」 where the line said 「filed as objectstack-ai#10255」. - `objectstack-ai#14541`'s contract review: 「the objectstack-ai#14541 contract review (condition N)」 now reads 「the contract review of commit 6d178a4 (condition N)」; that commit's message lists the conditions it carries. 「objectstack-ai#14541's §4」 and 「objectstack-ai#14541 §5」 in `error-response-generic-passthrough-object-parity.test.ts` are sections of `error-response-structured-arm-door-parity.test.ts` (the file `6d178a408` created), so they now name that file. 「measured on the objectstack-ai#14541 branch」 reads 「on the branch that landed as commit 6d178a4」. - A line that named a DEFECT by its number now says so: 「Before commit 9e04c3e the draft→active promotion door could not…」, 「Before commit 26f3588 the `/meta` doors decided ORGANIZATION SCOPE from the RAW url」, 「the defect commit 2443bb4 fixed」 and 「would be the defect commit 26f3588 fixed」. - `objectstack-ai#13255`: 「As written for objectstack-ai#13255 this file repaired nothing」 reads 「As first written (commit 43028a8)」, the commit that created the file and answered the measurement; 「CONTEXT-LOST family (objectstack-ai#13255), still unruled」 reads 「first measured by commit 43028a8」 (the ruling on that family never landed, which the line still says). - `objectstack-ai#13214` in the identity file: 「the half objectstack-ai#13214 marks UNMEASURED」 reads 「the half left UNMEASURED until commit 889ec5b」, and 「objectstack-ai#13214 asks for an INDEPENDENT reproduction」 reads 「commit 889ec5b is an INDEPENDENT reproduction」. - 「the objectstack-ai#8885 sweep」 reads 「the sweep behind commit 30b1c63」, the commit that registered the 9 codes the sweep found; 「objectstack-ai#14849 predicted」 reads 「The card behind commit 226e724 predicted」; 「the hazard objectstack-ai#13377 names」 reads 「the hazard commit e10cf34 was written to remove」; 「The concrete harm objectstack-ai#6704 names」 reads 「removed」. - Quoted ruling: `error-response-sandbox-arm-message.test.ts:340` sits inside a verbatim ruling quote, so the commit stands in an editorial bracket (「not from [commit 1c7adc7]'s list」), as PR objectstack-ai#20624 did. - Two markdown tables in comments (`meta-state-route-engine-outage.test.ts:76`, `objectql-slot-consumer-census.test.ts:43`): the rewritten cell is wider than its column, and its padding is reduced rather than widening the four sibling rows. ## The sites left **No deciding commit (3 sites, all in test files, so the census does not see them):** - `meta-object-owd-gate.test.ts:516` (objectstack-ai#8641): 「whether it should stay is objectstack-ai#8641's question」, an open decision. The commit that added the citation calls it a pointer to the open decision card, and no commit decides it. - `rest-sub-config-parse-not-cast.test.ts:321` (objectstack-ai#14365): the `z.partialRecord` question 「deferred to objectstack-ai#14365」 was never taken (`git log -S partialRecord`); `b3a63d32c` made it moot by retiring the record, which the other half of the same line now cites. - `import-integration.test.ts:1043` (#14026): not deleted, TRANSFERRED. The web endpoint answers 302 to objectstack-ai/objectui#10102, the REST read follows the redirect, and the board enumeration does not list it, so the census and the supplementary reading both class it `allocated-but-absent`. The line says how an issue was raised; no commit decides that, so form C has nothing to cite. **String sites kept as tokens (127).** 126 are test titles and test-code strings in 41 files. One is a non-test string: the `note` field of the REST route ledger's `GET /api/v1/meta/object/:name/state/:field` row at `rest-route-ledger.ts:290`, which ends 「(objectstack-ai#10179)」 (see Acceptance notes). ## Mechanical guard: no code token moves The check compares the TypeScript parser's leaf tokens (TypeScript 6.0.3, JSDoc nodes excluded, comments being trivia) of each touched file at base `a186aea996` against the working tree at `93e4d69ba6`, over all 85 touched `.ts` files. Controls mutate the head text in memory only, so nothing on disk moved for them. - Real run: 272,653 base tokens, **0 files with a token change** (exit 0). - Comment-insertion control (`error-response.ts`): 0 files changed (exit 0). - Code-insertion positive control (a declaration in the same file): DIFFER at token 0 (exit 1). - String positive control (the first string literal past offset 2000 of the same file, one character added inside it): DIFFER at token 26 (exit 1). Line balance: every touched file is +N/−N (451/451), and every line count is equal at base and head. A raw scan of the 86 changed files for control bytes finds none (its positive control on a scratch file with a U+0001 byte matches). ## Changeset This change ships bytes, so a `patch` changeset for `@objectstack/rest` is included, in PR objectstack-ai#20624's form and level. It says only that the provenance comments were re-anchored. Measured on the built package: `files[]` is `dist`, `README.md` and `CHANGELOG.md`. After `pnpm --filter @objectstack/rest build`, the rewritten docblocks reach `dist`: for example `53cbad9f7` appears 4 times in `dist/index.d.ts`, and `26f3588fb` 8 times and `b3a63d32c` 5 times in `dist/index.js`. The positive control, the unchanged sentence 「It was VALIDATE-ONLY from objectstack-ai#11637」 of the same `rest-server.ts` docblock whose first line now reads 「[commit 53cbad9] The parsed output is CONSUMED」, is in `dist/index.d.ts` beside it; a negative control phrase appears nowhere. ## Gates (head `93e4d69ba6`) This host has no `flock`, so `os-verify-lock.sh` ran in its declared unlocked mode. Its disclosure, verbatim, from each locked run at this head: ```text os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 47s · declare it in the PR body · pnpm --filter '@objectstack/rest...' build os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 102s (1m42s) · declare it in the PR body · pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=4 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 76s (1m16s) · declare it in the PR body · pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 2s · declare it in the PR body · pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2 os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this host, so the shared verify lock was NEVER taken and NOTHING was serialized · ran 9s · declare it in the PR body · pnpm --filter @objectstack/rest typecheck ``` The branch merged `origin/main` once (`93e4d69ba6`, merging `542670da6d`) before these runs, as the dispatch orders; `origin/main` has not moved since (read at 11:19Z). The merge brought PR objectstack-ai#20626 and PR objectstack-ai#20587 and touched none of this diff's files. The dependency closure was built first (`pnpm --filter '@objectstack/rest...' build`, 26 packages), then the whole workspace (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 71 tasks, 71 successful). - **Tests:** `vitest run --project local`: 227 files, 4,382 tests passed, 50 skipped. `--project repo` (which holds the touched `meta-state-route-doc-spelling.test.ts`): 1 file, 8 tests passed. Together they are all 228 test files of the package, so every touched test file ran. - **Typecheck:** `pnpm --filter @objectstack/rest typecheck` exits 0. `tsc --listFiles` counts 28 `src` files (no tests) under `tsconfig.json` and all 228 test files under `tsconfig.test.json`, which `check:test-typecheck` judges: 0 files, 0 errors, 0 pinned signatures in the ledger. - **Lint:** the repo-wide `pnpm lint` (`eslint . --no-inline-config`) exits 0 at `93e4d69ba6` (2026-09-29T11:19:30Z to 11:20:00Z). Not narrowed. - **Citation judging:** `node scripts/check-issue-citations.mjs --base origin/main` exits 0: 19 citations judged across 14 files (18 resolve, 1 resolves as a pull request). These are the live numbers that stay on rewritten lines. It defers `*.test.ts`, so the added-minus-removed count over the whole diff covers the rest: 0 numbers added. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at `93e4d69ba6` derived 68 families. All 68 exit 0, and `--ran` over a record carrying each exit code reads 「68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN」 (a derived zero). - `check:dual-build-cjs-loads` and `check:type-check-debt` first exited 3 (PREREQUISITE NOT MET, nothing measured) on the closure-only build; after the whole-workspace build both exited 0. - Among them: `check:doc-authoring`, `check:nul-bytes`, `check:rest-log-declared`, `check:route-envelope`, `check:system-context-census` (106 elevation read sites, the page's 102 symbols held) and `check:issue-citations` (self-test). - **Artifact rosters:** 33 of the 36 non-self-test roster rows exit 0 at `93e4d69ba6`, `check-changeset-fixed` (the one whose roster sits under `.changeset/`) and `check:route-ledger-census` among them. The other three, `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`, answer 「NOT WIRED」 (exit 2) without a pull request's context; they are run against this PR once it exists and reported on the card. ## Hypotheses (measured first) - **H0 holds.** The filtered census answers 191 dead sites at `a186aea996` (186 lines, 14 files, 51 numbers), equal to the card's count at `f11b5f20a2`: no net drift, although PR objectstack-ai#20601 (merged as `eb4b17c346`, before this base) touches four files in `packages/rest`. - **H1 holds.** After the rewrite the filtered census answers 0. The supplementary reading leaves 3 test-comment sites, the three listed above: an open decision, an untaken option and a transferred issue, none with a deciding commit. No site was held for an open PR: the claim's read and this stage's two reads of the open PRs' file lists (10:27:35Z, 7 open PRs; 11:30:34Z, 8 open PRs) found none touching `packages/rest`. - **H2 holds, by the token guard.** A comment-stripped comparison of every touched file (the parser's leaf tokens, JSDoc excluded) is empty, and its code and string controls fire. The emitted `dist` is not byte-identical, because the docblocks ship, which is why the changeset is `patch`. ## Acceptance notes - **Form D, not touched here.** 127 dead numbers stand inside string literals: 126 in test titles and test-code strings, and one in the `note` of the REST route ledger's legal-next-state row (`rest-route-ledger.ts:290`, 「(objectstack-ai#10179)」), which is ledger data, not an author-shown refusal. Ruling D (no number, the lesson in words) is a string change outside this comment-only scope; the card already carries a form-D stage for the lane. - **A transferred issue among the 404s.** #14026 answers 302 to objectstack-ai/objectui#10102 on its web endpoint. The census classes it `allocated-but-absent` (deleted and transferred are only told apart under `--probe-cause`), and `scripts/check-issue-citations.mjs`'s header says the `transferred` arm has no positive specimen on this tree; this is one. Noted, not filed. - **The grammar does not read a slash-joined number.** `CITATION_RE` refuses a `#` preceded by `/`, so the second number of `#A/#B` is never judged. In `packages/rest/src` six such dead numbers stood at 10 comment sites, all rewritten here; one more, `objectstack-ai#14389` in `objectstack-ai#14095/objectstack-ai#14389`, stands inside a string (`error-response-structured-arm-door-parity.test.ts:187`) and is kept. The same shape PR objectstack-ai#20624 and PR objectstack-ai#20612 reported. Noted, not filed. - **Outside the scope and the census surface.** `packages/rest/vitest.config.ts:21` cites objectstack-ai#17853, which answers 404; `packages/rest/test-typecheck-debt.json`, written by `gen:test-typecheck-debt`, carries objectstack-ai#13470, objectstack-ai#13454, objectstack-ai#13377 and objectstack-ai#13378 in its prose, all 404. Neither is under `src/**`. The other numbers in `vitest.config.ts`, `tsconfig.json` and `tsconfig.test.json` answer 200. - **Two comments stale on their own, not touched.** The anchor research found `rest-server.ts`'s `api` docblock near `:1115` and the 「zero read sites」 sentence at `:4092` both overtaken by `80153f5a4`, whose own acceptance notes record it. This PR re-anchors their citations and leaves their claims alone. - **An attribution corrected by the anchor.** `rest-server.ts:4092` credited its zero-read-site count to 「the objectstack-ai#14369 census」, which (`a3d5724c8`) excluded `api`; it now cites `53cbad9f7`, the commit that measured it. - **Base.** One merge of `origin/main` (`93e4d69ba6`) before the `--base origin/main` run, as the dispatch orders. ## Deviations - Ten sites beyond the census's read grammar carry a slash-joined dead number and are rewritten; six more lines are the other half of a rewritten sentence (listed under What changed). - The whole-workspace build ran with `--concurrency=4`, not 2, to stay inside the ten-minute foreground cap on this host; it took 1m42s. - Anchor research for 33 of the 77 numbers ran in three read-only research subagents; every proposal was verified here against the commit's message or diff, and the wording of each changed line was reviewed and corrected by hand in a second pass. - Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), and the pre-push trailer check passed on every push. The merge commit carries git's default message. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20534
Clause-②: no
What changes
parseDateCellinpackages/rest/src/import-coerce.tsis the reader behindPOST /api/v1/data/:object/importfordate,datetimeandtimecells. It had four faults:new Date(s), which reads the cell in the server process's zone and reads07/08/2026month-first;dateyear below 1000 without padding;datetimethroughDate.UTC(y, …), which puts years 0..99 in the 1900s.This PR carries out triage's answer A (
5883900872). The maintainer ruling on the card (5885066497) supersedes A on one point: year-first dates stay admitted.The set that is read. After trimming, a text cell is read only in these shapes:
YYYY-MM-DD;YYYY-MM-DDTHH:MM[:SS[.f]], thenZ,±HH:MM,±HHMMor nothing;YYYY-MM-DD HH:MM[:SS[.f]]with no zone, the export shape. xlsx date cells reach the reader in this shape.datetimestring since record validator: the temporal write arms trust Date.parse — date2026-02-30is stored verbatim (500 on PostgreSQL), datetime2026-02-30T10:00:00Zrolls over to March 2, and a non-ISO datetime is read in the host zone #20525.YYYY/M/DorYYYY-M-D: a four-digit year, a one- or two-digit month and day, and the same separator in both places;H:MMorH:MM:SS, with a one- or two-digit hour;Tin this form. It is one closed grammar,YEAR_FIRST_CELL, besideISO_TEMPORAL_CELL.timecell, also a bareHH:MM[:SS].The rules every admitted cell keeps:
namesRealCalendarDay, arithmetic, never aDateround trip).2026-02-30and2026/2/30are refused and never rolled over.24:00is refused.T24:00Znames its instant and reads as before.invalid_date, with the existingimport_invalid_date/import_invalid_datetime/import_invalid_timesentences. There is no new error code and no date-format option. Nothing is read in the host zone, and no field order is guessed:07/15/2026,15/07/2026and26/7/15stay refused.datebranch.2026/7/15→2026-07-15,0500/1/1→0500-01-01).Date, or a zone-bearing cell) takes core'stemporalStorageFormdaterule, imported from@objectstack/core.datetime. It is spelled from the day itself (…T00:00:00.000Z), for ISO and year-first alike, so0001-01-01is stored in year 1, not 1901.zonedWallClockToUtcMsin the business zone for adatetime, verbatim for atimeor adate.One source file changes:
parseDateCell, its docblock, and the private helpers beside it:ISO_TEMPORAL_CELL,YEAR_FIRST_CELL,namesRealCalendarDay,readIsoTemporalCell,readYearFirstCellandutcClock.NAIVE_DATE_TIMEandparseNaiveWallClockare replaced.PM hypotheses, measured
H0 holds
Measured through the real
/importroute, JSON rows, no business timezone. The runs usedInMemoryDriverandSqlDriver(better-sqlite3) underTZ=America/New_YorkandTZ=Asia/Shanghai, at basef11b5f20a2; the reader is byte-identical on today'smain(3a89d459af). Memory and SQLite gave the same answer on every cell, at base and at head.2026-02-302026-03-02T00:00:00.000Zinvalid_date2026-02-30 10:002026-03-02T10:00:00.000Z2026-02-30T10:00:00Z2026-03-02T10:00:00.000Z2026-02-30T10:00:00Z2026-03-0207/15/2026 10:002026-07-15T14:00:00.000Z2026-07-15T02:00:00.000Z07/08/20262026-07-08T04:00:00.000Z2026-07-07T16:00:00.000Z07/15/2026,15 July 20262026-07-152026-07-1407/15/2026 10:0014:00:0002:00:002026-07-15 24:002026-07-16T04:00:00.000Z2026-07-15T16:00:00.000Z0500-01-01,0001-01-01,0999-12-31500-01-01reached the write door)0500-01-01,0001-01-01,0999-12-312026/7/15,2026/07/15,2026-7-152026-07-152026-07-15(unchanged)2026/7/15 9:002026-07-15T09:00:00.000Z2026-07-15 09:00:00stores2026/2/302026-02-30reached it)2026-07-15,2026-07-15T10:00:00Z,2026-07-15 10:00:00,2026-07-15T10:00:00+08:00The write door takes
0500-01-01:POST /api/v1/data/:objectanswers201and stores it as written. The import now stores the same value, and a pin asserts they agree.H1 holds: the census,
mainagainst headThe census called
parseDateCelldirectly. The old reader ismain'simport-coerce.tsat3a89d459af, byte-identical to the base. The new reader is this head'ssrc. It covered 111 shapes, the 3 kinds, 2 host zones and 2 business-zone settings (none, andAsia/Shanghai), 666 rows. That is the first round's 98 shapes (588 rows) plus 13 year-first edges.main→ headt/z, a zone-naive24:00, a number, or a year-first date outside its one form.mainadmitted that are now refused, each by the ruling:2026/2/30(impossible day);2026/7-15(mixed separator);2026/7/15 24:00;2026/7/15T9:00(T);2026/7/15 9:00Z(zone);2026/7/15 9:00:00.5and2026/07/15 10:00:00.123(fraction).date(500-01-01→0500-01-01, including0500/1/1and aDateof year 500), or the 1900s fix for a bare day into adatetime(0050-01-01was1950-01-01T00:00:00.000Z).timefield. The cells are2026/7/15,2026/07/15,2026-7-15,2026-07-5,2028/2/29and0500/1/1, under both business-zone settings.mainread these throughnew Date(s)in the host zone:04:00:00in New York,16:00:00in Shanghai, and04:56:02/15:54:17for year 500. No host-independent reading can equal a value that differs by host.timefield is read onmainand on head alike:00:00:00.mainadmits. Every one stores the same value at head, apart from the padding, the real-day refusals, the ruled refusals above and thetimeexception.H2: the year pad, and where each rule comes from
datebranches call@objectstack/core'stemporalStorageForm(…, 'date'). ISO text branches never turn the year into a number; the grammar requires four digits and the cell's own digits are kept. The year-first branch pads month and day and keeps the four-digit year.namesRealCalendarDayis private torecord-validator.ts, andpackages/objectqlis read-only for this claim. It is copied here, word for word in its arithmetic, as one private helper that both readers share.H3 holds, in the direction expected, in both rounds
Round 1: removing the refusals. The ablation restored the
new Date(s)fallback for every cell the reader refuses, at head9b31e7bc76.5d87c0ac825f→f4cfe0c42335.64 failed | 171 passed (235).git diff HEADis empty.Round 2: removing the year-first branch. The anchor
readIsoTemporalCell(s) ?? readYearFirstCell(s);becamereadIsoTemporalCell(s);, viascripts/ablation-replace.mjsin wrap mode at head279ca425fa.4d5fb1969259→d8a032952d0a.Tests 22 failed | 237 passed (259).2026/6/3line;coerceRowfixture;2026/08/01 06:00:00line;2026/2/30refusal pins.4d5fb1969259), andgit diff HEADis empty.import-coerce.tsthrough relative imports, never adist/.Tests
packages/rest/src/import-date-cell-iso-real-day.test.ts(new). It uses the real/importroute overSqlDriver(better-sqlite3:memory:), underTZ=America/New_YorkandTZ=Asia/Shanghai. The zone switch is asserted withIntland with the July offset. There are 28 cases per zone:invalid_date, a sibling row is still written, and nothing is stored for the refused row. The rows are the card's 11 plus2026/2/30.0500-01-01,0001-01-01, and a datetime0001-01-01;date2026/7/15stored as2026-07-15;datetime2026/7/15 9:00.2026/7/15 9:00stores the same instant as2026-07-15 09:00:00;GET /export;packages/rest/src/import-coerce.test.ts. The[#20534]table has 42 refused and 36 admitted cases, each asserted equal under both host zones.2026/2/30,2026/7-15,2026/7/15 24:00,2026/7/15 9:60,2026/7/15T9:00,2026/7/15 9:00Z,2026/7/15 9:00:00.5,26/7/15and07/15/2026.main's spelling.coerceRowfixturedue: '2026/07/01'.import-integration.test.tsCSV cell2026/06/30and xlsx text cell'2026/07/01'. The xlsx row now also asserts its storeddue,2026-07-01.parseDateCell('2026/6/3', 'date')→2026-06-03, and the business-timezone2026/08/01 06:00:00→CROSS_MONTH_UTC.import-business-timezone.test.tsis byte-identical tomainagain.3b2e47349e,pnpm --filter @objectstack/rest test --maxWorkers=2gaveTest Files 227 passed (227)andTests 4382 passed | 50 skipped (4432).test:repogave1 passed (1),8 passed (8).pnpm --filter @objectstack/rest typecheckexits 0:tsc --noEmit, thencheck:test-typecheck: OK.2026/7/15,2026/7/15 9:00,2026/2/30,2026/7-15and2026-07-15 09:00:00. Memory and SQLite agree on all 46 cells under both zones, and 0 cells differ between zones.Gates, at
3b2e47349eorigin/mainwas merged twice this round: at7a1faf1a5d, then at3a89d459af(a version-packages commit and four others landed in between).turbo run buildover all of./packages/*and./packages/*/*: 71/71 tasks. The tree was clean afterwards.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 61 commands. All 61 ran with exit 0, after the last commit.--ran:61 derived, 61 run, 0 NOT-MEASURED, 0 UNRUN.check-changeset-fixed;check:authz-resolver,check:filter-alias-parity,check:error-status-conformance;check:route-ledger-census,check:tenant-chokepoint;check-published-list-mirrors,check:published-readme-exports;check:select-shard-packages,check:select-gate-families.check-single-claim-pathsran as a read withPR_NUMBER=20601:PR #20601 modifies none of the 1 declared at-most-one-writer path(s). It read the PR's file list from GitHub.pnpm lint: the whole repository, no narrowing.node scripts/check-issue-citations.mjs --base origin/main: 7 citations, all resolve.check-adr-0087-registrationnames the changeset[BREAKING+clause-②-narrowing] not-required (no-migration-prescription).check-changeset-no-majorreports nomajor.check-empty-changesetpasses.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
(The entry point printed this wording once for each command above. It is pasted once here, with every command it covered.)
Changeset
.changeset/20534-import-date-cell-iso-real-day.mdis@objectstack/restminor, with a line-initialClause-②: no (narrowing).Tor a zone, a fraction, and2026/7/15 24:00.2026/7/15, Excel's default in zh-CN and ja-JP, stays admitted, now with the real-day check and stored padded. It also names thetimeexception.not-required (no-migration-prescription).The shape follows PR #20517. The exported
coerceRownarrows with the door, and the changeset says so.Acceptance notes
InMemoryDriverleg is measured, not pinned. This departs from triage's pin list, as PR fix(rest)!: /import reads a comma in a number cell only as a thousands group, refusing the rest (#20497) #20517 did.@objectstack/driver-memory's test consumers are a ruled, ledgered set (pnpm check:driver-memory-census), so the census is not widened here.5885066497). They are Excel's default in zh-CN and ja-JP. They now keep the rules every other admitted cell keeps, and they read the same as onmainexcept for the ruled refusals and thetimeexception in the census.timebranch is covered too. Its fallback was the samenew Date(s):07/15/2026 10:00into atimefield was stored as14:00:00on a New York host and02:00:00on a Shanghai host. It is inparseDateCell, inside the claim's file surface, and it is the same host-zone reading triage ruled out.24:00is now refused, in both the ISO and the year-first form. It used to fall through tonew Date(s), in the host zone.2026-07-15T24:00:00Znames its instant and reads as before.dateordatetimecell with a bareDate.parse(packages/plugin-grid/src/ImportWizard.tsxvalidateValuein objectui), so it marks07/15/2026valid while the server refuses it.content/docs/data-modeling/import-mappings.mdxsays date cells are "parsed to storage form" and lists no spellings. There is no carrier, so it is noted here only./exportwrites adate/datetimecell with a year below 1000 unpadded (0500-01-01→500-01-01), so the export does not re-import #20602:/exportwrites a year-500 row unpadded (500-01-01), so the export does not re-import.zonedWallClockToUtcMsstores a zone-naive0050-01-01 10:00:00as1950-01-01T10:00:00.000Z. This PR leaves that reading as it was, for ISO and year-first cells alike:packages/core/**is read-only for this claim.Generated by Claude Code