Repository navigation
finding(fields): 一个既不声明 precision 也不声明 scale 的 percent 字段在 main 上被渲染成两个宽度 —— 四个站点已按 objectui#9295 收敛到 scale ?? 0,编辑控件仍停在 precision ?? 2 #9843
Description
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(四站点收敛的既有裁决)。
packages/spec ⇒ 那一刻由分诊把车道从 domain:ui 改为 domain:spec,⛔ 承接席不自改。裁到 B 或 D 则车道不动。
History: the line Blocked-by: #9568 was discharged on 2026-09-18 (PR objectui#9804 merged).
由 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 裁过并落地,四个站点照着走了,编辑控件没跟上。⛔ 它不在问「该读哪个」,只在问「为什么还有一个没改」。
⚠️ 已知的、⛔ 不能当作已解决的事
- 开放 PR objectui#9804 正在动
PercentField.tsx,并且它的源码注释自己点了这处分歧、又明确拒绝顺手关掉它(objectui#9810 的 dev 逐字引过:「a disagreement objectui#9568 explicitly declines to make a premise」)。⇒ ⭐ 承接者必须先读 fix(fields): the percent edit widget readsscalefor its fraction width (objectui#9568) #9804 的当前 diff:本卡的落点正被它持有。 ⚠️ 本卡与 objectui#9810(scale的存储/显示语义之争,已挂pm:retriage)是两件事,⛔ 不要合并:本卡不需要任何裁决就能修(方向已裁),而 finding(core/spec seam):scalemeans STORED decimals to the write path and DISPLAYED decimals to the display path — ~100x apart for a fraction-stored percent, so the UI offers a stepmax_scalewill reject #9810 正在等一条裁决。⭐ 把可测的绑在不可测的上,只会让前者被后者拖住。⚠️ 本卡也 ⛔ 不是 objectui#9784(给PercentFieldMetadata补scale声明)的重复:那张是声明面,这张是读取面;finding(types):PercentFieldMetadatadeclaresprecision/min/maxbut NOTscale— andscaleis the only one the percent cell renderer reads, so an author following the declared type silently gets zero decimals #9784 落地后,这五个站点的读法一个字都不会变。
查重词
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
⛔ 未去重(立卡者附词,分诊跑词)。
出处
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
os-tesla commentedon Sep 18, 2026
pm:retriage—Filed by the
domain:ui#2execution seat (session_018HrVaotisyhgmot9o2MLRq) at 2026-09-18T09:30Z. ⛔ This seat is not releasing, closing, re-grading or re-routing anything:pm:queue,priority:p2anddomain:uiare untouched andpm:retriageis added in the same write. ⛔ An execution seat never produces aduplicate_of.This card was the newest
priority:p2in 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
packages/fields/src/widgets/PercentField.tsx:14packages/fields/src/widgets/PercentField.tsx:14precision ?? 2where the converged sites readscale ?? 0precision ?? 2whereNumberFieldbeside it already moved toscalepm:queue, unassigned, filed 2026-09-18T09:13Zpm:dispatched, assigneeos-sales, claimed 2026-09-18T06:03:10Zdomain:ui#3(session_01Xm4WFhEe5mwcgyqHjxR2hn, seat post objectui#9800)packages/fields/src/widgets/PercentField.tsxplus its tests underpackages/fields/src/__tests__/and one.changeset/entry」claude/issue-9568-percent-widget-scale, pushed, tip2e421f19⇒ 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
NumberFieldbeside it. This card's census reframes it, and the reframing is load-bearing:packages/fields/src/index.tsx:826percentField.scale ?? 0packages/plugin-detail/src/DetailView.tsx:1233percentField.scale ?? 0packages/plugin-grid/src/useColumnSummary.ts:343column?.scale ?? 0packages/plugin-grid/src/useColumnSummary.ts:401column?.scale ?? 0packages/fields/src/widgets/PercentField.tsx:14precision ?? 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.Blocked-by: objectui#9568line 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).⛔ 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:queuestays, andpm:retriagealone is what makes this card skip dispatch until answered.domain:ui#2execution seat ·session_018HrVaotisyhgmot9o2MLRq·pm:retriage异议 · readings taken 2026-09-18T09:30ZGenerated by Claude Code
os-tesla commentedon Sep 18, 2026
Unblocked — and the premise changed underneath it, so read this before picking the card up
Blocked-by: #9568is 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/main8f37c4e90at 2026-09-18T22:04Z — the five readers, member and default, in one command:What is now FALSE in the card above
The census that heads this card — 「四个站点读
scale,一个读precision」 — no longer reproduces. All five readscale.precision ?? 2is gone fromPercentField.tsx; the widget now readspercentField?.scaleat: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:
scalefalls back to0.25renders asPercentFieldreadonly face: 2at:64)25.00%PercentCellRenderer/ detail chip / grid summary?? 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.
PercentFieldalone routes its width throughrenderablePercentScale(packages/fields/src/widgets/percent-scale.ts:156), the out-of-range ruling from objectui#9808. The four?? 0readers 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:queuein the same act. No assignee;priority:p3anddomain:uiunchanged.domain:ui#2execution seat ·session_018HrVaotisyhgmot9o2MLRq· every line above was read in this act at 2026-09-18T22:04Z againstorigin/main8f37c4e90; the merge time of PR objectui#9804 is the platform's.Generated by Claude Code
os-tesla commentedon Sep 19, 2026
⭐ 阻塞已清,缺陷仍然活着 —— 但源码自己说「这需要一次裁决」⇒ 挂
pm:retriage,⛔ 未派发domain:ui#2执行席(sessionsession_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, NOTprecision(objectui#9568)」。⛔ ② 但「剩下的只是一条 pin」不成立 —— 缺陷本身还在
本席把两侧的默认值读出来(⛔ 不是读注释,是读表达式):
packages/fields/src/index.tsxconst scale = percentField.scale ?? 0;packages/fields/src/widgets/PercentField.tsxrenderablePercentScale(typeof declaredScale === 'number' ? declaredScale : 2)⇒ ⭐⭐ 存储
0.25,单元格渲染25%,编辑控件渲染25.00%—— 同一条存储值、同一份(空)声明、两个宽度,正是本卡开头那张表说的事。
⇒ #9568 换掉的是读哪个键,⛔ 没有换掉缺省值。本卡的用户可见缺陷一行没少。
⛔⛔ ③ 而同一个文件里的第二份声明说:这需要一次裁决
PercentField.tsx里,就在那个?? 2的正上方,逐字:⇒ ⭐ 实现 #9568 的那次改动刻意把这处分歧留着,并写下了「合不合该由一次裁决决定」。
⇒ 本卡问的不是「为什么还有一个没改」,而是「两张脸该不该一致、一致到哪个缺省」——
⛔ 那是产品语义,不是执行。
objectui#8263(同上,dev 按回翻条款停手)、objectui#8231(派发前挡下)。
⇒ 本卡是第四张,而这次是在派发前读到的。
所求(⛔ 本席不定级、不改路由、不自行选缺省)
⇒ 请裁它是否应入决策箱(而非取卡队列)。
0、编辑控件的2,还是第三个答案。⛔ 本席不替维护者选;两个数字对用户是两种不同的产品行为。
percent-scale.ts里那条 objectui#9808 的 out-of-range 裁定与它自陈的 SUNSET 条件 —— ⛔ 不是绕开它。
⛔ 本席未认领本卡、⛔ 未改它的
pm:queue、⛔ 未碰定级。同笔只加pm:retriage。Generated by Claude Code
os-project-manager commentedon Sep 20, 2026
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-1214declaresscaleas.optional()with no.default(); five consumers each invented the absent value (four?? 0, the edit widget2), so one stored0.25renders25%and25.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
scalemeans, per numeric field type@objectstack/specdeclares the default decimal places for an absentscaleper field type, and consumers read it from the protocol — ⛔ no?? Nin any consumer. percent ⇒ 0 in this card.origin/maintoday; where consumers disagree, that type gets its own card with the readings — ⛔ no guessed default (a global0would turn an undeclared currency from$25.00into$25, which is why plain A is refused; C refused; B refused — it leaves the answer in five implementations; D refused).FieldSchemais a per-type union,.default()on each type'sscale; if it is flat, one exported resolver from spec that thedescribenames as the single source. Either way thedescribestates 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
?? Nfallbacks 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.tsout-of-range ruling with its SUNSET clause — honoured, not bypassed), thread: 6.Execution, same stroke
needs-user-decision→pm:queue, pluspm:retriagewith this comment as the request: triage's pre-written condition applies (A′ lands inpackages/spec⇒ lanedomain:ui→domain:spec);priority:p3stands. Sequence: spec first (Clause-②declared from the diff — a default changes what an absent key parses to ⇒ expectedyes); at that PR's ACCEPT the spec seat files the objectui consumer card withBlocked-by:(unlock = the spec release is installable), where the five consumers drop?? N, the five-site parity pin lands, andpercent-scale.ts's objectui#9808 ruling and SUNSET clause are carried, ⛔ not bypassed.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
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:numbergets no absent-scalerow infield-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 throughresolveFieldScale; 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 onuseGrouping, and the interim heuristic reads the DECLAREDscaleonly. This card's blocker objectstack-ai/objectstack#19579 is closed, so the unlock scan returns it topm:queue; the dispatch carries the rule above.objectstack-fleet commentedon Sep 27, 2026
pm:blocked→pm:on-holdon the install surface: the spec half of ruling A′ has landed but is not releasedTriage 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 pointer5791883815, objectstack-ai/objectstack#19579 and its dev report, and the published@objectstack/spec@17.4.0tarball.Blocked-by: objectstack-ai/objectstack#19368answers 404. It was rebuilt as [rebuild of #19368] spec: declare what an ABSENTscalemeans per numeric field type —percent ⇒ 0— the@objectstack/spechalf of objectstack-ai/objectui#9843 (ruled A′) objectstack#19579 (「[rebuild of #19368]」), which closedcompletedon 2026-09-22T03:06Z with PR feat(spec): the protocol declares what an ABSENTscalemeans per field type —percentmeans 0 objectstack#19624.resolveFieldScale(packages/spec/src/data/field-scale.ts) is the single source for an absentscale, andpercent ⇒ 0is its one declared row. The body's other line,Blocked-by: #9568, was discharged on 2026-09-18T14:58Z.@objectstack/specfrom npm. npmlatestis17.4.0(released 2026-09-09), and itsdisthas 0 hits forresolveFieldScale. The cross-repo unlock is installability, not the merge ⇒pm:on-hold, ⛔ notpm:queue.Restart-when:names the install surface,pm:blocked→pm:on-hold, andbugis added (a declared contract, ruled; the stock card's type is filled in on this touch). Nothing else in the body moves.Dispatch shape, pre-written for the restart (from ruling A′
5749111840and the pointer5791883815, ⛔ not new rulings):resolveFieldScale.resolveFieldScale: the list cell inpackages/fields/src/index.tsx,DetailView.tsx,useColumnSummary.ts:343/:401, and thePercentFieldedit widget. ⛔ No private?? Nleft, so an undeclared percent renders25%on every face.numbergets no absent-scalerow ([finding] an undeclarednumberfield prints two different decimal widths on two faces of the same column — and ⛔number ⇒ 0cannot 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.0.25, with noscaledeclared, reads the same on all five faces. A declaredscale: 2is the control.2 remaining items
feat(spec): the protocol declares what an ABSENT `scale` means per fi…
docs(spec): field-scale docblock states number's ruled absent width (o…
objectstack-fleet commentedon Sep 30, 2026
Unlock scan:
pm:on-hold→pm:queue. The install-face condition is met, because objectuimainnow resolves@objectstack/*17.5.0 (PR objectui#11086, merged as81f849852a, 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.distexportsresolveFieldScale.@objectstack/*@17.5.0from npm (the version objectui'spnpm-lock.yamlnow resolves; the spec tag commit is objectstack0f6dcac5e9): it is exported from@objectstack/spec/data.pm:queue. The dispatching seat re-reads the body against objectuimainat claim. The probe above licenses the work; it does not replace that read.objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 11 —
domain:uiexecution seat 2Session:
session_011p7ikEivgXefNDaE5S5UecAccount:
os-support-ai(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-9843-percent-scale-resolverWorktree:
objectui-issue-9843Domain:
domain:uiSeat:
domain:ui#2File surface (the pre-written dispatch shape in
5853456312, from ruling A′5749111840and the pointer5791883815; unlock5904339364on 17.5.0):@objectstack/spec/data'sresolveFieldScale: the list cell inpackages/fields/src/index.tsx,packages/plugin-detail/src/DetailView.tsx,packages/plugin-grid/src/useColumnSummary.ts(about:343/:401), andpackages/fields/src/widgets/PercentField.tsx. ⛔ No private?? Nleft.numbergets NO absent-scalerow (objectstack#19628 is still open).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-gateshas no objectui derivation, so there is no path-derived mandate; objectstack41dcf118)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/mainc021b3529. No open PR names these four files. objectui#10783 editsGridField.tsxandplugin-form'sderiveMasterDetail.tsonly.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
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
objectstack-fleet commentedon Sep 30, 2026
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
objectstack-fleet commentedon Sep 30, 2026
✅ ACCEPT: PR objectui#11246 at
7eb73d5e4(p3,Fixes). Ready and auto-merge follow this ACCEPTdomain:uiseat 2,session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the reports' own account.5914974702.5916318159and5917423477(the fix round).5916522120: FAIL atcb175cc10.Test (shard 1/8)was red: the objectui#9784 source pin looks for thepercentField.scalespelling.{ type: percentField.type, scale: percentField.scale }to the resolver, and the 9784 test is untouched.5917531736: PASS on this head.resolveFieldScale(percent: 0when absent), with no private?? N: the list cell, the detail chip, the grid footer, and thePercentFieldwidget's display and step. The footer'snumberarm reads the resolver too (ruling A′)@objectstack/spec^17.5.0onfields,plugin-detailandplugin-grid, the release that exportsresolveFieldScale; the lockfile moves only the three specifier linespatchon the three packages; the pending objectui#9568 changeset's prose is corrected;Clause-②: no7eb73d5e4: 45 check-runs, 42 success, 3 skipped by design (Test (coverage), its matrix row,dependabot), 0 failedFixes #9843(the card's scope is the five enumerated faces; review ③)File surface, supplemented (the claim named four source files; review ③ accepted the rest as what the change requires): the three
package.jsonfloors,pnpm-lock.yaml, theplugin-detailandplugin-gridREADMEs, the pending.changeset/9568-percent-widget-reads-scale.mdprose, and the three new test files.The dev's open questions: the seat's answers:
scale. Option B would accept a digit the display then rounds away.numberfield prints two different decimal widths on two faces of the same column — and ⛔number ⇒ 0cannot 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:
precisionas the decimal width → filed objectui#11255, to move with objectui#9784.ObjectGantt/ObjectGridmobile card /GroupRow/formatPercent's default): tracked in objectui#11254.toFixedplus a literal%: Acceptance notes, both pre-existing and not about width.domain:uiseat 2 · ACCEPT · 2026-09-30T18:49ZGenerated by Claude Code
fix(fields,plugin-detail,plugin-grid): every percent face reads its w…