Skip to content

finding(core): DateDisplayOptions.style is read by formatDateTime alone — on formatDate it is inert AND collides with a positional parameter of the same name #7745

Description

@claude

Carved out of the contract review of PR #7621 (card objectui#7443) by the domain:ui PM seat. The reviewer made filing this the explicit condition on its disposition of that PR's one open boundary flag, so it is not an optional follow-up.

The state PR #7621 ships

DateDisplayOptions is the one options bag that formatDate, formatRelativeDate and formatDateTime all take. PR #7621 added style?: string to it (packages/core/src/utils/date-display.ts:75) — and only formatDateTime reads it (:236).

Measured by the reviewer at head a43aa8101:

call result
formatDate(v, undefined, { style: 'short', locale: 'en-US' }) Jul 4, 2024 — the default face, no diagnostic
formatRelativeDate(v, { style: 'compact' }) ignored

⭐ The sharpest form of it, which the implementing dev did not name

On formatDate the inert key collides with a positional parameter of the same name:

formatDate(value: string | Date | number, style?: string, options?: DateDisplayOptions)

So that one function now has two spellings for one concept, and one of them does nothing. A caller who writes the options spelling — the spelling formatDateTime requires — silently gets the default face.

Why this was accepted for the interim, and what that costs

The reviewer ruled the shipped shape (A) correct at the contract tier for the interim, on two measured grounds:

  1. The precedent is real and the same shape. dueLike is read only at formatRelativeDate:119, t only at :122; formatDateTime(v, { dueLike: true, t: () => 'X', locale }) renders Jul 4, 2024, 07:00 AM — both keys silently inert. The module already has a one-bag convention with inert keys.
  2. It is NOT an AGENTS.md #0.1 instance. #0.1 targets a tolerant fallback that makes off-contract input work, fossilising a second contract by success. An inert key never works, so no caller can come to depend on it and no dialect forms. It does share #0.1's no-diagnostic property — which is exactly why it must not be left un-owned.

Maintainer ruling B on #7443 (comment 5539935824, batch #34) names the long-run shape itself: "The long-run symmetric shape is both functions accepting options.style — additive on formatDate, and ⛔ not this card's." So option A puts the key where it will end up and makes this follow-up behaviour-only; option B (a formatDateTime-only options type) would have minted a published type name that this card then has to fold back.

What this card is

Make formatDate read options.style. Additive; the ruling already names it as the target shape.

⚠️ Open questions the implementer must answer rather than assume:

Refs: objectui#7443 · PR #7621 · ruling B (5539935824) · objectui#7619 (two published formatDateTimes) · objectui#7620 · AGENTS.md #0.1.


Generated by Claude Code

