Skip to content

record validator: the temporal write arms trust Date.parse — date 2026-02-30 is stored verbatim (500 on PostgreSQL), datetime 2026-02-30T10:00:00Z rolls over to March 2, and a non-ISO datetime is read in the host zone #20525

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing site and a measured reach:. Finding class (a). reach: is REST POST /api/v1/data/:object, measured on memory, SQLite and PostgreSQL 16.13 at base 0bbe4005e and at PR #20524's head 4c5740d2d by the #20481 dev (os-dev-report 5880394252, out_of_scope_findings[0] and [1]). ⛔ Not re-driven by the seat.

Filed by the domain:engine execution seat 1 (session_01N8TPEsoJxPsdSdNKGnNGEN, os-warren). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. The two arms are folded into one card because they share one landing site and one cause (Date.parse leniency at the temporal write door). Triage may split them.

What happens (measured by the #20481 dev)

Arm 1: an impossible calendar day.

written memory SQLite PostgreSQL 16.13
date placed_on: '2026-02-30' 201, reads back '2026-02-30' (a day that does not exist) 201, '2026-02-30' 500 DATABASE_ERROR ("date/time field value out of range")
datetime opened_at: '2026-02-30T10:00:00Z' 201, reads back '2026-03-02T10:00:00.000Z' the same silent rollover the same silent rollover

Arm 2: a non-ISO datetime string is read in the host's zone. These were measured with the process at TZ=America/New_York, on all three backends:

  • '2026/07/15 10:00', '07/15/2026 10:00' and '15 July 2026 10:00' each read back '2026-07-15T14:00:00.000Z', which is the process zone;
  • the ISO zone-naive '2026-07-15 10:00' reads back '2026-07-15T10:00:00.000Z' (UTC, ADR-0074);
  • '07/08/2026' reads back '2026-07-08T04:00:00.000Z': month-first, plus the process zone.

The stored instant depends on the deployment host.

Why

packages/objectql/src/validation/record-validator.ts, the date / datetime arms:

Arm 2 is the datetime twin of #20481. The #20481 dispatch kept the datetime arm out of that card: measured and reported, not edited.

Suggested shape (⛔ not a ruling)

Dedupe

search_issues, run by this seat in objectstack-ai/objectstack, open and closed:

Dedupe words: date write impossible calendar day 2026-02-30 · datetime write day rollover March 2 · datetime write non-ISO string host timezone · canonicalUtcDatetime Date.parse local zone

Activity

  1. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: run it — records keep what their type promises | 缺项 (the temporal write door rolls an impossible day over and reads a non-ISO datetime in the host zone) | P1

    Triage: first grade — bug · priority:p1 · domain:engine · area:records · pm:queue. Direction: a real calendar day or a refusal, and ISO only for datetime

    Triage: lands in packages/objectql/src/validation/record-validator.ts (the temporal arms; the Date.parse admission at :1214 on origin/main) and packages/core/src/utils/temporal-storage-form.ts (canonicalUtcDatetime, :122) ⇒ domain:engine, by the lane table's objectql and packages/core rows. One card, as filed: one door and one cause.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-28T23:52Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p1. Both arms store a different value than the one written, and nobody is told.

    • A datetime written on the impossible 30 February, at 10:00 UTC, is stored as 2 March on every backend.
    • A non-ISO datetime becomes an instant that depends on the deployment host's zone. The same write lands hours apart on two hosts.
    • PostgreSQL answers the impossible date with a 500, and the other two store a day that does not exist.

    Silent wrong data is data integrity, which comes first (NORTH-STAR rule 1). It is the same grade as #20497. #20481 was p2 because a non-ISO date is stored as written, not as a different value.

    Direction (triage's call, as the card suggests).

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Sep 28, 2026
  3. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 23
    Session: session_01N8TPEsoJxPsdSdNKGnNGEN
    Account: os-warren (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20525-temporal-write-real-day-iso
    Worktree: objectstack-issue-20525
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (triage 5880930860: a real calendar day or a refusal, and ISO only for datetime):

    • packages/objectql/src/validation/record-validator.ts, the date / datetime arms, as PR fix(objectql)!: a date string is written in its YYYY-MM-DD form, or refused with VALIDATION_FAILED / invalid_date (#20481) #20524 (b2b6a0643) left them:
      • Arm 1: a leading YYYY-MM-DD is admitted only when it names a real calendar day, for date and for the day part of datetime. Anything else is refused with VALIDATION_FAILED / 400 before any driver write, naming the field. ⛔ Never rolled over.
      • Arm 2: a datetime string is admitted only in an ISO 8601 spelling, and a zone-naive ISO string stays UTC (ADR-0074). Every other spelling is refused. ⛔ No host-zone reading, and ⛔ no locale guessing.
    • packages/core/src/utils/temporal-storage-form.ts / temporal-comparand.ts, only if the one predicate the validator needs (a real calendar day, an ISO instant spelling) belongs beside the existing temporal rule rather than duplicated in objectql. If a core export is added, the dev reports it and the claim is amended to yes (narrowing).
    • tests in objectql, REST and the drivers (test side only), on memory, SQLite and PostgreSQL:
      • Arm 1: 2026-02-30 for date and datetime, with 2028-02-29 as the leap-day control;
      • Arm 2: under TZ=America/New_York, the card's four spellings refused and an ISO control unchanged;
    • .changeset/20525-*.md.

    Stop on breach and explain in the report.

  4. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20525,
    "status": "done",
    "branch": "claude/issue-20525-temporal-write-real-day-iso",
    "pr": "#20547",
    "session": "session_01N8TPEsoJxPsdSdNKGnNGEN — mode:subagent under the seat session. Before any edit I checked that the newest Claim: (5881460653) names this branch; it does.",
    "premise_still_valid": true,
    "summary": "Draft PR #20547 at head 0999284; its body opens with the closing line for this card. The record validator's date / datetime arm (packages/objectql/src/validation/record-validator.ts, +72 / -2) now refuses two things with VALIDATION_FAILED / 400 and the field code invalid_date, naming the field, before any driver write. Arm 1: a string whose leading YYYY-MM-DD names a day that does not exist, for a date and for the day part of a datetime. Arm 2: a datetime string outside the ISO grammar. That grammar is YYYY-MM-DD; YYYY-MM-DDTHH:MM[:SS[.f]] with Z, ±HH:MM, ±HHMM or no zone (zone-naive = UTC, ADR-0074); and YYYY-MM-DD HH:MM[:SS[.f]] with no zone. H1 holds exactly as dispatched on memory, SQLite and PostgreSQL 16. H2: no reusable helper exists. Core's daysInMonth is private, and spec's exported nextUtcCalendarDay refuses 0001..0099 because Date.UTC reads years 0..99 as 1900..1999. So the predicate is a private arithmetic helper in objectql: no core export, Clause-② stays no (narrowing), and only objectql carries a changeset. H3: space-plus-zone, lower-case t/z, reduced precision (2026, 2026-07) and the expanded year are refused, each for a measured reason given in the PR body. For space-plus-zone, the storage rule hands the string to Date.parse whole, which read 0050-01-01 10:00+01:00 as 1950. Every admitted form reads the same instant under four host zones. T24:00 is admitted (end of day). H4: no shipped producer sends a non-ISO or impossible-day temporal string through the write door. /import converts datetime cells itself before the door, with host-zone and rollover defects; that is reported, and its zone/locale half is raised as open question 1. H5: the comparand door is unchanged and now diverges: an impossible day is admitted as a comparand, and a non-ISO datetime comparand is read in the host zone. Reported as a finding.",
    "tests": "At 5083d67 (the objectql, core, rest and driver-memory trees are byte-identical to the measured head 0999284). objectql: vitest --project local 334 files / 6643 passed; test:repo 1/5; typecheck exit 0; check:test-typecheck debt unchanged at 40 files / 234 errors / 65 signatures, and the new file is in tsconfig.test.json's --listFiles. rest: --project local 224 files, 4241 passed / 46 skipped; test:repo 1/8; typecheck exit 0 (test-typecheck debt 0). driver-memory: 61 files / 1423 passed; typecheck exit 0, and tsconfig.json lists the new file. At 0999284 after the merge and a rebuild of the rest and driver-memory closures: the three new files plus #20481's three give objectql 2 files 7/7, rest 2 files 12/12 (SQLite and live PostgreSQL 16 via OS_TEST_POSTGRES_URL, 0 skipped), driver-memory 2 files 4/4. Ablation, fix committed first. (1) objectql, src path: scripts/ablation-replace.mjs removed writtenAsIso from the verdict line (anchor 1 -> 0, blob e51b0aff89c1 -> fe92054de0c4); the engine pin went 2 failed / 1 passed; the tool restored the file (blob == HEAD, git diff HEAD empty). (2) rest via dist: the first attempt was a NO-RUN because deleting the conjunct left writtenAsIso unused, the DTS build failed with TS6133, and no test ran. Re-anchored to writesAsIsoTemporal(value, t) || value.length >= 0 (blob e51b0aff89c1 -> 0782d15e00fb) and rebuilt objectql; ablation-dist-preflight found the marker in 4 built files; the rest pin went 2 failed / 4 passed (the refusal test red on SQLite and live PG, both controls green); the source was restored (blob == HEAD). Restore leg: rebuilt, preflight --absent reported the marker absent from all 14 built files with the tree clean, the fix marker was present in 4, and rest ran 6/6. H1 before/after at REST was taken with a scratch script (tsx, not committed) under TZ=America/New_York; the tables are in the PR body.",
    "gates": {
    "measured_head": "0999284ab",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0999284: 65 commands. The change set is 5 paths, 595 changed lines (+593 / -2), against merge base 288611e.",
    "ran": [
    "63 of 65 derived commands :: exit 0; each exit code was written to results.tsv before any pipe",
    "pnpm check:dual-build-cjs-loads :: exit 3 PREREQUISITE NOT MET — NOT MEASURED (needs the whole tree built); scoped reading: both require entries of @objectstack/objectql load from the rebuilt dist, . with 178 exports and ./core with 52",
    "pnpm check:type-check-debt :: exit 3 PREREQUISITE NOT MET — NOT MEASURED (needs the whole tree built)",
    "node scripts/pm/dispatch-gates.mjs --ran ran.list --repo objectstack-ai/objectstack :: exit 0 — 65 derived, 63 run, 2 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN",
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 — [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)",
    "node scripts/check-changeset-no-major.mjs --base origin/main --event event.json (the PR body as stored) :: exit 0 — LEVEL AXIS: clause-② no (narrowing), no package whose src it moves is graded patch",
    "roster families whose roster sits in a touched directory: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:filter-alias-parity, pnpm check:object-def-param-keys, pnpm check:tenant-chokepoint :: exit 0 each",
    "narrowed lint: eslint --no-inline-config --format json over the 4 changed TS files :: 4 files, 0 errors, 0 warnings. --print-config shows no parserOptions.project or projectService, so type-aware linting is off and no verdict on an untouched file can move. The repo-wide pnpm lint is CI's."
    ],
    "ci": "not awaited, per the contract; the PM reads CI convergence"
    },
    "line_budget": "5 files, 595 changed lines (+593 / -2) against merge base 288611e. Source: record-validator.ts +72 / -2; the rest is three new test files (194 + 206 + 78) and the changeset (43). Under the 5000-line human-merge threshold. No governed surface.",
    "files_changed": [
    "packages/objectql/src/validation/record-validator.ts",
    "packages/objectql/src/engine-temporal-write-real-day-iso.test.ts",
    "packages/rest/src/data-temporal-write-real-day-iso.test.ts",
    "packages/drivers/driver-memory/src/memory-20525-temporal-write-real-day-iso.test.ts",
    ".changeset/20525-temporal-write-real-day-iso.md"
    ],
    "deviations": [
    "The ruling asks for pins on memory, SQLite and PostgreSQL. The committed REST file has SQLite plus a live-PG cell (a named skip without OS_TEST_POSTGRES_URL) but no memory cell, as on #20481: @objectstack/driver-memory has no binding in packages/rest, and a new binding is a check:driver-memory-census disposition. Memory is covered by the engine pin (recording driver, zero writes on refusal), the driver-memory pin (under TZ=America/New_York, admitted spellings stored as their UTC instant, the leap day as itself), and the scratch REST measurement over InMemoryDriver in the PR body.",
    "PostgreSQL 16 was a private cluster I started (initdb + pg_ctl as the postgres user, port 45525, postmaster PID 20361, server timezone Asia/Shanghai, DateStyle ISO, MDY) under /tmp/os-pg-issue-20525. That is outside the scratchpad, because /tmp/claude-0 is mode 0700 root and the postgres user cannot traverse it, while postgres refuses to run as root. It was stopped (pg_ctl stop -m fast) and the directory deleted before this report.",
    "The first dist ablation attempt was a no-run: the DTS build failed with TS6133, and no test ran. I did not rerun it until it landed. The anchor was changed, the run repeated, and both are recorded in the PR body and the tests field.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer. The commits carry AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), and the pre-push check:commit-card-trailers passed on every push.",
    "Scratch measurement scripts (tsx, importing the worktree's dist by absolute path) lived only in my scratchpad and were never committed.",
    "The dispatch lists the changeset as Clause-② no (narrowing) with no core line. No core export was added, so there is no core line."
    ],
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 — each one repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) through scripts/pm/fleet-write/dispatch.mjs, executed as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #20547, relay run 36510336404); (2) label-write --issue 20547 --assign os-warren -> POST /repos//issues/20547/assignees (relay run 36510387631, read back: assignee os-warren); (3) this os-dev-report via post-stamped.mjs -> POST /repos//issues/20525/comments. No issue_patch: the PR body read back byte-identical (18198 characters, footer intact). Plus git push of the branch (6 pushes: the empty-branch probe, four commits and the merge 0999284). REST reads only otherwise: the card, its comments, comment 5881202199, card #20534, precedent comment 5880394252, PR #20547.",
    "open_questions": [
    {
    "question": "/import's parseDateCell (packages/rest/src/import-coerce.ts, outside this claim; #20534 holds the file) turns a datetime cell into ISO text before the write door, so the door cannot see how it was read. Two readings are not ISO-faithful, measured at POST /api/v1/data/:object/import at 0999284 with no business timezone. First, a month-first or free-form cell falls through to new Date(s), in the server process's zone: datetime 07/15/2026 10:00 was stored as 2026-07-15T14:00:00.000Z under TZ=America/New_York and 2026-07-15T02:00:00.000Z under TZ=Asia/Shanghai; 07/08/2026 was read month-first; a date cell 07/15/2026 or 15 July 2026 was stored as 2026-07-15 under New York and 2026-07-14 under Shanghai. Second, an impossible day is rolled over: datetime cells 2026-02-30, 2026-02-30 10:00 and 2026-02-30T10:00:00Z were stored as March 2. The claim routes an import format to a decision: what should /import answer for a temporal cell that is not a YYYY-first ISO spelling, or that names a day that does not exist?",
    "options": [
    "A — Refuse such a cell per row (import_invalid_datetime / import_invalid_date), the write door's own answer applied at the producer. Every day is checked for existence; no host-zone reading and no locale guess. Cost: a CSV typed in a locale format must be re-typed as YYYY-MM-DD[ HH:MM]. xlsx date cells arrive as Date and are unaffected, and the platform's own export writes YYYY-MM-DD HH:mm:ss in the business zone, which stays readable.",
    "B — Keep reading non-ISO cells, but in the business timezone (extend the #8485 naive-cell reading to month-first shapes), and refuse impossible days. Cost: a second temporal dialect lives in the importer, and 07/08/2026 is still a locale guess (July 8 vs August 7), stored silently.",
    "C — Leave it. Cost: the stored instant or day depends on the deployment host, and an impossible day is silently moved, contradicting the write door this card lands."
    ],
    "recommendation": "A. Real business need (measured): no shipped producer emits month-first temporal text. The export round trip uses the YYYY-first naive shape, which the importer already reads in the business zone, and xlsx cells arrive as Date. The only demand is a hand-typed locale CSV, which is unmeasured. Long-term: one temporal grammar for the write door and the importer, contract-first, with no host dependence; B adds a dialect. Preventing AI mistakes: a model-generated CSV with 07/08/2026 gets a per-row refusal naming the cell, instead of a wrong day stored silently; B keeps the silent guess. Startup focus: A removes a dialect and adds no gate; B widens the importer's surface; C keeps a defect. The rollover half (an impossible day) is not a choice under triage's Arm 1 (never rolled over) and belongs with A or B either way."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/:object/query at 0999284, TZ=America/New_York, on memory / SQLite / PostgreSQL 16. where opened_at $eq '2026-02-30T10:00:00Z' is admitted and matches the row stored at 2026-03-02T10:00:00.000Z on all three (rolled over). where opened_at $eq '07/15/2026 10:00' (and '2026/07/15 10:00') matches the row at 2026-07-15T14:00:00.000Z on all three (host zone). where placed_on $eq '2026-02-30' (a date) is admitted: 200 [] on memory and SQLite (compared as text) and 500 DATABASE_ERROR on PostgreSQL · evidence: the temporal-comparand door (objectql temporal-comparand-door.ts, calling core temporal-comparand.ts isUninterpretableTemporalComparand, whose readsAsCalendarDay is a leading-shape test and readsAsInstant hands non-ISO strings to Date.parse) is not changed by this card, so the write door is now strictly narrower. The write door's private predicate (record-validator.ts namesRealCalendarDay / ISO_DATETIME_WRITE_FORM) is what to lift into core as a move if this is ruled. Landing site: packages/core/src/utils/temporal-comparand.ts plus the door; the analytics raw-SQL decline shares the predicate · dedupe words: comparand impossible day 2026-02-30 rolled over · where datetime non-ISO host timezone · isUninterpretableTemporalComparand real calendar day · date comparand 2026-02-30 postgres 500",
    "class: a · reach: POST /api/v1/data/:object/import at 0999284, no business timezone, memory / SQLite (PostgreSQL too for the TZ=America/New_York rows). A datetime cell 2026-02-30 / 2026-02-30 10:00 / 2026-02-30T10:00:00Z is stored as 2026-03-02T00:00:00.000Z / 2026-03-02T10:00:00.000Z. A datetime cell 07/15/2026 10:00 is stored as 2026-07-15T14:00:00.000Z under TZ=America/New_York and 2026-07-15T02:00:00.000Z under TZ=Asia/Shanghai; 07/08/2026 becomes 2026-07-08T04:00:00.000Z (month-first). A date cell 07/15/2026 or 15 July 2026 is stored as 2026-07-15 under New York and 2026-07-14 under Asia/Shanghai · evidence: packages/rest/src/import-coerce.ts parseDateCell. Its fast path builds a datetime with Date.UTC and admits any day up to 31; the wall-clock path checks the day only against 31; the fallback is new Date(s) in the process zone, then getUTC* for a date. The importer emits valid ISO, so the write door cannot see either reading. Same function as #20534 (unpadded year), which holds the file; the seat may sibling or fold it. The zone/locale half is open question 1 (an import format, per the claim) · dedupe words: parseDateCell host timezone month-first · import datetime cell 2026-02-30 rolled over · import date cell 07/15/2026 day earlier Asia/Shanghai · parseDateCell new Date fallback process zone",
    "class: a · reach: POST /api/v1/data/:object/query at 0999284 on memory / SQLite / PostgreSQL 16. A datetime where opened_at $lte '0050-01-01' returns [] although a row is stored at 0050-01-01T10:00:00.000Z. The control where opened_at $lte '2026-07-15' includes that day's 2026-07-15T14:00:00.000Z row, so whole-day widening works for 2026 and is skipped for 0050 · evidence: packages/spec/src/data/calendar-day.ts nextUtcCalendarDay proves a day real by a Date.UTC round trip, and Date.UTC reads years 0..99 as 1900..1999. So it answers null for every day in 0001..0099 (measured: nextUtcCalendarDay('0050-01-01') = null, utcInstantMs('0050-01-01') = null; control '0100-01-01' answers), and the $lte / $between-max whole-day rule falls back to midnight. Years 0001..9999 are the supported range since #20264 (closed). This is also why the write door does not reuse it · dedupe words: nextUtcCalendarDay year below 100 Date.UTC 1900 · lte whole day widening year 0050 · utcInstantMs 0050-01-01 null",
    "carrier: none — noted, not filed. The write door refuses four ISO-adjacent datetime spellings that were stored correctly before: space-plus-zone, lower-case t/z, reduced precision 2026-07, and the expanded year. The measured reasons are in the PR's grammar section and the changeset. This is a deliberate reading of 'an ISO 8601 string is admitted' as H3's platform-written grammar, flagged for the reviewer."
    ]
    }

  5. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer to the os-dev report on #20525 (PR #20547)

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-29T02:06Z. ⛔ Not a claim. Claim 5881460653 stands.

    1. Open question 1 (what /import answers for a non-ISO or impossible-day temporal cell) does not block this PR, and it is routed to /import: parseDateCell emits a date cell year below 1000 unpadded (0500-01-01 → 500-01-01), so after PR #20524 a valid ISO date cell is refused per row as invalid_date, and before it a non-day was stored #20534 for triage.
    2. Deviations 1–6: admitted pending review.
    3. The four ISO-adjacent spellings now refused (space-plus-zone, lower-case t / z, reduced precision, the expanded year) are flagged for the at-tier review. It judges each refusal against triage's "an ISO 8601 string is admitted".
    4. Out-of-scope findings:
    5. Next: the at-tier contract review of PR fix(objectql)!: a temporal string is written on a real calendar day, and a datetime string in an ISO 8601 spelling, or refused with VALIDATION_FAILED / invalid_date (#20525) #20547 at 0999284ab (running).
  6. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20547 at 0999284ab3b36da8c7e9927e20b4ebe834929a90

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-29T02:21Z. Contract review of record: 5882418335 on PR #20547, at-tier, read-only, PASS on this head.

    Checklist, verified against GitHub rather than the reports:

    • Form: draft, base main, and Fixes #20525 is the only closing keyword in the body. Claim 5881460653 names the branch claude/issue-20525-temporal-write-real-day-iso and reads Clause-②: no (narrowing), which is also the line in the changeset and the PR body.

    • Scope: 5 files, +593 / −2:

      • record-validator.ts's date / datetime arm (+72 / −2), with three module-private helpers:
        • namesRealCalendarDay: exact for every month and every year 0001..9999, by arithmetic and with no Date round trip;
        • ISO_DATETIME_WRITE_FORM: a bare day; T time with Z, ±HH:MM, ±HHMM or no zone; a space-separated time with no zone;
        • writesAsIsoTemporal;
      • an engine pin (a recording driver, zero writes on refusal), a REST pin (SQLite, plus a live-PG cell named-skipped in CI) and a driver-memory pin under TZ=America/New_York;
      • the changeset.

      Every file is inside the claim. packages/core, packages/spec, the comparand door, the time arm and import-coerce.ts are untouched.

    • Changeset: @objectstack/objectql minor with a BREAKING banner under Clause-②: no (narrowing), and ADR-0087 not-required (no-migration-prescription). It gives what a caller sees, before → after, for both arms and the four ISO-adjacent spellings now refused, with the one-line fix. Check Changeset is success.

    • Governed surface: none. check-governed-merges --pr 20547 finds the PR not governed. It is 595 changed lines.

    • CI: 34 check-runs on the head, all concluded: 31 success, 3 skipped by roster, 0 failures. mergeable_state is clean.

    • Behaviour: the temporal write door stores a real day or refuses, and a datetime only in ISO 8601.

      • date 2026-02-30 (before: stored verbatim, or a 500 on PostgreSQL) and datetime 2026-02-30 at 10:00 UTC (before: rolled over to 2 March) are now refused with VALIDATION_FAILED / 400 invalid_date before any write.
      • Non-ISO datetime spellings, which were read in the host's zone or month-first, are refused.
      • The leap days 2028-02-29 pass. A Date, the refused epoch number and the year range are unchanged.
      • The four ISO-adjacent spellings (space-plus-zone, lower-case t / z, reduced precision, the expanded year) are refused. The review judged each within triage's "an ISO 8601 string is admitted", and the two that removed a correctly stored spelling are stated with the fix.
      • Shipped seeds, examples and the objectui writers write only admitted spellings.

    Open question 1 (/import's non-ISO and impossible-day cells): not a block, as the seat answered (5882273111) and the review verified, because /import is unchanged by this PR. It is folded into #20534 (5882288005) for triage.

    Deviations 1–6: accepted as the review answers them in ③.

    Carried out of this card:

    Landing: ready, then auto-merge through the queue. The merge closes this card (Fixes). The seat then verifies the squash on main and removes pm:dispatched in the same act.

  7. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record: PR #20547 merged. This card is closed completed by its Fixes line

    domain:engine#1 · session_01N8TPEsoJxPsdSdNKGnNGEN (os-warren) · written 2026-09-29T02:46Z.

    Verified on main:

    • The squash is 92ea760441a01305b8c36debc9e02a345a92a16b, merged 2026-09-29T02:45Z by the queue, with one parent. It is an ancestor of origin/main.
    • It carries 5 files, +593 / −2, the accepted head's list.
    • The squash's changed lines are identical to the accepted head 0999284ab's changes against its merge base 288611e3e (the same md5 over every added and removed line).
    • namesRealCalendarDay in record-validator.ts is present at the squash and absent at its parent, and so is the 20525-temporal-write-real-day-iso changeset.
    • The PR body's one closing keyword is Fixes #20525, so no other card was closed.

    Delivered: the temporal write door stores a real calendar day or refuses, and stores a datetime only from an ISO 8601 spelling.

    • An impossible day (2026-02-30) was stored verbatim, a 500 on PostgreSQL, or rolled over to 2 March. It is now refused with VALIDATION_FAILED / 400 invalid_date before any write, for date and for datetime's day.
    • A non-ISO datetime string, which was read in the host's zone or month-first, is refused.
    • Leap days, a Date and every admitted ISO spelling (zone-naive read as UTC) are unchanged.
    • @objectstack/objectql ships minor with a BREAKING banner, Clause-②: no (narrowing). The ACCEPT is 5882423594, and the contract review of record is 5882418335 (PASS).

    Carried out of this card:

    pm:dispatched is removed in the same act as this record. The domain, area and type labels stay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions