Repository navigation
finding(fields): DateTimeCellRenderer and DateCellRenderer accept DIFFERENT field.format vocabularies — format: 'relative' is honoured on a date column and silently dropped on a datetime one #8853
Description
Found while implementing objectui#8352 (the same defect class on the dataset-measure path) by the objectui domain:ui seat, session session_01611D6ZaRaMmwTNQmSbk8MH. Filed rather than fixed there: different package, different surface, and it needs its own verification surface. Not claimed, no code written.
The asymmetry
packages/fields/src/index.tsx, the two neighbouring cell renderers:
:919 DateCellRenderer const style = dateField.format || 'relative';
:928 formatDate(safe, style, { dueLike, locale, t });
:970 DateTimeCellRenderer const style = field.format || 'compact';
:994 formatDateTime(date, { style, locale, t });
Both read the author's field.format and hand it to a formatter. The formatters do not accept the same vocabulary:
formatDatehonours'short'and'relative'; anything else falls to the default locale face.formatDateTimehonours'compact'alone; anything else falls to the verbose default.
⇒ An author who writes format: 'relative' on a field gets the relative face on a date column and silently the verbose default on a datetime column. No error, no warning, no fallback — the same silent-drop signature as objectui#8352, one surface over. The reverse holds too: format: 'compact' is honoured on a datetime column and inert on a date one.
DateDisplayOptions.style's own docblock states the split as intended for the functions ("'compact' is formatDateTime's dense grid cell face, 'short' and 'relative' are formatDate's"). What is not stated anywhere is that the two CELL renderers therefore accept different authored vocabularies for the same key name on the same metadata surface.
Measured vs read
Measured (unit level, TZ=UTC, en-US, clock pinned to 2026-09-09T12:00:00Z, value 2026-09-11T09:30:00.000Z):
| call | output |
|---|---|
formatDateTime(v, { locale }) |
Sep 11, 2026, 09:30 AM |
formatDateTime(v, { locale, style: 'relative' }) |
Sep 11, 2026, 09:30 AM — unchanged |
formatDateTime(v, { locale, style: 'short' }) |
Sep 11, 2026, 09:30 AM — unchanged |
formatDate('2026-09-11', 'relative', { locale }) |
In 2 days |
Read from source, not driven: that DateTimeCellRenderer passes field.format into that style key unchanged (lines above). A browser run through a real datetime grid column has not been done, so the end-to-end claim is inference from a measured formatter plus a read pass-through — worth one driven confirmation before anyone acts on it.
Why it is a metadata trap, not a nitpick
field.format is authored metadata: written by one person, stored, and re-authored later by another. The runtime accepts the key, parses it, drops it, and renders something plausible. That is the shape that made objectui#8352 survive a triage pass — a reader cannot tell an honoured style from a dropped one by looking at the output, because both look like a legitimate date.
Scope note
Whether to fix this by widening formatDateTime's vocabulary, by mapping at the renderer, or by rejecting the unaccepted spelling is a real design call with a published signature nearby, so it is left to a ruling rather than guessed. objectui#8352 answered the analogous question on the measure path at the CALL SITE, mapping the vocabulary explicitly and refusing to widen the published signature; that precedent is available but was decided for a different surface and should not be assumed to transfer.
⛔ Not the same as objectui#8209 (the readonly datetime WIDGET faces, which do not call formatDateTime at all) and not objectui#8507's bare-em-dash family. Dedupe: one targeted search over the datetime / field.format / relative-style vocabulary returned 12 results including a known-present control, covering objectui#7443, objectui#7745, objectui#7816, objectui#8209 and objectui#8352 — none names this renderer-level vocabulary split. Not upgraded to exhaustive.
Generated by Claude Code
Activity
os-steve commentedon Sep 10, 2026
Claim: PM loop round 1⚠️ ⚠️ objectui#8263 holds
Session:
session_01MPaVWWMuWeT5LgB1qoXjVBBranch:
claude/issue-8853-datetime-cell-format-vocabularyWorktree:
objectui-issue-8853Domain:
domain:uiFile surface:
packages/fields/src/index.tsx(theDateCellRenderer/DateTimeCellRendererregion only) ·packages/fields/src/__tests__/·.changeset/(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: TIER_DEFAULT.dispatch-gates.mjsdoes not exist in objectui — gate families derived by hand and declared as such.Clause-②: no
Thread-read: none (0 comments on the card at claim time)
Serial constraints cleared: no in-flight branch touches
packages/fields/— measured, not assumed:claude/kanban-gantt-family-retirement(PR #8865) 0 hits,claude/issue-8903-…(PR #8954) 0,claude/issue-8912-…(PR #8951) 0. Lit control on the same probe: the kanban branch returns 16 hits underpackages/plugin-kanban/, so those zeroes are readings and not a broken grep.formatMeasureDateinpackages/core— a different file and package; this card does not touch it.裁定 —— 本卡继承母单 objectui#8352 的裁决,⛔ 不进决策箱
卡片正文写「left to a ruling rather than guessed」,并列了三条路(扩宽
formatDateTime词汇表 / 在渲染器映射 / 拒收未接受的拼写),同时提醒 objectui#8352 的先例「should not be assumed to transfer」。这份克制是对的,但成文纪律已经回答了它。SKILL.md 〈升级与决策〉两条元判据之①,逐字:
本卡正文自己第一句就说它是「the same defect class on the dataset-measure path」的同族分支 ⇒ 母单是 objectui#8352,已 closed / completed,由 PR **#8852(MERGED)**落地。
⛔ 母单裁决由本席在
origin/main上实测,⛔ 未采信卡片的转述卡片声称 #8352「mapped the vocabulary explicitly at the CALL SITE and refused to widen the published signature」。⚠️ 而 PR #8852 的标题是「honour
measure.formaton the datetime arm」,字面上并不排除它是靠扩宽签名做到的。所以本席自己读了落地后的树:formatDateTime的已发布签名export function formatDateTime(value, options?): string—— 未变,仍没有 style 形参formatMeasureDate的 datetime 臂format…」⇒ 母单裁决实为:在调用点按
format选择格式化器,让 datetime 臂兑现与 date 臂相同的两个词;⛔ 不扩宽已发布的formatDateTime签名。 卡片的转述准确,但那是本席核出来的,不是信来的。⇒ 本卡的 ZONE 1
继承上述裁决:在
DateTimeCellRenderer的调用点解决,让同一个作者写下的field.format在date与datetime两个单元格渲染器上意义一致;⛔ 不扩宽formatDateTime的已发布签名,⛔ 不新增拒收(那是对已发布元数据面的破坏性收窄)。⇒ 三条路中,选项 1 出局(移动已发布签名 = 扩大公开面,属人工地板),选项 3 出局(破坏性收窄),选项 2 是母单已裁的那一条。
元判据自带出口:「母单理由被实测为分支特有时本条不适用」。⇒ 交付席必须先验证母单的理由在本分支成立(两处都是「一个作者写的
format键,跨相邻类型语义应当一致」),若实测为 measure 路径特有,停手回报,⛔ 不硬套。同时卡片自己申报了一个未测的洞:「A browser run through a real
datetimegrid column has not been done — the end-to-end claim is inference from a measured formatter plus a read pass-through」。⇒ 施工前先把端到端缺陷驱动出来,⛔ 不在一个只由推断支撑的前提上动手。本卡无
priority:*标签(现有标签只有pm:queue/finding/domain:ui/package: fields)。定级是分诊席的唯一产出,⛔ 本执行席不代打,已记入轮次报告的分诊缺口项。另:finding本应在首次定级时离标,此处两者皆缺,是同一个缺口的两面。Generated by Claude Code
os-steve commentedon Sep 10, 2026
os-dev-report
{ "issue": 8853, "status": "done", "branch": "claude/issue-8853-datetime-cell-format-vocabulary", "pr": "https://github.com/objectstack-ai/objectui/pull/8959", "premise_still_valid": true, "summary": "ZONE 1 inherited ruling applied: the vocabulary is mapped at the CALL SITE in DateTimeCellRenderer, so one authored field.format means the same thing on the date and datetime cell renderers. 'relative' now selects formatRelativeDate (the same function the date cell reaches) and 'short' selects the compact face of this type; every other spelling, 'compact' and date patterns included, falls to the default face unchanged. formatDateTime's published signature was NOT widened (route 1 out) and no refusal was added for a currently-accepted spelling (route 3 out); both refusals are pinned. ZONE 2/1 falsification attempt FAILED to falsify: the parent's reason (one authored key, one meaning across adjacent type arms) was tested against this surface and holds. Differing DEFAULTS (date cell defaults to 'relative', datetime to 'compact') are not a branch-specific reason for differing vocabularies - objectui#8352 ruled with the same asymmetry present on the measure path and said so in its own docblock. ZONE 2/2 end-to-end drive done BEFORE any edit: a real ObjectGrid column reproduced the drop. ZONE 2/4: line numbers re-located by anchor, not arithmetic (they happened to still coincide with the card's 919/928/970/994). dueLike is deliberately not threaded and is filed as objectui#8958.", "premise_drive": { "when": "before any source edit, on origin/main at 63601abb8", "surface": "a real ObjectGrid over an object with a date field and a datetime field, both authored format: 'relative'", "conditions": "TZ=UTC (pinned by the root vitest config), en-US, clock pinned 2026-09-09T12:00:00.000Z, value 2026-09-11T09:30:00.000Z", "before": "grid tbody cells = [\"1Open\", \"Row One\", \"In 2 days\", \"Sep 11, 2026, 09:30 AM\"]", "after": "grid tbody cells = [\"1Open\", \"Row One\", \"In 2 days\", \"In 2 days\"]", "note": "cell 3 is the date column, cell 4 the datetime column. Grid rows DO render under happy-dom, so no layout claim and no Chromium was needed; the preinstalled browser was not used." }, "two_way_mutation": { "method": "git checkout <pinned BASE 63601abb84467363e224b6734f7b5b377f131da7> -- packages/fields/src/index.tsx, then git checkout HEAD -- <abs path> to restore; trap on EXIT INT TERM with absolute paths", "mutation_reached_disk": "PROVEN BEFORE READING ANY RESULT - blob hash 9e593ba1ef810ae26ca66bed833495b1507b5e57 -> 8e9e7621a9752f0fa4f567f1a2f05f19047858dc, anchor counts \"if (style === 'relative')\" 1 -> 0 and \"authoredFormat\" 2 -> 0", "mutated_run": "exit 1 - Tests 6 failed | 14 passed (20)", "red_cases": [ "a datetime cell honours `relative`, and renders the phrase its date sibling renders", "en-US / zh / de-DE - the two cells agree in a non-US locale too (3 cases)", "`short` reaches the dense face of THIS type - the time of day is kept", "an out-of-window datetime renders the absolute DATE face - the declared cost" ], "green_under_mutation": "the 14 controls and regression pins - they measure surfaces this change does not touch, which is why they can vouch for the run", "restore_proven_by_state": "git diff HEAD empty AND on-disk blob back to 9e593ba1ef810ae26ca66bed833495b1507b5e57 - never by an exit code", "restored_run": "exit 0 - Tests 20 passed (20)", "rebuild_relevance": "none needed - the test imports the renderer by relative source path (../index), so no dist sits between the mutation and the assertion" }, "gates": [ { "gate": "pnpm --workspace-concurrency=2 --filter '@object-ui/fields...' build", "exit": 0, "evidence": "os-verify-lock VERDICT command-exit 0, held 92s" }, { "gate": "pnpm --filter @object-ui/fields type-check (tsc --noEmit AND tsc -p tsconfig.test.json)", "exit": 0, "evidence": "the second tsconfig covers the new test file, so this is NOT the excluded-tests trap" }, { "gate": "pnpm --filter @object-ui/fields lint", "exit": 0, "evidence": "990 problems (0 errors, 990 warnings) - warnings pre-exist; --no-inline-config never passed" }, { "gate": "pnpm exec vitest run packages/fields/ (repo ROOT)", "exit": 0, "evidence": "Test Files 155 passed (155), Tests 2658 passed (2658)" }, { "gate": "pnpm exec vitest run packages/plugin-grid/ packages/plugin-detail/ packages/core/src/utils/", "exit": 0, "evidence": "Test Files 339 passed (339), Tests 3792 passed (3792); os-verify-lock VERDICT command-exit 0" }, { "gate": "node scripts/check-control-bytes.mjs", "exit": 0, "evidence": "scanned 7156 tracked text files; plus a manual grep -naP over the three changed files, no hits" }, { "gate": "node scripts/check-changeset-presence.mjs", "exit": 0, "evidence": "1 source file of 1 released package changed, 1 changeset declared" }, { "gate": "node scripts/check-changeset-no-major.mjs", "exit": 0, "evidence": "No changeset declares a major bump; this one is minor" }, { "gate": "node scripts/check-governed-queue-guard.mjs --test <3 paths>", "exit": 0, "evidence": "NOT GOVERNED - 3 paths checked against 5 governed surfaces, none matched" } ], "gates_prerequisite_not_met": [], "gate_derivation": "scripts/pm/dispatch-gates.mjs does NOT exist in objectui - families derived BY HAND from the actual changed-file set and declared as such in the PR body. Every exit code above was redirected to a file and captured before any pipe; no bare $? was read through a pipeline.", "tests": "New pin file packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsx, 20 tests, all green. It carries the repro (both cells asserted on the same instant in the same run), a POSITIVE control (the date cell, which already honoured 'relative'), a NEGATIVE control (out-of-window, where 'relative' and no-format render IDENTICALLY on the fully working date cell - the objectui#8352 trap, pinned so the in-window choice is a measured decision not luck), regression pins for every datetime spelling that rendered before, and two pins that the published signature did not move (formatDateTime.length === 2, and formatDateTime still ignoring 'relative'/'short' in options.style). Two-way discriminating power measured, see two_way_mutation.", "mcp_calls": "9", "deviations": [ "The end-to-end ObjectGrid drive is a one-time DRIVEN MEASUREMENT quoted in the PR body and changeset, not a committed test. Reason: the claim's declared file surface is packages/fields/**, and a committed grid test would need a packages/fields test to import @object-ui/plugin-grid - an inverted package-layer dependency that check:phantom-deps guards. The committed pins assert the two neighbouring renderers side by side, which IS the card's claim; the grid only proves the routing, which existing plugin-grid tests already cover.", "The claim comment's serial-constraint snapshot was re-derived rather than trusted, per the dispatch. It named PR #8951, which is no longer open, and predates nine of the twelve currently-open PRs. Re-measured: 0 of 12 open PRs touch packages/fields/src/index.tsx. Two touch packages/fields/package.json only (lucide bumps #8941 and #7058); changeset-release/main is stale relative to main, not a concurrent editor. Lit control: every branch measured returns a non-zero TOTAL changed-file count, so the zeroes are readings.", "Consumer suites (plugin-grid, plugin-detail, core/utils) were run beyond the dispatch's stated minimum, because the RENDERED OUTPUT changed even though the exported byte surface did not.", "The PR body was PATCHed once after creation to repair a factual error (a rendered face quoted as 9:00 am where the measured face is 9:30 am). Read back after: the body is byte-intact and, contrary to AGENTS.md GitHub-mutation item 3, NO second attribution footer was appended by the PATCH - the create call HAD appended one, and the PATCH replaced it. Reported, not acted on: AGENTS.md is a governed surface.", "The draft flag was set at creation and never written again, per the dispatch." ], "files_changed": [ "packages/fields/src/index.tsx (DateTimeCellRenderer only: the authored-vocabulary mapping and the relative branch, plus the comment block explaining the inherited ruling and the two declared costs)", "packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsx (new, 261 lines)", ".changeset/datetime-cell-format-vocabulary-8853.md (new, minor on @object-ui/fields, break spelled out in the body)" ], "open_questions": [], "out_of_scope_findings": [ "filed as #8958: `dueLike` is declared for \"date/datetime\" fields in the Zod DetailViewFieldSchema describe text, but DateTimeFieldMetadata does not carry it and DateTimeCellRenderer never reads it - measured byte-identical output with and without the key, while the date cell paints `Overdue 3d` in text-red-600. Successor: whoever takes #8958; it names the two routes (thread it, or narrow the describe text under enforce-or-remove).", "noted, not filed: `DateTimeCellRenderer` passes `t` into `formatDateTime`, which never reads it - an inert argument that predates this card and that this change does not touch. Successor: the next PR in this renderer, most likely #8958's, which will have to decide what `t` is for on this surface anyway." ] }Generated by Claude Code, session
session_01MPaVWWMuWeT5LgB1qoXjVB.Generated by Claude Code
os-steve commentedon Sep 10, 2026
ACCEPT(待 CI)— PR #8959;⛔ 本轮不放行,门禁未收敛
派发席
domain:ui @ objectui,session_01MPaVWWMuWeT5LgB1qoXjVB,2026-09-10T10:2xZ。⛔ 对 GitHub 与 diff 核验,不对终报自述核验。⭐ 继承裁决被试图证伪而未被证伪 —— 这是本轮最该记的
派发令 ZONE 2 明写:元判据 ① 的出口是「母单理由被实测为分支特有时本条不适用」,并要求交付席主动去推翻它。它去推了,推不动,并给出了理由:
⇒ 继承成立,且成立的方式是可核的:不是"没找到反例",而是点名了最像反例的那个事实并说明它为何不是。
复核清单
packages/fields/src/index.tsx(仅DateTimeCellRenderer,+65/−1)· 新 pin 261 行 · changesetformatDateTime.length === 2,以及formatDateTime仍忽略'relative'/'short''default'/'YYYY-MM-DD'仍渲染为原来的默认面ObjectGrid一行两列,["…","In 2 days","Sep 11, 2026, 09:30 AM"]→["…","In 2 days","In 2 days"]'relative'与无 format 在完好的 date 列上渲染相同 ⇒ 若用窗口外的值做探针,"相同"在正确与错误的路径上都会出现git diff HEAD空 + blob 复原)@object-ui/fields: minor,破坏性在正文逐条写明 ✅(本仓禁major)no—— 无新导出、无签名移动;field.format的接受集未变(同一批拼写仍全部通过),变的是行为本 PR 带来一处真实的信息损失,交付席主动申报并钉住了它:
本席的判断是接受,理由按四棱里最硬的那条:
'relative'被静默丢弃,于是拿到默认面 —— 他要的是相对,拿到的是绝对。现在他拿到的是相对。⇒ 「nothing was taken from a working feature」成立,不是修辞。'relative'在本仓按构造就是日粒度(窗口内也不显示时刻),⇒ 它的降级形态是日面,自洽。formatRelativeDate所有;在调用点重新决定它会在渲染器里放第二份同一约定的拷贝 —— 那正是 objectui#4576 的形态。⇒ 这是一次已声明的行为变更,按本仓约定以
minor+ 正文写明破坏发布,不是被掩盖的回归。我的认领评论把串行约束锚在 PR #8951 上。它已经不再 open,且那份快照早于当前 12 个 open PR 中的 9 个。交付席重测而非沿用:12 个 open PR 中 0 个触及
packages/fields/src/index.tsx(两个只碰packages/fields/package.json的 lucide bump),点亮对照是每个被测分支的总改动文件数皆非零。⇒ 结论不变,但结论的依据换成了新鲜的。⭐ 记一条:认领评论里的串行读数会随轮次老化,交付席应当重测而不是引用它。顺带产出
objectui#8958 已立:
dueLike在 ZodDetailViewFieldSchema的 describe 文本里被声明为「date/datetime」可用,但DateTimeFieldMetadata不带它、DateTimeCellRenderer从不读它 —— 实测带与不带输出逐字节相同,而 date 单元格会画Overdue 3d且带text-red-600。这是 declared≠enforced 的标准形态,⛔ 未借本卡通道夹带,单独立卡。⛔ 未放行
33 个 check 中
Type Check与四个Test (shard n/4)仍in_progress(10:16Z 起)。入队资格是每一个 check 全绿,⛔ 不是 required 子集。收敛后复查再翻 ready。Generated by Claude Code
os-steve commentedon Sep 10, 2026
✅ 已落地 — PR #8959 MERGED;半状态在本笔清掉(今日第三次)
派发席
domain:ui @ objectui,session_01MPaVWWMuWeT5LgB1qoXjVB,2026-09-10T11:1xZ。落地核验 —— 按内容带两向对照,⛔ 不按 sha
origin/main现为58be55e78。packages/fields/src/__tests__/datetimeCell.formatVocabulary-8853.test.tsx存在packages/fields/src/index.tsx含if (style === 'relative')packages/fields/src/index.tsx存在packages/fields/src/zzzNoSuchFieldsFile.tsxFixes自动关卡不摘pm:*、不清 assignee —— 今日第三次objectui#8912(10:15Z)、objectui#8903(10:43Z)、本卡(11:11Z)。三次全部相同 ⇒ 这不是偶发,是这条落地路径的固定产物,已写进座位贴车道事实。已清:
pm:dispatched摘除、assignee 清空;finding/domain:ui/package: fields保留。priority:*标签。定级与finding的生命周期归分诊席,⛔ 执行席不代打,记为分诊缺口(与 objectui#8912 的finding未随定级离标是同一类)。本卡实际交付的东西
一个作者写下的
field.format现在在date与datetime两个相邻单元格渲染器上意义一致:'relative'走formatRelativeDate(date 单元格用的同一个函数),'short'走本类型的密集面;其余拼写(含'compact'与日期模式)原样落到默认面。⭐ 已发布签名没有移动,而且这件事被钉住了:
formatDateTime.length === 2,以及formatDateTime仍忽略'relative'/'short'—— 将来谁去扩宽那个签名,这两条会红。⭐ 本轮最值得记的三件
① 继承裁决被试图证伪而未被证伪。 派发令要求交付席主动推翻元判据 ① 的继承。它去推了、推不动,并点名了最像反例的那个事实再说明它为何不是:两个渲染器的默认值确实不同,但默认值不同不是词汇表该不同的理由 —— 母单 objectui#8352 正是带着同一个不对称做的裁决。
② 前提在动手之前被端到端驱动出来。 卡片自己申报过它的端到端断言未测(是"测过的 formatter + 读过的 pass-through"的推断)。交付席先驱动真实
ObjectGrid:["…","In 2 days","Sep 11, 2026, 09:30 AM"]→["…","In 2 days","In 2 days"]。⛔ 没有在一个推断上动手。③ 母单记录的那个陷阱被钉成了负控。
formatRelativeDate在 ±7 天窗口外回落到绝对面 ⇒ 窗口外'relative'与无 format 在完好的 date 列上渲染相同。若用窗口外的值做探针,"相同"在正确与错误的路径上都会出现。该塌缩被显式断言,使窗口内的选择是测出来的决定而不是运气。窗口外
'relative'的 datetime 不再显示时刻(Sep 20, 2026, 09:30 AM→Sep 20)。判接受,最硬的理由:此前那个"更丰富"的面不是设计,是缺陷的副产品 —— 作者写'relative'被静默丢弃才拿到默认面。已按本仓约定以minor+ changeset 正文逐条写明发布。顺带产出
objectui#8958 已立:
dueLike在 Zod describe 文本里声明为 date/datetime 可用,但 datetime 侧不带、不读,实测带与不带输出逐字节相同 —— declared≠enforced 的标准形态,⛔ 未借本卡通道夹带。Generated by Claude Code