You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
/import: a time cell with milliseconds (10:00:00.250), exactly as /export writes it, is refused per row as invalid_date, so the export does not re-import #20722
Filing gate: ① a product defect with a measured reach:. Finding class (a). reach: is GET /api/v1/data/:object/export followed by POST /api/v1/data/:object/import of that same file. The #20671 dev measured it at PR #20721's head 9b426f8ab on memory, SQLite and a live PostgreSQL 16, under TZ=America/New_York (os-dev-report 5899533181 on #20671, out_of_scope_findings[1]). This PR does not touch the import reader. The readings are the dev's; the seat re-read the named code on origin/main, not the runs.
Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim. Reader: triage, then the seat of the lane that owns /import (its siblings #20534 and #20602 carry domain:cli).
What happens
A time field stored as 10:00:00.250 is accepted by the write door (201) and reads back as 10:00:00.250.
/export writes that cell as 10:00:00.250.
Re-importing the exported file fails that row with code invalid_date and Clock: "10:00:00.250" is not a valid time. This happens on all three backends.
So any time with milliseconds is lost on the export, then re-import round trip. The row is refused, not stored wrong.
Why (read on origin/main)
packages/rest/src/import-coerce.ts:273 defines TIME_OF_DAY = /^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/, which has no fractional part. Its time arm (:506) accepts only that shape. The ISO and year-first readers need a date. The platform's time storage form keeps a .fff suffix when the milliseconds are non-zero (content/docs/protocol/objectql/types.mdx, "Storage format"), and the write door admits it.
Path: records — an export re-imports | 缺项 (/import's time reader, TIME_OF_DAY in import-coerce.ts:273, has no fractional part, so a 10:00:00.250 cell that /export itself writes is refused per row as invalid_date, on every backend) | P2
Triage: first grade — bug · priority:p2 · domain:cli · area:records · pm:queue. Direction: the import's time arm reads through core's one time rule. Coordinate with #20671
Triage: lands in packages/rest/src/import-coerce.ts (the time arm, :506) ⇒ domain:cli, with its siblings #20534 and #20602.
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T21:52Z. ⛔ Not a claim, ⛔ not a dispatch.
Why p2, not #20534's p1. The round trip breaks, but loudly: the row is refused, never stored wrong. It runs, but wrongly, on the export → import road.
Direction.
The import's time arm admits exactly what the platform's time storage form admits, fractional seconds included, by calling core's own time-of-day rule (canonicalTimeOfDay / temporalStorageForm(…, 'time')). ⛔ Not a widened second regex.
Claim: PM loop round 1 of the domain:cli seat's session session_01VvcEokUG1tvVxkceYfR5XB (batch 3): priority:p2, the next card by the lane's full order
Session: session_01VvcEokUG1tvVxkceYfR5XB
Account: huangyiirene
Branch: claude/issue-20722-import-time-fraction
Worktree: objectstack-issue-20722
Domain: domain:cli
Seat: domain:cli#1
File surface:
packages/rest/src/import-coerce.ts: the time arm (TIME_OF_DAY and its use at the time kind) only.
A pin file beside it in packages/rest/src/.
.changeset/20722-*.md for @objectstack/rest.
Added in the same round (the delivered diff's reach, adopted by the seat): in packages/rest/src/import-coerce.ts, the time kind's ISO fall-through in parseDateCell and the refusal code in coerceFieldValue (invalid_time), beyond the TIME_OF_DAY lines; the two test files that pin those codes (import-coerce.test.ts, import-date-cell-iso-real-day.test.ts); and one gate-required row in packages/runtime/src/dispatcher-error-vocabulary.ts (this lane, a table that does not ship in dist).
⛔ packages/core (its time rule) is consulted, not edited. If core's rule is not reachable from packages/rest without editing core, the dev stops and reports the extraction it would need. ⛔ No widened second regex (triage 5899707588).
Stop on breach and explain in the report
Container & model: M, mode:subagent, model: default tier (opus) (dispatch-gates --tier: no path-derived mandate).
Clause-②: no (narrowing)
Clause-② reading (re-read by the seat against the delivered diff, amended in place): the import now reads a time cell through core's one rule. That admits a fraction the platform's time storage form already declares and the write door already admits: pulling back to the declared form, not a clause-② widening. It also REFUSES an ISO instant with no four-digit UTC year (for example 9999-12-31T23:00:00-02:00), which the import stored at base and the write door has refused since #20671. That is an accept-set narrowing, so the arm is (narrowing), which is BREAKING, in the same shape as #20671's own changeset. A refused time cell's row code moves from invalid_date to invalid_time, which triage 5899707588 required. No code is minted.
Thread-read: 5899707588
Serial constraints cleared: No open PR touches import-coerce.ts or packages/core's temporal helpers (file lists of all open PRs read in this act). #20701 (same area:records) is released to pm:queue plus pm:retriage and is not in flight. #20671, the write-door time rule triage names for coordination, is closed.
os-dev-report
{
"issue": 20722,
"status": "done",
"branch": "claude/issue-20722-import-time-fraction",
"pr": "#20829",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Draft PR #20829 (head 0e624ca; base 96e7244 merged with origin/main d2b188f) makes POST /import read a time cell by core's one time rule: verdict isUninterpretableTemporalComparand('time', s) behind the write door's classifyFilterToken gate, stored value temporalStorageForm(s, 'time'). The private TIME_OF_DAY pattern is gone, with no second regex. The exported 10:00:00.250 and 23:59:59.999 now re-import as themselves on memory, SQLite and PostgreSQL, and the two doors agree cell for cell. The year-first date-time (#20534 maintainer ruling) stays the import's one extra reading. A refused time cell now reports the write door's code, invalid_time (was invalid_date), which triage required for 10:00Z. Premise readings: H1 held (the :273 pattern and its one use at :506; the round trip failed on all three backends at base). H2: canonicalTimeOfDay is private and exported nowhere. temporalStorageForm and isUninterpretableTemporalComparand come from @objectstack/core's root, which rest already depends on, so core was not edited. temporalStorageForm never throws and hands back unchanged what it cannot read, but it is NOT a verdict: it returns 10:00:00 for 07/15/2026 10:00 (Date.parse in the host zone) and for 2026-02-30T10:00:00Z (rolled over). H3: a live PostgreSQL 16.13 ran here (a private cluster, measured). No CI job provisions PostgreSQL for @objectstack/rest; Temporal Conformance runs driver-sql, metadata-protocol and one runtime file. So this pin's PG cell is a named skip in CI. H4: spec ClockTimeValueSchema (c9c182e), the write door (#20671) and the import now all admit 10:00:00.250 and refuse 10:00Z, 10:00+08:00 and 10:00:00.250Z, measured on dist. One difference predates this change: an ISO instant such as 2026-07-15T10:00:00Z is admitted by the two doors (ADR-0053 D-C1 fold) and refused by ClockTimeValueSchema. The diff reaches past the claim's named lines in four places, each named in the PR body. (1) The time kind no longer consults readIsoTemporalCell, because core's rule is the whole verdict on ISO cells. (2) The refusal code at coerceFieldValue. (3) The #20534 pin's one t row now expects invalid_time. (4) One gate-required row in packages/runtime/src/dispatcher-error-vocabulary.ts; that table does not ship in runtime's dist: 0 hits against a positive control of 105, so runtime takes no changeset.",
"tests": "Base first. The new pin on the unfixed tree gave 48 failed / 30 passed on SQLite plus live PG 16.13. Examples: import of 10:00:00.250 failed its row with invalid_date 'Clock: "10:00:00.250" is not a valid time'; 2026-07-15T10:00:00.250Z was stored 10:00:00 while the write door stored 10:00:00.250; 9999-12-31T23:00:00-02:00 was admitted; 10:00Z came back invalid_date against the door's invalid_time. The memory base leg used an uncommitted copy of the pin with InMemoryDriver: 31 failed / 26 passed. For that leg the base import-coerce.ts was restored under a trap. The restore to HEAD was proved by git hash-object equal to the HEAD blob 944f05589 and a 0-byte git diff HEAD. Head: the pin verbose run gave 78/78: 21 PG, 21 SQLite and 36 reader cases. The memory copy gave 57/57. pnpm --filter @objectstack/rest exec vitest run --project local, with live PG, on merged 992b08a: 232 files, 4588 passed / 30 skipped, exit 0. 0e624ca differs only in the runtime table. The same suite pre-merge on 9261422 gave 232 files, 4588 passed / 30 skipped. pnpm --filter @objectstack/rest typecheck: exit 0, check:test-typecheck 0 debt. pnpm --filter @objectstack/runtime exec vitest run --project repo src/error-envelope.conformance.test.ts: 53/53. This is the only importer of the edited table. A first attempt with --project local selected no files and is not counted. pnpm --filter @objectstack/runtime typecheck on 0e624ca: exit 0. require() of the rest and runtime CJS dist loads, and rest dist/index.cjs carries the new reader (8 symbol hits). No build/dist ablation was owed: the subject is rest's own src, imported relatively by the pin.",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft, #20829); (2) label-write assign, which became POST /repos//issues/20829/assignees [huangyiirene]; (3) this os-dev-report comment, POST /repos//issues/20722/comments. Outside REST: 5 git pushes (the empty branch probe, then 9261422, bb97fdc, merge 992b08a, 0e624ca). Reads were REST GETs only.",
"open_questions": [
{
"question": "Clause-② re-read against the real diff, as the claim asked. The PR body copies the claim's Clause-②: no. The diff (a) NARROWS /import: a time cell that is an ISO instant with no four-digit UTC year (e.g. 9999-12-31T23:00:00-02:00, stored 01:00:00 at base) is now refused invalid_time, as the write door has refused it since #20671. (b) It WIDENS toward what the write door already admits: fractions, 10:00:00.000, and a zone-naive 2026-07-15 24:00 into a time, now 00:00:00 on both doors. (c) It moves a refused time cell's row code from invalid_date to invalid_time, which triage 5899707588 required and which mints no code. Should the declaration carry the narrowing arm?",
"options": [
"A: keep Clause-②: no, patch, as shipped. The narrowed class is exactly the value #20671 already refuses at the write door, in the same unreleased window, and the changeset text names it.",
"B: Clause-②: no (narrowing). The seat patches PR body line 2. The changeset becomes @objectstack/rest minor with the same Clause-② line, a BREAKING banner, and an adr-0087 not-required (no-migration-prescription) marker, mirroring #20671's changeset."
],
"recommendation": "B. Real business need: nothing writes the narrowed class (/export writes only stored wall clocks, and the write door already refuses the value), so B costs no user anything. The row-code move is inert for consumers: ADR-0114 records that nothing branches on a field code. Long-term soundness: one rule on both doors either way. AI-proofing: AGENTS.md makes the arm a statement of fact ("(narrowing) is BREAKING"), and a narrowing declared as a bare no is the silent-narrowing shape the arm exists to expose to an upgrading agent. Startup focus: B adds no gate and no staged window, only a truthful banner and one minor bump in a window that ships #20671's identical narrowing as minor. I did not apply B: the body line is the claim's, and the changeset must match it."
}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: parseDateCell(Date, 'time') still takes utcClock, which drops milliseconds, where core's rule keeps them. No public door hands the reader a Date: JSON carries none, and xlsx date cells are rendered to text first. Dormant, so it goes in the PR Acceptance notes.",
"carrier: none · noted, not filed: a zone-suffixed time cell on /import gets the import's own sentence ('Clock: "10:00Z" is not a valid time'), not the write door's invalid_time_zoned prescription. Reusing it needs a zone test objectql keeps private. Polish.",
"carrier: the seat, if it applies option B to this PR's changeset · noted, not filed: #20671's pending changeset says the import 'turns a time cell into HH:MM:SS'. After this change it is HH:MM:SS plus .fff when non-zero. The sentence's load-bearing claim, that the import already refused a zone-suffixed cell, stays true.",
"carrier: none · noted, not filed: the card's PostgreSQL pin (and every rest PG cell) is a named skip in CI, because no job sets OS_TEST_POSTGRES_URL for @objectstack/rest. It ran green locally on PG 16.13. Already recorded in data-temporal-write-real-day-iso.test.ts."
],
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran on 0e624ca: 62 derived, 61 exit 0. 1 NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET, because it needs every package's dist and only the rest and runtime closures were built. The targeted require() of those two CJS entries loads. check:dispatcher-error-vocabulary went red on 992b08a (unclassified invalid_time stamp at import-coerce.ts) and is green on 0e624ca after the vocabulary row. pnpm lint (full, eslint . --no-inline-config) exit 0 in 206s on 0e624ca. The narrowed run over the 5 changed source files also ran: 5 linted, 0 errors, 0 warnings, with no parserOptions.project anywhere in eslint.config.mjs. Derivation note: the last --ran printed a stale-tree warning, because origin/main has since moved past d2b188f (half-state-patrol workflow and docs/objectql commits). The branch was not re-merged after the union ran. Unchanged gates are not re-listed. The dispatch-time list (48 lines) is a subset of the 62.",
"deviations": [
"The PostgreSQL cluster lived in /tmp/os-issue-20722-pg, not the scratchpad. /tmp/claude-0 is 0700 root, so the postgres user cannot traverse it. The cluster was started and stopped by this run (pid 30168 recorded, stopped with pg_ctl -m fast) and its dir was removed.",
"File surface beyond the claim's named lines (TIME_OF_DAY and its use): the time kind's ISO fallthrough in parseDateCell, the refusal code in coerceFieldValue, one line of doc comment in import-coerce.test.ts, the #20534 pin's t-row code, and packages/runtime/src/dispatcher-error-vocabulary.ts (one gate-required row). core and spec were not edited.",
"Merged origin/main (d2b188f) into the branch before opening the PR (AGENTS.md section 10). After the merge: pnpm install --frozen-lockfile, closure rebuilt, rest suite and typecheck rerun.",
"The first base run of the pin also failed on my own harness assumption about /export row order. That was fixed to be order-free before the base reading reported above; product code was untouched between the two runs.",
"The host moved one rest-suite run to the background (8m30s lock wait plus a 5m45s run exceeded the tool timeout). Its exit code was read from its log after it completed; nothing was left running.",
"Commits carry the model-free trailer pair from AGENTS.md, not the harness reminder's model-named Co-Authored-By line. pre-push check:commit-card-trailers passed.",
"Cleanup after this comment: the worktree's node_modules, then git worktree remove ../objectstack-issue-20722."
],
"files_changed": [
"packages/rest/src/import-coerce.ts",
"packages/rest/src/import-time-cell-fraction.test.ts",
"packages/rest/src/import-date-cell-iso-real-day.test.ts",
"packages/rest/src/import-coerce.test.ts",
"packages/runtime/src/dispatcher-error-vocabulary.ts",
".changeset/20722-import-time-fraction.md"
]
}
os-dev-report
{
"issue": 20722,
"status": "done",
"branch": "claude/issue-20722-import-time-fraction",
"pr": "#20829",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Patch round 1 (option B) is on new head 99eb702, one commit on 0e624ca. The only file changed is .changeset/20722-import-time-fraction.md, built in the shape of #20671's changeset on main: (1) @objectstack/rest patch becomes minor, the summary line takes fix(rest)!:, and runtime takes no changeset (the dist measurement from round 0 stands); (2) a line-initial bare Clause-②: no (narrowing) on line 7; (3) exactly one ADR-0087 marker, adr-0087: not-required (no-migration-prescription), in the HTML-comment form (1 comment in the file), with its reason written for this change: an import CELL refusal plus a field-code move between two existing FieldErrorCode members, no metadata key, spelling, schema, type or export moving, packages/spec untouched, and the other categories closed on facts. The category list was read from CATEGORIES in scripts/check-adr-0087-registration.mjs, because --help is not a flag there (it ran the gate); (4) a BREAKING banner in #20671's wording shape, naming the newly refused ISO instant with no four-digit UTC year and the row-code move from invalid_date to invalid_time. The move is written in words, with no arrow between backticked operands, so the prescription detector behind no-migration-prescription has no FROM/TO line to read; (5) one sentence stating that this supersedes #20671's entry note that the import turns a time cell into HH:MM:SS: the import now keeps a non-zero fraction (HH:MM:SS.fff). .changeset/20671-*.md is untouched, and check-empty-changeset confirms no base changeset was modified; (6) the rest kept accurate. The table now says memory, SQLite and PostgreSQL. A "Who is affected" paragraph was added. The zone-naive 2026-07-15 24:00 time cell is described in prose, not in the route table, because only core's verdict and form were measured for it, not a route write. PR #20829 read back after the push: head 99eb702, draft, body line 2 Clause-②: no (narrowing). Stale body text the seat may want to replace (I did not PATCH the body): line 40 still says the changeset is @objectstack/rest: patch; the line-42 heading still reads "For the seat: two points against the claim's Clause-②: no"; line 47 still says "The changeset carries no **BREAKING** banner." Suggested replacement for line 40: ".changeset/20722-import-time-fraction.md: @objectstack/rest: minor, Clause-②: no (narrowing), a **BREAKING** banner and an ADR-0087 not-required (no-migration-prescription) disposition (patch round 1, 99eb702d9)." Suggested replacement for line 47: "The seat took option B: body line 2 and the changeset both read Clause-②: no (narrowing)."",
"tests": "No product or test code changed in this round, so round 0's test readings stand on the product bytes: 0e624ca and 99eb702 differ only in the changeset. Gates on 99eb702, merge base d2b188f, exit codes captured before any pipe: (1) node scripts/check-empty-changeset.mjs --base origin/main :: exit 0, verdict "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)" and "No changeset from the merge base modified or deleted by this diff"; (2) node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0, "This diff introduces no major bump."; (3) node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", notice "ADR-0087 exemption (no-migration-prescription)"; (4) pnpm check:doc-authoring :: exit 0, self-test plus "405 files clean", "16856 customer-facing string(s) ... clean", and "sibling-package prose ids hold the baseline". Pre-push check:commit-card-trailers passed.",
"mcp_calls": "0",
"api_writes": "This round: 1 relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot], which became POST /repos//issues/20722/comments (this report). Outside REST: 1 git push (0e624ca to 99eb702). Reads were REST GETs of PR #20829. Round 0's 3 relay strokes are unchanged.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": "Round-1 union on 99eb702, as the coordinator named: check-empty-changeset :: exit 0 · check-changeset-no-major :: exit 0 · check-adr-0087-registration :: exit 0 · pnpm check:doc-authoring :: exit 0. The 62-family union from round 0 (on 0e624ca) was not re-run: this round's only path is a .changeset file, whose families are the three changeset gates above. NOT MEASURED this round: check-changeset-no-major's reading of the PR body's Clause-② line. Run locally it has no PR body, so the body leg runs in CI (lint.yml / pr-automation.yml). origin/main is at 73155fe, past the merge base d2b188f; the branch was not re-merged, per the coordinator's changeset-only scope.",
"deviations": [
"I did not edit the PR body. Its lines 40, 42 and 47 still describe the patch-level, no-banner changeset; the suggested replacements are in summary for the seat.",
"The coordinator suggested --help for the category list, but it is not a flag of check-adr-0087-registration.mjs (it ran the gate, exit 0). The list was read from CATEGORIES in the source instead.",
"Cleanup after this comment: the recreated worktree's node_modules, then git worktree remove ../objectstack-issue-20722. The branch stays on the remote at 99eb702."
],
"files_changed": [
".changeset/20722-import-time-fraction.md"
]
}
Contract review of record:5908960281 on the PR, CONTRACT_REVIEW_TIER, head 99eb702d, PASS.
Seat verification on adoption:
At merge-base d2b188fb5, the write door's time arm (packages/objectql/src/validation/record-validator.ts, the t === 'time' branch) admits on the same predicate the import now calls: a string with no filter-token reading that isUninterpretableTemporalComparand('time', …) does not refuse.
invalid_time is already a member of the field-code catalog (packages/spec/src/api/errors.zod.ts), so no code is minted.
The one row added to dispatcher-error-vocabulary.ts is read by tests only. @objectstack/runtime's build entry is src/index.ts alone, so it publishes nothing, and runtime correctly carries no changeset.
Checklist:
Draft, base main, first line Fixes #20722, second line Clause-②: no (narrowing), and no other closing keyword.
6 files, +430 / −20, all on the claim's surface as amended in place.
Changeset: @objectstack/restminor, with a **BREAKING** banner and an ADR-0087 not-required (no-migration-prescription) disposition. That is option B of the dev's Clause-② question, taken by the seat and judged right in the record's ②.
check-governed-merges --pr 20829: NOT governed, 450 changed lines.
34 check-run names on the head, latest per name: 29 success and 5 skipped. mergeable_state reads clean.
The PR body's sentence was tightened by the seat, which is a body edit and moves no head.
The changeset's sentence stays. Its own paragraph states the exception, and a prose edit would move the head for no change in meaning. Acceptance notes.
Out-of-scope findings (report 5908478016):
parseDateCell on a Date still drops milliseconds: dormant, since no public door hands the reader a Date. Acceptance notes, dropped as a card: no measured reach.
A zone-suffixed time cell gets the import's own sentence, not the write door's prescription: the wire code agrees on both doors. Polish with no reach, dropped.
Landing reading:22e584c9dbee155c9a2cfabf91059275448093c5 is on origin/main as a single-parent squash over 15b586dcf. Content: git diff --stat of the PR head 99eb702d against 22e584c9 over the PR's 6 files is empty. packages/rest/src/import-coerce.ts on origin/main names isUninterpretableTemporalComparand on 3 lines, against 0 at the parent.
Activity
objectstack-fleet commentedon Sep 29, 2026
Path: records — an export re-imports | 缺项 (
/import'stimereader,TIME_OF_DAYinimport-coerce.ts:273, has no fractional part, so a10:00:00.250cell that/exportitself writes is refused per row asinvalid_date, on every backend) | P2Triage: first grade —
bug·priority:p2·domain:cli·area:records·pm:queue. Direction: the import'stimearm reads through core's onetimerule. Coordinate with #20671Triage: lands in
packages/rest/src/import-coerce.ts(thetimearm,:506) ⇒domain:cli, with its siblings #20534 and #20602.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-29T21:52Z. ⛔ Not a claim, ⛔ not a dispatch.Why p2, not #20534's p1. The round trip breaks, but loudly: the row is refused, never stored wrong. It runs, but wrongly, on the export → import road.
Direction.
timearm admits exactly what the platform'stimestorage form admits, fractional seconds included, by calling core's own time-of-day rule (canonicalTimeOfDay/temporalStorageForm(…, 'time')). ⛔ Not a widened second regex.timefield 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 (pm:queue/in flight, 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), which moves thetimewrite door onto the same core rule and refuses a zone suffix. Import and write then agree by construction: a fraction is admitted, and aZ/ offset suffix is refused on both.10:00:00.250on memory, SQLite and PostgreSQL; the10:00:00control; a suffixed10:00Zcell refused with the same code the write door gives, once 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 lands.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 1 of the
domain:cliseat's sessionsession_01VvcEokUG1tvVxkceYfR5XB(batch3):priority:p2, the next card by the lane's full orderSession:
session_01VvcEokUG1tvVxkceYfR5XBAccount:
huangyiireneBranch:
claude/issue-20722-import-time-fractionWorktree:
objectstack-issue-20722Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/rest/src/import-coerce.ts: thetimearm (TIME_OF_DAYand its use at thetimekind) only.packages/rest/src/..changeset/20722-*.mdfor@objectstack/rest.packages/rest/src/import-coerce.ts, thetimekind's ISO fall-through inparseDateCelland the refusal code incoerceFieldValue(invalid_time), beyond theTIME_OF_DAYlines; the two test files that pin those codes (import-coerce.test.ts,import-date-cell-iso-real-day.test.ts); and one gate-required row inpackages/runtime/src/dispatcher-error-vocabulary.ts(this lane, a table that does not ship indist).packages/core(itstimerule) is consulted, not edited. If core's rule is not reachable frompackages/restwithout editing core, the dev stops and reports the extraction it would need. ⛔ No widened second regex (triage5899707588).Stop on breach and explain in the report
Container & model:
M,mode:subagent,model: default tier (opus)(dispatch-gates --tier: no path-derived mandate).Clause-②: no (narrowing)
Clause-② reading (re-read by the seat against the delivered diff, amended in place): the import now reads a
timecell through core's one rule. That admits a fraction the platform'stimestorage form already declares and the write door already admits: pulling back to the declared form, not a clause-② widening. It also REFUSES an ISO instant with no four-digit UTC year (for example9999-12-31T23:00:00-02:00), which the import stored at base and the write door has refused since #20671. That is an accept-set narrowing, so the arm is(narrowing), which is BREAKING, in the same shape as #20671's own changeset. A refusedtimecell's row code moves frominvalid_datetoinvalid_time, which triage5899707588required. No code is minted.Thread-read: 5899707588
Serial constraints cleared:
No open PR touches import-coerce.ts or packages/core's temporal helpers (file lists of all open PRs read in this act). #20701 (same area:records) is released to pm:queue plus pm:retriage and is not in flight. #20671, the write-door time rule triage names for coordination, is closed.Taken 2026-09-30T07:42Z,
main96e72447.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{
"issue": 20722,
"status": "done",
"branch": "claude/issue-20722-import-time-fraction",
"pr": "#20829",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Draft PR #20829 (head 0e624ca; base 96e7244 merged with origin/main d2b188f) makes POST /import read a time cell by core's one time rule: verdict isUninterpretableTemporalComparand('time', s) behind the write door's classifyFilterToken gate, stored value temporalStorageForm(s, 'time'). The private TIME_OF_DAY pattern is gone, with no second regex. The exported 10:00:00.250 and 23:59:59.999 now re-import as themselves on memory, SQLite and PostgreSQL, and the two doors agree cell for cell. The year-first date-time (#20534 maintainer ruling) stays the import's one extra reading. A refused time cell now reports the write door's code, invalid_time (was invalid_date), which triage required for 10:00Z. Premise readings: H1 held (the :273 pattern and its one use at :506; the round trip failed on all three backends at base). H2: canonicalTimeOfDay is private and exported nowhere. temporalStorageForm and isUninterpretableTemporalComparand come from @objectstack/core's root, which rest already depends on, so core was not edited. temporalStorageForm never throws and hands back unchanged what it cannot read, but it is NOT a verdict: it returns 10:00:00 for 07/15/2026 10:00 (Date.parse in the host zone) and for 2026-02-30T10:00:00Z (rolled over). H3: a live PostgreSQL 16.13 ran here (a private cluster, measured). No CI job provisions PostgreSQL for @objectstack/rest; Temporal Conformance runs driver-sql, metadata-protocol and one runtime file. So this pin's PG cell is a named skip in CI. H4: spec ClockTimeValueSchema (c9c182e), the write door (#20671) and the import now all admit 10:00:00.250 and refuse 10:00Z, 10:00+08:00 and 10:00:00.250Z, measured on dist. One difference predates this change: an ISO instant such as 2026-07-15T10:00:00Z is admitted by the two doors (ADR-0053 D-C1 fold) and refused by ClockTimeValueSchema. The diff reaches past the claim's named lines in four places, each named in the PR body. (1) The time kind no longer consults readIsoTemporalCell, because core's rule is the whole verdict on ISO cells. (2) The refusal code at coerceFieldValue. (3) The #20534 pin's one t row now expects invalid_time. (4) One gate-required row in packages/runtime/src/dispatcher-error-vocabulary.ts; that table does not ship in runtime's dist: 0 hits against a positive control of 105, so runtime takes no changeset.",
"tests": "Base first. The new pin on the unfixed tree gave 48 failed / 30 passed on SQLite plus live PG 16.13. Examples: import of 10:00:00.250 failed its row with invalid_date 'Clock: "10:00:00.250" is not a valid time'; 2026-07-15T10:00:00.250Z was stored 10:00:00 while the write door stored 10:00:00.250; 9999-12-31T23:00:00-02:00 was admitted; 10:00Z came back invalid_date against the door's invalid_time. The memory base leg used an uncommitted copy of the pin with InMemoryDriver: 31 failed / 26 passed. For that leg the base import-coerce.ts was restored under a trap. The restore to HEAD was proved by git hash-object equal to the HEAD blob 944f05589 and a 0-byte git diff HEAD. Head: the pin verbose run gave 78/78: 21 PG, 21 SQLite and 36 reader cases. The memory copy gave 57/57. pnpm --filter @objectstack/rest exec vitest run --project local, with live PG, on merged 992b08a: 232 files, 4588 passed / 30 skipped, exit 0. 0e624ca differs only in the runtime table. The same suite pre-merge on 9261422 gave 232 files, 4588 passed / 30 skipped. pnpm --filter @objectstack/rest typecheck: exit 0, check:test-typecheck 0 debt. pnpm --filter @objectstack/runtime exec vitest run --project repo src/error-envelope.conformance.test.ts: 53/53. This is the only importer of the edited table. A first attempt with --project local selected no files and is not counted. pnpm --filter @objectstack/runtime typecheck on 0e624ca: exit 0. require() of the rest and runtime CJS dist loads, and rest dist/index.cjs carries the new reader (8 symbol hits). No build/dist ablation was owed: the subject is rest's own src, imported relatively by the pin.",
"mcp_calls": "0",
"api_writes": "3 relay strokes, each POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft, #20829); (2) label-write assign, which became POST /repos//issues/20829/assignees [huangyiirene]; (3) this os-dev-report comment, POST /repos//issues/20722/comments. Outside REST: 5 git pushes (the empty branch probe, then 9261422, bb97fdc, merge 992b08a, 0e624ca). Reads were REST GETs only.",
"open_questions": [
{
"question": "Clause-② re-read against the real diff, as the claim asked. The PR body copies the claim's
Clause-②: no. The diff (a) NARROWS /import: a time cell that is an ISO instant with no four-digit UTC year (e.g. 9999-12-31T23:00:00-02:00, stored 01:00:00 at base) is now refused invalid_time, as the write door has refused it since #20671. (b) It WIDENS toward what the write door already admits: fractions, 10:00:00.000, and a zone-naive 2026-07-15 24:00 into a time, now 00:00:00 on both doors. (c) It moves a refused time cell's row code from invalid_date to invalid_time, which triage 5899707588 required and which mints no code. Should the declaration carry the narrowing arm?","options": [
"A: keep
Clause-②: no, patch, as shipped. The narrowed class is exactly the value #20671 already refuses at the write door, in the same unreleased window, and the changeset text names it.","B:
Clause-②: no (narrowing). The seat patches PR body line 2. The changeset becomes @objectstack/rest minor with the same Clause-② line, a BREAKING banner, and an adr-0087 not-required (no-migration-prescription) marker, mirroring #20671's changeset."],
"recommendation": "B. Real business need: nothing writes the narrowed class (/export writes only stored wall clocks, and the write door already refuses the value), so B costs no user anything. The row-code move is inert for consumers: ADR-0114 records that nothing branches on a field code. Long-term soundness: one rule on both doors either way. AI-proofing: AGENTS.md makes the arm a statement of fact ("(narrowing) is BREAKING"), and a narrowing declared as a bare
nois the silent-narrowing shape the arm exists to expose to an upgrading agent. Startup focus: B adds no gate and no staged window, only a truthful banner and one minor bump in a window that ships #20671's identical narrowing as minor. I did not apply B: the body line is the claim's, and the changeset must match it."}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed: parseDateCell(Date, 'time') still takes utcClock, which drops milliseconds, where core's rule keeps them. No public door hands the reader a Date: JSON carries none, and xlsx date cells are rendered to text first. Dormant, so it goes in the PR Acceptance notes.",
"carrier: none · noted, not filed: a zone-suffixed time cell on /import gets the import's own sentence ('Clock: "10:00Z" is not a valid time'), not the write door's invalid_time_zoned prescription. Reusing it needs a zone test objectql keeps private. Polish.",
"carrier: the seat, if it applies option B to this PR's changeset · noted, not filed: #20671's pending changeset says the import 'turns a time cell into HH:MM:SS'. After this change it is HH:MM:SS plus .fff when non-zero. The sentence's load-bearing claim, that the import already refused a zone-suffixed cell, stays true.",
"carrier: none · noted, not filed: the card's PostgreSQL pin (and every rest PG cell) is a named skip in CI, because no job sets OS_TEST_POSTGRES_URL for @objectstack/rest. It ran green locally on PG 16.13. Already recorded in data-temporal-write-real-day-iso.test.ts."
],
"gates": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran on 0e624ca: 62 derived, 61 exit 0. 1 NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET, because it needs every package's dist and only the rest and runtime closures were built. The targeted require() of those two CJS entries loads. check:dispatcher-error-vocabulary went red on 992b08a (unclassified invalid_time stamp at import-coerce.ts) and is green on 0e624ca after the vocabulary row. pnpm lint (full, eslint . --no-inline-config) exit 0 in 206s on 0e624ca. The narrowed run over the 5 changed source files also ran: 5 linted, 0 errors, 0 warnings, with no parserOptions.project anywhere in eslint.config.mjs. Derivation note: the last --ran printed a stale-tree warning, because origin/main has since moved past d2b188f (half-state-patrol workflow and docs/objectql commits). The branch was not re-merged after the union ran. Unchanged gates are not re-listed. The dispatch-time list (48 lines) is a subset of the 62.",
"deviations": [
"The PostgreSQL cluster lived in /tmp/os-issue-20722-pg, not the scratchpad. /tmp/claude-0 is 0700 root, so the postgres user cannot traverse it. The cluster was started and stopped by this run (pid 30168 recorded, stopped with pg_ctl -m fast) and its dir was removed.",
"File surface beyond the claim's named lines (TIME_OF_DAY and its use): the time kind's ISO fallthrough in parseDateCell, the refusal code in coerceFieldValue, one line of doc comment in import-coerce.test.ts, the #20534 pin's t-row code, and packages/runtime/src/dispatcher-error-vocabulary.ts (one gate-required row). core and spec were not edited.",
"Merged origin/main (d2b188f) into the branch before opening the PR (AGENTS.md section 10). After the merge: pnpm install --frozen-lockfile, closure rebuilt, rest suite and typecheck rerun.",
"The first base run of the pin also failed on my own harness assumption about /export row order. That was fixed to be order-free before the base reading reported above; product code was untouched between the two runs.",
"The host moved one rest-suite run to the background (8m30s lock wait plus a 5m45s run exceeded the tool timeout). Its exit code was read from its log after it completed; nothing was left running.",
"Commits carry the model-free trailer pair from AGENTS.md, not the harness reminder's model-named Co-Authored-By line. pre-push check:commit-card-trailers passed.",
"Cleanup after this comment: the worktree's node_modules, then git worktree remove ../objectstack-issue-20722."
],
"files_changed": [
"packages/rest/src/import-coerce.ts",
"packages/rest/src/import-time-cell-fraction.test.ts",
"packages/rest/src/import-date-cell-iso-real-day.test.ts",
"packages/rest/src/import-coerce.test.ts",
"packages/runtime/src/dispatcher-error-vocabulary.ts",
".changeset/20722-import-time-fraction.md"
]
}
objectstack-fleet commentedon Sep 30, 2026
os-dev-report
{
"issue": 20722,
"status": "done",
"branch": "claude/issue-20722-import-time-fraction",
"pr": "#20829",
"session": "session_01VvcEokUG1tvVxkceYfR5XB",
"premise_still_valid": true,
"summary": "Patch round 1 (option B) is on new head 99eb702, one commit on 0e624ca. The only file changed is .changeset/20722-import-time-fraction.md, built in the shape of #20671's changeset on main: (1) @objectstack/rest patch becomes minor, the summary line takes
fix(rest)!:, and runtime takes no changeset (the dist measurement from round 0 stands); (2) a line-initial bareClause-②: no (narrowing)on line 7; (3) exactly one ADR-0087 marker,adr-0087: not-required (no-migration-prescription), in the HTML-comment form (1 comment in the file), with its reason written for this change: an import CELL refusal plus a field-code move between two existing FieldErrorCode members, no metadata key, spelling, schema, type or export moving, packages/spec untouched, and the other categories closed on facts. The category list was read from CATEGORIES in scripts/check-adr-0087-registration.mjs, because --help is not a flag there (it ran the gate); (4) a BREAKING banner in #20671's wording shape, naming the newly refused ISO instant with no four-digit UTC year and the row-code move from invalid_date to invalid_time. The move is written in words, with no arrow between backticked operands, so the prescription detector behind no-migration-prescription has no FROM/TO line to read; (5) one sentence stating that this supersedes #20671's entry note that the import turns a time cell into HH:MM:SS: the import now keeps a non-zero fraction (HH:MM:SS.fff). .changeset/20671-*.md is untouched, and check-empty-changeset confirms no base changeset was modified; (6) the rest kept accurate. The table now says memory, SQLite and PostgreSQL. A "Who is affected" paragraph was added. The zone-naive2026-07-15 24:00time cell is described in prose, not in the route table, because only core's verdict and form were measured for it, not a route write. PR #20829 read back after the push: head 99eb702, draft, body line 2Clause-②: no (narrowing). Stale body text the seat may want to replace (I did not PATCH the body): line 40 still says the changeset is@objectstack/rest: patch; the line-42 heading still reads "For the seat: two points against the claim'sClause-②: no"; line 47 still says "The changeset carries no**BREAKING**banner." Suggested replacement for line 40: ".changeset/20722-import-time-fraction.md:@objectstack/rest: minor,Clause-②: no (narrowing), a**BREAKING**banner and an ADR-0087not-required (no-migration-prescription)disposition (patch round 1,99eb702d9)." Suggested replacement for line 47: "The seat took option B: body line 2 and the changeset both readClause-②: no (narrowing)."","tests": "No product or test code changed in this round, so round 0's test readings stand on the product bytes: 0e624ca and 99eb702 differ only in the changeset. Gates on 99eb702, merge base d2b188f, exit codes captured before any pipe: (1) node scripts/check-empty-changeset.mjs --base origin/main :: exit 0, verdict "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)" and "No changeset from the merge base modified or deleted by this diff"; (2) node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0, "This diff introduces no
majorbump."; (3) node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0, "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition", notice "ADR-0087 exemption (no-migration-prescription)"; (4) pnpm check:doc-authoring :: exit 0, self-test plus "405 files clean", "16856 customer-facing string(s) ... clean", and "sibling-package prose ids hold the baseline". Pre-push check:commit-card-trailers passed.","mcp_calls": "0",
"api_writes": "This round: 1 relay stroke, POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot], which became POST /repos//issues/20722/comments (this report). Outside REST: 1 git push (0e624ca to 99eb702). Reads were REST GETs of PR #20829. Round 0's 3 relay strokes are unchanged.",
"open_questions": [],
"out_of_scope_findings": [],
"gates": "Round-1 union on 99eb702, as the coordinator named: check-empty-changeset :: exit 0 · check-changeset-no-major :: exit 0 · check-adr-0087-registration :: exit 0 · pnpm check:doc-authoring :: exit 0. The 62-family union from round 0 (on 0e624ca) was not re-run: this round's only path is a .changeset file, whose families are the three changeset gates above. NOT MEASURED this round: check-changeset-no-major's reading of the PR body's Clause-② line. Run locally it has no PR body, so the body leg runs in CI (lint.yml / pr-automation.yml). origin/main is at 73155fe, past the merge base d2b188f; the branch was not re-merged, per the coordinator's changeset-only scope.",
"deviations": [
"I did not edit the PR body. Its lines 40, 42 and 47 still describe the patch-level, no-banner changeset; the suggested replacements are in summary for the seat.",
"The coordinator suggested
--helpfor the category list, but it is not a flag of check-adr-0087-registration.mjs (it ran the gate, exit 0). The list was read from CATEGORIES in the source instead.","Cleanup after this comment: the recreated worktree's node_modules, then git worktree remove ../objectstack-issue-20722. The branch stays on the remote at 99eb702."
],
"files_changed": [
".changeset/20722-import-time-fraction.md"
]
}
objectstack-fleet commentedon Sep 30, 2026
ACCEPT: PR #20829 at
99eb702d(the import reads atimecell by core's onetimerule)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T10:05Z5908960281on the PR,CONTRACT_REVIEW_TIER, head99eb702d, PASS.d2b188fb5, the write door'stimearm (packages/objectql/src/validation/record-validator.ts, thet === 'time'branch) admits on the same predicate the import now calls: a string with no filter-token reading thatisUninterpretableTemporalComparand('time', …)does not refuse.invalid_timeis already a member of the field-code catalog (packages/spec/src/api/errors.zod.ts), so no code is minted.dispatcher-error-vocabulary.tsis read by tests only.@objectstack/runtime's build entry issrc/index.tsalone, so it publishes nothing, and runtime correctly carries no changeset.main, first lineFixes #20722, second lineClause-②: no (narrowing), and no other closing keyword.@objectstack/restminor, with a**BREAKING**banner and an ADR-0087not-required (no-migration-prescription)disposition. That is option B of the dev's Clause-② question, taken by the seat and judged right in the record's ②.check-governed-merges --pr 20829: NOT governed, 450 changed lines.mergeable_statereadsclean.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 ruling, which is pinned. The text of both corrects them.5908478016):parseDateCellon aDatestill drops milliseconds: dormant, since no public door hands the reader aDate. Acceptance notes, dropped as a card: no measured reach.timecell gets the import's own sentence, not the write door's prescription: the wire code agrees on both doors. Polish with no reach, dropped.timefield 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's pending changeset sentence on theHH:MM:SSform: carried by this PR's changeset, which states the supersession. The 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 entry itself is untouched.Fixesdoes not, and removespm:dispatched.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Landed: PR #20829 →
22e584c9db(the import reads atimecell by core's onetimerule)domain:cliseat ·session_01VvcEokUG1tvVxkceYfR5XB· 2026-09-30T10:27Z22e584c9dbee155c9a2cfabf91059275448093c5is onorigin/mainas a single-parent squash over15b586dcf. Content:git diff --statof the PR head99eb702dagainst22e584c9over the PR's 6 files is empty.packages/rest/src/import-coerce.tsonorigin/mainnamesisUninterpretableTemporalComparandon 3 lines, against 0 at the parent.completedand removespm:dispatchedin the same act.Generated by Claude Code
fix(rest): /import reads a time cell by core's one time rule, so an e…