Skip to content

nits(core,fields) from PR #7621's review: the changeset's byte-identity claim is imprecise for consumers, and (field as any)?.format had a free narrower cast #7747

Closed

Description

@claude

Filed by the domain:ui PM seat from the contract review of PR #7621 — the two non-blocking items that PR does not carry, recorded so they are not lost when it lands. ⛔ Neither gated the review's PASS.

1. The changeset's byte-identity sentence is imprecise for a consumer

.changeset/7443-datetime-compact-style.md says, in effect, "Every existing cell renders byte-identically."

Measured in-repo it is true: the reviewer's census found zero in-repo datetime fields that author a format, so no cell in this repository changes. But the sentence is a release-note input, and it is read by consumers who are not this repository.

The accurate form: "every cell without an authored non-'compact' format renders byte-identically." PR #7621's own body already declares that DateTimeCellRenderer starts reading field.format — the changeset simply does not spell out the one visible consequence.

⚠️ Worth stating for whoever fixes it: format on a datetime field is objectui's own vocabulary — DateTimeFieldMetadata.format?: string, and @objectstack/spec declares no format on datetime fields. It is undocumented. So "what values are legal, and what does an unrecognised one do" is itself unanswered, and the honest changeset sentence may need that gap named rather than papered over.

2. (field as any)?.format where a narrower cast was free

packages/fields/src/index.tsx (the DateTimeCellRenderer cell) reads const style = (field as any)?.format || 'compact'.

Some narrowing is genuinely load-bearing: FieldMetadata is a 37-member union, BaseFieldMetadata has no format, and format is declared only on Date/DateTime/Time/Phone/AutoNumber — so field.format on the union is TS2339.

But as any is wider than the job needs: DateTimeFieldMetadata is exported from @object-ui/types (types/src/index.ts:21) and that file already imports types from there, so (field as DateTimeFieldMetadata | undefined)?.format was available at zero cost.

⚠️ The DateCellRenderer precedent that was cited for it does more work — it reads accessorKey / key, which are genuinely not on DateFieldMetadata. Here only .format is read, and .format is on the subtype. Reviewer's verdict: lazy, precedented, not load-bearing. (no-explicit-any is warn-level in this repo, so CI Lint is green either way — which is exactly why this needs a card rather than a gate.)

Refs: objectui#7443 · PR #7621 · sibling follow-ups from the same review: #7745, #7746.


Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 5, 2026
  2. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    PM 预清障(domain:ui PM 席,session_01KbJQ1y1J12nZxYzFWhP8Q3,round 6):本卡第 1 项要改的是别人 PR 的、已合并的** changeset。派发前必须先证明这动作合法 —— 已证明,允许。读数如下,接手人不必重推一遍。**

    前提一:那个文件还在

    origin/main = 6771c805a(PR #7784 的合并提交)。

    git ls-tree origin/main .changeset/7443-datetime-compact-style.md --name-only
    → .changeset/7443-datetime-compact-style.md
    

    ⇒ 仍在树上,尚未被发版消费。若它已被 release 吃掉,第 1 项就不是「改一句话」而是「改已发的 release note」——那会撞上 content/docs/releases/ 那条禁令(发版说明集中写、不按 PR 累加),必须改走独立 docs PR 或转 issue。现在不是这个形状。

    前提二:改别人的 changeset 是否被门拦

    scripts/check-changeset-overwrite.mjs 就是管这件事的门。逐条读它自己的头部,不是凭印象:

    • 它的默认档是 REPORT-ONLY,exit 0;只有 OS_CHANGESET_OVERWRITE_ENFORCE=1 才拦。仓库里没有开这个开关。
    • ⭐ 更强的一条:它测过整段历史才决定报告而不拦 —— main 上 5281 个 first-parent 提交里,12 个提交修改了已存在的 changeset(19 个文件),19 个全部合法,其中明确点名的合法类别就包括**「对散文的事实性更正」**(举的例子正是 "eleven"→"ten" locale packs、以及一处包名笔误)。
    • 门自己的结论原话:「a blocking gate would have failed every one of those pull requests」。

    ⇒ 本卡第 1 项 = 对散文的事实性更正,恰好落在那 19 分之 19 里。

    ⚠️ 但有一条必须守住:包名一个都不能少

    同一份测量析出了一条更窄的信号,接手人必须遵守:

    19 处合法修改中有 18 处保留了它在 base 上声明过的每一个包名 —— bump 等级会变、散文会变,名字不会消失。唯一的例外删掉的是一个在本 workspace 里解析不到任何包的笔误名。overwrite 事故是相反的形状:前一份声明的名字整个没了。

    ⇒ 接手时只改那一句散文,⛔ 不动 frontmatter、⛔ 不动 bump 等级、⛔ 不删任何包名。改完把 git diff 贴进 PR 正文,让「名字集合不变」可核。

    第 2 项无障碍

    packages/fields/src/index.tsx 把 (field as any)?.format 收窄为 (field as DateTimeFieldMetadata | undefined)?.format。该文件已经从 @object-ui/types import 类型,DateTimeFieldMetadata 在 types/src/index.ts:21 导出 ⇒ 零成本。⚠️ no-explicit-any 在本仓是 warn 级,两种写法 CI Lint 都绿 —— 所以这是卡不是门,也所以不要顺手把它升成 error,那是另一张卡的事。

    Clause-② 预判:no

    判据是内容不是路径。第 1 项只改散文;第 2 项是纯类型层收窄,运行期零行为差。⇒ 不改变契约的接受/拒绝行为,不扩大公开面。⚠️ 作废条件:任何让 DateTimeCellRenderer 在运行期读出不同值的改动 —— 那就是 yes,停下上报,⛔ 不要在 no 申报下推 yes 的 diff。

    变更集

    第 2 项动了发版包的 src/ ⇒ 按 AGENTS.md:161 机械要求,必须新增一个自己的 .changeset/*.md。⛔ 那是新文件,与本卡第 1 项要更正的 7443-datetime-compact-style.md 是两回事,别混。纯类型收窄不发版 ⇒ 写空 frontmatter形式(--- 紧跟 ---)显式声明不发版,那是合法的一等通过写法。

    ⇒ 本卡已解除派发前障碍,保持 pm:queue,等并发槽位。


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    Claim: PM loop, round 6 — all-opus lane (Clause-② no)
    Session: session_01KbJQ1y1J12nZxYzFWhP8Q3
    Branch: claude/issue-7747-datetime-format-nits
    Worktree: objectui-issue-7747
    Domain: domain:ui
    File surface: .changeset/7443-datetime-compact-style.md (⚠️ 别人 PR 的、已合并的变更集 —— 只改一句散文), packages/fields/src/index.tsx (一处 cast 收窄), 外加本卡自己的新 .changeset/*.md. ⛔ Stop on breach: 任何 frontmatter/bump 等级/包名的改动、任何 packages/fields/src/index.tsx 之外的运行期改动、任何把 no-explicit-any 升级成 error 的顺手改动。Explain in the report.
    Container & model: S, mode:subagent, model: opus
    Clause-②: no — 第 1 项只改散文;第 2 项 (field as any) → (field as DateTimeFieldMetadata | undefined) 是纯类型层收窄,运行期零行为差。不改变契约接受/拒绝行为,不扩大公开面。⚠️ Void condition: 任何让 DateTimeCellRenderer 在运行期读出不同值的改动 —— 那就是 yes,停下上报,⛔ 不在 no 申报下推 yes 的 diff。
    Serial constraints cleared: none —— 无在飞 PR 触碰 packages/fields/src/index.tsx 或 .changeset/7443-datetime-compact-style.md;本卡的来源 PR #7621 已合并。⚠️ 同批兄弟卡 #7745(DateDisplayOptions.style 被 formatDateTime 读取)与本卡同属 #7621 复审析出,但文件面不相交且本卡不 pin 断言其行为 ⇒ 无读耦合。

    派发前障碍已由本席清除,读数在 comment 5554176643,⛔ 不必重推:变更集文件仍在 main 上(未被发版消费),check-changeset-overwrite.mjs 是 report-only 且其自身历史测量把「对散文的事实性更正」列为 19/19 合法。


    两项都要做,⛔ 不许只做一项

    第 1 项 —— 把散文改准,别改多

    .changeset/7443-datetime-compact-style.md 现在说「每个已有单元格都逐字节相同」。在本仓内测是真的(复审普查:零个 in-repo datetime 字段写了 format),但变更集是发版说明的输入,读它的是本仓之外的消费者,而 PR #7621 自己的正文已经声明 DateTimeCellRenderer 开始读 field.format。

    准确形状:「每个没有写非 'compact' 的 format 的单元格逐字节相同」。

    ⚠️⭐ 但卡里有一条更硬的提醒,⛔ 不要粉饰过去:format 是 objectui 自己的词汇(DateTimeFieldMetadata.format?: string),@objectstack/spec 在 datetime 字段上没有声明 format,而且它没有文档。所以「哪些值合法、不认识的值会怎样」本身是未回答的。⇒ 诚实的写法是把这个缺口点名,而不是写一句读起来像已经定义好的话。先去实测 DateTimeCellRenderer 拿到一个不认识的 format 会怎样(回退 'compact'?抛错?原样输出?),把测出来的行为写进散文;⛔ 测不出来就写「未定义」,不要猜。

    ⛔ 硬约束(来自 check-changeset-overwrite.mjs 自己的历史测量):19 处合法修改里 18 处保住了 base 上声明过的每一个包名,唯一的例外删的是解析不到任何包的笔误名;overwrite 事故的形状恰恰是前一份声明的名字整个没了。⇒ 只动那一句散文,frontmatter 一行不碰,包名一个不少,git diff 贴进 PR 正文让「名字集合不变」可核。

    第 2 项 —— cast 收窄,⛔ 不要顺手扩大

    packages/fields/src/index.tsx 的 DateTimeCellRenderer 现在写 const style = (field as any)?.format || 'compact'。

    要收窄多少是有依据的,不是审美:FieldMetadata 是 37 成员联合,BaseFieldMetadata 上没有 format,format 只声明在 Date/DateTime/Time/Phone/AutoNumber 上 ⇒ 直接 field.format 是 TS2339。某种收窄是载荷的。但 as any 比需要的宽:DateTimeFieldMetadata 从 @object-ui/types 导出(types/src/index.ts:21),而该文件已经从那里 import 类型 ⇒ (field as DateTimeFieldMetadata | undefined)?.format 零成本。

    ⚠️ 复审当时被援引的 DateCellRenderer 先例做的事更多 —— 它还读 accessorKey / key,那些确实不在 DateFieldMetadata 上。这里只读 .format,而 .format 就在那个子类型上。复审判词:lazy, precedented, not load-bearing。

    ⛔ no-explicit-any 在本仓是 warn 级,两种写法 CI Lint 都绿 —— 这正是它需要一张卡而不是一道门的原因。⛔ 不要顺手把规则升成 error,⛔ 不要顺手清理文件里其他的 any:那是另一张卡的事,塞进来就是擅自扩面。

    变更集

    第 2 项动了发版包的 src/ ⇒ 按 AGENTS.md:161 机械要求,必须新增一个自己的 .changeset/*.md。⛔ 那是新文件,与第 1 项要更正的 7443-datetime-compact-style.md 是两回事,别混、别把第 1 项的更正当成「已经有变更集了」。纯类型收窄不发版 ⇒ 写空 frontmatter(--- 紧跟 ---)显式声明不发版,那是合法的一等通过写法。⛔ 永不写 major(scripts/check-changeset-no-major.mjs 会拒)。

    验收

    • pnpm --filter @object-ui/fields test 与 type-check 绿,报出实际文件数。
    • 第 2 项的收窄确实成立要证明,不是声称:在收窄后的那一行上做一次点亮控制(比如临时把它换成一个不含 format 的子类型),确认 tsc 变红并指名该行;然后在 trap … EXIT INT TERM 下恢复,用 blob hash 与空 git diff HEAD 证明已还原。⛔ 不要用 as any 版本当对照 —— 那个版本本来就不会红,测不出任何东西。
    • 第 1 项贴 git diff,显示只有散文行变化、frontmatter 与包名集合不变。
    • 报告里写清 DateTimeCellRenderer 对不认识的 format 的实测行为;⛔ 没测出来就写「未测出」,不要写猜测。

    契约复审

    Clause-② 是 no ⇒ 本卡不排在契约复审后面。⚠️ 若工作过程中发现必须动运行期行为,那就是上面声明的作废条件:停下、上报。


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    PM 收单(round 6):dev 席回报 status: done,PR objectui#7792 已开(draft)。两项都交付,并带回一条对卡面的更正**。**

    已核对

    判据 读数
    第 1 项的硬约束(包名一个不能少) check-changeset-overwrite.mjs exit 0,且它打印的「declared at base」与「declares now」完全相同,三个包名全部保住 ⇒ 本席在预清障里立的那条约束可核地满足了,不是声称
    第 2 项 (field as any)?.format → (field as DateTimeFieldMetadata | undefined)?.format,外加给既有的 @object-ui/types 类型 import 补一个词。该文件的 no-explicit-any 警告 137 → 136,且减掉的那条就是被改的那一行
    变更集 新文件 .changeset/7747-datetime-format-cast-narrowing.md,空 frontmatter;check-changeset-presence / -no-major / -fixed / -overwrite 全 exit 0
    Clause-② 仍 no,且是测过的:cast 在 emit 时被擦除,改动后逐行复现了改动前的行为 ⇒ 作废条件是被检查过而不是被假定
    测试 pnpm --filter @object-ui/fields type-check 两条腿都 exit 0;仓根 vitest run packages/fields/ = 133 files / 2178 tests 全过

    ⭐ 点亮控制做对了两个细节

    1. ⛔ 没有用 as any 版本当对照 —— 派发令点名禁止过,理由是它永远不会红,测不出任何东西。它换成了 BooleanFieldMetadata(同一个 barrel 导出、且不带 format):tsc --noEmit exit 2,恰好一条错误,src/index.tsx(889,62): error TS2339: Property 'format' does not exist on type 'BooleanFieldMetadata' —— 指名到行到列。
    2. ⭐ 还原的证明靠 hash,不靠「trap 有没有触发」:POST_RESTORE c16ba226 等于 HEAD blob c16ba226,git diff HEAD 为空,真实 cast 出现恰一次。它还点出 git checkout HEAD -- <绝对路径> 与裸形式的区别(后者从 index 恢复)。这两条都是本席后续派发令会继续要求的形状。

    ⭐ 对卡面的一条更正 —— 它加强而不是削弱本卡的论点

    卡面写的是「@objectstack/spec 在 datetime 字段上没有声明 format」。实测:spec 的字段 schema 确实带 format —— 但它长在一个扁平的、非判别式的、被所有字段类型共用的 field schema 上,类型是自由字符串可选项,没有 enum、没有按类型的值域,描述写着 "Format string (e.g. email, phone)",并且 'compact' / 'short' / '' / 'not-a-real-style' 一律接受。

    ⇒ 准确的表述是:键存在,而 datetime 的值词汇表确实不存在。 这正是卡要求「点名这个缺口而不是粉饰它」的那个缺口,只是位置比卡面说的更精确。⭐ 本卡的可行动结论一字未变,所以 premise_still_valid 为真 —— 这是把论点钉得更死,不是推翻它。

    ⭐ 未识别的 format 到底会怎样 —— 测出来了,答案是三个猜测都不对

    临时 harness(读完即删,不在 PR 里),固定一个时刻 2024-07-04T07:00:00.000Z、en-US,跑九个取值:

    取值 渲染
    缺席 · '' · null · 'compact' 紧凑面 7/4/2024 7:00 am
    'not-a-real-style' · 'short' · 'relative' · 'YYYY-MM-DD' · 数字 42 冗长面 Jul 4, 2024, 07:00 AM

    ⇒ 一个不认识的值不抛错、不原样透传、也不回退到 'compact' —— 它静默选中 formatDateTime 的冗长默认面。⛔ 派发令里我列的三个猜测(回退 compact / 抛错 / 原样输出)一个都不对,这正是为什么要求去测而不是写猜测。

    后续件

    objectui#7791 —— CapabilityMultiSelectField.specParity-6285.test.tsx 把 VOCABULARY_ROOT 设成 '.'(进程 cwd),于是 pnpm --filter @object-ui/fields test 让它 2/7 红,而仓根形式 7/7 绿 —— 一次假红,恰好发生在 AGENTS.md 声明「与 CI 同一结论」的那条调用上。到达时无标(半状态),本席已分诊为 p2 并已派发(认领评论 5554505191)。派发令里明确:⛔ 只修测试侧路径解析,AGENTS.md 是治理面、一个字节都不许碰,那句被证伪的话写进 PR 正文交维护者定夺。

    ⇒ 下一步:等 CI 终态 → 逐项枚举 → 转 ready-for-review → 入队。


    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    ContributorAuthor

    落地确认:PR #7792 已合并,本卡由 Fixes 自动关闭。

    项 读数
    merged true @ 2026-09-05T20:42:32Z
    merge_commit_sha dfcbedee5e8ff7241cb34bb08e68e04fe6c778a1
    diff 3 files, +41 / −4
    入队 → 合并 added_to_merge_queue 20:27:39Z → merged 20:42:32Z,其间无 removed_from_merge_queue
    闭合关键词 正文只有 7747 一个 ⇒ 无误伤
    本卡 closed_at 20:42:33Z
    后续载体 objectui#7791 open,pm:dispatched(本席已分诊 p2 并派发)

    ⚠️ 两条留给未来引用者的读数

    1. 本卡正文说「@objectstack/spec 在 datetime 字段上没有声明 format」——不准确。 实测:spec 的字段 schema 确实带 format,但它长在一个被所有字段类型共用的扁平、非判别式 schema 上,类型是自由字符串可选项,无 enum、无按类型值域,描述写着 "Format string (e.g. email, phone)",并且 'compact' / 'short' / '' / 'not-a-real-style' 一律接受。⇒ 准确表述:键存在,而 datetime 的值词汇表确实不存在。 这是把本卡的论点钉得更死,不是推翻 —— 落地的散文按这个准确形状写。

    2. ⭐ 未识别的 format 的实际行为,测出来了(临时 harness,读完即删、不在 PR 里;固定 2024-07-04T07:00:00.000Z / en-US,九个取值):

      取值 渲染
      缺席 · '' · null · 'compact' 紧凑面 7/4/2024 7:00 am
      'not-a-real-style' · 'short' · 'relative' · 'YYYY-MM-DD' · 数字 42 冗长面 Jul 4, 2024, 07:00 AM

      ⇒ 不认识的值不抛错、不原样透传、也不回退到 'compact' —— 它静默选中 formatDateTime 的冗长默认面。⛔ 派发时列出的三个猜测一个都不对。

    变更集覆写的硬约束,可核地满足了

    check-changeset-overwrite.mjs exit 0,且它打印的「declared at base」与「declares now」完全相同 ⇒ 改的是散文,frontmatter 逐字节未动,三个包名全部保住。


    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

    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfindingpm:dispatchedpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions