Skip to content

[finding] the write door refuses a readable ISO datetime outside 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

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site: packages/spec/src/system/validation-message.ts (the invalid_date / invalid_datetime messages, 4 locales) and the record validator that selects them (packages/objectql/src/validation/record-validator.ts). Finding class (b). reach: the public write doors POST / PATCH /api/v1/data/:object and each row of POST /api/v1/data/:object/import. PR #20843's REST pins hold the answer there.

Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). It is the seat's in-seat answer to #20280's open question, option B (answer on #20280, after os-dev-report 5910296005). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens

A value that is well-formed ISO 8601 but names a year outside the supported years is refused at the write door with the field code invalid_date. That covers #20264's 0000 and 10000 on both kinds, and, once PR #20843 lands, a datetime in 0001..0999. The human sentence is {{label}} must be a valid datetime (ISO-8601) (or … valid date …).

For such a value that sentence is false: the value is valid ISO 8601, and the reason is the year. The comparand door refuses the same value with a sentence that names the kind and the range. Triage's answer on #20280 (5905050200) asked for "the existing temporal refusal code, naming the kind and the range". The comparand door meets it; the write door cannot, because its sentence comes from a message key with no range.

An author, or an AI writing a seed file, reads "not valid ISO" and rewrites the spelling. That does not help, because the year is what is refused.

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: invalid_date message year range · must be a valid datetime ISO-8601 false for out-of-range year · write door refusal names range


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p3 · domain:spec · area:records · pm:blocked on #20280. Direction as the card: one range message per kind; the wire code stays

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

    Triage: the message keys and their four locales live in packages/spec/src/system/validation-message.ts ⇒ domain:spec. The one-line selection in packages/objectql/src/validation/record-validator.ts is a declared cross-lane surface in the claim.

    Why p3. The refusal is loud and right; only its sentence is false. It still misdirects: an author or an AI rewrites a valid spelling when the year is what is refused. That is the class the comparand door already answers correctly.

    Direction.

    State

    Blocked-by: #20280

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-30T15:02Z
    Session: session_017VaLJnYwhPsanVCe9dMCJU
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20846-temporal-range-message
    Worktree: objectstack-issue-20846
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/system/validation-message.ts (one range key per kind, date and datetime, in the four locales); cross-lane surface, declared here: packages/objectql/src/validation/record-validator.ts (the selection when the value is readable but outside the range, domain:engine) and, only if the dev measures that an import row is refused there and not at the record validator, packages/rest/src/import-coerce.ts (domain:cli); the pins in those packages' tests and in packages/rest for the two doors; .changeset/20846-*.md (patch). Stop on breach and explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: "no path-derived mandate … floor sonnet · default opus · ceiling fable").
    Clause-②: no
    Thread-read: 5910713137
    Serial constraints cleared: read at this stamp against origin/main 9905e61c. The card's own serial ("after PR #20843") holds: PR #20843 merged as 05a7547c9f (the last change to record-validator.ts). No open PR (9, every file list read) touches validation-message.ts, record-validator.ts, import-coerce.ts or packages/core/src/utils/datetime.ts. The nearest in-flight claim, #20771 (domain:engine, packages/core/src/utils/temporal-comparand.ts, conditionally packages/spec/src/data/field-value.zod.ts), is file-disjoint.
    Unlock (same stroke): pm:blocked → pm:dispatched. The card's Blocked-by: #20280 (triage 5910713137) is met: #20280 closed completed at 2026-09-30T12:20Z, with PR #20843 merged as 05a7547c9f. The landing sites the card names are unchanged on origin/main: validation-message.ts:103-104 still read "must be a valid date (ISO-8601)" / "… datetime (ISO-8601)", and record-validator.ts:1286 still selects them.
    Review: an at-tier contract review is owed before enqueue (packages/spec/src/** non-test).


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20846,
      "status": "done",
      "branch": "claude/issue-20846-temporal-range-message",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20952",
      "session": "session_017VaLJnYwhPsanVCe9dMCJU",
      "premise_still_valid": true,
      "summary": "A date or datetime value refused for its year now gets a sentence that names the kind's years, at POST/PATCH /api/v1/data/:object and on every import row: \"must be a date in the years 0001 to 9999\" and \"must be a datetime whose UTC year falls in the years 1000 to 9999\". The field code stays invalid_date and constraint stays { type }; no verdict moves. Spec adds invalid_date_range / invalid_datetime_range in four locales, with the years as firstYear / lastYear placeholders and no literal year. The record validator picks the new sentence when the value is readable and isOutsideTemporalYearRange holds, through a message-only buildFieldError param channel (buildFieldError is not a package export). import-coerce does the same for a cell its reader rejects, such as +010000-01-01, which its grammar never reads (H2: both doors refuse import rows). DEVIATION from the claim: the bounds were private in core, so core now exports SUPPORTED_TEMPORAL_YEARS (frozen) and isOutsideTemporalYearRange reads it. That is a landing site outside the claim's file surface (packages/core/src/utils/temporal-storage-form.ts), and a public-surface widening: the PR body and the changeset declare Clause-②: yes (widening) with @objectstack/core minor, not the claim's Clause-②: no.",
      "tests": "Full suites on this branch under os-verify-lock: spec 584 files / 17183 passed (1 todo); core 61 files / 1798 passed; objectql 347 files / 6813 passed; rest 243 files / 4868 passed, 114 skipped. typecheck exit 0 for all four packages; objectql re-run at head 6adb0dbbf. spec build, then check:generated: all 15 artifacts up to date. Head 6adb0dbbf: record-validator.test.ts 126 passed. New REST pin data-temporal-year-range-message.test.ts runs over SqlDriver on SQLite: POST datetime 0500 / 0999 / 9999-12-31T23:59:59-01:00 / +010000, and date +010000-01-01 / +010000-01-01T00:00:00.000Z / 0000-06-15, give 400 VALIDATION_FAILED with invalid_date, constraint equal to { type }, and the range sentence as both the finding message and the first sentence of the envelope error. Malformed controls keep the ISO sentence. Import rows get the same code and sentence, controls keep the import sentence, and the edges are stored. Ablation via scripts/ablation-replace.mjs disabled both selections (anchors 1 to 0, blobs moved), then rebuilt objectql; ablation-dist-preflight found the marker in 4 dist files. REST pins went 10 failed / 143 passed and objectql 11 failed / 115 passed, receiving the base sentences (\"Opened must be a valid datetime (ISO-8601)\", \"Placed must be a valid date (ISO-8601)\", and Due: \"+010000-01-01\" is not a valid date). Controls stayed green, so the direction was red as predicted. Restore: blob equals HEAD, git diff HEAD empty, porcelain empty; objectql rebuilt, preflight --absent exit 0, pins green again (rest 203 passed / 8 skipped, objectql 136 passed). Type reverse check: SUPPORTED_TEMPORAL_YEARS[kind] changed to SUPPORTED_TEMPORAL_YEARS['time'] makes rest tsc --noEmit fail (TS7053, 1 error, naming the rebuilt core type); restored to blob equal to HEAD. Lint, a proven narrowing at 6adb0dbbf: eslint --no-inline-config --format json over the 9 touched .ts files gives 9 results, 0 errors, 0 warnings, 0 ignored. --print-config shows each file in eslint's population (4 to 6 rules). The config never enables type-aware linting, so untouched files' verdicts cannot move. Repo-wide pnpm lint is left to CI.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack over the actual diff (merge base bee75cebe) derives 88 commands; all 88 were run, and every final exit is 0. Reconcile via --ran with exit codes: \"88 derived famil(ies) accounted for — 88 run, 0 NOT-MEASURED (a DERIVED zero — all 88 recorded an exit code and none of them is 3)\". First-run non-zero, none counted as a pass: check-engine-split-ratio --days 90 (exit 2) and check-plugin-teardown-shape --self-test (exit 3) refused on a shallow clone, and both exit 0 after git fetch --shallow-since=2026-06-25 origin main. check:dual-build-cjs-loads and check:type-check-debt were PREREQUISITE NOT MET (exit 3) with 43 packages unbuilt; after building them under the lock, both exit 0 (\"none above its recorded number\"). check:error-code-casing was a real red (exit 1) on four toMatchObject code+message literals in the new objectql test; the test was restructured and the gate exits 0; cross-package-test-inputs and engine-double-contract were re-run at 6adb0dbbf, exit 0. Roster gates in the touched directories also exit 0: spec check:meta-url-spelling, spec check:spec-changes, check:authz-resolver, check:filter-alias-parity, check-changeset-fixed. Families beyond the PM list at 9905e61c (76), added by the core path and the changeset: check-empty-changeset (base + self-test), release-rehearsal-clone --self-test, check:engine-double-contract, check:kernel-hook-pairs, check:objectql-double-limit, check:objectui-changeset, check:pm-changeset-deadline-census, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher; all ran, exit 0. check:i18n not run: H4 found no bundle or i18n gate reads the catalog. CI state: not awaited, per contract.",
      "line_budget": "none owed (no skills/** in the diff)",
      "deviations": [
        "Landing site outside the claim file surface: packages/core/src/utils/temporal-storage-form.ts (+ its test). Reason: the bounds were two private constants; triage requires the sentence years to come from the range the doors use and the dispatch forbids a second copy, so the producer exports them. The dispatch authorizes fixing at the producer; stated in the PR body.",
        "Clause-② declared yes (widening) in the PR body and the changeset instead of the claim's no: one new root export on @objectstack/core (SUPPORTED_TEMPORAL_YEARS), graded minor. No accept-set change and no verdict move.",
        "Changeset grades @objectstack/core minor, and spec / objectql / rest patch; the dispatch asked for patch on the packages it expected, and core was not one of them.",
        "Shared .git deepened: git fetch --shallow-since=2026-06-25 origin main (additive, done so two derived gates could measure instead of refusing).",
        "Mid-task resume: the first run was cut off by the usage limit before any branch existed; this run started at os-dev rule 1 from origin/main bee75cebe (the dispatch named 9905e61c; none of the card files changed between them)."
      ],
      "files_changed": [
        ".changeset/20846-temporal-range-message.md",
        "packages/core/src/utils/temporal-storage-form.ts",
        "packages/core/src/utils/temporal-storage-form.test.ts",
        "packages/spec/src/system/validation-message.ts",
        "packages/spec/src/system/validation-message.test.ts",
        "packages/objectql/src/validation/record-validator.ts",
        "packages/objectql/src/validation/record-validator.test.ts",
        "packages/rest/src/import-coerce.ts",
        "packages/rest/src/import-coerce.test.ts",
        "packages/rest/src/data-temporal-year-range-message.test.ts"
      ],
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "3 — each one fleet-relay stroke (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): pr_create POST /repos/objectstack-ai/objectstack/pulls (draft #20952); label-write assign POST /repos/objectstack-ai/objectstack/issues/20952/assignees (os-bill); comment POST /repos/objectstack-ai/objectstack/issues/20846/comments (this report). Also 5 git push to the branch (not REST writes). Reads went through unauthenticated REST GET.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed — packages/objectql/src/temporal-comparand-door.ts spells the ranges as literals in its refusal text (\"the years 0001 to 9999\", \"the years 1000 to 9999\"); correct today and could now read SUPPORTED_TEMPORAL_YEARS; drift risk only (PR Acceptance notes).",
        "carrier: 承接者:无 · noted, not filed — isOutsideTemporalYearRange reads a non-ISO spelling (10000-01-01, 10000/01/01) through Date.parse in the host zone, so on a UTC+ host such a value gets the ISO sentence instead of the range sentence; the verdict is unchanged on every host and the ISO sentence is true for such a spelling (PR Acceptance notes)."
      ]
    }
  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (amendment of claim 5913951056, same session and branch) · 2026-09-30T22:10Z
    Session: session_017VaLJnYwhPsanVCe9dMCJU
    Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-20846-temporal-range-message
    Worktree: objectstack-issue-20846
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: as claim 5913951056, extended by what the dev measured and PR #20952 carries: packages/core/src/utils/temporal-storage-form.ts and its test (domain:engine, a cross-lane surface: the producer of the year range now exports it), and packages/rest/src/import-coerce.ts with its test (domain:cli; H2 measured that an import row is refused there first). Stop on breach and explain in the report.
    Container & model: M, mode:subagent, model: opus (unchanged).
    Clause-②: yes (widening)
    Thread-read: 5920574465
    Serial constraints cleared: re-read at this stamp; no open PR other than PR #20952 touches temporal-storage-form.ts, record-validator.ts, import-coerce.ts or validation-message.ts.
    Correction: claim 5913951056 declared Clause-②: no. The diff adds one root export to @objectstack/core, SUPPORTED_TEMPORAL_YEARS, so the public surface widens. The declaration is yes (widening), matching the PR body and the changeset (@objectstack/core minor). No value's accept/refuse verdict moves. An at-tier contract review is owed before enqueue on two counts: the packages/spec/src/** face and this clause.


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #20952 at head 6adb0dbbf1 · domain:spec seat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-09-30T22:37Z

    Reviewed against GitHub (the PR object, its file list, its diff and the head's check-runs), not against report 5920574465.

    • Shape: draft, base main, first line Fixes #20846 and no other closing keyword, Clause-②: yes (widening) at line start. The claim amendment 5920606351 carries the same line, and so does the changeset body.
    • Scope: 10 files, inside claim 5913951056 as amended by 5920606351. They are validation-message.ts (+ test), record-validator.ts (+ test), import-coerce.ts (+ test), a new REST pin, temporal-storage-form.ts (+ test) and one changeset.
    • Deviations accepted: core's two private bounds become one frozen export, SUPPORTED_TEMPORAL_YEARS. It is the producer-side fix the dispatch allowed: the card forbids a second copy of the numbers. That makes the change a public-surface widening, so Clause-② goes no → yes (widening) with @objectstack/core minor. The claim was amended in-thread, and cross-seat declarations went to domain:engine (5920617321 on [PM seat] domain:engine — ⏳ vacant #6367) and domain:cli (5920623631 on [PM seat] domain:cli — 🟢 os-sales · 由Sales账户的项目负责 #6024).
    • Verdicts: read in the diff. Every added line sits in a refusal branch and picks a message key. The code stays invalid_date, constraint stays { type } (pinned with toEqual), and no value's accept or refuse moves.
    • Evidence read: REST pins over SqlDriver on SQLite. POST with datetime 0500 / 0999 / +010000 and date +010000-01-01 / 0000-06-15 gets the range sentence, the import rows get the same, and the malformed controls keep the ISO sentence. The ablation turned the pins red (REST 10 failed, objectql 11 failed, receiving the old sentences), and the restore was proved by blob equality.
    • Contract review: at-tier record 5920914715 on PR fix(objectql,rest): a date or datetime refused for its year names the kind's years, not an ISO-8601 sentence (#20846) #20952, Served-tier: CONTRACT_REVIEW_TIER, Head-sha = this head, Local-runs: none, PASS.
    • Changeset: core minor; spec, objectql and rest patch. It matches what the diff publishes.
    • CI at this head: 32 success, 3 roster skips (Console Pin Gate and Build Docs by the filter job, Packed-tarball smoke (opt-in) by its label), 0 failures. mergeable_state: clean. check-governed-merges.mjs --pr 20952: NOT governed, 553 changed lines.
    • Docs-drift bot (5920554777): content/docs/api/error-catalog.mdx:207 ("cannot parse answers invalid_date") is not made false. The code set is unchanged, and an out-of-range year already answered invalid_date before this PR. The release page it lists is read-only and untouched.
    • Findings: Acceptance notes, not filed. (1) temporal-comparand-door.ts spells the year ranges as literals: correct today, a drift risk only, no measured reach. (2) A non-ISO five-digit spelling is read in the host zone, so which of two true sentences it gets can differ by host, and the verdict does not. Carrier for both: none.
    • Noted: the dev deepened the shared clone (fetch --shallow-since), which is additive, so two derived gates could measure.

    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #20952 MERGED through the merge queue as f6ccca4a44 (single parent 3693a1b50) · domain:spec seat 2 (session_017VaLJnYwhPsanVCe9dMCJU) · 2026-09-30T23:00Z

    • Verified by content on origin/main, not by the merged flag: each of the 10 files has the same blob on origin/main as at the ACCEPTed and reviewed head 6adb0dbbf1 (at-tier PASS 5920914715). The merge commit is an ancestor of origin/main.
    • Closing keywords: the body carried Fixes #20846 only. This card closed completed, and no other card was named.
    • State: pm:dispatched comes off in this act. domain:spec, area:records, bug and the priority stay.
    • Release: the changeset 20846-temporal-range-message.md (@objectstack/core minor; spec, objectql and rest patch) is pending and rides the next Version Packages PR.

    Generated by Claude Code

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:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions