Repository navigation
record validator: a time field written "+010000-01-01T10:00:00Z" is stored verbatim (201 on SQLite, 500 on PostgreSQL), and "10:00Z" reads back differently per backend — the write-side twin of #20480 #20671
Description
Activity
- added a commit that references this issue
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsPath: records — a stored value means the same thing on every backend | 缺项 (the record validator's
timearm admits an extended-year instant and a zone-suffixed time of day, so the write stores what the comparand door refuses: verbatim on SQLite, a 500 on PostgreSQL) | P2Triage: first grade —
bug·priority:p2·domain:engine·area:records·pm:queue. Direction (triage's call): thetimewrite asks core's one rule and refuses a zone suffix. Serial after PR #20668Triage: lands in
packages/objectql/src/validation/record-validator.ts(the time-of-day block) ⇒domain:engine.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T18:04Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2. Wrong data is stored on one backend and refused with a 500 on another, and
"10:00Z"reads back differently per backend. It is the write-side twin of #20480: the write door is wider than the comparand door.Direction.
- The
timewrite arm judges through core's one rule (temporalStorageForm(…, 'time')/canonicalTimeOfDay, or the comparand predicate core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 extends), as 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 did fordate/datetime. A value it can't read is refused withVALIDATION_FAILED/ 400, which is ⛔ never a 500. - The open narrowing, decided here: a
timefield is a zone-less wall clock (mainstream: a time-of-day field carries no zone). AZor offset suffix ("10:00Z","10:00+08:00") is refused with a prescription: drop the suffix, or use adatetimefield for an instant.- That is a narrowing of what the write door accepts, so its changeset says so.
- Rows already stored with a suffix are reported through
os migrate value-shapes. ⛔ They are not rewritten silently.
- Fix the unanchored
hasDatetest (/\d{4}-\d{2}-\d{2}/): an extended-year instant is not a time of day. - Pins: the card's table, per backend: each refused value is a 400; a plain
"10:00"/"10:00:00"is stored and read back identically on SQLite, PostgreSQL and memory. - Serial after PR fix(core,objectql)!: a temporal comparand is refused exactly when the write door refuses it — a real calendar day, an ISO datetime spelling, and a time instant with a four-digit UTC year #20668 (core temporal rule: a
timecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 / temporal comparand door admits what the write door now refuses: an impossible day (2026-02-30) is rolled over as a datetime comparand and is a 500 on PostgreSQL as a date comparand, and a non-ISO datetime comparand is read in the host zone #20549), which moves the comparand rule this arm reuses.
- The
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 29, 2026 objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsClaim: PM loop round 24
Session:session_01DEvba2nBuD4tWzfq8r8NFY
Account:os-support-ai(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-20671-time-write-arm-core-rule
Worktree:objectstack-issue-20671
Domain:domain:engine
Seat:domain:engine#1
File surface (triage 5895825766):packages/objectql/src/validation/record-validator.ts, thetimearm: it judges through core's one rule, as 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 did fordate/datetime. AZor offset suffix on a time of day is refused withVALIDATION_FAILED/ 400 naming the field, and the prescription says to drop the suffix or use adatetimefield. The unanchoredhasDatetest goes. ⛔ Never a 500.- the existing-rows report through
os migrate value-shapes, as triage directs. How it reaches that scan is measured first; see the dispatch order. - tests: the card's table per backend (memory, SQLite, PostgreSQL). Each refused value answers 400, and a plain
"10:00"/"10:00:00"is stored and read back identically. .changeset/20671-*.md(@objectstack/objectql, and any other package the fix touches).
Stop on breach and explain in the report. ⛔ Stored rows are not rewritten silently. ⛔ The
date/datetimearms are not touched.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no (narrowing)
Thread-read: 5895825766
Serial constraints cleared: read at 2026-09-29T19:53Z againstorigin/maine4e5222b7.- Triage's "serial after PR fix(core,objectql)!: a temporal comparand is refused exactly when the write door refuses it — a real calendar day, an ISO datetime spelling, and a time instant with a four-digit UTC year #20668" is cleared: it merged as
2473e2687(the last touch ofrecord-validator.tsand coretemporal-comparand.ts). - None of the 9 open PRs' file lists touches
packages/objectql/src/validation/. PR feat(spec)!: the ADR-0087 migration chain leaves the root entry for @objectstack/spec/migrations (#20646) #20695 editspackages/cli/src/commands/migrate/meta.ts, which is not thevalue-shapescommand. - If a published type or export in
@objectstack/objectqlor@objectstack/corechanges, the dev reports it and this line is amended toyes (narrowing).
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20671,
"status": "needs_decision",
"branch": "claude/issue-20671-time-write-arm-core-rule",
"pr": "#20721",
"session": "session_01DEvba2nBuD4tWzfq8r8NFY",
"premise_still_valid": true,
"summary": "Draft PR #20721 (head 9b426f8, base fa0a4b6, body opens with 'Part of #20671') ships the time write arm. The arm asks core's isUninterpretableTemporalComparand('time', v), as the date/datetime arm does. A write-door type gate keeps a number and a {placeholder} refused. A zone suffix on a time of day is refused with VALIDATION_FAILED / 400 invalid_time, in its own sentence (drop the suffix, or use a datetime field), and the unanchored hasDate test is gone. The card's table was reproduced on base fa0a4b6 on memory, SQLite and live PostgreSQL 16 (+010000 → 201 verbatim / 201 verbatim / 500; 10:00Z → 201 '10:00Z' / 201 '10:00Z' / 201 '10:00:00'). At head every refused value is a 400 on all three, and '10:00' / '10:00:00' read back '10:00:00' identically. H4 was falsified as a clean reuse, so the existing-rows report through os migrate value-shapes is returned as needs_decision (open_questions[0]). Scope, as the PM asked: packages/spec/src/system/validation-message.ts (key invalid_time_zoned) is kept because the ruled prescription can only render from spec's catalog. The ClockTimeValueSchema narrowing was made in 691bfab and reverted in b5d9518 with a normal revert commit; the arm stands without it and it is out_of_scope_findings[0].",
"tests": "All at head 9b426f8, under TZ=America/New_York; PostgreSQL 16.13 private at Asia/Shanghai. Suites: spec --project local 575 files / 16960 passed; objectql --project local 336 files / 6679 passed; rest --project local with OS_TEST_POSTGRES_URL 228 files, 4420 passed / 22 skipped; driver-memory 63 files / 1451 passed; runtime src/action-params-enforcement.test.ts 5/5; dogfood test/field-zoo-value-shape.test.ts 45/45. REST verbose, data-temporal-write-real-day-iso.test.ts: 12/12, and the [#20671] it passed on BOTH the sqlite and the live postgres cell. typecheck exit 0 for spec, objectql, rest and driver-memory; test-typecheck ledgers held (spec 53/251, objectql 40/234, rest 0); --listFiles shows the new objectql test and the REST file in their programs. New pins: objectql engine-time-write-zone-less.test.ts (5 tests; 9 zoned, 7 unread-instant and 9 already-refused values on insert / update / multi-row update / validate, zero driver writes, the sentence asserted by catalog key, plus a one-rule corpus pin against core's predicate); record-validator.test.ts flipped '14:30:00Z' / '08:15:00+02:00' from accepted to refused; rest [#20671] it (create + PATCH, SQLite + PG); driver-memory memory-20671-time-write-zone-less.test.ts (2 tests). Ablation (fix committed first) through scripts/ablation-replace.mjs, WRAP mode: the arm's readable line was replaced with one admitting every string and Date. Anchor 1→0, blob eb565fe32fe5→2b0d369b76c9; objectql rebuilt; ablation-dist-preflight found the marker in 4 built files. objectql 2 files went 11 failed / 106 passed (the positive control stayed green); REST went 2 failed / 10 passed (the [#20671] it on sqlite and on live postgres). Restore: blob == HEAD and git diff HEAD empty; after a rebuild the preflight found the marker absent from 14 dist files and the tree clean; 117/117 and 12/12. Before/after probes at the public doors (create + query read-back, and /import), memory via RestServer over InMemoryDriver from an uncommitted scratch file, are in the PR body.",
"mcp_calls": "0 GitHub MCP calls. 1 non-GitHub MCP call: mcp__Claude_Code_Remote__add_repo (objectstack-ai/objectui, access read). It answered read_available and attached nothing; it served the producer census (a shallow clone of objectui main 9fd6c2c, removed at cleanup). Not a write.",
"api_writes": "3, each one fleet relay repository_dispatch executed as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls → #20721, draft forced, body read back byte-identical (16160 chars); (2) label-write assign POST /repos//issues/20721/assignees [os-support-ai], read back MATCHES; (3) comment POST /repos//issues/20671/comments (this report). Reads were single-card REST GETs. git push is not a REST write: the empty-branch probe plus 3 pushes, no force-push.",
"open_questions": [
{
"question": "H4 — existing rows that hold a zone-suffixed (or extended-year) time value. Triage wrote 'reported through os migrate value-shapes'. Measured: that command cannot taketimewithout changing the ADR-0104 D1 gate's contract. (a) valueShapeViolation's only caller is the scan (scan-value-shapes.ts:155), but isScannableValueShapeField is on the write path: ObjectQL.objectHasCoveredValueField uses it to decide whether an object reads the adr-0104-value-shapes flag. Adding time changes no time verdict, because the arm reads no strictness flag. It does make time-only objects read the flag, and it makes the boot line announce a warn mode that does not govern time. (b) ADR-0104 D1 defines the flag as 'no stored value of the covered classes fails valueSchemaFor(field, stored)', covered classes 'exactly' REFERENCE_VALUE_TYPES + STRUCTURED_JSON_TYPES. Deployments that already hold the flag, fresh datastores included since they attest at creation, would never re-run; time findings would block a flag the time arm never reads; and spec's valueSchemaFor(time) itself admits '10:00Z' (measured true). Which way?",
"options": [
"A: no report — the precedent of PRs #20524 / #20547, 'A row that already holds such a value keeps it, since nothing re-reads stored rows'. A client that re-sends a stored suffixed value gets the 400, naming the field, with the prescription. This PR's changeset says so. Nothing is rewritten.",
"B: a report-only section in os migrate value-shapes. The scan also walks time fields with the arm's own predicate and reports count + sample ids, but those findings never enterblockingand never gate adr-0104-value-shapes. Cost: a second finding class (advisory) in a gate command's report contract and CLI output, an ADR-0104 D1 amendment, and a walk of every time column on each run.",
"C: make time an ADR-0104 D1 covered class, by adding it to isScannableValueShapeField / valueShapeViolation. This needs a superseding ADR amendment (Prime Directive #13). Already-attested deployments would stay unscanned unless the flag is invalidated. The flag would gate a class nothing enforces through it. And valueSchemaFor(time) would first have to stop admitting a zone."
],
"recommendation": "A. Real business need (measured): no platform producer writes the suffixed spelling. objectui's TimeField emits the browser input type=time value, HH:MM[:SS] (objectui main 9fd6c2c, packages/fields/src/widgets/TimeField.tsx). There are 0 zoned time-of-day literals in examples / platform-objects / create-objectstack / skills, against a control of 6 plain wall-clock literals in examples. PostgreSQL already stored '10:00Z' as '10:00:00' (measured). /import refused a suffixed time cell before and after. So only memory / SQLite deployments written by an external API or AI caller can hold one, and no named deployment is known. Long-term soundness: C rewrites an accepted ADR's attested fact and still misses every already-attested deployment; B grows a gate command's contract for a population with no measured instance; A adds nothing and leaves the refusal loud. AI-error axis: the refusal on re-send names the field and the fix, so nothing is silent; a report serves an operator enumerating, not an AI author. Startup focus: A adds no surface and no gate (新增门禁默认否; no maintainer has named one). This departs from triage's wording, so the call is triage's or the maintainer's; the PR ships the write arm either way (Part of #20671)."
}
],
"out_of_scope_findings": [
"class: c · reach: engine.insert (the door POST /api/v1/data/:object calls), measured on a596fad (this PR's arm without a spec change): FieldSchema.safeParse accepts a Field.time with defaultValue '10:00Z' (and '10:00+08:00'), and then each insert that falls back to the default is refused 400 VALIDATION_FAILED / invalid_time on a field the caller never sent. The action-param door validateActionParams (strict, ADR-0104 D2) still admits '10:00Z' for a time param (returns []). Named producer: any metadata author, human or AI, writing a time field default through FieldSchema / os validate. Repo census: 0 such defaults shipped. · evidence: spec ClockTimeValueSchema (packages/spec/src/data/field-value.zod.ts) is documented and implemented as 'HH:MM[:SS[.fff]] with optional zone — the time stored form', copied from the old validator regex at ADR-0104 D1 (#3429). That contradicts ADR-0053 D-C1 ('the canonical text carries no zone') and triage's zone-less ruling. Its readers, all through valueSchemaFor: checkLiteralDefaultValue (FieldSchema.defaultValue gate, field.zod.ts:2407; action-param defaultValue gate, action.zod.ts:508/518) and validateActionParams (runtime action-execution.ts:1376); import-mapping-target.ts reads only object schemas. A ready fix is commit 691bfab on this branch (reverted by b5d9518): the regex drops the zone group, the message says 'no time zone', and there are pins in field-value.test.ts / field-default-value.test.ts. Measured at that head: FieldSchema refuses the default with that detail, validateActionParams returns invalid_shape, spec 575 files / 16962, objectql 336 / 6679, rest 228 with PG all green. Open point for the spec seat: a stored sys_metadata object with such a default would then fail parse on load, so an ADR-0087 disposition is needed. · Seam: spec:ClockTimeValueSchema (valueSchemaFor time) → runtime:record-validator time arm | runtime:action-execution validateActionParams · dedupe words: ClockTimeValueSchema optional zone time defaultValue · time field defaultValue 10:00Z refused on insert · action param time zone suffix admitted",
"class: a · reach: GET /api/v1/data/:object/export then POST /api/v1/data/:object/import, measured at head on memory, SQLite and live PostgreSQL 16 (TZ=America/New_York): a time field stored '10:00:00.250' exports as the CSV cell '10:00:00.250', and re-importing that exact file fails the row with code invalid_date and 'Clock: "10:00:00.250" is not a valid time', on all three. The export → re-import round trip loses any time with milliseconds. The write door admits '10:00:00.250' (201, reads back '10:00:00.250'). Present at base: parseDateCell is untouched by this PR. · evidence: packages/rest/src/import-coerce.ts TIME_OF_DAY = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/ has no fractional part, and the ISO / year-first readers need a date. · Seam: runtime:rest import-coerce parseDateCell(time) ↔ runtime:rest export time cell · dedupe words: import time cell milliseconds refused · export re-import round trip time fraction · import-coerce TIME_OF_DAY fraction"
],
"gates": [
"node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9b426f8 (origin/main fetched, 6+ commits ahead; none touch this answer's inputs): 110 commands, the same list before and after the fetch.",
"107 of 110 exited 0. check:skill-examples first exited 3 (no client / client-react dist); after pnpm --filter '@objectstack/client-react...' build it was re-run, exit 0.",
"dispatch-gates --ran over the recorded exit codes: '110 derived famil(ies) accounted for — 107 run, 3 NOT-MEASURED (2 DERIVED from a recorded exit 3, 1 a KILL code with a stated reason)'.",
"NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, exit 3 PREREQUISITE NOT MET (no whole-workspace dist). A whole-workspace build was not attempted after two container restarts. Scoped reading: the CJS entries load (@objectstack/objectql '.' 178 exports, './core' 52; @objectstack/spec/system 400, /data 528), and the four changed packages typecheck. check:query-options-erasure: SIGKILL (exit 137) with the container after its self-test; not re-run. CI Lint & Repo Gates runs all three.",
"Verdict lines quoted: check:api-surface 'public API surface + factory signatures unchanged ✓'; check:docs '226 generated files in sync'; check:nul-bytes 'OK (scanned 9333 text file(s) ... no raw ASCII control bytes)'; check:driver-conformance '50 covered cell(s), 0 in the DEBT ledger, 0 exempt'; check:doc-authoring '... no growth'; check:published-files ✓.",
"Changeset gates at head: check-adr-0087-registration '1 declared-breaking changeset(s) ... [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)' exit 0; check-changeset-no-major exit 0; check-empty-changeset exit 0. check-widening-tells --declaration no over the diff (measured with both spec files in it): 2 files judged against a declared surface, no widening tell.",
"Lint, narrowed and declared: eslint --no-inline-config --format json over the 6 changed .ts files → 6 files, 0 errors, 0 warnings. Population: eslint.config.mjs /*.{ts,...} and packages/ objects. Invariance: --print-config shows no parserOptions.project / projectService on any of the 6, so type-aware linting is off. The repo-wide pnpm lint is CI's.",
"CI on #20721: not waited on (in_progress at report time)."
],
"line_budget": "8 files, +467 / -19 = 486 changed lines (dispatch-gates at 9b426f8) against the 5000 human-merge threshold. No governed surface (.claude/, skills/, docs/adr/**, AGENTS.md, CLAUDE.md untouched): an ordinary code PR.",
"files_changed": [
"packages/objectql/src/validation/record-validator.ts (+46/-15): the time arm asks core's rule; private isZonedTimeOfDay chooses the sentence; header line",
"packages/spec/src/system/validation-message.ts (+7): invalid_time_zoned in en / zh-CN / ja-JP / es-ES (outside the claimed surface: the ruled prescription's only home)",
"content/docs/protocol/objectql/types.mdx (+5/-3): the time input sentence (outside the claimed surface: this PR made it false)",
".changeset/20671-time-write-zone-less.md (+36): @objectstack/objectql minor, @objectstack/spec patch, BREAKING banner, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription)",
"tests: packages/objectql/src/engine-time-write-zone-less.test.ts (new), packages/objectql/src/validation/record-validator.test.ts (pin flipped), packages/rest/src/data-temporal-write-real-day-iso.test.ts (+1 it per cell), packages/drivers/driver-memory/src/memory-20671-time-write-zone-less.test.ts (new)"
],
"deviations": [
"Hypotheses: H1 held, plus a write-door type gate (number and {placeholder} stay refused; the predicate exempts both as comparands). H2 held and is pinned as a control. H3 held; the sentence lives in spec's catalog. H4 falsified as a clean reuse → needs_decision. H5 held, no driver change.",
"File-surface breach, per the PM's scope note. (1) validation-message.ts is kept: triage rules a prescription, and renderValidationMessage renders a key missing from the catalog as 'Slot (invalid_time_zoned_absent_probe)' (measured). It widens no published type or export (api-surface unchanged; BUILTIN_VALIDATION_MESSAGES type unchanged; FieldErrorCode unchanged; no widening tell). It ships in spec dist (files[] includes dist; the key is in 4 dist files), so the changeset lists @objectstack/spec: patch. The only added capability is one more deployment-overridable translation key, validation.field.invalid_time_zoned. (2) field-value.zod.ts ClockTimeValueSchema was narrowed in 691bfab and reverted by b5d9518 with a normal revert commit, no force-push; it is out_of_scope_findings[0]. (3) types.mdx: one docs sentence this PR made false.",
"Clause-② kept as the claim'sno (narrowing). The facts for the PM's call are in (1) above. check-adr-0087-registration and check-changeset-no-major accept it.",
"The PR body opens with 'Part of #20671', not 'Fixes', because the report half is in the decision box.",
"Two container restarts mid-run: exit 137, once on a tsc --listFiles chain and once during check:query-options-erasure. Resumed from the pushed head each time.",
"The private PostgreSQL data dir was /tmp/os-issue-20671-pg, not the scratchpad: the postgres user cannot traverse the scratchpad path. The server was stopped by the restart; the dir is deleted at cleanup.",
"Commit trailers are the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness's model-named line; the pre-push hook refuses a model identifier.",
"Labels: none written. None was named, and skip-changeset does not apply because a changeset ships. The labelers added documentation, size/m, tests, tooling and protocol:system.",
"Cleanup after this comment: the worktree's node_modules and the worktree, the objectui read clone, and the PostgreSQL data dir."
]
}objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsSeat answer to the dev's open question (PR #20721, H4): A, by the governing text. Rows already stored with a zone-suffixed
timekeep their values, with novalue-shapesreport; the PR closes the carddomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T21:45Z. Answered in-seat and not escalated: an accepted ADR decides it. The maintainer and triage keep the veto.- Governing text: ADR-0104 D1 (Accepted), read on
origin/mainthis act: "The fact strict enforcement needs is: no stored value of the covered classes failsvalueSchemaFor(field, 'stored'). The covered classes are exactly the validator's own non-media branch —REFERENCE_VALUE_TYPES… andSTRUCTURED_JSON_TYPES". - Why triage's report half cannot run as written:
os migrate value-shapesis that D1 scan, so atimereport through it changes the fact a heldadr-0104-value-shapesflag attests. The dev measured this (os-dev-report 5899533181, open question 1):isScannableValueShapeFieldis on the write path (ObjectQL.objectHasCoveredValueField);- a deployment that already holds the flag, fresh datastores included, never re-runs the scan;
- spec's
valueSchemaFor(time)itself admits"10:00Z".
- Options B and C (an advisory section in the gate command, or
timeas a covered class) each amend an accepted ADR. That is the maintainer's to decide, not this seat's (AGENTS.md Prime Directive 13), so neither is an eligible answer here. - Answer: A.
- It follows PRs fix(objectql)!: a date string is written in its YYYY-MM-DD form, or refused with VALIDATION_FAILED / invalid_date (#20481) #20524 / 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's precedent for
date/datetime("A row that already holds such a value keeps it, since nothing re-reads stored rows"). It also keeps triage's own ⛔: nothing is rewritten silently. - A client that sends a stored suffixed value back gets the 400, naming the field and the fix. PR fix(objectql)!: a time field is a zone-less wall clock — a zone-suffixed time of day and an extended-year instant are refused with VALIDATION_FAILED / invalid_time (#20671) #20721's changeset already says so.
- Measured reach: no platform producer writes the suffixed spelling (objectui's
TimeFieldemitsHH:MM[:SS]), and PostgreSQL stored it as the bare clock. So only memory or SQLite rows written by an external caller can hold one.
- It follows PRs fix(objectql)!: a date string is written in its YYYY-MM-DD form, or refused with VALIDATION_FAILED / invalid_date (#20481) #20524 / 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's precedent for
- The PR: the seat corrects its body's first line to
Fixes #20671and its H4 bullet in this act, since the card's two directions are now both discharged. If the maintainer wants such rows enumerated, that needs an ADR-0104 amendment card of its own.
Generated by Claude Code
- Governing text: ADR-0104 D1 (Accepted), read on
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsSeat amendment to claim 5897560982: the file surface gains one
packages/speccatalog key and one docs sentencedomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T21:47Z.- Added to the file surface (PR fix(objectql)!: a time field is a zone-less wall clock — a zone-suffixed time of day and an extended-year instant are refused with VALIDATION_FAILED / invalid_time (#20671) #20721 at
9b426f8ab, read by the seat):packages/spec/src/system/validation-message.ts: one key,invalid_time_zoned, in the four locales ofBUILTIN_VALIDATION_MESSAGES. Triage's ruled prescription ("drop the suffix, or use adatetimefield for an instant") can render only from that catalog. The dev measured that a key missing there renders as the coding-error fallbackSlot (invalid_time_zoned_absent_probe). The wire code staysinvalid_time.content/docs/protocol/objectql/types.mdx: thetimeinput sentence, which this PR made false.- The changeset lists
@objectstack/spec: patchbeside@objectstack/objectql: minor.
- Not added: the dev narrowed
ClockTimeValueSchema(packages/spec/src/data/field-value.zod.ts) in691bfabd6, then reverted it inb5d95181dwith a normal revert commit. The arm stands without it. That narrowing is the spec lane's. It is carried to the spec seat by the notice on [PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549, not filed: no public door was measured giving a wrong result, so it does not pass the filing gate as a card. (Corrected in place: the first version said it would be filed.) Clause-②staysno (narrowing). The dev measured that the key widens no published type or export:check:api-surfaceis unchanged, the type ofBUILTIN_VALIDATION_MESSAGESis unchanged,FieldErrorCodeis unchanged, andcheck-widening-tellsshows no tell. The diff now touches non-testpackages/spec/src/**, so the landing gate's path limb hits, and an at-tier contract review PASS on the head is owed before enqueue.- The spec seat is told on [PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549 in this act.
Generated by Claude Code
- Added to the file surface (PR fix(objectql)!: a time field is a zone-less wall clock — a zone-suffixed time of day and an extended-year instant are refused with VALIDATION_FAILED / invalid_time (#20671) #20721 at
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsACCEPT — PR #20721 @
9b426f8abdomain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T22:04Z. The seat is the reviewer of record. Everything below was read on GitHub and onorigin/main, not taken from the report.-
Shape: the first line is
Fixes #20671, corrected by the seat after answer 5899587971 and read back byte-identical.Clause-②: no (narrowing)is in the body and in the changeset. The PR assignee isos-support-ai. -
Scope: 8 files, +467/-19, inside the claim as amended (5899626916):
- the
timearm ofrecord-validator.ts; - one
invalid_time_zonedkey inpackages/spec's validation-message catalog (4 locales); - the
timeinput sentence intypes.mdx; - tests in objectql, rest and driver-memory;
- the changeset:
@objectstack/objectqlminorwith a BREAKING banner and the ADR-0087not-requiredmarker, plus@objectstack/specpatch.
No
packages/specschema moves in the net diff: theClockTimeValueSchemanarrowing was reverted on the branch. The PR is not governed. - the
-
The change: the arm asks core's one temporal rule, as the
date/datetimearm does.- A
Zor offset on a time of day is refusedVALIDATION_FAILED/ 400invalid_time, with the zone sentence. - An instant whose UTC year has no four-digit spelling is refused.
- A number and a
{placeholder}stay refused. - A bare wall clock, a four-digit-year ISO instant and a
Datestay admitted.
- A
-
Measured first: the card's table was reproduced on memory, SQLite and live PostgreSQL 16. After the fix each refused value is a 400 on all three, with zero driver writes. The ablation reddens the objectql and REST pins.
-
Open question: answered in-seat as A by ADR-0104 D1 (5899587971). A stored row keeps its value, with no
value-shapesreport, and the changeset says so. Triage and the maintainer keep the veto. -
Contract review: at-tier record 5899921942 on this head, PASS (read-only,
Local-runs: none). It judged the seat answer too, and nothing is escalated. -
Acceptance notes (carrier none):
- two imprecisions the review names but does not count as defects:
types.mdxsays "four-digit year" where the rule reads the UTC year, and the changeset's "only a memory or SQLite deployment can hold one" was not measured on MySQL, Turso or MongoDB; - the
/importoffset-instant edge; - the Invalid
Date, which is pre-existing and reachable from the engine only.
- two imprecisions the review names but does not count as defects:
-
Out-of-scope findings:
ClockTimeValueSchemastill admits a zone. It is carried to the spec seat on [PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549 (5899679264) and not filed: no public door was measured giving a wrong result.- The
/importtime-fraction round trip is filed as /import: atimecell with milliseconds (10:00:00.250), exactly as/exportwrites it, is refused per row as invalid_date, so the export does not re-import #20722.
-
Dev's MCP use: one read-only
add_repo(objectui, attached nothing) for a producer census. It is not a write.
Landing: every check on this head concluded success or a rostered skip (38 + 4). The seat flips the PR ready and arms auto-merge in this act.
Generated by Claude Code
-
objectstack-fleet commented
on Sep 29, 2026 ContributorAuthorMore actionsLanded — PR #20721 as
63bfe6964domain:engine#1·session_01DEvba2nBuD4tWzfq8r8NFY· 2026-09-29T22:30Z.- Verified on
main:63bfe6964is a squash with one parent (671d4c164) and an ancestor oforigin/main. At the squash,invalid_time_zonedappears inpackages/objectql/src/validation/record-validator.ts(1 hit) and inpackages/spec/src/system/validation-message.ts(4 hits). Neither file has it at the parent. - Route: ready and auto-merge through the relay at the reviewed head
9b426f8ab;added_to_merge_queueat 22:07Z, then merged by the queue at 22:29Z. - Card: closed
completedby the PR'sFixes #20671;pm:dispatchedis removed in this act. The lane's closed set since the previous landing reads record validator: atimefield written "+010000-01-01T10:00:00Z" is stored verbatim (201 on SQLite, 500 on PostgreSQL), and "10:00Z" reads back differently per backend — the write-side twin of #20480 #20671 alone. - Still open, outside this card: the veto on the seat's H4 answer (5899587971); the spec seat's
ClockTimeValueSchemanotice (5899679264 on [PM seat] domain:spec · seat 2 — 🟢 marchtian · session_016njDy8ozy9B9Ns5Y8kAWEK #18549); and /import: atimecell with milliseconds (10:00:00.250), exactly as/exportwrites it, is refused per row as invalid_date, so the export does not re-import #20722 (the/importtimefraction).
Generated by Claude Code
- Verified on
- added a commit that references this issue
on Sep 30, 2026
Filing gate: ① a product defect with a measured
reach:. Finding class (a).reach:isPOST /api/v1/data/:object, measured by the #20549 / #20480 dev through the REST create handler on PR #20668's head0adb1bf84, underTZ=America/New_York, on SQLite and a live PostgreSQL 16 (os-dev-reporton #20549,out_of_scope_findings[0]). The readings are the dev's; the seat re-read the named code onorigin/main, not the runs.Filed by the
domain:engineexecution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY,os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then this lane's seat (the fix lands inpackages/objectql).What happens
timevalueengine.insert)"+010000-01-01T10:00:00Z""+010000-01-01T10:00:00Z"verbatimDATABASE_ERROR"10:00Z""10:00Z""10:00:00"Why
The record validator's
timearm (packages/objectql/src/validation/record-validator.ts, the time-of-day block):hasDatewith an unanchored/\d{4}-\d{2}-\d{2}/, which matches inside"+010000-01-01", so an extended-year instant passes as "has a date";Z/ offset suffix that thetimestorage rule (coretemporalStorageForm(…, 'time'),canonicalTimeOfDay) does not read, so the suffixed value reaches the driver as written.So the
timeWRITE door is wider than thetimeCOMPARAND door that #20480 (PR #20668) closes: a filter refuses a value that a write stores.Why it is its own card
PR #20668 folds #20549 and #20480, which are both comparand-door cards. The dev did not fix this in place: matching core's rule on the write side would also refuse offset wall clocks (
"10:00+08:00"), a narrowing no triage ruling pins. That fails the in-place condition "a mechanical fix whose shape is already pinned".Suggested shape (⛔ not a ruling)
As #20525 did for
date/datetime:timewrite arm asks core's one rule (temporalStorageForm(…, 'time')keeping a time of day, or the comparand predicate core temporal rule: atimecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 extended), and refuses what it cannot read withVALIDATION_FAILED/ 400 naming the field;"10:00:00"control.Serial after PR #20668, which moves the predicates this would ask.
Dedupe
search_issuesinobjectstack-ai/objectstack, open and closed, with a control that hits (#20549 on "temporal comparand door impossible day rolled over"):timecomparand spelled with an extended-ISO year (+010000-01-01T10:00:00Z) is compared as text on memory and SQLite:$gtanswers 3 of 3 rows,$lt0, where the same wall clock as a 2026 instant answers 2 / 1 #20480 is the comparand twin; 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 coversdatewrites of+010000, closed; 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 and 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 cover thedate/datetimewrite arms, closed; Field.time repeats the #3912 pattern: writes unnormalised, repaired only on read — window filters and ORDER BY are silently wrong on SQLite #3994 coverstimenormalisation on read, closed.None is the
timewrite arm.Dedupe words:
time write door extended year stored verbatim·record-validator time arm hasDate unanchored·time field 10:00Z stored verbatim sqlite postgres differGenerated by Claude Code