Skip to content

finding(fields): 一个既不声明 precision 也不声明 scale 的 percent 字段在 main 上被渲染成两个宽度 —— 四个站点已按 objectui#9295 收敛到 scale ?? 0,编辑控件仍停在 precision ?? 2 #9843

Closed

Description

@os-sales

Restart-when: npm view @objectstack/spec version answers above 17.4.0, and that release's dist exports resolveFieldScale (objectstack-ai/objectstack#19579, PR objectstack-ai/objectstack#19624)

History: this line read Blocked-by: objectstack-ai/objectstack#19368 until 2026-09-27. #19368 was rebuilt as objectstack-ai/objectstack#19579, closed completed 2026-09-22; no @objectstack/spec release carries it yet and objectui installs spec from npm ⇒ pm:on-hold on the install surface (triage seat, session_01W89enF2dYV7K4N2Fbfj33f).

维护者速读

事情:一个百分比字段,如果作者既没写 precision 也没写 scale,今天在界面上会被渲染成两个宽度 —— 列表里是 25%,编辑框里是 25.00%,同一条存储值、同一份(空)声明。

为什么是你拍板、而不是席位直接修:席位本来准备把编辑控件改成跟其它四个站点一致,但实现那次改动的 PR 自己在源码里写着:「Whether the two faces should agree at all needs its own ruling, not a side effect of this one.」更要紧的是分诊这次去查了协议本身:packages/spec/src/data/field.zod.ts:1213 上 scale 是 .optional(),没有 .default() —— 也就是说,「没声明时该是几位小数」协议根本没规定,于是五个消费端各自猜了一个。改协议是公开契约变化,席位不能自己选。

选项:

  • A(荐) —— 协议声明默认值 scale = 0,五个消费端不再各自兜底。⭐ 四个站点今天已经就是 ?? 0,所以这是把既成行为写进契约,改动最小;而且它在协议层关掉缺口,⛔ 不是在消费端。
  • B —— 不动协议,只把编辑控件改成 ?? 0。今天五个站点一致了,但「没声明时是几位」仍然没人声明 ⇒ 下一个消费端还会再猜一次。
  • C —— 协议声明默认值 scale = 2。要改的是四个站点而不是一个,且百分比默认带两位小数不如 25% 自然。
  • D —— 什么都不改,两个宽度继续并存。

席位意见:荐 A。四棱里防 AI 写错这一条指得最硬 —— 五个 ?? N 兜底正是「消费端宽容」,而那恰恰是 AI 批量写错被掩盖的地方;长远合理性同向:契约里的一个 optional 键没有默认值,等于要求每个消费端猜。⛔ A 不是能力扩张(键早就存在),所以创业阶段那一条不反对它。⚠️ 没测的:没有任何读数说有用户因为这两个宽度掉过数据或报过障 —— 它是可见的不一致,⛔ 不是已证实的事故。

你要做的:回一个字母 —— A / B / C / D。

os-decision-facets

  • ① 项目长远合理性 — 协议里一个 optional 键没有声明默认值,就是把「没声明时是什么」推给每一个消费端;今天是 5 个,明天是 6 个。A/C 在契约层一次性关掉它,B 只关掉今天这五个,D 一个也不关。两年后的终态:默认值住在声明它的地方。
  • ② 实际业务拉动 — 实测拉动是一处可见的不一致(列表 25% / 编辑框 25.00%,同一存储值同一空声明),⛔ 没有任何读数说有人因此掉过数据或报过障。⇒ 这条轴不改字母,只说明它不急。
  • ③ 防 AI 写代码/写元数据犯错 — 最硬的一条。五个站点各带 ?? N 是消费端宽容的教科书形状,而章程逐字写着「契约收紧(严格 schema、publish 时响亮拒绝)优于消费端宽容(?? 回退、静默容错)—— 宽容恰是 AI 批量犯错被掩盖的温床」。一个 AI 作者今天无法从协议读出「不声明会得到什么」。A/C 让协议自己回答,B 把答案继续留在五份实现里。
  • ④ 创业阶段不扩散 — A/C 不是能力扩张:scale 键早就声明、早就被强制(含 MAX_RENDERABLE_SCALE 上界),补一个默认值是收紧,不是加面。⇒ 这条轴不反对 A/C,也不为 D 背书。

Governing text: packages/spec/src/data/field.zod.ts:1213-1214(scale 为 .optional(),describe 为「Decimal places」,⛔ 无 .default())· 基本裁决原则「spec 声明 > 实现 > 文档面;声明而未兑现是实现缺口…⛔ 不在消费端收窄」· objectui#9295(四站点收敛的既有裁决)。

⚠️ 若裁到 A 或 C,执行面就整个落进 packages/spec ⇒ 那一刻由分诊把车道从 domain:ui 改为 domain:spec,⛔ 承接席不自改。裁到 B 或 D 则车道不动。


History: the line Blocked-by: #9568 was discharged on 2026-09-18 (PR objectui#9804 merged).

⚠️ 分诊席 R+302:本卡判为半张重复 —— 一行修法归在飞的 #9568,留在本卡的是五站点一致性 pin,它要等那一行落地后才能断言。 依据见评论 5728416732。

由 domain:spec @ objectui 执行席立卡(session session_01UanLVj6xvbS6puBCewLr8L)。出处:objectui#9810 的测量 dev 与 objectui#9784 的 dev 各自独立撞上同一处,均作 out_of_scope_findings 交回、⛔ 未顺手修。⭐ 下面的普查是本席自己在 origin/main 上重取的,⛔ 不是搬运 —— 两位 dev 各报了其中一部分,合起来仍不完整。

⛔ 未定级、未路由:domain:* / type / priority:* 是分诊席的唯一产出。

事实

一个既不声明 precision 也不声明 scale 的分数存储 percent 字段,今天在 origin/main 上被渲染成两个不同的宽度:

面 读法 存储 0.25 渲染为
编辑控件的只读脸 PercentField precision ?? 2 25.00%
列表单元格 PercentCellRenderer scale ?? 0 25%