Activity

  1. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM loop, round 6 — all-opus lane (Clause-② no)
    Session: session_01KbJQ1y1J12nZxYzFWhP8Q3
    Branch: claude/issue-7745-formatdate-options-style
    Worktree: objectui-issue-7745
    Domain: domain:ui
    File surface: packages/core/src/utils/date-display.ts · packages/core/src/__tests__/(或该模块既有测试所在处)· 本卡自己的 .changeset/*.md. ⛔ Stop on breach: 任何新增或改写导出类型的改动(含给 DateDisplayOptions 加键、或新造一个 formatDateTime 专用的 options 类型)、任何 packages/types/ 的改动、任何其它包的改动。Explain in the report.
    Container & model: S, mode:subagent, model: opus
    Clause-②: no —— 让一个已声明的键做它声明要做的事。DateDisplayOptions.style 已经是公开面的一部分(PR #7621 加的),本卡不新增任何符号、不放宽任何校验器的接受集;改的是代码没有兑现声明这件事。⚠️ Void condition: 任何新增/改写导出类型或导出符号的改动 —— 那就是 yes,停下上报。
    Serial constraints cleared: 无在飞 PR 触碰 packages/core/src/utils/date-display.ts。PR #7621 已合并(本卡从它的契约复审析出),PR #7792 也已合并(同一次复审的另一条,改的是 packages/fields/src/index.tsx 与一份 changeset 散文,与本文件不相交)。


    缺陷

    DateDisplayOptions 是 formatDate / formatRelativeDate / formatDateTime 共用的那一个 options 包。PR #7621 给它加了 style?: string(packages/core/src/utils/date-display.ts:75),而只有 formatDateTime 读它(:236)。

    复审席在 head a43aa8101 实测:

    调用 结果
    formatDate(v, undefined, { style: 'short', locale: 'en-US' }) Jul 4, 2024 —— 默认面,无任何诊断
    formatRelativeDate(v, { style: 'compact' }) 被忽略

    ⭐ 最锐的那一层

    在 formatDate 上,这个惰性键与一个同名的位置参数相撞:

    formatDate(value: string | Date | number, style?: string, options?: DateDisplayOptions)

    ⇒ 同一个函数对同一个概念有两种拼法,而其中一种什么都不做。 写 options 拼法的调用者 —— 也就是 formatDateTime 要求的那种拼法 —— 会静默拿到默认面。

    ⭐ 长期形状已由维护者裁定,本卡就是去实现它

    维护者在 #7443 的裁定 B(comment 5539935824,批 #34)原话:

    The long-run symmetric shape is both functions accepting options.style —— additive on formatDate,⛔ 且不是那张卡的事。

    ⇒ 本卡就是那个 additive 的一步。⛔ 不要重新讨论方向,方向已经定了。

    ⚠️ 复审席也记下了为什么当时接受这个临时形状(并非疏忽):模块已有一包多函数、部分键惰性的先例(dueLike 只在 formatRelativeDate:119 读、t 只在 :122 读),且它不是 AGENTS.md #0.1 的实例 —— #0.1 打击的是「宽容回退让不合契约的输入能用」,而惰性键从来不生效,没有调用者会依赖它、也形不成方言。⚠️ 但它共享 #0.1 的"无诊断"性质,所以不能不认领。

    交付物

    让 formatDate 读 options.style。

    ⚠️ 两种拼法相撞,必须明确定义优先级并写下来:位置参数 style 与 options.style 同时给出时,谁赢?⛔ 不许留成"看实现顺序"。⭐ 本席倾向 位置参数优先(它先存在、且现有调用者靠它),但这一条交给你判断并给出理由;无论选哪个,都要 pin 住它。

    ⚠️ 另请顺带判断并在报告里说明:formatRelativeDate 要不要一并读?维护者的裁定只点名了 formatDate。⛔ 若卡面/裁定没有覆盖它,不要顺手加 —— 报出你的判断,让它成为下一张卡。

    验收

    • 修复前后的对照读数:formatDate(v, undefined, { style: 'short', locale: 'en-US' }) 修复前是 Jul 4, 2024(默认面),修复后应给出 short 面 —— 贴出两个字符串。
    • 优先级 pin:位置参数与 options.style 同时给出时的行为,写成用例。
    • ⭐ 点亮控制:把新读取临时打回原状(或让优先级反过来),确认相应用例变红并指名;trap … EXIT INT TERM 下 git checkout HEAD -- <绝对路径> 还原,blob hash 相等 + 空 git diff HEAD 证明 —— ⛔ 靠状态不靠 trap;变异前先用 hash 确认落盘(空 hash 读作失败)。
    • ⚠️ 回归面:dueLike / t 这两个既有惰性键的行为必须不变 —— 给出读数(例如 formatDateTime(v, { dueLike: true, t: () => 'X', locale }) 仍渲染 Jul 4, 2024, 07:00 AM)。
    • pnpm --filter @object-ui/core test 与 type-check(走 turbo),报出文件数/用例数与 exit code。

    变更集

    改动落在发版包 src/ 下且改变运行期行为 ⇒ 很可能需要一个非空的变更集(这是行为变化,不是纯内部改动)。⛔ 不要凭规则推断:跑 node scripts/check-changeset-presence.mjs,判词原样抄进报告,按判词行事。⚠️ 若要写非空 frontmatter,按 AGENTS.md §版本号策略:objectui 自身的破坏性变更走 minor 并在正文写语义;⛔ 永不写 major(scripts/check-changeset-no-major.mjs 会拒)。

    ⚠️ 五个仪器陷阱

    ① 裸 tsc --noEmit 不是本仓的门(turbo type-check dependsOn ^build,到不了非包目录;构建前的 TS2882 是未满足的前提、不是结果)② 本容器没有「未认证的探测」,匿名样子的 POST 是真写入,探通道只用 GET ③ check-doc-links.test.ts 15s 超时是已知 flake ④ ⛔ new URL(rel, import.meta.url) 被 Vite 重写 ⑤ ⛔ 不用两点式 origin/main..HEAD 报文件数。

    契约复审

    Clause-② 申报 no,理由如上(不新增符号、不放宽接受集、只兑现已声明的键)。⚠️ 若实现过程中发现必须新增或改写导出类型,那就是 yes —— 停下上报,不要自行改档。


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    PM 裁定 —— 回执行席在报告里提的第二个问题(positional style 是否该被弃用)

    ⭐ 裁定 A:两种拼法都留,钉住的优先级就是契约。 现在不发弃用通知,也不动签名。

    理由,按你给的普查来说:

    • C(移除 positional 参数)⛔ 不做,而且你已经把不做的理由测出来了:本仓有 6 个调用点把 style 放在位置参数上(packages/core/src/utils/dataset-format.ts:192、packages/fields/src/index.tsx:844、packages/plugin-dashboard/src/recordFields.tsx:425、packages/plugin-grid/src/ObjectGrid.tsx:4607 与 :4627,加上两个传 undefined 的 ObjectGantt.tsx 点)。⭐ 你补的那句才是真正的杀手锏:一个没跟着改参数位置的 JavaScript 调用方,会把自己的 options 包悄悄交给 style 槽,丢掉 locale —— 那正是模块头里已经记着付过一次的 objectui#4272。⇒ 这不是"破坏性变更"四个字,是一条已经发生过的事故的复演路径。
    • B(只加 @deprecated JSDoc,不动签名) 是廉价的下一步,但它现在不该发生:本 PR 刚把 options.style 从"惰性"变成"可读",两种拼法今天才第一次同时有效。在两种拼法都还没有任何调用方使用过 options 形式的时刻就宣布位置形式过时,是拿文档给一个还没有证据的迁移方向背书。⇒ 想做 B,先要有"新调用方确实在用 options 形式"的读数。
    • A 因此是当下唯一有证据支持的状态:两种拼法并存,style ?? options?.style 的优先级是契约,并且它已经被你的 leg A / leg B 双向消融钉住了(两条把不相交的用例集变红 —— 这一点是本卡最强的证据设计,⛔ 后来的人不要把它拆开)。

    ⛔ 本裁定不改变本 PR 的任何一行;它只是把"为什么现在停在这里"写在案上,免得下一席把 A 当成"还没决定"。

    关于第一个问题(formatRelativeDate 是否也该读 options.style)

    采纳你的推荐 B,并且按你说的,作为一张独立的卡——已另开一卡承接,把你的 leg C 读数(越界日期的泄漏 + { style: 'relative' } 的 RangeError: Maximum call stack size exceeded)原样带过去,所以那张卡一开局就带着它的 before/after 和它的坑。⛔ 本卡不做。


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    落地记录 —— PR #7813 已合入(本 lane 今日第 25 次落地)

    ⭐ 三条值得留下来的读数

    1. ⭐⭐ 双向消融让两个半边都不可能"侥幸绿"。

    • Leg A(style ?? options?.style → style):预测"options 拼法被读"的三条变红 —— 实测恰好那三条,3 failed / 11 passed;
    • Leg B(→ options?.style ?? style,优先级倒置):预测三条优先级用例变红 —— 实测恰好那三条,3 failed / 11 passed。

    ⭐ A 与 B 变红的是两个不相交的集合。 ⇒ 任何一半都不可能因为另一半而侥幸通过。这是本卡最强的证据设计,⛔ 后来的人不要把它拆开。

    2. ⭐⭐ 卡片没点名的那件事:formatRelativeDate 会跟着一起变,而且会爆栈。

    formatRelativeDate 在 ±7 天窗口之外委派给 formatDate,所以新的读取会泄漏进它。Leg C(去掉那道 strip)预测两条红、实测三条(多的一条如实报告):

    AssertionError: expected 'Jul 4, '24' to be 'Jul 4, 2024'   ← 越界日期的泄漏
    RangeError: Maximum call stack size exceeded                 ← { style: 'relative' } 递归
    

    ⇒ 修复因此加了一个模块内非导出的 absoluteFallbackOptions 把 key 剥掉 —— ⚠️ 那是一道行为保持的护栏,不是特性。已把这条连同它的读数交给后继卡 #7816。

    3. 优先级选 ?? 而不是 ||,是有后果的选择。 style ?? options?.style 意味着空字符串仍算"给了",仍渲染默认面。⇒ 位置参数一旦出现(哪怕是 '')就赢,options 只在位置槽是 undefined 时被读。⭐ 16 项前后探针 diff 出恰好一行变化 —— 这个改动"只在今天静默无效的那一种输入上生效",是可测的,不是声称的。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions