Repository navigation
datetime has two display conventions: DateTimeCellRenderer inlines its own and never calls formatDateTime #7443
Description
Found while implementing #7178 (routing a date-valued dataset measure through the list-cell date path). Source-confirmed on main at 6411def25. Filing unassigned — recording, not claiming.
The claim
For the date field type there is one display convention. For datetime there are two, and the cell renderer uses the one that is not the module's own function.
datecells →DateCellRenderer→formatDate(value, field.format || 'relative', { dueLike, locale, t }). One path, shared, andfield.formatis read as a display style.datetimecells →DateTimeCellRenderer→ does not callformatDateTime. It inlines its own pair:
date.toLocaleDateString(locale, { month: 'numeric', day: 'numeric', year: 'numeric' })
date.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase()
Meanwhile formatDateTime — exported from the same barrel, right beside formatDate — renders:
date.toLocaleDateString(locale, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
Measured for 2024-07-04T07:00:00.000Z in en-US:
| path | renders |
|---|---|
DateTimeCellRenderer (the datetime cell) |
7/4/2024 7:00 am |
formatDateTime (the module's datetime function) |
Jul 4, 2024, 07:00 AM |
Both are locale-correct. They are simply two conventions for one field type. DateTimeCellRenderer also never reads field.format, so the style vocabulary a date field gets is unavailable to a datetime field.
Why this is worth a card
It is the same shape as objectui#4576, which is the precedent this repo already paid for once: sharing the scaling of percent was not enough while the convention lived in two places, and a German session read 1.234,5 % in a list cell beside 1.234,5% in a dashboard measure. Here the split is between two cells of adjacent field types rather than between a cell and a measure, but the mechanism is identical — two Intl option bags for one concept, kept in step by nothing.
There is a third spelling in the same family: formatCellValue in packages/components/src/renderers/complex/data-table.tsx sniffs ISO strings and builds its own Intl.DateTimeFormat with { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' } — close to formatDateTime but independently authored.
Consequence already visible
#7442 (the #7178 fix) routes a datetime-shaped measure through formatDateTime, following ObjectGantt.tsx's sniffed-ISO dispatch, which is the repo's existing answer for an ISO string arriving with no field type attached. That is the best available target, but it means a datetime measure on a dashboard tile and a datetime cell in a list render the same instant in two different conventions. The ruling for #7178 asked for "the same path a list cell uses for that field"; for date that is exact, and for datetime it cannot currently be exact, because the datetime cell does not use the shared function.
Suggested direction
Not obvious, and deliberately not chosen here — the compact 7/4/2024 7:00 am face may well be the intended design for a dense grid cell rather than an accident (the code says as much about hour12). Two candidate shapes:
DateTimeCellRenderercallsformatDateTimeand the compact face becomes a named style on it, the way'short'is a named style onformatDate. One convention, one home, andfield.formatbecomes readable fordatetimetoo.- The compact face is declared to be the datetime cell convention and
formatDateTimeis documented as the non-cell one, so the divergence is intentional and written down rather than silent.
Either closes it; leaving it undocumented is the part that reads as a latent #4576.
Grading
Observation-class. Nothing is wrong on screen today — both faces are locale-correct and legible — and no data is lost or misread. The reusable half is the undocumented second convention, not a user-facing defect.
Note that packages/plugin-list/src/ListView.tsx and packages/components/src/renderers/complex/data-table.tsx were held by other in-flight PRs when this was filed.
Activity
huangyiirene commentedon Sep 3, 2026
Triage — R+125.
finding→needs-user-decision·priority:p3·domain:uikept. ⛔ Not claimed, not dispatched.os-decision-facets
Verification — tree
origin/mainf0f774b, read 2026-09-03T19:04:17ZConfirmed.
packages/fields/src/index.tsx:866—DateTimeCellRendererinlinestoLocaleDateString+toLocaleTimeStringwithhour12: trueand never callsformatDateTime. Control:formatDate(occurs once in the same file, soDateCellRendererdoes route through the shared function. The asymmetry is real.⭐ Sharper than the card states: the signature is
DateTimeCellRenderer({ value }: CellRendererProps)— it destructures onlyvalue. So "never readsfield.format" is not an omission in the body; the renderer has no access to the field at all. ⇒ Option 1 is not a one-line swap: it needs the parameter widened first, which is a signature change on an exported renderer.⭐ One fact the card does not have, and it narrows option 2 by half
The compact face is already annotated as deliberate, in the code, immediately above the
hour12line:⇒ Option 2 ("declare the compact face the cell convention") is partly already done. What is missing is the other half:
formatDateTimeis not documented as the non-cell convention, and the third spelling —formatCellValue's independently authoredIntl.DateTimeFormatincomponents/src/renderers/complex/data-table.tsx— is reconciled with neither.Lane checked, ⛔ not changed
domain:ui.DateTimeCellRenderer,formatDateTimeandformatDateall live inpackages/fields— a published library. Third card this session where re-verification confirmed the existing label (after #7519, #7201), ⛔ not a re-route.四棱
① 长期正确性(主导) —— 一个概念两套⚠️ 但这里有个 #4576 没有的复杂度:紧凑面可能是对的。密集网格里
Intloption bag、靠人力保持同步,正是 objectui#4576 已经付过一次学费的形态(德语会话里1.234,5 %与1.234,5%并排)。7/4/2024 7:00 am比Jul 4, 2024, 07:00 AM更省宽度,代码注释也已经这么说了。⇒ 本轴要求的不是「统一」,而是**「把哪一个是哪一个写下来」** —— 三处拼法各自成立却互不知情,才是长期成本。② 真实业务拉力 —— 弱但非零,且已兑现一次:#7442 把 datetime measure 路由到
formatDateTime,于是同一时刻在仪表盘 tile 与列表格里两种写法。#7178 的裁决要「列表格用的同一条路」,对date精确,对datetime做不到。③ 防 AI 编码错误 —— 第四个作者会照着最近的那份抄,而三份都「看起来对」。⛔ 注意:选项 1 会加重这一轴,除非同时把紧凑面变成
formatDateTime上的具名 style —— 否则紧凑面就成了没有归宿的第四种写法。④ 创业期不扩张 —— 选项 2 是纯散文,零风险;选项 1 改导出渲染器的签名,并改变产品里每一个 datetime 单元格的显示。面不变大,但用户可见面变了。
建议(一句):走 2 的补全形 —— 紧凑面已在代码里声明为有意,补上另一半(
formatDateTime是非单元格约定,data-table.tsx的第三处收敛到其中之一),⛔ 不改任何渲染结果。回退:若维护者认为
datetime单元格也该拿到date那套field.format风格词表,则走 1,并且必须同批把紧凑面做成具名 style —— ⛔ 不接受「改成formatDateTime然后紧凑面就没了」,那会静默改掉所有列表的日期显示。置信缺口(必填):我没有量产品里 datetime 单元格的实际密度与列宽,也没量有多少视图依赖紧凑面才不换行。若紧凑面其实并不省多少宽度,① 的「紧凑面可能是对的」这一前提就垮了,建议应从 2 翻向 1(统一到一种)。这一条足以翻转。
For whoever implements, either way
⛔ Do not leave the third spelling out of scope.
formatCellValueindata-table.tsxis independently authored and close-but-not-equal toformatDateTime; a fix that reconciles two of three leaves the same defect with one fewer instance. Stop-condition: if reconciling it turns out to changedata-table's rendering, ⛔ stop and report — that is a separate visible change and needs its own line in the ruling.Generated by Claude Code
os-warren commentedon Sep 4, 2026
Maintainer ruling recorded — 1′: one home for the
datetimeconvention —formatDateTimegains a named'compact'style (the current cell face, byte-identical), the cell calls it, the third spelling converges.needs-user-decision→pm:queuein the same stroke.Director seat, summon 14 (
session_01LsEjuNMPitCHwEfYftZ1um, GitHubos-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #29, verbatim 「同意」 on the presented recommendation (1′; fallback 2). Premises re-verified onorigin/main24e027eat 05:24Z:packages/fields/src/index.tsx:866DateTimeCellRenderer({ value }: CellRendererProps)inlinestoLocaleTimeStringwithhour12: true(:886-889, the "compact Airtable-style cell" comment at:883);data-table.tsx:770carries the independently authored third option bag.The ruling
formatDateTime(packages/fields, the module's own function) gains a named style'compact'whose output is byte-identical to today'sDateTimeCellRendererface (7/4/2024 7:00 aminen-US; thehour12note moves with it). The default style stays whatformatDateTimerenders today.DateTimeCellRendererreadsfieldfromCellRendererProps(already carried; it only destructuredvalue) and callsformatDateTime(value, field?.format ?? 'compact', { locale, t })— sofield.formatstarts working fordatetimethe way it does fordate, and every existing cell renders unchanged.formatCellValueincomponents/src/renderers/complex/data-table.tsxcallsformatDateTime(default style) instead of its ownIntl.DateTimeFormatbag. Its output is measured before and after; ⛔ if a pixel changes there, it is listed in the PR as a separate visible change with the before/after strings — not silently folded.Execution constraints, for the
domain:uiseat'compact'renders the exact former cell string for a fixed instant and locale (en-USand one non-US locale),datecell path untouched,data-tablebefore/after asserted.@object-ui/fieldsgains an accepted style value; no published contract key moves) — re-judged from the diff.packages/plugin-list/src/ListView.tsxanddata-table.tsxwere held by other PRs when this was filed — re-measure at claim time.Ruling authority: maintainer 2026-09-04 (batch #29). Related: #7178 / #7442 · #4576 (the precedent this repo paid for once).
Generated by Claude Code
claude commentedon Sep 4, 2026
os-dev-report
{ "issue": 7443, "status": "done", "branch": "claude/issue-7443-datetime-compact-style", "pr": "https://github.com/objectstack-ai/objectui/pull/7621", "premise_still_valid": true, "summary": "Ruling 1' implemented on all three items. formatDateTime gains a named 'compact' style that is today's DateTimeCellRenderer face byte-for-byte; DateTimeCellRenderer now reads field and selects field.format (it destructured value only, so datetime had no style vocabulary at all); data-table's formatCellValue calls formatDateTime for its datetime branch instead of a third independently authored Intl bag. ITEM 3 STOP-CONDITION: no pixel moved — before and after are identical in all three locales (en-US 'Jul 4, 2024, 07:00 AM' -> 'Jul 4, 2024, 07:00 AM'; zh '2024年7月4日 07:00' -> '2024年7月4日 07:00'; de-DE '4. Juli 2024, 07:00' -> '4. Juli 2024, 07:00'), because the bag formatCellValue built for itself is character-for-character the bag formatDateTime's default branch builds. The date-only branch of formatCellValue is deliberately NOT converged (formatDateTime always carries a time and formatDate's default drops the year in the current year, so either route WOULD move a pixel: 'Jul 4, 2026' -> 'Jul 4'); filed as #7620 instead. BREAKING, and it is the one thing to look at: the ruling's call shape formatDateTime(value, style, options) required a style slot the function did not have — it was (value, options?), one parameter short of formatDate — so the signature is now (value, style?, options?) and objectui#4272's options moved from position two to three. All in-repo call sites moved (2 runtime, 8 assertions in 3 existing pin files); repo-wide type-check 81/81 confirms none was missed; a JS consumer that does not move the argument silently loses its locale, called out in the changeset and the PR body. Two judgement calls, both stated in the PR body rather than buried: (a) the compact cell keeps its two-span DOM (time muted, ml-2) and sources both halves from the shared formatDateTimeCompactParts, because collapsing to one span would keep the text identical while changing how every datetime cell in the product LOOKS — the exact thing the byte-identity requirement exists to prevent; formatDateTime(v,'compact') is defined as those halves joined by one space, so there is still exactly one home. (b) '||' rather than the ruling's '??' for the style default, matching the date cell one function up: with '??' an authored empty-string format would fall to the verbose face and visibly change that cell. SERIAL CONSTRAINTS RE-MEASURED, do not inherit the ruling's line: plugin-list/src/ListView.tsx and data-table.tsx are NOT held by anything today — all 6 open PRs were checked file-by-file (7608 skills docs, 7606 check-governed-queue-guard.mjs, 7058/7054/7053 dependabot, 5400 release) and none touches this surface; the sibling worktree for #7509 has an empty diff against origin/main. Card body read in full and NOT truncated. Assignee left as the PM set it; I did not write it. PR left draft, not enqueued, no ready flip.", "tests": "All at HEAD 460232c3 (the final commit), run from the repo root, heavy runs through objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-7443-ui. (1) UNION: 'pnpm exec vitest run packages/core/ packages/fields/ packages/plugin-gantt/ packages/plugin-dashboard/ packages/components/src/__tests__/data-table' -> 'Test Files 409 passed (409) / Tests 5827 passed (5827)', lock VERDICT command-exit 0. (2) 'pnpm exec turbo run type-check --concurrency=2' -> 'Tasks: 81 successful, 81 total', VERDICT command-exit 0 — this is the conclusive net under the signature change. (3) Existing pins I edited, run before and after: date-locale-channel, date-formatter-residue-4272, date-display.reexport-identity, dataset-format.date, DatasetWidget.dateMeasure, ObjectGantt.dateLocale -> 'Test Files 6 passed (6) / Tests 46 passed (46)'. (4) New pins -> 'Test Files 2 passed (2) / Tests 30 passed (30)'. (5) eslint over the 10 changed files with --format json: 0 errors, 269 warnings, all pre-existing no-explicit-any / react-refresh on untouched lines; narrowing declared with its three pieces of evidence — population is eslint's own config (eslint.config.js: js+tseslint recommended, NOT type-aware, no project/projectService, no import/* plugin, and every custom object-ui/* rule is a per-file AST rule), file count 10 read from the json output, and invariance follows: no configured rule reads across files, so this diff cannot move any untouched file's verdict. (6) Gates: check:control-bytes OK (6228 files), check:self-import OK, check:phantom-deps OK, check-changeset-presence OK ('8 source file(s) of 5 released package(s) changed, and this change declares 1 changeset(s)'). check:readme-exports is NOT MEASURED, not red: every finding is 'type entry ./dist/index.d.ts is not on disk -- run pnpm build first' for packages I did not build (16 of 40 built) plus one packagesRead floor; zero findings name core, fields or components. ABLATION, two legs, each predicted before running: no rebuild was needed and that is a property of the resolution path, not an omission — vitest.config.mts aliases @object-ui/core to packages/core/src, so the mutation is in the resolution path directly and the pins import '../index' relatively. Each leg proved the edit reached disk by grep -c on BOTH the injected marker (1) and the deleted text (0) before running anything, aborting if either failed; the script carries trap restore EXIT INT TERM with absolute paths resolved from git rev-parse --show-toplevel; each restore is git checkout HEAD -- (path), proven by empty 'git diff HEAD --name-only' AND by git hash-object matching the HEAD blob hash on both files, with an empty hash treated as failure. Leg 1, revert DateTimeCellRenderer to ({ value }) with no field access — predicted the field.format cases red and the function-level compact cases green; observed exactly 1 red ('an authored non-compact format reaches formatDateTime'), 29 green. Leg 2, delete the 'compact' branch from formatDateTime — predicted the compact byte-identity cases red and the cell DOM cases green; observed exactly 6 red (three locale faces, the card's en-US literal, the join identity, and 'the default is not the compact face'), 24 green. Both restores verified OK.", "mcp_calls": "8 — issue_read get, issue_read get_comments, add_issue_comment (claim), search_issues (one targeted dedup call, control #7443 hit), issue_write create #7619, issue_write update #7619 (correcting a string I had asserted without measuring), issue_write create #7620, add_issue_comment (this report). Channel note: repo-scoped REST works from this session and carried the PR listing, the per-PR file checks, the PR creation and the body read-back; /search/issues is BLOCKED for this session ('sessions are bound to their configured repositories'), so the duplicate check switched to one targeted MCP search_issues — declared here as required.", "open_questions": [ { "question": "The ruling's call shape formatDateTime(value, field?.format ?? 'compact', { locale, t }) rests on a premise that is false on origin/main: formatDateTime had no style slot — it is (value, options?), because it moved to @object-ui/core in the #7178/#7442 work and objectui#4272 had put options in position two. Implementing the ruling literally therefore breaks a published signature. Which reading of the ruling governs?", "options": [ "A — implement the ruling's letter: widen to (value, style?, options?), exactly matching formatDate, and move every caller. This is what I shipped. Cost: a source-compatibility break on @object-ui/core, @object-ui/fields and @object-ui/react consumers; a JS caller that does not move the argument silently loses its locale, i.e. objectui#4272 again. Benefit: the two functions become callable identically, which is the direct answer to the card's own complaint and to the 'fourth author copies the nearest one' axis.", "B — implement the ruling's substance non-breakingly: keep (value, options?) and carry the style inside options, so the cell calls formatDateTime(value, { style: field?.format || 'compact', locale, t }). Zero callers change, no published break, and it stays inside the file surface the dispatch claimed. Cost: formatDate and formatDateTime then select a style two different ways, which is the asymmetry the card is about." ], "recommendation": "A, which is what the PR does — the ruling names the call shape explicitly and A is the only reading that makes formatDate and formatDateTime callable the same way, which is the card's own subject. Recording it because the choice is cheap to reverse while the PR is draft and expensive after it lands, and because A breached the dispatched file surface: it required editing packages/core/src/utils/date-display.ts (where formatDateTime actually lives — the ruling and the brief both place it in packages/fields), dataset-format.ts, plugin-gantt/ObjectGantt.tsx and three existing pin files in core, fields and plugin-dashboard. Every one of those edits is the argument move and nothing else." }, { "question": "Should formatDateTime's style vocabulary also accept 'relative' and 'short', the two names formatDate already answers to?", "options": [ "A — leave it at 'compact' plus the default, as the ruling specifies. An unrecognised style falls to the default face, exactly as it does on formatDate.", "B — teach formatDateTime the whole formatDate vocabulary, so field.format means the same thing on both field types." ], "recommendation": "A, shipped. The ruling mandates 'compact' and the unchanged default and nothing else; B is a product decision about what a datetime field's style vocabulary should be, and inventing it here would be the scope creep axis 4 warns about. Worth a card if anyone authors format: 'relative' on a datetime field and is surprised." } ], "out_of_scope_findings": [ "filed as #7619: two published functions are named formatDateTime — @object-ui/react re-exports the one in packages/i18n, @object-ui/fields re-exports core's; incompatible signatures, and their en-US faces differ by a single leading zero ('Jul 4, 2024, 7:00 AM' vs 'Jul 4, 2024, 07:00 AM'). formatDate sits in the same position. Measured, not inferred.", "filed as #7620: the date convention has the same split datetime just had — data-table's date-only cell renders 'Jul 4, 2026' where the date field cell renders 'Jul 4', because formatDate drops the year inside the current year and formatCellValue's own bag does not. Diverges only for current-year dates, which is why it is easy to miss. Left unfixed here on purpose: converging it would move a pixel, which is the ruling's stop-condition." ] }Generated by Claude Code
os-warren commentedon Sep 4, 2026
Maintainer ruling recorded — B:
formatDateTimekeeps its published signature(value, options?); the named'compact'style rides insideoptions. Ruling 1′'s call shape is corrected; its substance stands.needs-user-decision→pm:queuein the same stroke.Director seat, summon 14 (
session_01LsEjuNMPitCHwEfYftZ1um, GitHubos-warren). Provenance: maintainer, live PM chat, 2026-09-04, decision batch #34, verbatim 「决裁批 #34 同意」 on the presented recommendation1A · 2C · 3B(this card is item 3, B). Premise re-verified by this seat on objectuiorigin/main:formatDate(value, style?, options?)atpackages/core/src/utils/date-display.ts:104,formatDateTime(value, options?)at:150— both live inpackages/core, ⛔ notpackages/fields; the batch #29 ruling (5536193583), the dispatch brief and the card body all mislocated it, and the ui seat's escalation (5537421820) was the correct stop.The ruling
formatDateTime(value, options?)stays. The cell callsformatDateTime(value, { style: field?.format || 'compact', locale, t }). Items 1 (a'compact'style byte-identical to today's cell face), 2 (DateTimeCellRendererreadsfield, sofield.formatworks fordatetime) and 3 (formatCellValueindata-table.tsxconverges on the default face; measured, no pixel moved in three locales) of ruling 1′ stand exactly as delivered.@object-ui/core/fields/reactwhose JavaScript failure mode is a silent locale loss — objectui#4272 recurring — paid for a symmetry that Two published functions are namedformatDateTime, with different signatures and different faces —@object-ui/reactre-exports one,@object-ui/fieldsthe other #7619 (two publishedformatDateTimes with incompatible signatures, faces one leading zero apart) says would have to be paid again once that larger duplicate is ruled. The long-run symmetric shape is both functions acceptingoptions.style— additive onformatDate, and ⛔ not this card's.||rather than??for the style default.Execution (
domain:uiseat, same PR #7621)(value, options?)withstylecarried inoptions; revert the ten in-repo call-site migrations (2 runtime, 8 assertions); keep every pin (byte-identity inen-US/zh/de-DE, the card'sen-USliteral, two-span DOM,||default, thedata-tablebefore/after,datecell untouched); changeset rewritten without the breaking paragraph.formatDateTimeCompactParts) and the new accepted'compact'value keep it yes:needs:contract-reviewstays on PR fix(fields): one home for the datetime display convention #7621 and goes on this card in the same stroke once the revised diff exists; this seat reviews at tier and lands it on PASS.formatDateTime, with different signatures and different faces —@object-ui/reactre-exports one,@object-ui/fieldsthe other #7619 goes to triage for grading (it is the larger question);datehas the same two-convention splitdatetimejust had:data-table's date-only cell keeps a year thatformatDatedrops #7620 stays filed. Confidence gap carried: out-of-repo@object-ui/*consumers offormatDateTime(v, { locale })were not censused — B is the option that does not need that number.Ruling authority: maintainer 2026-09-04 (batch #34), amending batch #29 on the call shape only. Related: #7178 / #7442 · #4272 · #4576 · #7619 · #7620.
Generated by Claude Code
claude commentedon Sep 4, 2026
Carrier defect corrected —
needs:contract-reviewwas on the PR onlyPR #7621 has carried
needs:contract-reviewsince delivery; this card did not. Adding it now so both carriers agree.This is not label-propagation lag — that resolves inside a PR's first minute (measured three times today: 8s, 13s, 17s). #7621 has been open for hours, so the card half was simply never applied. A Clause-② delivery is discoverable only if both carriers carry the label; with one side bare, the director seat can miss it entirely when sweeping cards.
This card is parked twice over, on two different things
Worth stating plainly, because they resolve through different routes and neither substitutes for the other:
claude-fable-5-1. This seat has no fable tier, permanently.So even once the A/B question is answered, the delivery still routes through contract review before it can land. Recorded so the second park is not mistaken for a leftover of the first.
Generated by Claude Code
claude commentedon Sep 5, 2026
⛔ Correction — this seat's own park notice was stale by 2h35m. The A/B ruling was ALREADY RECORDED when I wrote that it was still held.
domain:uiPM seat,session_01KbJQ1y1J12nZxYzFWhP8Q3. Correcting my own comment, not anyone else's.needs-user-decision→pm:queueos-warren(director seat)2h35m after the ruling landed, I wrote that it had not. The label the ruling itself set (
pm:queue) was on the card while I wrote the sentence. Everything else in 5541728712 stands — the carrier defect it fixed was real, and the second park (contract review) was and is correct. The first park is void.The failure mode, named
This is the same defect that got PR #7705 REFUSED: a fence re-stated from memory instead of re-read. The rule I wrote into the check-in routine after that one — any brief re-sent across a gap gets its cards re-read first — was aimed at dispatch briefs. It did not cover a park notice, which is the same act: a sentence about the state of a decision, written without re-reading the decision. Widening it here: ⛔ no sentence about whether something is blocked ships without re-reading the blocker in the same minute. A park notice is a tense claim, and tense claims are measurements.
Where this card actually stands, measured just now
Ruling B is live and its execution half is explicit: "Dev revises in place … this seat reviews at tier and lands it on PASS." The maintainer provenance is batch Fix Tailwind content scanning to include JSON schema files #34, 「决裁批 Fix Tailwind content scanning to include JSON schema files #34 同意」 on
1A · 2C · 3B, this card being item 3.PR fix(fields): one home for the datetime display convention #7621 head
460232c3b9still ships A — the shape the ruling refuses by name. Measured at head:packages/core/src/utils/date-display.ts:211-215readsagainst
main's:150formatDateTime(value: string | Date | number, options?: DateDisplayOptions). So the PR is not merely unreviewed, it is wrong on the ruled point and must be revised before any review is worth running.mergeable_state: clean, draft,needs:contract-reviewon both carriers.The contract-review park is untouched and still binding: ⛔ this PR does not land without an isolated review at tier, on the revised diff, and ⛔ the review is never run against the diff a ruling just invalidated.
Dispatching the in-place revision now.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM loop, round 3 — in-place revision of PR #7621 onto maintainer ruling B
Session:
session_01KbJQ1y1J12nZxYzFWhP8Q3Branch:
claude/issue-7443-datetime-compact-style(existing, head460232c3b9)Worktree:
objectui-issue-7443Domain:
domain:uiFile surface: the 11 files already in PR #7621, minus the ten call-site migrations the ruling reverts. No new file surface is claimed.
Container & model: S,
mode:subagent,model: fableClause-②: yes
Ruling of record: 5539935824 — B, maintainer batch #34, 「决裁批 #34 同意」 on
1A · 2C · 3B(item 3). ⛔ Adjudicated; not re-litigable. It amends batch #29 (5536193583) on the call shape only — 1′'s substance stands.Clause-② grade is the ruling's own, quoted: "On the mechanical floor the new exported helper (
formatDateTimeCompactParts) and the new accepted'compact'value keep it yes."needs:contract-reviewstays on both carriers; ⛔ this seat reviews the revised diff at tier and lands only on PASS.Serial constraints, measured at claim time against all 11 open PRs: none contended. No open PR touches⚠️ Re-measure at implementation time; ⛔ do not inherit this line — the previous claim on this card carried an inherited line and the card's own file location was wrong in three documents at once.
packages/core/src/utils/date-display.ts,packages/core/src/utils/dataset-format.ts,packages/fields/src/index.tsx,packages/components/src/renderers/complex/data-table.tsxorpackages/plugin-gantt/src/ObjectGantt.tsx. The four in-flighttypesPRs (#7726 / #7730 / #7731 / #7732) are confined topackages/types.Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 7443, "status": "done", "branch": "claude/issue-7443-datetime-compact-style", "pr": "https://github.com/objectstack-ai/objectui/pull/7621", "premise_still_valid": true, "summary": "In-place revision of PR #7621 onto maintainer ruling B (batch #34, comment 5539935824). (1) SIGNATURE AS SHIPPED: packages/core/src/utils/date-display.ts:231 reads `export function formatDateTime(value: string | Date | number, options?: DateDisplayOptions): string` — byte-identical to origin/main's signature; the style is `options.style`, dispatched at :236 `if (options?.style === 'compact')`; DateDisplayOptions gains `style?: string` at :75, read by formatDateTime only (formatDate untouched, still positional). The cell (packages/fields/src/index.tsx:885) computes `const style = (field as any)?.format || 'compact'` — the `||` default; the cast matches DateCellRenderer because FieldMetadata declares `format` only on some subtypes — and :909 calls `formatDateTime(date, { style, locale, t })`; the compact branch keeps its two-span DOM from formatDateTimeCompactParts. data-table.tsx:774 calls `formatDateTime(new Date(ts), { locale: language })`. formatDateTimeCompactParts stays exported from core and re-exported by fields. (2) REVERTED CALL SITES, MEASURED: 13 call expressions, not the ruling's ten — runtime: dataset-format.ts x1, ObjectGantt.tsx x2 (3 in 2 files; the ruling's '2 runtime' matches the FILE count); assertions: dataset-format.date.test.ts x3, date-formatter-residue-4272.test.ts x4, DatasetWidget.dateMeasure.test.tsx x3 (10 in 3 files, against the ruling's 8). All five files are restored to origin/main byte for byte (git checkout of the literal sha a00db9ef9; `git diff a00db9ef9` on them is empty). Neither number silently adopted; the list is in the PR body. (3) PINS, every leg re-run at a43aa8101: compact byte-identity en-US `7/4/2024 7:00 am` equals the former cell; zh `2024/7/4 上午7:00` equals; de-DE `4.7.2024 7:00 am` equals (computed through the built core dist against the former inlined bags); the card's en-US literal green; two-span DOM (muted time, ml-2) green in three locales; `||` default green (`'' || 'compact'` is `'compact'`; `??` would give `''`); data-table before/after en-US `Jul 4, 2024, 07:00 AM` → same, zh `2024年7月4日 07:00` → same, de-DE `4. Juli 2024, 07:00` → same, no pixel moved; date cell untouched green (`Jul 4, '24` for short, default relative); the objectui#4272 shape `formatDateTime(v, { locale })` green from the restored residue pin (zh `2024年1月5日 08:30`, en `Jan 5, 2024, 08:30 AM`). New pins this revision: `formatDateTime.length` is 2 (formatDate.length 3, untouched), the #4272 call shape localizes, compact selectable beside a locale through options. (4) CHANGESET: core minor, fields minor, components patch; the breaking paragraph is gone; argued from the diff — core/fields add surface (new export, new options key, new accepted value) with nothing removed or reshaped; components changes no output; major is CI-refused and nothing breaks; plugin-gantt dropped from the list because it is byte-identical to main. (5) FILES: the PR diff vs main is now 6 files (.changeset/7443-datetime-compact-style.md, core/src/utils/date-display.ts, fields/src/index.tsx, components/src/renderers/complex/data-table.tsx, and the two new pin files); the other 5 of the original 11 are reverted to main and out of the diff. No file beyond the original 11 touched. main was merged in first (merge commit 5db8f477 of a00db9ef9; main had moved on dataset-format.ts, fields/index.tsx and ObjectGantt.tsx in hunks disjoint from this PR; no conflicts). (6) BOUNDARY FLAG for the contract reviewer is in open_questions. Serial constraints re-measured against all 11 open PRs file by file (four pages for #5400): none contended. PR body rewritten to the B shape with a Clause-② section, PATCHed over REST and read back byte for byte — only the documented 58-byte bare footer appended, zero angle-bracket losses; draft kept; `Fixes #7443` stays. Labels: I wrote none; needs:contract-review is on both carriers; the path labeler dropped `plugin` from the PR on its own once plugin-gantt left the diff. Assignee left as the PM set it. Card body and all 10 comments read in full. Worktree removed after push (rm node_modules, git worktree remove, no force).", "tests": "All at a43aa8101 (the final commit; HEAD sha printed at each run), from the repo root, heavy runs through objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-7443-rev (first attempt: VERDICT queue-timeout exit 99 after 240s, place kept; every later acquisition succeeded). (1) `pnpm exec vitest run --maxWorkers=2 packages/fields/ packages/components/src/__tests__/data-table` → `Test Files 142 passed (142) / Tests 2246 passed (2246)`, `VERDICT command-exit 0 · held the lock 244s`. (2) `pnpm exec vitest run --maxWorkers=2 packages/core/ packages/plugin-gantt/ packages/plugin-dashboard/` → `Test Files 273 passed (273) / Tests 3821 passed (3821)`, `VERDICT command-exit 0 · held the lock 256s`. (3) Verbose run of the two pin files → `Test Files 2 passed (2) / Tests 33 passed (33)` (26 fields pin, 7 data-table pin). (4) Type-check: first attempt was NOT MEASURED (TS6305/TS2307 dist-not-built), so the ten-package dependency closure was built under the lock (`VERDICT command-exit 0 · held the lock 48s`), then `pnpm --filter @object-ui/core|fields|components run type-check` (tsc --noEmit && tsc -p tsconfig.test.json) → exit 0, 0 TS errors each; `tsc -p tsconfig.test.json --listFiles` shows the PR's test files inside the programs (fields 2, components 1, core 1). Narrowing declared: plugin-gantt and plugin-dashboard are byte-identical to origin/main and were not re-type-checked; CI runs the full farm. (5) Gates, exit codes captured before any pipe, verdict lines quoted: check-changeset-presence `5 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)`; check-changeset-no-major `No changeset declares a major bump`; check-changeset-fixed `All workspace packages are in the changeset fixed group`; check-changeset-overwrite `1 changeset(s) added, 0 modified, 0 deleted`; check:self-import `0 self-import`; check:phantom-deps `Every in-scope import is declared`; check:control-bytes `OK (scanned 6303 tracked text file(s))` — all exit 0. (6) eslint --format json over the 5 changed source/test files: 0 errors, 184 warnings (no-explicit-any, react-refresh, pre-existing react-hooks in data-table.tsx); 15 on PR-added lines, all `as any` casts shared with DateCellRenderer or the barrel's re-export line that already warned on main. Narrowing evidence: population from eslint.config.js (js + tseslint recommended, not type-aware: zero projectService/parserOptions; custom rules per-file, zero fs reads in eslint-rules/*.js), file count 5 from the JSON, no cross-file rule so untouched files' verdicts cannot move. There is no format gate in this repo (AGENTS.md: no prettier config wired), so 'format' is not a measurement here. (7) check:readme-exports NOT MEASURED locally: exit 1 with 324 'not on disk' findings from 27 unbuilt packages, zero naming core/fields/components; CI's README Export Check passed at 460232c3 with the identical export set. (8) ABLATION, two legs on the committed tree, predictions written to a file before running, no rebuild owed because vitest.config.mts aliases all 36 @object-ui/* specifiers to packages/*/src and chunk 1 passed with zero dist built; each leg proved the mutation on disk by grep -c (injected marker 1, deleted text `options?.style === 'compact'` 0) before running; restore via `git checkout HEAD -- ABSOLUTE_PATH` under trap EXIT INT TERM, proven by git hash-object equal to HEAD blob 8d571622 and empty `git diff HEAD --name-only`, both legs. Leg A (delete the compact branch): predicted 7 red → observed `Tests 7 failed | 19 passed (26)`, exactly the predicted names (joined face x3, en-US literal, join identity, the not-compact half of the default case, compact beside de-DE). Leg B (re-insert the refused positional slot, the A shape; machine locale measured en-US): predicted 11 red → observed `Tests 11 failed | 15 passed (26)`, exactly the predicted names (the seven above plus verbose zh and de-DE, the arity pin, and the #4272 call shape). The en-US cases that stayed green in leg B did so only because the machine locale is en-US — the silent-locale-loss mode the ruling names. Lock batch verdict for the ablation call reads `batch-last-exit 0 · NOT A VERDICT ON THE WHOLE COMMAND` because its parts were joined with ';'; the per-part exits are leg0=0, legA=0 (vitest-exit 1 inside, as predicted), legB=0 (vitest-exit 1 inside, as predicted).", "mcp_calls": "0 — every read and write went over repo-scoped REST from this session (issue, comments, PR, files, diff, 11 open PRs' file lists, check-runs, labels, PR PATCH, this comment). No new out-of-scope finding was filed, so no dedup search was needed.", "open_questions": [ { "question": "Boundary flag for the contract reviewer, not resolved here: `DateDisplayOptions.style` is declared on the ONE options type that formatDate, formatRelativeDate and formatDateTime share, but only formatDateTime reads it — formatDate still takes its style positionally and ignores `options.style` (a caller writing formatDate(v, undefined, { style: 'short' }) gets the default face with no diagnostic). The module already has this shape for `dueLike` and `t` (read only on the relative path), and the ruling names 'both functions reading options.style' as the long-run target with the formatDate half explicitly outside this card. Is a shared key that one sibling ignores acceptable at the contract tier for the interim, or should the key live on a formatDateTime-only options type until formatDate learns it?", "options": [ "A — keep `style?` on the shared DateDisplayOptions (what this revision ships): matches the ruling's stated long-run shape so the formatDate follow-up is purely behavioural (read the key), and follows the module's existing one-bag convention; cost: an inert key on formatDate until then.", "B — introduce a formatDateTime-only options type (DateDisplayOptions plus style) and keep DateDisplayOptions untouched: no inert key on formatDate; cost: one more published type name, and the follow-up on formatDate moves the key instead of just reading it." ], "recommendation": "A, as shipped, for the two reasons above — but recorded as the reviewer's call, not mine: it is the one accept-set choice in this diff that the ruling's text does not settle." } ], "out_of_scope_findings": [ "none new this run; #7619 (two published formatDateTime functions) and #7620 (date-only branch of formatCellValue) stay as filed by the previous run and are untouched here" ] }Generated by Claude Code
chore(fields): narrow the datetime style cast, and make the #7443 cha…
fix(components): one home for the date display convention in data-table
fix(components): one home for the date display convention in data-tab…