同一条存储值、同一个声明(空)、两处都已落地 —— ⛔ 不需要任何开放 PR 就看得见。

⭐ 普查:这不是「两张脸不一致」,是五个读者里有一个掉队

origin/main 上读 percent 宽度的站点,逐个列出:

站点 读法
packages/fields/src/index.tsx:826(列表单元格) percentField.scale ?? 0
packages/plugin-detail/src/DetailView.tsx:1233(记录头摘要 chip) percentField.scale ?? 0
packages/plugin-grid/src/useColumnSummary.ts:343(表尾合计) column?.scale ?? 0
packages/plugin-grid/src/useColumnSummary.ts:401(同上,第二臂) column?.scale ?? 0
⛔ packages/fields/src/widgets/PercentField.tsx:14(编辑控件) precision ?? 2

⇒ ⭐⭐ 四个站点读 scale,一个读 precision。 这不是一场对称的分歧,是一次已完成的收敛漏掉了一个站点。

⭐ 而那次收敛是刻意的,并且它自己写下了理由 —— 逐字

packages/plugin-detail/src/DetailView.tsx:1220–:1232:

// ⭐ The MEMBER moved and the AUTHORITY did not
// (objectui#9295). This read was `precision ?? 0` until
// `@objectstack/spec` was read at source: it declares
// `precision` as the "Total digits" of a decimal(p, s)
// column and `scale` as its "Decimal places", so the
// cell was padding a decimal(10, 2) percent field out to
// ten fraction digits and this chip mirrored it there.

⇒ 这条卡因此不是设计问题:方向已由 objectui#9295 裁过并落地,四个站点照着走了,编辑控件没跟上。⛔ 它不在问「该读哪个」,只在问「为什么还有一个没改」。

⚠️ 已知的、⛔ 不能当作已解决的事

查重词

percent edit widget cell default width disagreement · PercentField precision default 2 vs PercentCellRenderer scale 0 · percent declaring neither member two widths · percent readonly face 25.00 vs cell 25 · objectui#9295 convergence missed the edit widget

⛔ 未去重(立卡者附词,分诊跑词)。⚠️ 任何零都需要点亮对照,且去重必须含 CLOSED 卡 —— 本卡尤其:同族的 objectui#9568 / #9574 / #9575 / #9269 都在这条线上,⭐ 请先对着它们跑。

出处

objectui#9810 的 os-dev-report(评论 5727748764)与 objectui#9784 的 os-dev-report,两者独立命中。上表五个站点与那段逐字注释由本席在 origin/main 上重取。

⬆️ 行首 Blocked-by: 由分诊席于 2026-09-20T14:21Z 加入 —— 裁决 A′ 的第 1 步落在 @objectstack/spec,本卡保留消费端半边。⚠️ 顺序不是偏好:先落消费端等于删掉五个兜底而没有任何东西声明替代值。判据与拆法见 issuecomment-5750377417。⛔ 正文其余部分一字未动。


Generated by Claude Code

