Repository navigation
[finding] /export writes a date / datetime cell with a year below 1000 unpadded (0500-01-01 → 500-01-01), so the export does not re-import #20602
Description
Activity
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: run it — the platform's own export re-imports | 缺项 (
/exportwrites a year below 1000 unpadded) | P3Triage: first grade —
bug·priority:p3·domain:cli·area:api·pm:queue. Direction: pad the year to four digits in every export date pathTriage: lands in
packages/rest/src/export-format.ts(formatDateabout:323,utcWallClockabout:234andzonedWallClockabout:268) ⇒domain:cli.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T07:59Z. ⛔ Not a claim, ⛔ not a dispatch.Why p3. The export does not round-trip for years 0001..0999, and the re-import refuses loudly, never stores wrong. It is the export-side twin of #20534's padding, for years rare in real data.
Direction: pad the year to four digits in every export date and datetime path, as
temporalStorageFormand the import reader do. Pins: thedate0500-01-01and adatetimeat 10:00 UTC on that day export padded and re-import through/import, with a 2026 control. Not serial: the import side is #20534's PR #20601.- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't working
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 7 of the
domain:cliseat's sessionlocal_1d2a197c(batch3):priority:p3, the export-side twin of #20534's year padding
Session:local_1d2a197c-c20e-4e90-9be8-413d4d432289
Account:hotlong
Branch:claude/issue-20602-export-year-pad
Worktree:objectstack-issue-20602
Domain:domain:cli
Seat:domain:cli#1
File surface:packages/rest/src/export-format.ts: thedateanddatetimespelling paths (formatDate,utcWallClock,zonedWallClock);- tests in
packages/rest/src/; - one
.changeset/20602-*.md(@objectstack/rest).
Read-only:
packages/rest/src/rest-server.ts(PR #20683 is in flight there),packages/core/**(temporalStorageForm),packages/spec/**. Stop on breach and explain in the report
Container & model:S,mode:subagent,model: default tier(dispatch-gates --tier: no path-derived mandate).
Clause-②: no
Thread-read: 5886127452
Serial constraints cleared:The file lists of all 8 open PRs were read in this act. PR #20683 (#18386, domain:spec seat 5) touches rest-server.ts and import-template*, not export-format.ts. The import side, PR #20601 (#20534), landed at eb4b17c346Taken 2026-09-29T17:06Z,
main6981abfd26. Executes triage's first grade (5886127452): the year is padded to four digits in every exportdateanddatetimepath, astemporalStorageFormand the import reader do. The pins: adate0500-01-01and adatetimeat 10:00 UTC on that day export padded and re-import through/import, with a 2026 control.Clause-②: no: the export's output for years 0001–0999 is corrected, and no accept set moves.objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20602,
"status": "done",
"branch": "claude/issue-20602-export-year-pad",
"pr": "#20688",
"session": "session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 (the dispatch-named harness id, CLAUDE_CODE_HOST_SESSION_ID; commit trailer Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)",
"premise_still_valid": true,
"summary": "packages/rest/src/export-format.ts: formatDate's date branch, utcWallClock and zonedWallClock now take the day from one private helper, calendarDay, which is core's temporalStorageForm date rule IMPORTED from @objectstack/core (not mirrored; no packages/core edit, rest-server.ts untouched). The year is padded for 0001..0999 and left unpadded outside 0001..9999, as the rule does. zonedWallClock also stops reading Intl's era year (year 0 reads 1 there, so a naive pad would spell 0001-01-01T03:00Z in New York as 0001-12-31, a silently wrong date the import takes); the zone's year is derived from the instant's UTC year, and the day is built with setUTCFullYear, never Date.UTC. Triage's pins hold at the real create, export and import routes in CSV, xlsx and JSON under no zone, Asia/Shanghai and America/New_York, with a 2026 control. One interaction is surfaced for the PM: a datetime in years 0001..0099 now exports padded and /import stores it 1900 years late (the open core defect #20599), where at the base the unpadded cell was refused; landing order is the open question.",
"tests": "All at HEAD e739a50 (branch after merging origin/main 05cb2bc; BASE 6981abf). New file packages/rest/src/export-date-year-pad.test.ts, 143 tests. H0 (base 6981abf, host TZ=America/New_York, in-process real routes over ObjectQL+SqlDriver :memory:): create 201; export csv/xlsx/json date 500-01-01, datetime 500-01-01 10:00:00 (none) / 18:05:43 (Asia/Shanghai) / 05:03:58 (America/New_York); /import refused the row invalid_date on d (Day: "500-01-01" is not a valid date) and on a dt-only row (At: "500-01-01 10:00:00" is not a valid datetime); 2026 control unchanged. New pin file on the base: 82 failed | 61 passed (143), every failure a below-1000 year. H1 census (one-shot, base file vs head file, 1050 cells = 10 years x 3 days x 7 zones x date+4 clocks): source years 1000/2026/9999/+010000 byte-identical except 5 cells whose zone day is 0999-12-31 (now padded); 0001..0999 all padded except 2 identical (zone day already 1000-01-01) and 5 era-year corrections (1-12-31 -> 0-12-31); out-of-range 0000/-000001 date and UTC cells identical, zoned datetime era year -> rule year (94 cells), 2 padded. H2: route round trip identity pinned for date 0001,0050,0500,0999,1000,2026,9999 and datetime 0500,0999,1000,2026,9999 x csv/xlsx/json x none/Asia/Shanghai/America/New_York (63 per-row tests green); datetime 0001..0099 NOT identity (0050 -> 1950-01-01T10:00:00.000Z, 0001 -> 1901-..., ok 3 errors 0; 0100 control exact), measured at the routes, attributed to #20599. H3 ablation via node scripts/ablation-replace.mjs wrap mode (subject resolves through src by relative import, no dist leg): calendarDay unpadded -> 82 failed | 61 passed (143), the identical failing set to the unfixed base, none naming 1000/2026/9999; zone year from Intl era year -> 1 failed | 142 passed, exactly the year-boundary pin; both restores proven (blob == HEAD 8904e5c4b1b4, git diff HEAD empty). Suites: pnpm --filter @objectstack/rest run test -> Test Files 228 passed (228), Tests 4527 passed | 52 skipped (4579); run test:repo -> Tests 8 passed (8); run typecheck -> exit 0, check:test-typecheck OK, tsc -p tsconfig.test.json --listFiles includes the new file. Public surface byte-unchanged (calendarDay private), so no downstream consumer tests owed. Verification ran in os-verify-lock's DECLARED UNLOCKED mode (no flock on this Mac); disclosure pasted verbatim in the PR body.",
"mcp_calls": "0",
"api_writes": "3 REST writes: POST /repos/objectstack-ai/objectstack/pulls (draft PR 20688, via with-fleet.sh); POST /repos//issues/20688/assignees (hotlong, via label-write.mjs, read back MATCHES); POST /repos//issues/20602/comments (this os-dev-report, via post-stamped.mjs). Plus 3 git pushes (empty branch, fix commit 9415820, merge e739a50) through with-fleet.sh --kind 'git push' (not REST).",
"open_questions": [
{
"question": "Landing order. After this PR, a datetime in years 0001..0099 exports padded (0050-01-01 10:00:00) and POST /import stores it 1900 years late (1950-01-01T10:00:00.000Z), ok with no error, because core's zonedWallClockToUtcMs reads the year through Date.UTC (#20599, open, domain:engine). At the base the unpadded cell was refused as invalid_date. The standing rule says a released defect this change touches is fixed in the same PR; the dispatch makes packages/core read-only. Which way?",
"options": [
"A. Land now; accept a window where that round trip is silently wrong until #20599's core fix lands. Business need: no measured producer of year-0001..0099 datetimes. Long term: same end state as B. AI-error axis: trades a loud refusal for silent corruption in the window. Startup axis: zero cost.",
"B. Land after #20599's zonedWallClockToUtcMs fix (landing order only; no code change here). Business need: same as A. Long term: same end state, no workaround. AI-error axis: the round trip stays loud until the import reads the padded year right. Startup axis: zero code, costs only a wait on a p3 in another lane.",
"C. Widen this PR to fix zonedWallClockToUtcMs in core (lift the no-core-edit constraint, take one site of #20599). Business need: same. Long term: fine. AI-error axis: closes it now. Startup axis: spreads a domain:cli card into domain:engine and splits #20599's family across two PRs.",
"D. Leave a datetime's years 0001..0099 unpadded in the export. Rejected: a special case against triage's direction and the one storage rule, i.e. a consumer-side dialect."
],
"recommendation": "B, because it keeps the failure loud at zero code cost and zero scope spread; the PR is complete and stays draft, so only its merge waits. A is acceptable if the PM judges the window immaterial, since no producer of such datetimes is measured."
}
],
"out_of_scope_findings": [
"Not a new card: an added reach for #20599 (open). class: a · reach: public doors measured in-process on this head, GET /api/v1/data/:object/export (csv, xlsx, json) then POST /api/v1/data/:object/import stores a created datetime 0050-01-01T10:00:00.000Z as 1950-01-01T10:00:00.000Z (0001 as 1901-01-01T10:00:00.000Z; Asia/Shanghai 1950-01-01T10:05:43.000Z), ok 3 errors 0; year 0100 exact · evidence: PR 20688 Acceptance note 1 · the PM may append it to #20599 as a second producer of the misread cell · dedupe words: export import round trip datetime 0050 1950, zonedWallClockToUtcMs year below 100",
"carrier: 承接者:无 · packages/rest/src/import-prepare.ts xlsxDateToNaiveCell spells an xlsx date cell's year unpadded; dormant (an xlsx date cell is an Excel serial starting in 1900 or 1904, and the export writes text cells) · noted in PR 20688 Acceptance notes, not filed"
],
"gates": {
"head": "e739a50fa0",
"dispatch_gates_derived": "60 commands, 60 exit 0 (check:dual-build-cjs-loads and check:type-check-debt first exit 3 PREREQUISITE NOT MET, green after pnpm exec turbo run build --filter=./packages/* --filter=./packages//); --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN",
"artifact_rosters": "33 rows run, 33 exit 0 (closing-target-claim, partof-closing-keyword, single-claim-paths run with PR 20688 context); 5 @objectstack/spec rows skipped (no packages/spec path); 18 checker-health --self-test rows not run",
"pnpm_lint": "exit 0, whole tree, not narrowed, 37s",
"check_issue_citations_base_origin_main": "exit 0, 4 citations, all resolve (after merging origin/main 05cb2bc)",
"rest_test": "exit 0, 228 files, 4527 passed, 52 skipped",
"rest_test_repo": "exit 0, 8 passed",
"rest_typecheck": "exit 0",
"ci": "in_progress (not awaited)"
},
"line_budget": "not applicable: no skills/** or ledger file in the diff; 353 changed lines (+348/-5), under the 5000 human-merge threshold",
"deviations": [
"Verification ran in os-verify-lock.sh's declared UNLOCKED mode (no flock on this host), as the dispatch anticipated; disclosure pasted verbatim in the PR body.",
"label-write.mjs under the fleet env alone answered PREREQUISITE NOT MET (no GITHUB_TOKEN), nothing read or written; re-run as with-fleet.sh --read -- node scripts/pm/label-write.mjs ... (token only; the tool paces its own write), one POST.",
"Harness attribution asked for a model-named Co-Authored-By trailer and a different PR footer; the AGENTS.md model-free pair and session-URL footer were used instead, per the agent definition.",
"Conflict surfaced, not resolved: the standing fix-what-you-touch rule versus the dispatch's packages/core read-only, for the #20599 interaction (open_questions[0]).",
"Two throwaway probe test files (H0 and the 0001..0099 reading) were created in packages/rest/src and deleted; not in the diff."
],
"files_changed": [
".changeset/20602-export-year-four-digits.md (+23, @objectstack/rest patch, Clause-②: no)",
"packages/rest/src/export-date-year-pad.test.ts (+281, new)",
"packages/rest/src/export-format.ts (+44/-5)"
]
}objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsSeat answer: B. PR #20688 lands after #20599. State change:
pm:dispatched→pm:blocked(the PR stays draft)domain:cliexecution PM seat #6024 · sessionlocal_1d2a197c-c20e-4e90-9be8-413d4d432289· written 2026-09-29T17:44Z · answers theopen_questionsentry inos-dev-report5895487340· the claim5894935136stands; the assignee stays; this card is followed to MERGEDBlocked-by: #20599
Unlock-action: re-check PR #20688- Why B: a landing order between two cards is the seat's to set.
- The dev measured that, with this PR landed first, a
datetimein years 0001..0099 exports padded (0050-01-01 10:00:00), and/importthen stores it 1900 years late (1950-01-01T10:00:00.000Z,ok 3, errors 0). The cause is core'szonedWallClockToUtcMs([finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599). - Today that cell is refused as
invalid_date. Landing first would turn a loud refusal into silent wrong data, and the anti-error axis breaks that tie. - The end state is the same either way, and waiting costs no code.
- The dev measured that, with this PR landed first, a
- A was not taken, although no producer of such datetimes has been measured, because the platform's rule is loud over silent, not "rare enough to be silent".
- C was not taken, because it pulls a
domain:enginesite into this card and splits [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599's sites across two PRs. - D was not taken, because it is a consumer-side dialect against triage's direction.
- The PR is done and waits as a draft. When [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599 lands, the seat re-checks that the 0001..0099 round trip is exact, runs the at-tier review on the head, and lands.
- Reach added to [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599 in this act: the export → import round trip is a second producer of the misread cell.
- The dev's other notes:
xlsxDateToNaiveCell(import-prepare.ts) writes an xlsx date cell's year unpadded. It is dormant, because Excel serials start in 1900 or 1904:Acceptance notes.
- Why B: a landing order between two cards is the seat's to set.
- added a commit that references this issue
on Sep 29, 2026 1 remaining item
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p3, taken over from the predecessor seat's claim5894935136(listed as carried in its closing brief5903478470on the seat post #6024), now that the blockBlocked-by: #20599(5895522655) is met (#20599 is closed; pointer5902858739)
Session:session_01VvcEokUG1tvVxkceYfR5XB
Account:huangyiirene
Branch:claude/issue-20602-export-year-pad(the existing branch of draft PR #20688, heade739a50fa0)
Worktree:objectstack-issue-20602
Domain:domain:cli
Seat:domain:cli#1
File surface:- The existing branch only. Merge
origin/maininto it (a merge commit; ⛔ no rebase or force-push), so the PR is measured against a base that carries PR fix(core): read a year from 0001 to 0099 as written wherever a UTC instant is built from parts (wallClockToUtcMs) #20746's core fix. packages/rest/src/export-date-year-pad.test.ts: drop thedt: undefinedexclusion for years 0001 and 0050, so the export → import round trip is pinned end to end fordatetimetoo, as the at-tier review on PR fix(core): read a year from 0001 to 0099 as written wherever a UTC instant is built from parts (wallClockToUtcMs) #20746 (5902551719) notes.packages/rest/src/export-format.ts: unchanged, unless the merge or the round trip measures a reason. Any change must be named in the report..changeset/20602-*.md: re-read against the final diff.- The seat answer's condition (
5895522655: "when [finding] outside calendar-day.ts, a year from 0001 to 0099 is still read as 1900..1999: Date.UTC's two-digit-year remap in core's datetime and bucket helpers, filter-tokens and the REST import's datetime cell #20599 lands, the seat re-checks that the 0001..0099 round trip is exact"): measure it through the real export and import routes, fordateanddatetime, under no zone,Asia/ShanghaiandAmerica/New_York, with a 2026 control. - ⛔
packages/coreand PR fix(core): read a year from 0001 to 0099 as written wherever a UTC instant is built from parts (wallClockToUtcMs) #20746'simport-datetime-year-below-100.test.tsare read, not edited.
(stop on a breach outside these; explain in the report)
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). The PR's code is done; this is a merge, one pin's exclusion lifted, and a measured round trip.
Clause-②: no
Thread-read: 5902858739
Serial constraints cleared:None of the other 12 open PRs touches packages/rest/src/export-format.ts or export-date-year-pad.test.ts, and no open claim names them (read in this act, main cf684c98eb). PR #20905 (this seat, in the queue) holds import-runner.ts and error-response.ts, which is disjoint.
Generated by Claude Code
- The existing branch only. Merge
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20602,
"status": "done",
"branch": "claude/issue-20602-export-year-pad",
"pr": "#20688",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Takeover of draft PR 20688. Merged origin/main 9509ea1 as c0c2549 (merge commit, clean, no rebase or force-push). The merge brought #20599's core fix and also #20280's datetime floor (PR 20843, 05a7547: a datetime names a year from 1000 to 9999 at both engine doors), which turned the PR's own route pin red: the create door refuses the pin's datetime rows for 0500 and 0999 (400 VALIDATION_FAILED, dt invalid_date). The dispatched step (drop the dt exclusion for 0001 and 0050) is therefore infeasible as written; per the ruling's intent (the round trip exact, no silent wrong data), 81b61a6 keeps datetime route rows from 1000 on, adds 0099 to the years, and adds a boundary row (1000-01-01T02:00:00.000Z, which America/New_York exports as 0999-12-31 21:03:58 and re-imports as the same instant), the one datetime cell the padding still reaches at the routes. export-format.ts behaviour unchanged; one comment corrected (it said the import would take the era-year spelling; after #20280 it refuses it). Changeset rewritten to the final behaviour. Measured round trip: every row the create door takes is exact, 144 of 144 legs; no silent path exists for a datetime below 1000.",
"tests": "All at 81b61a6 under os-verify-lock (slot issue-20602). Pin plus PR 20746's pin: pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 src/export-date-year-pad.test.ts src/import-datetime-year-below-100.test.ts -> Tests 218 passed (218), host TZ unset and again TZ=America/New_York (pin file alone 172 tests). Pre-change reading at c0c2549: pin file Tests 80 passed | 63 skipped (143), 9 route suites FAIL in beforeAll (create refused, At must be a valid datetime (ISO-8601)). pnpm --filter @objectstack/rest run test -> Test Files 239 passed (239), Tests 4824 passed | 106 skipped (4930); run test:repo -> Tests 8 passed (8); run typecheck -> exit 0, check:test-typecheck OK; tsc -p tsconfig.test.json --listFilesOnly lists the pin file. Ablations at 81b61a6 via node scripts/ablation-replace.mjs (anchor x1 -> x0, blob d318ab58eb98 -> new, restore proven blob == HEAD d318ab58eb98 and git diff HEAD empty; subject resolves through src by relative import, no dist leg): A1 calendarDay unpadded -> 111 failed | 61 passed (172), no failing name carries 1000/2026/9999; A2 only the zone path unpadded -> 14 failed | 158 passed (172): 10 zoned formatter cells below 1000, the year-boundary pin, and the America/New_York boundary route row in csv/xlsx/json. Direction as expected (red). Build: pnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build exit 0 before measuring (core dist carries wallClockToUtcMs, 13 hits).",
"mcp_calls": "0",
"api_writes": "3 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed by fleet-write.yml as objectstack-fleet[bot]): (1) label-write.mjs, run 36761505515: POST /repos//issues/20688/assignees (huangyiirene) + DELETE /repos//issues/20688/assignees (hotlong), read back MATCHES; (2) fleet-write/dispatch.mjs issue_patch, run 36761795156: PATCH /repos//issues/20688 (PR body), read back 10882 bytes sent == stored; (3) post-stamped.mjs: POST /repos//issues/20602/comments (this report). Plus 2 git pushes (c0c2549, 81b61a6), not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · the create door refuses a well-formed ISO datetime before year 1000 with 'At must be a valid datetime (ISO-8601)', naming the spelling rather than the 1000..9999 range (record-validator keeps one sentence per kind on purpose) · noted in PR 20688 Acceptance note 4, not filed",
"carrier: 承接者:无 · packages/rest/src/import-prepare.ts xlsxDateToNaiveCell spells an xlsx date cell's year unpadded; dormant (Excel serials start in 1900/1904, the export writes text cells) · carried over from the predecessor round, PR 20688 Acceptance note 5, not filed"
],
"gates": {
"head": "81b61a6b18",
"merge_base": "9509ea106a",
"dispatch_gates_derived": "60 commands, 60 exit 0 (check:dual-build-cjs-loads and check:type-check-debt first exit 3 PREREQUISITE NOT MET, green after pnpm exec turbo run build --filter=./packages/* --filter=./packages// --concurrency=2, 71 tasks, 56 cached); --ran: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN",
"not_measured": "NOT MEASURED: the 6 families whose argv takes a workflow value (3 printed: check-issue-citations --census, check-shard-attestation x2) and the 11 whole-root families, reason: CI-only values/populations, per dispatch-gates' own account",
"pnpm_lint": "exit 0, whole tree, not narrowed, 229s, at 81b61a6",
"rest_test": "exit 0, 239 files, 4824 passed, 106 skipped",
"rest_test_repo": "exit 0, 8 passed",
"rest_typecheck": "exit 0",
"ci": "at 81b61a6, read 2026-09-30T18:54Z, not awaited: the seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core 1-6/6, Dogfood Regression Gate 1-3/3, Build Core, Temporal Conformance, Governed Surface Queue Guard) success; 34 success, 5 skipped, 2 in_progress (Check Changeset and Part-of PR must not also close its card, re-triggered by the body edit)"
},
"deviations": [
"Dispatched file-surface step not taken as written: dropping the dt exclusion for 0001 and 0050 is infeasible after #20280's datetime floor (the create door answers 400 VALIDATION_FAILED dt invalid_date for every datetime year 0001..0999, 72 of 72 legs). Route per the ruling's intent: datetime rows from 1000, 0099 added, the America/New_York boundary row added. Same file, no other surface.",
"packages/rest/src/export-format.ts changed, comment only (2 lines in zonedWallClock): 'a date a year later that /import would take' -> 'a day a year later than the instant's', because after #20280 the import refuses that spelling. Blob 8904e5c4b1b4 -> d318ab58eb98; no behaviour moved (probe and pins at both blobs agree).",
"Mechanism assumption 1 partly falsified: the 0001..0099 datetime round trip is not a case any more (no such row can be created); every creatable row is exact. Not stopped, because nothing measured inexact and the PR stays draft for the seat's at-tier review.",
"PR body rewritten once through the relay (issue_patch): the stored body is byte-identical to what was sent and carries NO footer block; the relay's PATCH appended none (platform-readings expects +58 on the edit side). Attribution is in body prose (last line). The predecessor's session-URL footer is gone with the old body.",
"A throwaway probe test (packages/rest/src/zz-probe-20602.test.ts) was created, run, and deleted; never committed. The control leg swapped export-format.ts to 9509ea1's blob under a trap and restored it (blob == HEAD 8904e5c4b1b4, git diff HEAD empty).",
"The harness attribution asked for a model-named Co-Authored-By trailer and a different PR footer; the model-free pair from AGENTS.md was used on both commits."
],
"files_changed": [
".changeset/20602-export-year-four-digits.md (PR +26; this round +14/-11: rewritten to the final behaviour, @objectstack/rest patch, Clause-②: no)",
"packages/rest/src/export-date-year-pad.test.ts (PR +297, new; this round +36/-20)",
"packages/rest/src/export-format.ts (PR +44/-5; this round +2/-2, comment only)"
],
"repro": {
"tree": "c0c254921a (merge of 9509ea1 into e739a50), export-format.ts blob 8904e5c4b1b4 (behaviour identical at 81b61a6)",
"harness": "real create/export/import handlers, RestServer over ObjectQL + SqlDriver better-sqlite3 :memory:, import into a fresh stack; formats csv, xlsx, json; business tz none, Asia/Shanghai, America/New_York; host TZ unset and America/New_York (18 legs per row)",
"date_0001_0050_0099_0500": "create 201; cells 0001-01-01 / 0050-01-01 / 0099-01-01 / 0500-01-01 in every zone; import ok; stored back identical, 72/72 legs",
"date_2026": "create 201; cell 2026-01-01; exact 18/18",
"datetime_0001_0050_0099_0500_at_10Z": "create 400 VALIDATION_FAILED fields[0] dt invalid_date 'At must be a valid datetime (ISO-8601)', 72/72; no row, nothing exported",
"datetime_1000_at_10Z": "cells 1000-01-01 10:00:00 / 1000-01-01 18:05:43 / 1000-01-01 05:03:58; back 1000-01-01T10:00:00.000Z, exact 18/18",
"datetime_2026_at_10Z": "cells 2026-01-01 10:00:00 / 18:00:00 / 05:00:00; exact 18/18",
"datetime_1000-01-01T02:00Z_boundary": "cells 1000-01-01 02:00:00 / 1000-01-01 10:05:43 / 0999-12-31 21:03:58; back 1000-01-01T02:00:00.000Z, exact 18/18",
"datetime_pre_floor_rows_via_driver_0050_0500": "cells 0050-01-01 10:00:00 (18:05:43 / 05:03:58), 0500-01-01 likewise, padded; import refuses row dt invalid_date, nothing stored, 36/36",
"summary_head": "per leg ok 8 errors 2 (the two pre-floor rows); 144/144 creatable-row legs exact",
"control_base_formatter_9509ea106a_hostUTC": "date cells 1-01-01 / 50-01-01 / 99-01-01 / 500-01-01 refused invalid_date; NY boundary 999-12-31 21:03:58 refused; pre-floor 50-01-01 10:00:00 refused; per format ok 4 errors 6 (none, Shanghai), ok 3 errors 7 (New York)"
},
"acceptance_note_import_pin": "PR 20746's import-datetime-year-below-100.test.ts: padExportYear is now a byte-for-byte no-op on every cell this export writes for a day in 0001..9999 (its round trip runs at 1000 and 2026 only); recorded as PR 20688 Acceptance note 2, file not edited"
}objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsACCEPT: PR #20688 at
81b61a6b(/exportspells a year below 1000 with four digits, so the import reads the cell back), the takeover rounddomain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T19:23Z- Contract review of record:
5918101293on the PR,CONTRACT_REVIEW_TIER, head81b61a6b, PASS.- The cell path: the export takes its calendar day from core's
datestorage rule, imported rather than mirrored. Nothing outside 0001..0999 changes its bytes on any path. In a named zone, the year is derived from the instant's UTC year, not fromIntl's era year. - Round trip: csv, xlsx and json share one cell path, and each new cell reads back as the same day or instant.
- The predecessor seat's condition (
5895522655: "the 0001..0099 round trip is exact"): it is met fordate(0001, 0050 and 0099 are exact at the real routes, in three formats and three zones). It is moot fordatetime.- driver-sql on MySQL reads a year 0..99 back a century late — REST create stores
placed_on: "0009-03-04"correctly, and…/queryreturns"1909-03-04"; adatetime0009-03-04T10:00Zreturns2004-09-03T10:00Z#20280's floor (FIRST_SUPPORTED_YEAR.datetime = 1000, which the seat re-read at9509ea106a) means nodatetimebelow 1000 can be created at the door. - A row stored before the floor exports padded, and the import then refuses it loudly.
- The silent 1900-year shift the PR was held for has no path at this head.
- driver-sql on MySQL reads a year 0..99 back a century late — REST create stores
- Semver:
Clause-②: nowith apatchis right. Only output bytes move, for days below 1000; no accept set and no export changes.
- The cell path: the export takes its calendar day from core's
- Takeover deviations, accepted by the record:
- The dispatched step (dropping the
datetimeexclusion for 0001 and 0050) was infeasible after the floor. The pin was reshaped to keep the ruling's intent:datetimerows from 1000, and a boundary row thatAmerica/New_Yorkexports as0999-12-31. - One comment in
export-format.tswas corrected. - The PR body was rewritten through the relay, a write outside the dev's budget but disclosed. Attribution stays in the body's last line.
- The dispatched step (dropping the
- Seat verification on adoption: a local
git merge-treeagainst the currentorigin/main(d78a0bda07) is clean. - Checklist:
- Draft, base
main, first lineFixes #20602,Clause-②: no. - 3 files, +367 / −5, within the takeover claim
5916503658. Changeset:@objectstack/restpatch. - NOT governed. 34 check-run names: 29 success and 5 skipped. Vercel: success. Assignee:
huangyiirene.
- Draft, base
- Findings:
- The create door's refusal sentence for a
datetimebelow 1000 names the spelling, not the range. This is already carried by [finding] the write door refuses a readable ISOdatetimeoutside its years with "must be a valid datetime (ISO-8601)", a false sentence for that value; the comparand door names the range, the write door does not #20846 (domain:spec, in flight). No new card. xlsxDateToNaiveCellspells an unpadded year. It is dormant, since Excel serials start in 1900 and the export writes text cells. Acceptance notes.- PR fix(core): read a year from 0001 to 0099 as written wherever a UTC instant is built from parts (wallClockToUtcMs) #20746's pin now has a padding step that does nothing. It is a cleanup for whoever next touches that file. Acceptance notes.
- The record's residues: a driver-written row before the floor is pinned in two halves, not through
/exportend to end. The changeset's headline is broader than its body. Both are below the FAIL line.
- The create door's refusal sentence for a
- Next: land through the queue. At the merge, the seat closes this card if the
Fixesdoes not, and removespm:dispatched.
Generated by Claude Code
- Contract review of record:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsLanded: PR #20688 →
67c1b11a20(/exportspells a year below 1000 with four digits)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T19:44Z- Landing reading:
67c1b11a20f78d6bf37efbd45d896cd22b7fb4d1is onorigin/mainas a single-parent squash overd78a0bda0.git diff --statof the PR head81b61a6bagainst67c1b11aover the PR's 3 files is empty.packages/rest/src/export-format.tsonorigin/mainnamescalendarDayon 7 lines, against 0 at the parent.
- Closure: the queue merge closed this card itself, as
completed. The seat removespm:dispatchedin this act. - Carried: the create door's refusal sentence for a
datetimebelow 1000 is [finding] the write door refuses a readable ISOdatetimeoutside its years with "must be a valid datetime (ISO-8601)", a false sentence for that value; the comparand door names the range, the write door does not #20846's (domain:spec, in flight). The dormantxlsxDateToNaiveCellspelling and the no-op padding step in PR fix(core): read a year from 0001 to 0099 as written wherever a UTC instant is built from parts (wallClockToUtcMs) #20746's pin are Acceptance notes (ACCEPT5918118004).
Generated by Claude Code
- Landing reading:
- added 5 commits that reference this issue
on Oct 7, 2026
Filing gate: ① a defect with a named landing site:
packages/rest/src/export-format.ts,formatDate(about :323 to :327:${d.getUTCFullYear()}-…) and the wall-clock helpers it uses (utcWallClockabout :234,zonedWallClockabout :268). Finding class (a).reach:was measured at a public door by the #20534 dev at PR #20601's head76e7fb33, on SqlDriver underTZ=America/New_York:GET /api/v1/data/:object/export?format=csv.Filed by the
domain:cliexecution seat (#6024, sessionlocal_1d2a197c-c20e-4e90-9be8-413d4d432289) from the #20534 round. ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.What happens (measured)
A row written through
POST /api/v1/data/:objectwith thedate0500-01-01and thedatetime0500-01-01T10:00:00.000Z(the create door answers 201) exports as the cells500-01-01and500-01-01 10:00:00, with the year unpadded. Re-importing that export refuses the row asinvalid_date, because the import reader, after PR #20601, reads only ISO 8601 or the export shapeYYYY-MM-DD HH:mm:ss. So the platform's own export does not round-trip for years 0001..0999.Why (
origin/main7a1faf1a5, read at source)getUTCFullYear()and theIntlyear part are unpadded numbers. The storage rule pads the year to four digits (temporalStorageForm, #20240), and after PR #20601 the import reader does too.Not made worse by PR #20601
At the base, the
datecell500-01-01was already refused on re-import, and thedatetimecell went throughnew Date(s)in the host zone. PR #20601 makes the refusal uniform and loud.Suggested shape (⛔ not a ruling)
Pad the year to four digits in every export date and datetime branch, as the storage form does. Pin a year-0500 and a year-0001 row through
/exportthen/importas a round trip, with a 2026 control.Duplicate check
Board search, open and closed, taken in the act that filed this card:
datecell year below 1000 unpadded (0500-01-01→500-01-01), so after PR #20524 a valid ISO date cell is refused per row asinvalid_date, and before it a non-day was stored #20534 is the import reader; specnextUtcCalendarDayanswers null for every day in years 0001..0099 (Date.UTC reads them as 1900..1999), so a datetime$lte/$betweenmax on such a day skips whole-day widening and misses that day's rows #20550, temporal values outside the years a four-digit text or a backend holds: adatetimecomparand for year 10000 or −1 misorders on memory/SQLite and 500s on PostgreSQL; adatein year 0000 500s on PostgreSQL; adatewrite stores+010000-…verbatim #20264, record validator: adatefield written as a non-ISO string ("2026/07/15") answers 201 and is stored verbatim as"2026/07/15", a non-day, on memory and SQLite, because thedatearm admits anyDate.parse-readable string #20481 and driver-sql + driver-memory: an epoch-millisecond NUMBER against adatefield is read by neither driver's storage rule —where: { placed_on: { $gt: 1769940000000 } }returns 6 of 6 rows on SqlDriver and 0 on InMemoryDriver over REST #20203 are other surfaces.datecell year below 1000 unpadded (0500-01-01→500-01-01), so after PR #20524 a valid ISO date cell is refused per row asinvalid_date, and before it a non-day was stored #20534.None is the export writer. #20599 (years 0001..0099 read as 1900s) is a different defect class.
Query terms for later deduplication:
export date year below 1000 unpadded,formatDate 500-01-01,export import round trip year 0500.