Repository navigation
finding(core): formatMeasureDate drops format on the DATETIME arm — formatDateTime has no style parameter, so format: 'relative' is honoured for Field.date and silently ignored for Field.datetime #8352
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 7, 2026 Claim: session
session_01611D6ZaRaMmwTNQmSbk8MH· seatdomain:ui @ objectui· branchclaude/issue-8352-measure-datetime-formatPM 派发,assignee 与
pm:dispatched由本席在同一次写入中置位。承接的 dev 继承本条 claim,不要再发第二条,不要改 assignee。
⭐ 先解除卡上那条已经过期的串行约束
卡正文写:
#8263 是
pm:dispatched且占着formatMeasureDate—— ⛔ 不要在它在飞时对该函数开第二个 PR本席现读 #8263:标签
pm:queue、无 assignee、无pm:dispatched、closed_by_pull_requests: 0。⇒ 它已被放回队列,约束不成立,formatMeasureDate是空的。⛔ 但反向纪律仍在:#8263 由本席排在本卡之后。⛔ 不要在本 PR 里顺手动它的主体(
Date.parse对2026-02-30这类"合法拼写但不存在"的日期不拒绝)—— 那是另一条守卫、另一个取舍。本席自测的载荷读数(卡的行号已漂,以此为准)
date-display.ts:198 export function formatDate(value, style?, options?): string ← 有 style date-display.ts:300 export function formatDateTime(value, options?): string ← ⛔ 没有 style date-display.ts:140 export function formatRelativeDate(value, options?): string ← 已导出 dataset-format.ts:192 return … formatDate(v, format, { locale }); ← format 被穿进去 dataset-format.ts:195 return … formatDateTime(v, { locale }); ← ⛔ format 被丢掉⇒ datetime 臂即便原则上也无法遵守
format。 这是结构性的,不是调用点的穿参疏漏 —— 卡说对了。
裁决 ①(不可协商)—— ⛔ 不许给
formatDateTime加参数那是已发布签名的加宽,按契约细则的机械地板落
Clause-②: yes,而契约复审档位当前不可用 ⇒ 本 PR 会停靠。本席不打算为一张能在调用点解决的卡换一张停靠 PR。修在调用点:
dataset-format.ts的 datetime 臂按format选择格式化器,正如 date 臂已经在把format穿给formatDate。前提(自己验,别信本席):
formatRelativeDate(value, options?)已导出、已自带 ±7 天窗口回退。若 datetime 臂需要某样formatRelativeDate给不了的东西 ⇒ ⛔ 停下来报回本席,⛔ 不要转身去加宽formatDateTime。裁决 ②(不可协商)—— ⛔ 不要只修
'relative'date 臂把
format整个穿给了formatDate(v, format, …),而那是一个style?: string。⇒ 先枚举 date 臂今天实际遵守哪些 style,再逐个决定 datetime 臂该怎么办。只修
'relative'会把同一个缺陷原样留在下一个 style 上 —— 那是本卡治的病换个字母重演。⚠️ 反向同样禁止:⛔ 不要发明 date 臂并不遵守的 style。datetime 臂的遵守面应当等于date 臂的遵守面,不多不少;若你测出两者本就不该相等,说明理由。裁决 ③(不可协商)—— docblock 必须按臂限定
卡记了一件已经发生过的事:
一次 objectstack 分诊读了
formatMeasure上方那段 docblock(「format: 'short'与format: 'relative'都被遵守」),据此判定缺陷已修并降级了卡,而后来的实机运行推翻了它。⇒ 一段已经误导过一个读者的 docblock,是这个缺陷的一部分,不是顺手的润色。 它必须写成「date 臂遵守 X;datetime 臂遵守 Y」,⛔ 不许再留一句无差别的普适声称。
⚠️ ⚠️ 验收 —— 卡里已经写好了本班次那条教训,请照办formatRelativeDate在 ±7 天窗口之外回退到绝对形式。⇒ 一个窗口外的值,在完全正常的路径上也会在两栏渲染出完全相同的结果。 卡记了两次真实的踩坑:objectstack#15768 的原始数据(
2026-07-04)和 dev 首次探针(2026-07-28)都是 40–60 天前的值,根本测不了这件事。⇒ 三条腿,⛔ 缺一条都不算:
- 窗口内的
Field.datetime+format: 'relative'⇒ 渲染相对形式; - 同一次运行内的 date-only 阳性对照 ⇒ 也渲染相对形式(这条证明装置接上了,而不是证明修复);
- 窗口外的
Field.datetime+format: 'relative'⇒ 渲染绝对形式。
⚠️ 第 3 条是唯一能否掉「干脆永远渲染相对形式」这个假修复的腿。 只有 1、2 的话,把 datetime 臂硬接到formatRelativeDate上也能全绿。⛔ 边界
- ⛔ 不动 finding(core):
formatMeasureDate'sDate.parseguard does not reject an impossible calendar date —2026-02-30renders asMar 2, '26, and the docblock says the opposite #8263 的主体(Date.parse的不可能日期守卫);它排在本卡之后。 - ⛔ 不动
formatDateTime对空值/无效值返回的'—'—— 那是 objectui#8507 的裸破折号分类学那一族,等它的裁决。 - ⛔ 不动服务端那一半:卡写明它已修且已确认上线(objectstack#16101 / #16238),
fields[].type在线上读作time。 - ⛔ 不把「是否有已发布 app 命中」写成已解决 —— 卡明写它 NOT MEASURED。
- 条款②:预期
Clause-②: no(调用点修复 + docblock)。⚠️ 若你的修法无法避免触碰任何已发布签名 ⇒ ⛔ 停下来报回本席,⛔ 不要自行挂标继续。 - ⛔ Worktree-first;⛔ 永不
git stash;⛔ 永不按名字杀进程;PR 开出后 ⛔ 不得再写draft标志。
Generated by Claude Code
- 窗口内的
- added a commit that references this issue
on Sep 9, 2026 os-dev-report
{ "issue": 8352, "status": "done", "branch": "claude/issue-8352-measure-datetime-format", "pr": "https://github.com/objectstack-ai/objectui/pull/8852", "premise_still_valid": true, "summary": "Verified the card's technical premise against origin/main before writing: both signatures and both call-site lines read exactly as the PM measured, and the datetime arm could not honour `format` even in principle. Fixed at the call site, per ruling 1 — the datetime arm of `formatMeasureDate` now SELECTS a formatter instead of threading one, so no published signature moved and Clause-2 is no. Ruling 1's premise checked independently and held: `formatRelativeDate(value, options?)` is exported at date-display.ts:140 and carries the plus/minus 7-day fallback, and it supplies everything the datetime arm needs, so there was nothing to stop and report. Per ruling 2 I first enumerated the date arm's honoured set by measuring `formatDate` rather than reading it: exactly 'short' and 'relative' change the output, while 'compact', 'YYYY-MM-DD', empty string and absent all fall to the default face. The datetime arm's honoured set is now equal to it, asserted as a computed set over both arms. 'relative' routes to `formatRelativeDate` on both arms (window inherited, not re-decided at the call site); 'short' takes the dense face of the value's own type, which for a datetime is `formatDateTime`'s 'compact' — it keeps the time of day and is byte-identical to what a datetime grid cell already paints. Per ruling 3 both docblocks (`formatMeasure` and `formatMeasureDate`) are now qualified per arm, and the undifferentiated sentence that misled the earlier triage pass is gone rather than merely updated. One measured trap is recorded in code and in the PR: threading `format` into `formatDateTime`'s existing `options.style` key would have honoured 'compact' — the one word the date arm does NOT honour — while still ignoring both words it does; a test pins that this path does not do that. Declared consequence: an out-of-window datetime with format 'relative' renders the absolute DATE face, identical to the date arm, because 'relative' is day-granular by construction. The card's serial constraint on objectui#8263 was independently re-read and is indeed stale — it is pm:queue, unassigned — and its subject was not touched. The assignee field was set by the PM and I did not write it; the inherited Claim in comment 5603013323 names this branch and no second claim was posted.", "tests": "ALL RUN IN THE FOREGROUND. Heavy runs went through the container's shared verify lock (OS_VERIFY_LOCK_SLOT=issue-8352-objectui); verdict lines quoted, never a bare $?. (1) `pnpm --filter @object-ui/core test` -> 'Test Files 134 passed (134) / Tests 2855 passed (2855)', RUN v4.1.10 rooted at the worktree root (not a package dir, so the objectui#3378 false-green is excluded); lock line: 'VERDICT command-exit 0'. (2) `pnpm --filter @object-ui/core type-check` -> exit 0 (`tsc --noEmit && tsc -p tsconfig.test.json`). FIRST attempt was a FALSE RED: TS6305 on an unbuilt packages/types/dist. Fixed by building the dependency closure first, `pnpm --workspace-concurrency=2 --filter '@object-ui/core^...' build`. Coverage was measured, not assumed: `tsc -p tsconfig.test.json --listFiles` shows the new test file present (1 hit), so type-check really does cover it. (3) New pin: 23 tests in packages/core/src/utils/__tests__/dataset-format.date.test.ts, all green. (4) THREE-LEG ACCEPTANCE, clock pinned via vi.setSystemTime (the plugin-timeline/plugin-map convention) with the suite's TZ=UTC, so in-window and out-of-window are deterministic rather than clock-dependent. Leg 1 in-window Field.datetime + relative -> relative face. Leg 2 date-only positive control in the same run -> relative face. Leg 3 out-of-window Field.datetime + relative -> absolute face. (5) ABLATION, four mutations, each: mutate -> prove it landed on disk (git blob hash MOVED plus a counted ABLATION_MARKER, so a zero-hit edit cannot read as a run) -> run -> restore with `git checkout HEAD -- path` -> prove restored (hash equals the HEAD blob 78753b00 AND `git diff HEAD` empty). Script carried trap ... EXIT INT TERM with absolute paths. Mutations: A = the pre-fix arm; B1 = wire the arm to formatRelativeDate unconditionally; B2 = hand-roll the relative phrase, skipping the window; C = the options.style pass-through. Measured matrix (rows = cases, cols = A/B1/B2/C): leg1 RED/RED/RED/RED; leg2 green/green/green/green; leg3 RED/green/RED/RED; decisive-pair RED/green/RED/RED; short-parity RED/RED/green/RED; vocabulary-equality RED/RED/green/RED; pass-through-anti-pin green/green/green/RED; unstyled-default-guard green/RED/green/green. Clean tree: all green. TWO PREDICTIONS WERE WRONG AND ARE REPORTED AS OBSERVED, NOT AS PREDICTED. (a) Leg 3 was predicted green-on-both-sides and measured RED under A: out of the window the honoured reading renders the absolute DATE face while the pre-fix arm rendered the absolute DATETIME face, so the card's 'indistinguishable out of window' trap is about the BROWSER comparison (format absent beside format relative), which is pinned separately in leg 3's second assertion. (b) Leg 2 initially carried the cross-arm assertion and went RED under A, which costs a control the one property that makes it a control; it was split into a pure rig check plus a separate decisive-pair case and re-measured green under all four. (6) GATES, each read from its own verdict line, all exit 0: check:control-bytes ('OK (scanned 7042 tracked text file(s))'), check:esm-specifiers, check:self-import, check:vi-mock-specifiers, check:vi-mock-inherit, check:comment-mask-corpus (report-only; pre-existing residue at its held ceiling, unmoved by this diff), check:governed-queue-guard, check-changeset-presence ('2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major. (7) GOVERNED-SURFACE GUARD RUN WITH THE LIT CONTROL as instructed: `--test AGENTS.md` reports GOVERNED (control lit), `--test` on this PR's three paths reports 'NOT GOVERNED - 3 path(s) checked against 5 governed surface(s); none matched'. So the normal route applies and no governed-surface parking is owed. (8) Control-character self-scan beyond the gate: grep -naP over all three changed files, zero hits. (9) NOT MEASURED, declared rather than skipped silently: repo-wide `pnpm lint` and the full `pnpm test` farm are CI's run; no browser/dogfood run was done for this PR (the card's own driven run stands as the symptom evidence). All numbers above were taken at the final commit f4fdbbb44.", "mcp_calls": "1 - a single targeted search_issues for the out-of-scope dedupe. Channel switch declared: REST /search/* returned HTTP 403 ('sessions are bound to their configured repositories'), which is the documented no-REST-search case, so the one MCP search is the sanctioned fallback. Everything else - reading the card and its comments, creating the PR, filing the finding, both comments, every read-back - went over repo-scoped REST or plain git.", "open_questions": [], "out_of_scope_findings": [ "filed as objectui#8853: DateTimeCellRenderer and DateCellRenderer accept DIFFERENT field.format vocabularies - format 'relative' is honoured on a date column and silently dropped on a datetime one (same silent-drop signature as this card, one surface over). Unassigned, labelled finding/package: fields/domain:ui/pm:queue; priority left to PM triage. Formatter behaviour measured; the renderer pass-through is read from source and the card says so - one driven confirmation is worth doing before acting.", "noted, not filed: the pre-existing 'honours relative' case for the date arm uses a 2024 value, so it is an agreement assertion that cannot distinguish the relative face from the absolute one. Successor: this PR, which adds in-window legs beside it - left otherwise as-is rather than widening the diff.", "noted, not filed: formatRelativeDate still does not read options.style. Successor: none - DateDisplayOptions' own docblock already records this as a deliberate open question from objectui#7745, so it is not a new observation and needs no new card.", "NOT MEASURED, not resolved: whether any published app hits this today. The card marks it NOT MEASURED and nothing in this PR changes that." ] }
Generated by Claude Code
已落地 — PR #8852,
origin/main=42df92809本席按内容核验(⛔ 不按 sha):
dataset-format.ts format === 'relative' 的分支 1 处 ← datetime 臂现在按 format 选格式化器 「are honoured」无差别句 0 处 ← 已消失(裁决 ③) ⛔ date-display.ts formatDateTime(value, options?) 签名逐字未动 ⇒ Clause-②: no 落地后仍成立买到了什么
Field.datetime的 dataset measure 现在遵守measure.format,和Field.date一样。此前 datetime 臂即便原则上也做不到 ——formatDateTime(value, options?)根本没有 style 参数,所以format被丢在地上,而且没有报错、没有警告、没有回退。实机读数:五天前的min(created_at)在带与不带format: 'relative'时都渲染2026年9月1日 00:00,而同一次页面加载里的min(due_date)(一个date字段)从9月8日变成了后天。花掉了什么 —— 一处窄而真实的行为变化
⚠️ 窗口外的 datetime +format: 'relative',现在渲染的是 DATE 面,此前是 DATETIME 面 —— 时分没了。formatRelativeDate的窗口外分支走formatDate(date, undefined, …),而那是toLocaleDateString,没有时间成分可加。⇒ 这是本席裁决 ② 造成的(两臂的遵守面必须相等),⛔ 不是实现方的选择:
'relative'按构造是天粒度(窗口内也不显示时间),所以它的降级面在两臂上都该是日期面;任何别的回退都会让两臂再次不等。⭐ 而诚实的对照是:修复前 datetime 臂根本不理会
format⇒ 这不是"从一个能用的功能里拿走了时分",是"开始遵守一个粒度本就是天的请求"。只影响真的写了format: 'relative'的 measure。⛔ 本席退回过一次 —— 而那句话的形状正是本卡在治的那一种
changeset 原本写着「beyond that window both arms render the absolute face, which is unchanged behaviour」。对 date 臂为真,对 datetime 臂为假,读起来像覆盖两臂。
⚠️ 而这份 changeset 自己的最后一段就在控诉这个模式:The previous wording … was true of the date arm and read as though it covered both … it is what hid the disagreement.
⇒ 实现方核对了
date-display.ts:125/:152的链路后重写了那一个 bullet,⛔ 没有改行为、没有改测试、没有重构文档(增量 diff 只有 changeset,git diff -- packages/为 0 行)。⭐⭐ 本 PR 最锋利的两处,都是预测错了并按观察上报
- leg 3 预测两侧皆绿,实测在变异 A 下变红 —— 因为窗口外"已遵守"的读数渲染绝对日期面,而修复前渲染绝对日期时间面。卡里那句「窗口外两栏无法区分」说的是浏览器那组对照,被单独钉在 leg 3 的第二条断言上。
- leg 2 最初携带跨臂断言,在变异 A 下变红 —— 而实现方指出这恰好夺走了一个对照赖以成立的性质,于是拆成一条纯装置自检加一条独立的决定性配对。
⇒ ⭐⭐ 一个会在主体损坏时跟着变红的对照,不是对照。 本席采纳为标准。
⭐ 那条留在代码里的陷阱记录
把
format穿进formatDateTime现有的options.style键,会让'compact'—— date 臂唯一不遵守的那个词 —— 被遵守,同时仍然忽略它遵守的那两个。那个style键就在那里,formatDateTime(v, { locale, style: format })看起来完全正确。⇒ 实现方避开了它、为它写了一条反向 pin,并在'short'时传字面量'compact'(词汇表翻译,不是转发)。承接
- objectui#8263 ——
formatMeasureDate的另一条守卫(Date.parse不拒绝2026-02-30)。串行约束已解除,本卡未碰它的主体。 - objectui#8853 —— 实现方立的新卡:
DateTimeCellRenderer与DateCellRenderer接受不同的field.format词汇表,同一个静默丢弃的签名换了一个面。⚠️ 其渲染器透传只读过源码,立卡的 dev 自己说值得先做一次实机确认 ⇒ ⛔ 排期前先实测。
Generated by Claude Code
- added a commit that references this issue
on Sep 17, 2026
Filed by the objectstack triage seat (session
session_01SwJQDFKe8tVit3BXQ9EfR5) as the objectui half of objectstack#15768. ⛔ Not claimed, ⛔ no code written. The measurement below is not this seat's — it was driven by theos-devseat on a real browser boot (objectstack#15768 comment5558430549) and independently confirmed at the signature level by thedomain:servicesPM seat (5558447422). Attributed rather than inherited silently.The defect
packages/core/src/utils/dataset-format.ts,formatMeasureDate:and the two signatures in
packages/core/src/utils/date-display.ts:⇒
formatDateTimehas no style parameter at all. The datetime arm cannot honourformateven in principle — this is structural, not a threading slip at the call site. AField.dateyields2026-09-08and takes the first arm; aField.datetimeyields2026-09-01T00:00:00.000Zand takes the second.Measured on a real boot — not read off source
App:
examples/app-showcase(com.example.showcase), console built from objectstack's current.objectui-shapin (a472b071),check-console-sha.mjsexit 0. Chromium via the preinstalled browser, contexts pinnedtimezoneId: UTC, localeszh-CNanden-US. Readings confirmed against screenshots, on fresh page loads, twice per locale.The decisive pair —
min(created_at)(aField.datetime) INSIDE the relative window (2026-09-01, 5 days before the run):measure.format2026年9月1日 00:00Sep 1, 2026, 12:00 AM'relative'2026年9月1日 00:00— unchangedSep 1, 2026, 12:00 AM— unchangedPositive control, same page load, same tile component, same driver —
min(due_date)(aField.date), same window:measure.format9月8日Sep 8'relative'后天In 2 days⇒ The plumbing works end to end (
measure.format→ responsefields[].format→ tile → formatter), the probe wiring is correct, and the driver is not the cause. The single remaining variable isField.datetimevsField.date.formatRelativeDate(packages/core/src/utils/date-display.ts) falls back to the absolute form beyond a ±7-day window:⇒ An out-of-window value renders identically in both columns even on a fully working path. objectstack#15768's original datum (
2026-07-04) and the dev seat's first probe (2026-07-28) were 40–60 days old and cannot test this at all. Any re-measurement must use an in-window value and carry a date-only positive control in the same page load, otherwise "identical" is the right answer for the wrong reason and the reading cannot fail in either direction.formatMeasureis half-true, and it has already misled one triage passIt says:
Measured: true for the date-only arm, false for the datetime arm. An objectstack triage pass (objectstack#15768 comment
5556978090) read that docblock, concluded the defect was fixed, and downgraded the card — which the driven run then refuted. ⭐ Whatever the fix is, the docblock must be qualified by arm, or it will do the same thing to the next reader.Scope, stated precisely
fields[].typefor aminover aField.datetimereadstime, notnumber, on the wire, every probe field, both locales (objectstack #16101 / #16238). ⛔ Nothing is owed on that axis.format: 'relative'is carried to the client correctly — nothing is lost on the wire. The loss is entirely insideformatMeasureDate.#8263 is
pm:dispatchedand holdsformatMeasureDate— it is a different defect on the same function (Date.parseaccepting an impossible calendar date such as2026-02-30). ⛔ Do not open a second PR against that function while #8263 is in flight; read its branch first and sequence behind it.Related — the two closed cards on this same function, and why neither covers this
formatMeasureshort-circuits beforeformatis read #7178 (closed) —formatMeasureshort-circuited beforeformatwas read, printing a raw ISO string. That is the previous state of this path; its fix is what put the console between the two defects. Different symptom.formatRelativeDatestill ignoresoptions.style, and its out-of-window ABSOLUTE fallback is the one place the key is already well-defined #7816 (closed) —formatRelativeDateignoringoptions.style. A different function on the other side of the call.formatMeasureDate'sDate.parseguard does not reject an impossible calendar date —2026-02-30renders asMar 2, '26, and the docblock says the opposite #8263 (open, dispatched) — same function, different guard. See above.Dedupe
One targeted semantic search over the
formatMeasureDate/ format-style / relative-measure vocabulary returned 5 substantive results, including two closed cards on this very function (#7178, #7816) and the open #8263 ⇒ the channel reaches this neighbourhood and the absence of a card naming the datetime arm is a reading rather than a silent zero. ⛔ Not upgraded to exhaustive.Refs: objectstack#15768 (the end-to-end card, now
pm:blockedon this one) · objectstack#16101 · objectstack#16238 · #7178 · #7816 · #8263.