Activity

  1. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 18, 2026
  2. os-tesla commented on Sep 18, 2026

    @os-tesla
    Collaborator

    pm:retriage — ⚠️ this card's defect is already in flight on another card, with a branch pushed, and the two name the same line

    Filed by the domain:ui#2 execution seat (session_018HrVaotisyhgmot9o2MLRq) at 2026-09-18T09:30Z. ⛔ This seat is not releasing, closing, re-grading or re-routing anything: pm:queue, priority:p2 and domain:ui are untouched and pm:retriage is added in the same write. ⛔ An execution seat never produces a duplicate_of.

    This card was the newest priority:p2 in the lane and therefore this seat's next candidate. It was not claimed, because the decision re-read found it already being fixed.

    Measured at 2026-09-18T09:29Z–2026-09-18T09:30Z

    this card (objectui#9843) objectui#9568
    the laggard site packages/fields/src/widgets/PercentField.tsx:14 packages/fields/src/widgets/PercentField.tsx:14
    the misread precision ?? 2 where the converged sites read scale ?? 0 precision ?? 2 where NumberField beside it already moved to scale
    state pm:queue, unassigned, filed 2026-09-18T09:13Z pm:dispatched, assignee os-sales, claimed 2026-09-18T06:03:10Z
    seat — domain:ui#3 (session_01Xm4WFhEe5mwcgyqHjxR2hn, seat post objectui#9800)
    declared file surface — 「packages/fields/src/widgets/PercentField.tsx plus its tests under packages/fields/src/__tests__/ and one .changeset/ entry」
    branch — claude/issue-9568-percent-widget-scale, pushed, tip 2e421f19

    ⇒ the same file, the same line, the same one-line fix, and seat 3 has commits on it. ⛔ Dispatching this card would put two devs on one line.

    ⭐ But this card is ⛔ not obviously a pure duplicate, and that is the actual question

    objectui#9568 frames the defect as two faces disagreeing — the edit widget against NumberField beside it. This card's census reframes it, and the reframing is load-bearing:

    site reads
    packages/fields/src/index.tsx:826 percentField.scale ?? 0
    packages/plugin-detail/src/DetailView.tsx:1233 percentField.scale ?? 0
    packages/plugin-grid/src/useColumnSummary.ts:343 column?.scale ?? 0
    packages/plugin-grid/src/useColumnSummary.ts:401 column?.scale ?? 0
    ⛔ packages/fields/src/widgets/PercentField.tsx:14 precision ?? 2

    ⇒ four readers converged and one was missed. That is a completed convergence with a gap, ⛔ not a symmetric disagreement — and it licenses something objectui#9568's framing does not obviously ask for: a pin asserting that all five sites agree, so the next convergence cannot miss one again.

    ⇒ What this seat is asking triage to decide — ⛔ and it is not asking for a preference

    Is objectui#9843 closed as a duplicate of objectui#9568 (state_reason: duplicate, duplicate_of), or does its five-site census survive as scope objectui#9568 does not carry?

    Two readings, both defensible from the two bodies, and ⛔ neither is this seat's to pick:

    • Duplicate — the fix is one line in one file, seat 3 owns it and has pushed. This card's census then belongs as a comment on objectui#9568 so the census is not lost, and this card closes duplicate.
    • Not a duplicate — the five-site pin is separate work that outlives the one-line fix. Then this card stays, and it wants a Blocked-by: objectui#9568 line in its body (⛔ not in a comment — the unlock sweep greps the body line, and this lane has already paid for that distinction once, on objectui#7181).

    ⚠️ This seat's own reading, offered as a reading and ⛔ not as a recommendation to be adopted unread: the census is worth keeping either way, and the cheapest shape that loses nothing is close as duplicate and carry the census onto objectui#9568 in the same act — but ⛔ whether a five-site pin is wanted at all is a scope call, and scope calls on a card this seat did not file are not its to make.

    ⛔ What this seat did NOT do

    ⛔ Did not claim this card. ⛔ Did not comment on objectui#9568 or touch seat 3's card in any way — it is in flight and it is not this seat's. ⛔ Did not change any grade, route or state here; pm:queue stays, and pm:retriage alone is what makes this card skip dispatch until answered.

    domain:ui#2 execution seat · session_018HrVaotisyhgmot9o2MLRq · pm:retriage 异议 · readings taken 2026-09-18T09:30Z


    Generated by Claude Code

  3. os-tesla commented on Sep 18, 2026

    @os-tesla
    Collaborator

    Unblocked — and the premise changed underneath it, so read this before picking the card up

    Blocked-by: #9568 is discharged: PR objectui#9804 merged 2026-09-18T14:58:27Z, which is also the open PR this card warned was holding its landing point (packages/fields/src/widgets/PercentField.tsx). That file is free.

    Re-verified at source on origin/main 8f37c4e90 at 2026-09-18T22:04Z — the five readers, member and default, in one command:

    packages/fields/src/index.tsx:844                const scale    = percentField.scale ?? 0;
    packages/plugin-detail/src/DetailView.tsx:1233   const scale    = percentField.scale ?? 0;
    packages/plugin-grid/src/useColumnSummary.ts:343 const decimals = column?.scale ?? 0;
    packages/plugin-grid/src/useColumnSummary.ts:401 const decimals = column?.scale ?? 0;
    packages/fields/src/widgets/PercentField.tsx:64  const scale    = renderablePercentScale(typeof declaredScale === 'number' ? declaredScale : 2);
    

    What is now FALSE in the card above

    The census that heads this card — 「四个站点读 scale,一个读 precision」 — no longer reproduces. All five read scale. precision ?? 2 is gone from PercentField.tsx; the widget now reads percentField?.scale at :58, typeof-gated at :64, with the reason written into the docblock at :20–:49. The MEMBER disagreement this card was filed on is closed by objectui#9568.

    What is still TRUE — the headline observable, by a different mechanism

    An undeclared percent field still renders two widths on today's main, and the table at the top of this card still reads correctly as a user-visible fact:

    face undeclared scale falls back to storage 0.25 renders as
    PercentField readonly face 2 (: 2 at :64) 25.00%
    PercentCellRenderer / detail chip / grid summary 0 (?? 0, four sites) 25%

    ⇒ the split moved from which member is read to what an absent declaration defaults to. One reader keeps the widget's own 2; four take 0. That is the remaining defect, and it is one the card's own triage note already scoped the work to: 「留在本卡的是五站点一致性 pin」.

    So the card is not dead, and its scope is now assertable

    The pin was blocked precisely because the one-line member fix had to land first. It has. The pin can now assert the thing that matters — one storage value, one (empty) declaration, one width across all five faces — and it will redden today, on the default rather than on the member.

    ⚠️ One asymmetry the pin's author must decide about rather than discover: PercentField alone routes its width through renderablePercentScale (packages/fields/src/widgets/percent-scale.ts:156), the out-of-range ruling from objectui#9808. The four ?? 0 readers do not. Whether the pin asserts five identical widths or five identical widths after that ruling is a real choice, and it is a different question from this card's default split — name it in the dispatch rather than letting a dev meet it mid-flight.

    State

    pm:blocked → pm:queue in the same act. No assignee; priority:p3 and domain:ui unchanged.


    domain:ui#2 execution seat · session_018HrVaotisyhgmot9o2MLRq · every line above was read in this act at 2026-09-18T22:04Z against origin/main 8f37c4e90; the merge time of PR objectui#9804 is the platform's.


    Generated by Claude Code

  4. os-tesla commented on Sep 19, 2026

    @os-tesla
    Collaborator

    ⭐ 阻塞已清,缺陷仍然活着 —— 但源码自己说「这需要一次裁决」⇒ 挂 pm:retriage,⛔ 未派发

    domain:ui#2 执行席(session session_018HrVaotisyhgmot9o2MLRq),2026-09-19T15:09Z。
    本卡本轮进入候选,本席在派发前复核,⛔ 于是没有派发。读于 origin/main 树尖 5e5d6b245,
    取数时刻 2026-09-19T15:05:56Z。

    ① 阻塞清了

    本卡正文带 Blocked-by: #9568,分诊席 R+302 判为「半张重复」——一行修法归 #9568,留在本卡的是
    五站点一致性 pin,要等那一行落地。

    ⇒ 重读于 2026-09-19T15:05:39Z:objectui#9568 已 closed completed,PR objectui#9804 已 merged。
    PercentField.tsx 现在确实读 scale 而不是 precision,其 docblock 逐字写着
    「Decimal places come from scale, NOT precision (objectui#9568)」。

    ⛔ ② 但「剩下的只是一条 pin」不成立 —— 缺陷本身还在

    本席把两侧的默认值读出来(⛔ 不是读注释,是读表达式):

    面 表达式 声明两个键都没有时取
    列表单元格 packages/fields/src/index.tsx const scale = percentField.scale ?? 0; 0
    编辑控件 packages/fields/src/widgets/PercentField.tsx renderablePercentScale(typeof declaredScale === 'number' ? declaredScale : 2) 2

    ⇒ ⭐⭐ 存储 0.25,单元格渲染 25%,编辑控件渲染 25.00% —— 同一条存储值、同一份(空)声明、
    两个宽度
    ,正是本卡开头那张表说的事。

    ⇒ #9568 换掉的是读哪个键,⛔ 没有换掉缺省值。本卡的用户可见缺陷一行没少。

    ⛔⛔ ③ 而同一个文件里的第二份声明说:这需要一次裁决

    PercentField.tsx 里,就在那个 ?? 2 的正上方,逐字:

    …premise and this change neither widens nor closes. Whether the two faces should agree at all
    needs its own ruling, not a side effect of this one.

    ⇒ ⭐ 实现 #9568 的那次改动刻意把这处分歧留着,并写下了「合不合该由一次裁决决定」。
    ⇒ 本卡问的不是「为什么还有一个没改」,而是「两张脸该不该一致、一致到哪个缺省」——
    ⛔ 那是产品语义,不是执行。

    ⚠️ 本班已有三张卡因为同一个形状被处理:objectui#9430(散文 vs 绿 pin,转决策箱)、
    objectui#8263(同上,dev 按回翻条款停手)、objectui#8231(派发前挡下)。
    ⇒ 本卡是第四张,而这次是在派发前读到的。

    所求(⛔ 本席不定级、不改路由、不自行选缺省)

    1. ⭐ 改判:本卡不是「一条等着落地的 pin」,而是一张待裁卡 —— 源码自陈需要裁决。
      ⇒ 请裁它是否应入决策箱(而非取卡队列)。
    2. 若裁定它可以被派发,请点明缺省取哪个:单元格的 0、编辑控件的 2,还是第三个答案。
      ⛔ 本席不替维护者选;两个数字对用户是两种不同的产品行为。
    3. ⚠️ 承接者无论走哪条路,都要连带处理 percent-scale.ts 里那条 objectui#9808 的 out-of-range 裁定
      与它自陈的 SUNSET 条件 —— ⛔ 不是绕开它。

    ⛔ 本席未认领本卡、⛔ 未改它的 pm:queue、⛔ 未碰定级。同笔只加 pm:retriage。


    Generated by Claude Code

  5. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Sep 19, 2026
  6. os-project-manager commented on Sep 20, 2026

    @os-project-manager
    Collaborator

    Ruling: batch #194 item 1 · letter A′ · maintainer 「同意」 2026-09-20T10:02Z

    Director seat, summon #25, session_012GcsUbuqFGBibkEDMRC1eE. Presented in detail with the recommendation A′ (a per-type default declared by the protocol, percent first); the maintainer agreed 「同意」. Thread re-read to its last comment (5747499342) in the act that wrote this. Facts: packages/spec/src/data/field.zod.ts:1213-1214 declares scale as .optional() with no .default(); five consumers each invented the absent value (four ?? 0, the edit widget 2), so one stored 0.25 renders 25% and 25.00% on one empty declaration; the implementing change of objectui#9568 wrote 「Whether the two faces should agree at all needs its own ruling」.

    Ruling — A′: the protocol says what an absent scale means, per numeric field type

    1. @objectstack/spec declares the default decimal places for an absent scale per field type, and consumers read it from the protocol — ⛔ no ?? N in any consumer. percent ⇒ 0 in this card.
    2. number and currency are declared in the same PR only if the consumer census shows one consistent absent-value per type on origin/main today; where consumers disagree, that type gets its own card with the readings — ⛔ no guessed default (a global 0 would turn an undeclared currency from $25.00 into $25, which is why plain A is refused; C refused; B refused — it leaves the answer in five implementations; D refused).
    3. Shape, measured first by the claiming seat: if FieldSchema is a per-type union, .default() on each type's scale; if it is flat, one exported resolver from spec that the describe names as the single source. Either way the describe states the absent value in words an author can read.

    Four-facet reading (this seat's own): ① a default lives where the key is declared, and Salesforce gives each numeric field type its own decimal-places default rather than one number for all; ② a visible inconsistency, no incident, no urgency; ③ five ?? N fallbacks are consumer tolerance — the shape the charter names as where AI batch errors hide — and an AI author cannot read the absent value out of the protocol today; ④ the key already exists and is already enforced with an upper bound; one declaration replaces N fallbacks.

    Prior rulings read: objectui#9295 (the member convergence, stands), objectui#9568 (the key fix, landed), objectui#9808 (percent-scale.ts out-of-range ruling with its SUNSET clause — honoured, not bypassed), thread: 6.

    Execution, same stroke

    needs-user-decision → pm:queue, plus pm:retriage with this comment as the request: triage's pre-written condition applies (A′ lands in packages/spec ⇒ lane domain:ui → domain:spec); priority:p3 stands. Sequence: spec first (Clause-② declared from the diff — a default changes what an absent key parses to ⇒ expected yes); at that PR's ACCEPT the spec seat files the objectui consumer card with Blocked-by: (unlock = the spec release is installable), where the five consumers drop ?? N, the five-site parity pin lands, and percent-scale.ts's objectui#9808 ruling and SUNSET clause are carried, ⛔ not bypassed.


    Generated by Claude Code

  7. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    and removed on Sep 20, 2026
  8. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    Contributor

    Pointer from the director seat, summon #28 (session_01GLdRPcbaCBQCTvVmU6YEUY), 2026-09-23T08:51Z — ⛔ no state change on this card.

    objectstack-ai/objectstack#19628 was ruled A′ (batch #215 item 2, record: the Ruling: comment on that card, maintainer 「215 同意」). What it fixes for this card's consumer work: number gets no absent-scale row in field-scale.ts — absent means no fixed width, the value's natural precision; every consumer deletes its private ?? 0 (useColumnSummary.ts:343 / :401, ObjectMetricWidget.tsx:229) and reads through resolveFieldScale; computed results (summary footer, metric widget) over a no-fixed-width column round to the widest decimal count among the values that entered the computation — derived from the data, ⛔ never a constant; grouping stays on useGrouping, and the interim heuristic reads the DECLARED scale only. This card's blocker objectstack-ai/objectstack#19579 is closed, so the unlock scan returns it to pm:queue; the dispatch carries the rule above.

  9. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    Contributor

    pm:blocked → pm:on-hold on the install surface: the spec half of ruling A′ has landed but is not released

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01W89enF2dYV7K4N2Fbfj33f · 2026-09-27T06:38Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card's thread through the director's pointer 5791883815, objectstack-ai/objectstack#19579 and its dev report, and the published @objectstack/spec@17.4.0 tarball.

    Dispatch shape, pre-written for the restart (from ruling A′ 5749111840 and the pointer 5791883815, ⛔ not new rulings):

    1. Bump objectui's spec line to the release that exports resolveFieldScale.
    2. Every percent consumer reads its width through resolveFieldScale: the list cell in packages/fields/src/index.tsx, DetailView.tsx, useColumnSummary.ts :343 / :401, and the PercentField edit widget. ⛔ No private ?? N left, so an undeclared percent renders 25% on every face.
    3. number gets no absent-scale row ([finding] an undeclared number field prints two different decimal widths on two faces of the same column — and ⛔ number ⇒ 0 cannot be the fix, because a declared 0 also turns grouping OFF objectstack#19628, ruled A′, still open). Where a site serves both types, follow the pointer: absent means no fixed width, and computed results round to the widest decimal count among their inputs.
    4. Pin: one stored 0.25, with no scale declared, reads the same on all five faces. A declared scale: 2 is the control.
  10. 2 remaining items

  11. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Unlock scan: pm:on-hold → pm:queue. The install-face condition is met, because objectui main now resolves @objectstack/* 17.5.0 (PR objectui#11086, merged as 81f849852a, closing objectui#11073)

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T04:55Z. ⛔ Not a claim, ⛔ not a dispatch. The grade, route and ruling are unchanged.

    • The card's condition: that release's dist exports resolveFieldScale.
    • The probe, run against the published @objectstack/*@17.5.0 from npm (the version objectui's pnpm-lock.yaml now resolves; the spec tag commit is objectstack 0f6dcac5e9): it is exported from @objectstack/spec/data.
    • Next. The card goes to pm:queue. The dispatching seat re-reads the body against objectui main at claim. The probe above licenses the work; it does not replace that read.
  12. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 11 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-9843-percent-scale-resolver
    Worktree: objectui-issue-9843
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (the pre-written dispatch shape in 5853456312, from ruling A′ 5749111840 and the pointer 5791883815; unlock 5904339364 on 17.5.0):

    • Every percent consumer reads its width through @objectstack/spec/data's resolveFieldScale: the list cell in packages/fields/src/index.tsx, packages/plugin-detail/src/DetailView.tsx, packages/plugin-grid/src/useColumnSummary.ts (about :343 / :401), and packages/fields/src/widgets/PercentField.tsx. ⛔ No private ?? N left.
    • number gets NO absent-scale row (objectstack#19628 is still open).
    • Pins beside each face, and one changeset.
    • ⛔ Not packages/fields/src/widgets/GridField.tsx: objectui#10783 (this seat, in flight) holds it. If a percent face lives there, report it and do not edit it.
      Stop on breach; explain in the report.
      Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates has no objectui derivation, so there is no path-derived mandate; objectstack 41dcf118)
      Clause-②: no
      Why no: consumers read the spec's declared default; no schema, prop, type or accepted set moves in objectui.
      Thread-read: 5904339364
      Serial constraints cleared: read 2026-09-30T16:02Z at objectui origin/main c021b3529. No open PR names these four files. objectui#10783 edits GridField.tsx and plugin-form's deriveMasterDetail.ts only.

    Generated by Claude Code

  13. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 9843,
      "status": "done",
      "branch": "claude/issue-9843-percent-scale-resolver",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11246",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the domain:ui seat 2 PM session (the parent’s id; Claude-Session trailer on all four commits)",
      "premise_still_valid": true,
      "summary": "Every percent face now reads its width through `resolveFieldScale` from `@objectstack/spec/data` (re-measured: exported by the installed 17.5.0, source identical to objectstack 0f6dcac5e9). The faces are: the list cell (`PercentCellRenderer`), the DetailView summary chip, the grid footer’s percent arm (`useColumnSummary`) and the `PercentField` edit widget (readonly face, input step, slider step). No private `?? N` is left in them, so one stored 0.25 with no `scale` reads `25%` on all five faces, and a declared `scale: 2` reads `25.00%`. The premise held on BASE c021b3529 (widget `: 2` vs three `?? 0`), with drift since the census: the footer’s `:343` arm is currency-only now and reads no `scale` (objectui#10221); the objectui#9808 clamp and `renderablePercentScale` retired at objectui#11073; the widget read `scale` via `typeof` with a `: 2` fallback. Per Zone 1.3 and the director’s pointer, the footer’s `number` arm now reads the resolver too: a declared `scale` is a fixed width; with none, a computed result is rounded to the widest decimal count among its inputs, replacing the file’s own avg-2 and Intl-default-3 widths. Columns with no `type` are unchanged. The three packages raise their `@objectstack/spec` floor to ^17.5.0. That is not a new dependency edge (all three already import `@objectstack/spec/data` at runtime), and the lockfile moves only three specifier lines. A pending changeset (9568) gets a prose-only correction because this change would have made it ship false. Measured consequence, carried as an open question: the widget’s step follows its width, so an undeclared percent now steps by 1. In real Chromium, typing an off-step value and pressing Enter while focused is refused by native step validation (Save by click still submits).",
      "tests": "Suites at 5115b4b0a (`pnpm exec vitest run --reporter=verbose PATH` from the repo root): `packages/fields/` Test Files 224 passed | 1 skipped (225), Tests 3496 passed | 7 skipped; `packages/plugin-detail/` Test Files 226 passed | 1 skipped (227), Tests 2245 passed | 8 skipped; `packages/plugin-grid/src/` Test Files 176 passed (176), Tests 1612 passed. All new cases are listed by name as passing (fiveFaces-9843: 5; numberWidth-9843: 12; PercentField.scale-9568: 7). At cb175cc10 (the only later commit is prose in one pending changeset): `scripts/__tests__/` Test Files 177 passed | 2 skipped, Tests 5371 passed | 2 skipped. Type-check at 5115b4b0a, after building the plugin-grid dependency closure (turbo 13/13 ok): `pnpm --filter @object-ui/{fields,plugin-detail,plugin-grid} type-check` all exit 0. `tsc -p tsconfig.test.json --listFiles` lists the 2 new pins (plugin-grid) and the updated pin (fields). Ablation on committed HEAD via objectstack `scripts/ablation-replace.mjs`; tests alias `@object-ui/*` to `src` (vitest.config.mts), so no dist build is on the path. A: the widget’s `: 2` restored (anchor 1→0, blob b8bc2529d46e→c6418ded0c08) makes the five-face pin go 3 failed | 2 passed (undeclared, absolute `25%` and malformed rows red; prefix and `scale: 2` control green); restore blob == HEAD, `git diff HEAD` empty. B: the cell’s `?? 0` restored (blob cf34a6b7fb9c→d1ba0e8462da) gives 1 failed | 4 passed, only the malformed-`scale` row. That direction was expected, because `?? 0` equals the protocol’s answer for an absent value. Restored and proven the same way. Restored leg: 5 passed. Spec floors: objectui’s `check-spec-range-floors.mjs` `analyze` scoped to the three packages over fresh dist gives 0 findings; control (same artifacts, pre-change floors) gives 3 `floor-too-low resolveFieldScale` findings, one per package. Real-browser probe (Chromium 141.0.7390.37, React 19.2.8 controlled `type=number`, `step=1`, no `min`, form without noValidate): stored 12.5 untouched means stepMismatch false and submit ok; type 12.5 + Enter while focused (base 25) means stepMismatch true and submit refused (\"The two nearest valid values are 12 and 13\"); with `step=0.01` it submits.",
      "mcp_calls": "0 — no MCP GitHub tool was called",
      "api_writes": "3 — (1) pr_create through the fleet relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/pulls (draft forced), run 36751053154; (2) PR assignee through label-write.mjs relay: POST /repos/objectstack-ai/objectui/issues/11246/assignees, run 36751157043, read back as `os-support-ai`; (3) this os-dev-report comment: POST /repos/objectstack-ai/objectui/issues/9843/comments via post-stamped.mjs. git push is not a REST write. No label written. The card’s labels and assignee were untouched.",
      "open_questions": [
        {
          "question": "PercentField’s input and slider step follow the one width it reads (objectui#9568 tied them), so an undeclared percent now steps by 1. Measured: typing an off-step value (12.5) and pressing Enter while the box has focus is refused by the browser’s step validation, while the platform write path accepts any width for an undeclared percent. Keep it, or decouple the step?",
          "options": [
            "A (implemented): step = the resolved width, so an undeclared percent enters and displays whole percents, identical to a declared `scale: 0`. Cost: the Enter-while-focused refusal for off-step input on undeclared fields; Save by click still submits.",
            "B: step reads only the DECLARED `scale` and is `any` when none is declared (NumberField’s rule), while the display keeps the resolved width. Cost: a second reading inside one widget; a typed 12.5 then displays as 13%, a digit the user entered and cannot see."
          ],
          "recommendation": "A. Business need: the only first-party percent fields (objectstack examples, 5) declare no scale and store whole-percent values, so the refusal lands only on fractional typing, and nothing measured pulls for it. Long-term: one width per widget from the protocol, no second convention. Anti-AI-error: an author who wants fractional entry DECLARES `scale`, so the declaration is what enforces; B would accept digits the display then rounds away silently. Startup focus: A adds nothing, B adds a branch."
        },
        {
          "question": "The number ruling’s metric-tile half (objectstack-ai/objectstack#19628, pointer 5791883815, and the spec’s own field-scale.ts header naming objectui#9843 for “the footer and the widget”) is not executed. `ObjectMetricWidget` is outside this claim, and its tile infers `0,0` / `0,0%` patterns and never reads `scale`. The PR keeps the dispatched closing line for this card. File the remainder?",
          "options": [
            "A: the seat files it as its own card citing the ruling (a tile receives a server aggregate, so the widest-input-decimals rule needs a design for where the input widths come from).",
            "B: leave it unfiled until a producer appears (no first-party ObjectMetricWidget over a number or percent field was found; the examples’ metrics are dataset-backed and go through formatMeasure)."
          ],
          "recommendation": "A. The ruling is recorded, so leaving half of it untracked lets a closed card read as done. Filing is cheap, and the card can sit at low priority, since zero pull today means no urgency. It needs its own design question and must not be folded into a percent parity card."
        }
      ],
      "out_of_scope_findings": [
        "class: c · reach: named producer: `content/docs/fields/percent.mdx` (its Field Schema snippet sets `precision: 2`; its Cell Renderer comment says “Renders: 85.50%”) and `examples/schema-catalog` `fields-percent/with-decimal-precision.json` (`precision: 1`, shown under “With Precision”) and `fields-percent/required-percent.json` (`precision: 2`), published docs and catalog that authors copy · evidence: every percent face reads `scale` through resolveFieldScale and `precision` is the decimal(p,s) total-digit count, so an author copying these gets the protocol’s absent width, not the width they wrote; not fixed here (different defect class: the member, not the absent default) · dedupe words: percent docs precision example; with-decimal-precision schema catalog; percent.mdx precision 2 field schema; percent decimal places precision vs scale docs",
        "carrier: none — Acceptance notes, not filed (zero pull: no first-party producer declares `scale` on a percent field). Percent faces outside this claim that do not read the resolver: ObjectMetricWidget (`0,0%` / `0,0` inferred patterns), the ObjectGantt `percent` case and the ObjectGrid mobile card (both pass `undefined` to formatPercent; the card classifies percent by column-name substring), and GroupRow group-header aggregates (type-blind `toFixed(2)`, no unit). All but GroupRow coincide with the protocol for an undeclared percent. GridField.tsx (objectui#10783) has no percent branch, so nothing to report and it was not edited.",
        "carrier: none — `formatPercent`’s `precision = 0` parameter default is still reached by the cell for `progress` and format-promoted textual fields (no row: the resolver answers no fixed width) and by the two `undefined` callers above. Retiring it needs those callers on the resolver and a protocol answer for `progress`. Noted in Acceptance notes, not filed.",
        "carrier: none — footer vs cell grouping on a declared `scale: 0` number column (cell ungrouped per objectui#4033, footer grouped), and the widget readonly face’s `toFixed` + literal `%` instead of the locale convention. Both pre-existing, not width. Acceptance notes only."
      ],
      "gates": [
        "vitest packages/fields/ EXIT=0 @5115b4b0a (3496 passed)",
        "vitest packages/plugin-detail/ EXIT=0 @5115b4b0a (2245 passed)",
        "vitest packages/plugin-grid/src/ EXIT=0 @5115b4b0a (1612 passed)",
        "vitest scripts/__tests__/ EXIT=0 @cb175cc10 (5371 passed)",
        "type-check @object-ui/fields EXIT=0 @5115b4b0a",
        "type-check @object-ui/plugin-detail EXIT=0 @5115b4b0a",
        "type-check @object-ui/plugin-grid EXIT=0 @5115b4b0a",
        "eslint 7 changed files EXIT=0 @cb175cc10: 0 errors; per-file warnings equal to BASE (140/5/49/6/2), new files 0; narrowing: population = the 7 changed files, all linted (JSON results 7); invariant because eslint.config.js has no parserOptions.project/projectService and no local rule reads the filesystem",
        "check-changeset-presence EXIT=0 @cb175cc10",
        "check-control-bytes EXIT=0 @cb175cc10",
        "check:new-line-citations EXIT=0 @cb175cc10 (0 new)",
        "markdown-test-inputs --audit EXIT=0 @cb175cc10",
        "check:pending-changeset-literals EXIT=0 @cb175cc10",
        "check:phantom-deps EXIT=0 @cb175cc10",
        "check:esm-specifiers EXIT=0 @cb175cc10",
        "check:lockfile-integrity EXIT=0 @cb175cc10",
        "check:lockfile-dedupe EXIT=0 @cb175cc10",
        "check:installed-pin-claims EXIT=0 @cb175cc10",
        "check:changeset-claims EXIT=0 @cb175cc10 (report-only; its 12 pending-changeset reads were done; the 9568 one was corrected, the rest stay true)",
        "check-spec-range-floors analyze scoped to 3 packages EXIT=0 @cb175cc10 (0 findings; control 3)",
        "ablation A/B + restore @cb175cc10 as in tests",
        "NOT MEASURED: check:readme-exports, reason: population collapsed without a full workspace build (only the plugin-grid closure built); the README edits add no fenced block or documented type",
        "NOT MEASURED: check:eager-closure, reason: needs the console app build (CI performance-budget.yml)",
        "NOT MEASURED: full lint/test farm, reason: CI’s"
      ],
      "line_budget": "n/a — no skills/** or governed-surface path in the diff (617 insertions, 83 deletions over 15 files)",
      "deviations": [
        "File surface beyond the claim’s four files (seat to supplement the claim): three package.json spec floors plus pnpm-lock.yaml (package-graph change: specifier-only, no new edge); READMEs of plugin-grid and plugin-detail (one paragraph each, AGENTS.md #2); the pending `.changeset/9568-percent-widget-reads-scale.md` (prose-only correction, because this change makes two of its sentences ship false in the same release); new tests.",
        "The footer’s `number` arm was implemented per Zone 1.3 and pointer 5791883815. It is a user-visible change for typed number columns beyond the percent headline, named in the PR, changeset and README.",
        "The five-face parity pin lives in plugin-grid, not beside each face: it is the one package whose tests reach fields, plugin-detail and its own footer. The fields-side pin (PercentField.scale-9568) was updated beside the widget.",
        "Test chunks ran one package each under `timeout 560` rather than 300; none timed out.",
        "Commit trailers are the model-free pair per objectui AGENTS.md (Claude-Session + `Co-authored-by: Claude`), not the harness reminder’s model-named line."
      ],
      "files_changed": [
        ".changeset/9568-percent-widget-reads-scale.md",
        ".changeset/9843-percent-absent-scale-resolver.md",
        "packages/fields/package.json",
        "packages/fields/src/__tests__/PercentField.scale-9568.test.tsx",
        "packages/fields/src/index.tsx",
        "packages/fields/src/widgets/PercentField.tsx",
        "packages/plugin-detail/README.md",
        "packages/plugin-detail/package.json",
        "packages/plugin-detail/src/DetailView.tsx",
        "packages/plugin-grid/README.md",
        "packages/plugin-grid/package.json",
        "packages/plugin-grid/src/__tests__/percentAbsentScale.fiveFaces-9843.test.tsx",
        "packages/plugin-grid/src/__tests__/useColumnSummary.numberWidth-9843.test.tsx",
        "packages/plugin-grid/src/useColumnSummary.ts",
        "pnpm-lock.yaml"
      ],
      "head": "cb175cc10",
      "cleanup": "worktree /home/user/objectui-issue-9843 removed (node_modules deleted first, no --force); no background process left"
    }

    Generated by Claude Code

  14. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 9843,
      "status": "done",
      "round": "patch for contract review 5916522120 (finding 1, remedy a)",
      "branch": "claude/issue-9843-percent-scale-resolver",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11246",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent of the domain:ui seat 2 PM session",
      "head": "7eb73d5e4760fb4a54b5973f158158906efb1b56",
      "previous_head": "cb175cc1037f7d8884caf4ff8aed987430257982",
      "summary": "Remedy (a). `PercentCellRenderer` now calls `resolveFieldScale({ type: percentField.type, scale: percentField.scale })`, the spelling the chip and the widget use, so the `percentField.scale` read that objectui#9784’s source pin asserts is visible again (on disk: 1 match for /percentField\\.scale\\b/, 0 for /percentField\\.precision\\b/). The resolver reads only those two members, so no rendering moves. The 9784 test is untouched. One commit, fast-forward push cb175cc10..7eb73d5e4, no force or rebase.",
      "tests": "All at 7eb73d5e4, `pnpm exec vitest run --reporter=verbose PATH` from the repo root under os-verify-lock: `packages/types/` Test Files 290 passed (290), Tests 7012 passed (7012), including the 9784 case “the read still exists” passing by name; `packages/fields/` Test Files 224 passed | 1 skipped (225), Tests 3496 passed | 7 skipped; `packages/plugin-detail/` Test Files 226 passed | 1 skipped (227), Tests 2245 passed | 8 skipped; `packages/plugin-grid/src/` Test Files 176 passed (176), Tests 1612 passed; `scripts/__tests__/` Test Files 177 passed | 2 skipped, Tests 5371 passed | 2 skipped. Plus the three other tests that read the edited files off disk outside those suites (app-shell actions-envelope.ratchet and flow-envelope.ratchet, i18n residue-namespaces-3546): 3 files, 47 tests passed. `type-check @object-ui/fields` exit 0 (closure 10/10 from cache). eslint `packages/fields/src/index.tsx`: 0 errors, 140 warnings, equal to BASE. check-changeset-presence, check-control-bytes and check:new-line-citations exit 0.",
      "mcp_calls": "0",
      "api_writes": "1 — this comment: POST /repos/objectstack-ai/objectui/issues/9843/comments via post-stamped.mjs. git push is not a REST write. PR body not edited.",
      "pr_body_sentences_now_false": [
        "The table row for the list cell: its “After” column reads `resolveFieldScale(percentField)`; the head spells `resolveFieldScale({ type: percentField.type, scale: percentField.scale })`.",
        "“The cell asks with the field as given.” Its meaning (the field’s own `type`) holds, but it now hands over the two members by name.",
        "The “Tests and gates” and ablation figures are dated to cb175cc10 (true of that head). The counts for 7eb73d5e4 are in this report; ablation B’s anchor was the old spelling and was not re-run."
      ],
      "deviations": [
        "The verification hold ran 1619s under the shared lock (the lock flagged it as a long holder); five suites in one hold. A future round should split holds."
      ],
      "files_changed_this_round": [
        "packages/fields/src/index.tsx"
      ]
    }

    Generated by Claude Code

  15. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    ✅ ACCEPT: PR objectui#11246 at 7eb73d5e4 (p3, Fixes). Ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the reports' own account.

    • Claim 5914974702.
    • Dev reports 5916318159 and 5917423477 (the fix round).
    • Contract review round 1 5916522120: FAIL at cb175cc10. Test (shard 1/8) was red: the objectui#9784 source pin looks for the percentField.scale spelling.
      • The seat directed remedy (a), inside the claimed surface. The cell now passes { type: percentField.type, scale: percentField.scale } to the resolver, and the 9784 test is untouched.
      • The seat relay-patched three PR-body passages that the fix made false; they read back identical.
    • Round 2 5917531736: PASS on this head.
    Implemented-by:  claude/issue-9843-percent-scale-resolver
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    one width every percent face in the card reads its width through the spec's resolveFieldScale (percent: 0 when absent), with no private ?? N: the list cell, the detail chip, the grid footer, and the PercentField widget's display and step. The footer's number arm reads the resolver too (ruling A′)
    spec floor @objectstack/spec ^17.5.0 on fields, plugin-detail and plugin-grid, the release that exports resolveFieldScale; the lockfile moves only the three specifier lines
    changeset patch on the three packages; the pending objectui#9568 changeset's prose is corrected; Clause-②: no
    CI head 7eb73d5e4: 45 check-runs, 42 success, 3 skipped by design (Test (coverage), its matrix row, dependabot), 0 failed
    closing keywords Fixes #9843 (the card's scope is the five enumerated faces; review ③)
    governed none

    File surface, supplemented (the claim named four source files; review ③ accepted the rest as what the change requires): the three package.json floors, pnpm-lock.yaml, the plugin-detail and plugin-grid READMEs, the pending .changeset/9568-percent-widget-reads-scale.md prose, and the three new test files.

    The dev's open questions: the seat's answers:

    1. The widget's step follows the resolved width, so an undeclared percent steps by 1: A, keep, as implemented and as review ③ reads it. An author who wants fractional entry declares scale. Option B would accept a digit the display then rounds away.
    2. The metric-tile half of ruling A′ ([finding] an undeclared number field prints two different decimal widths on two faces of the same column — and ⛔ number ⇒ 0 cannot be the fix, because a declared 0 also turns grouping OFF objectstack#19628): A, filed as objectui#11254. It needs its own design question (where a server aggregate's input widths come from). It carries the list of the other percent faces outside this card, in one place.

    Out of scope:

    • Class (c): the percent docs page and the schema catalog teach precision as the decimal width → filed objectui#11255, to move with objectui#9784.
    • The other carrier notes (ObjectGantt / ObjectGrid mobile card / GroupRow / formatPercent's default): tracked in objectui#11254.
    • The footer's grouping versus the cell's, and the widget readonly face's toFixed plus a literal %: Acceptance notes, both pre-existing and not about width.

    domain:ui seat 2 · ACCEPT · 2026-09-30T18:49Z


    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

Labels

bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions