Repository navigation
the row-cap renderers read a numeric string the spec refuses outright — kept deliberately twice this week, never declared #10145
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 20, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRe-measurement at source, replacing the deleted triage comment
domain:spec@ objectui execution seat · sessionsession_01877XiBYSaRCk2CU7cMSg3S· 2026-09-24T05:46Z · objectuiorigin/main62597c5· read withgit show/git greponorigin/main, never the working tree. Maintainer instruction in this seat's session, verbatim: 「这些卡你自己分析和排查一下」.⚠️ The clone is shallow: its oldest commit is8ed4b95at 2026-09-20T12:48Z, so commits between filing and that instant are not visible. ⛔ Grading and routing stay with the triage seat; this comment supplies evidence only.Verdict: CONFIRMED. It remains a KEEP/STOP product question, not a defect.
packages/plugin-detail/src/renderers/recordActivityFeed.ts:600-603(normalizeLimit) andrecord-history.tsx:88-91(normalizeHistoryLimit) both doNumber(value)followed by an integer>0 check. Keeping the coercion is documented as deliberate (:590-593,:83-86), and the path is shared with chatter/discussion (:595-598).- Pinned at
recordActivityFeed.test.ts:439,…rowLimitRefusal-10096.test.tsx:135,180andrecord-history.rowLimitRefusal-10005.test.tsx:114. - Spec source:
ui/component.zod.ts:1404RecordActivitylimit: z.number().int().positive().default(20)and:1822RecordHistoryz.number().int().positive().optional()both refuse a string. - ⭐ Beyond the card (checked in node):
Number()also admitstrue→1,[7]→7,'0x10'→16 and' 5 '→5. A KEEP ruling therefore covers more than "numeric strings", and a STOP sweep must count these shapes too. - Clause-②: STOP is
no(it narrows; needs a migration note). KEEP-and-declare in published docs or contract isyes(it publishes a wider accepted set than spec). Size S either way.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actions待裁:行数上限(
limit)要不要继续接受'5'这种字符串 ——domain:spec@ objectui 执行席落卡,2026-09-24T13:55Z,sessionsession_01877XiBYSaRCk2CU7cMSg3S一句话问题: 作者在「活动记录 / 历史记录」块里把条数上限写成
'5'(字符串),发布时 spec 会拒收;但如果这样的元数据已经进了系统,渲染器照样按 5 条显示。契约说「不行」,渲染器说「行」,两边口径不一。背景(实测,objectui
main,读数见本卡重测评论5808440866):recordActivityFeed.ts的normalizeLimit与record-history.tsx的normalizeHistoryLimit都先Number(value)再判正整数;spec 两处都是z.number().int().positive(),字符串一律拒。另外Number()还把true→1、[7]→7、'0x10'→16、' 5 '→5 都当成合法。objectui#10093 与 objectui#10096 两次刻意保留了这个行为,并各自写了 pin。Governing text: objectui
AGENTS.md#0.1,原文节选:「do not add a lenient fallback/alias in the renderer (… coercing a malformed shape …) to make non-compliant metadata "work." A tolerant fallback fossilizes the wrong convention into a second de-facto contract」。协议声明:spec 已声明为 number,⛔ 本卡不改协议。前提与 re-check: ① 两个 resolver 仍是
Number(value)——git grep -n "Number(value)" origin/main -- packages/plugin-detail/src/renderers/recordActivityFeed.ts packages/plugin-detail/src/renderers/record-history.tsx(阳性对照:同文件DEFAULT_ACTIVITY_LIMIT/normalizeHistoryLimit必中)。② spec 仍拒字符串 —— 在 objectstackpackages/spec/src/ui/component.zod.tsgreplimit: z.number().int().positive()。选项 做什么 客户可感知的后果 A 停止(STOP) 渲染器与契约同口径:只收正整数, '5'、true、[7]等回落默认并响亮告警;四个 pin 翻转;附迁移说明已存元数据里写成字符串的块,条数会从「5」变回默认值,并在控制台看到告警;新写的元数据在发布时就被拒 B 保留并声明(KEEP) 保留 Number()宽容,并在站点与文档里写明「渲染器额外接受数字字符串」行为零变化;但文档公开承认一个比 spec 更宽的「第二口径」(需要条款② 复核,因为是扩大公开接受面) 业务含义直译: A 相当于「海关只认一种证件」—— 规则一致,代价是少数拿旧证件的人要换证;B 相当于「海关私下也认复印件,还贴了告示」—— 当下没人被拦,但以后每个新窗口都得记住这条例外。
四轴(业务立场):
- 项目长远合理性(权重最高): 两年后的平台应该是「元数据发布时被契约一次判定,渲染器不再二次解读」——主流低代码平台(Salesforce 元数据 API、Retool 组件 schema)都是契约单一口径。A 缩小特例;B 把一个特例写进公开文档,永久化。
- 实际业务拉动: 没有任何卡或实测显示有作者依赖字符串写法;
⚠️ 也没测过已存元数据里有多少条字符串(这是置信缺口)。 - 防 AI 犯错: AI 最常见的错误之一就是把数字写成字符串。A 下它在发布时被响亮拒绝、改一次就对;B 下它「看起来能用」,同类错误会被批量掩盖,直到遇到一个不宽容的渲染器才爆。
- 创业阶段不扩散: 按「过渡也从紧」,别名/宽容拼写默认立即退休;B 等于新增并维护一个声明面。
os-decision-facets ① 长远:A 删除渲染器侧第二口径(特例 −1);B 把宽容写进公开文档(契约增生 +1) ② 拉动:零已知作者依赖字符串写法;已存量未测 ③ 防 AI:A = 发布时响亮拒绝;B = 静默容忍并被文档背书 ④ 不扩散:A remove;B declare-and-maintain Prior rulings read: "numeric string" "Number(value)" "limit coercion" → objectui#10093 / #10096 两次「刻意保留、留待裁决」, 无裁决; ADR none; thread: 1 推荐:A(停止)· 只看①选 A;②③④ 是否翻转:否 置信缺口:已存元数据中字符串 `limit` 的条数未测;若实测非零,A 仍成立,但迁移说明需点名影响范围推荐:A;回退项:B。裁后执行: 裁 A ⇒ 本席派一张 dev:两个 resolver 改为只收正整数,非法值回落默认并
console.warn(点名块与原值);四个 pin 翻转为拒收断言(不删);changeset 写迁移说明;派发前先统计全仓同形Number(value)读点,一并处理。裁 B ⇒ 站点注释 + 文档声明 + 区分 KEEP/STOP 的 pin,走条款② 复核。
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsRuling: batch #222 item 2 · letter A (STOP — the row-cap renderers read what the contract reads) · maintainer 「同意」 (chat, director seat summon #29,
session_01EcrTi7s5oDYPHS4Pi7h31d) 2026-09-24T15:19ZDirector seat, summon #29. Presented with recommendation A (fallback B); the maintainer agreed. Thread re-read to its last comment (5815491428) in this act. Protocol first: spec declares
limitasz.number().int().positive()on bothrecord:activityandrecord:history, and objectui AGENTS.md #0.1 forbids a lenient renderer fallback.Ruled: A (STOP). The ruling applies to the whole family:
normalizeLimit(recordActivityFeed.ts) andnormalizeHistoryLimit(record-history.tsx) admit only a positive integer number.- Every other value falls back to the default with a loud
console.warnnaming the block and the raw value. That includes'5',' 5 ','0x10',trueand[7], all measured in 5808440866. - The four pins flip to refusal assertions:
recordActivityFeed.test.ts:439,…rowLimitRefusal-10096.test.tsx:135,180,record-history.rowLimitRefusal-10005.test.tsx:114. ⛔ They are not deleted. - The changeset carries a migration note: stored metadata with a string
limitdrops to the default. - Before dispatch, census the other
Number(value)row-cap reads of the same shape (chatter/discussion share the path per:595-598) and cover them in the same PR.
⛔ B not taken: it would publish a second accepted set wider than the spec.
Execution:
needs-user-decision→pm:queuein this stroke.priority:p2anddomain:specstand. Clause-②: no (it narrows).
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsClaim: PM loop round 8 — objectui#10145, executing ruling 5816948508 (batch #222 item 2, letter A, STOP): the row-cap resolvers admit only a positive-integer number
Session:session_01877XiBYSaRCk2CU7cMSg3S
Branch:claude/issue-10145-row-cap-stop
Worktree:objectui-issue-10145
Domain:domain:spec
Seat:domain:spec#1
File surface:packages/plugin-detail/src/renderers/recordActivityFeed.ts(normalizeLimit, about:601) andrecord-history.tsx(normalizeHistoryLimit, about:89), plus any other row-cap read of the same shape the dev's census finds (chatter / discussion, per the ruling);- the four pins the ruling names, flipped and not deleted:
recordActivityFeed.test.ts:439,recordActivityFeed.rowLimitRefusal-10096.test.tsx:135,180,record-history.rowLimitRefusal-10005.test.tsx:114; - one
.changeset/10145-…mdwith a migration note.
Container & model:S,mode:subagent,model: opus(TIER_DEFAULT)
Clause-②: no (it narrows, per the ruling)
Thread-read: 5816948508
Serial constraints cleared: measured at 2026-09-24T16:48Z. Onorigin/main, theNumber(value)row-cap reads arerecord-history.tsx:89andrecordActivityFeed.ts:601(2 hits inplugin-detail/src, not counting test prose). Open PRs inplugin-detailtouch onlyrecord-related-list.tsx(objectui#10303) andrecord-details.tsx(objectui#10279). No overlap.
Dispatch authority: director ruling 5816948508 (maintainer 「同意」), plus objectstack-ai/objectstack#19980 and 「任务很多,并发保持3」.
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsos-dev-report
{ "issue": 10145, "status": "done", "branch": "claude/issue-10145-row-cap-stop", "pr": "https://github.com/objectstack-ai/objectui/pull/10309", "session": "session_01877XiBYSaRCk2CU7cMSg3S (subagent, so this is the parent PM seat's id, domain:spec#1)", "premise_still_valid": true, "summary": "Executed ruling 5816948508 (A, STOP). normalizeLimit (record:activity, record:chatter, record:discussion) and normalizeHistoryLimit (record:history) now admit only typeof number + positive integer. The predicate is the same one the objectui#9925 read points use. Every other authored value ('5', ' 5 ', '0x10', true, [7], and also 1.5/0/-3/NaN/Infinity, which already fell back) drops to the default. Each renderer then emits one console.warn from an effect keyed on the message, naming the block and the raw value with its type; undefined/null stay silent. I added the warn for the already-refused numeric values on evidence: objectui#10097 ruled A (always warn, comment 5807911435) for exactly these two silent read points and requires their silence pins to go. The two silence pins are flipped to warning pins, so this PR also carries #10097's acceptance for these two sites, and that card is still open for the PM to reconcile. Census: 2 resolvers of the Number(value) shape serving 4 block types, both covered; the other row-cap reads already test typeof number or are different keys/shapes (table in the PR body). Spec check: @objectstack/spec 17.4.0 safeParse refuses '5', ' 5 ', '0x10', true, [7] and 1.5 (invalid_type) and 0/-3 (too_small) on RecordActivityProps, RecordHistoryProps and RecordChatterProps.feed. objectstack origin/main e8f163f component.zod.ts declares limit: z.number().int().positive() on both. Changeset .changeset/10145-row-cap-admits-only-numbers.md is minor on @object-ui/plugin-detail with a migration note; the docs row in content/docs/plugins/plugin-detail.mdx is updated. assignee was os-bill at dispatch, untouched.", "tests": "Head 5cdeedc74 (merge-base 8b1f06619; origin/main had not moved at push). (1) pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build: VERDICT command-exit 0, 12 of 47 projects. (2) pnpm --filter @object-ui/plugin-detail type-check (tsc --noEmit && tsc -p tsconfig.test.json, which includes src/**/*.test.ts(x)): VERDICT command-exit 0. (3) pnpm exec vitest run --maxWorkers=2 packages/plugin-detail/src/renderers/: 'Test Files 55 passed (55) / Tests 657 passed (657)', VERDICT 0. (4) eslint --no-inline-config on the 8 changed TS files: 0 errors (--format json: 8 files); warnings per file equal to the base, and the new test file has 2 no-explicit-any warnings (the sibling 'items as any' shape). (5) check-changeset-presence: pass ('7 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'). check-changeset-no-major: pass. check:new-line-citations: 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. check:control-bytes, check:doc-fences, check:changeset-claims, check:pending-changeset-literals, check:doc-example-readers: exit 0. NOT MEASURED: check:doc-snippets and check:doc-examples both exit 2 with PRECONDITION NOT MET (they need a 34-package build). The docs edit is one prose table cell with no code fence; declared to CI. Reverse verification at committed HEAD: I restored 'const n = Number(value)' in normalizeHistoryLimit via python replace. On-disk proof: grep -c injected=1, removed=0. The history pin file then gave 'Tests 8 failed | 16 passed (24)'; the 8 red are the flipped '5' wire pin, the ' 5 '/'0x10'/true/[7] refusals and 6 warn cases, and the lit controls stayed green. The trap-backed restore used git checkout HEAD --: git diff HEAD is 0 bytes, and hash-object equals the HEAD blob (baa1324e4525). src is imported relatively, so no dist is involved.", "mcp_calls": "0", "api_writes": "3 — git push of the branch (not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectui/pulls, draft, run 36031748378 success, PR #10309, body read back byte-identical); fleet-write relay comment (POST /repos/objectstack-ai/objectui/issues/10145/comments, this report). No labels were written (the dispatch forbade it).", "open_questions": [], "out_of_scope_findings": [ "class: b · record-related-list.tsx passes pageSize as typeof schema.limit === 'number' && schema.limit > 0 with no integer check, so an authored limit: 2.5 reaches RelatedList as a fractional page size. The spec says RecordRelatedListProps.limit is z.number().int().positive().default(5) (objectstack component.zod.ts, RecordRelatedList). Contract text: 'limit: z.number().int().positive().default(5)'. Seam: spec:RecordRelatedListProps.limit → renderer:RecordRelatedListRenderer (pageSize prop to RelatedList). It refuses silently too, unlike the objectui#10097 'always warn' ruling. Dedupe words: `related list limit fraction` · `RecordRelatedListProps.limit non-integer` · `record:related_list pageSize 2.5` · `row cap integer check related list`. carrier: open PR objectui#10303 touches record-related-list.tsx. Not filed; left for the seat." ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsACCEPT: objectui#10145, delivered by PR objectui#10309 at head
5cdeedc. Sessionsession_01877XiBYSaRCk2CU7cMSg3S,domain:spec@ objectui seat, reviewed against the diff andorigin/main.Executes ruling 5816948508 (A, STOP) as written.
normalizeLimit(record:activity, record:chatter, record:discussion) andnormalizeHistoryLimit(record:history) now admit only atypeof numberpositive integer. This is the same predicate the objectui#9925 read points use.'5',' 5 ','0x10',trueand[7]now drop to the default.1.5,0,-3,NaNandInfinityalready did.- Each renderer emits one
console.warnfrom an effect, keyed on the declaration, naming the block and the raw value with its type.undefinedandnullstay silent. This matches the channel inrecord-reference-rail.tsx. - The four named pins are flipped, not deleted. A new chatter pin was added.
- The census covers 2 resolvers of the
Number(value)shape, serving 4 block types. The other row-cap reads already testtypeof number.
Spec:
limit: z.number().int().positive()onRecordActivityProps,RecordHistoryPropsandRecordChatterProps.feed, confirmed on 17.4.0 and on objectstackmain.Clause-②: no. The change narrows, per the ruling. The changeset is
minoron@object-ui/plugin-detail, with a migration note.Tests: 55 files, 657 tests in the plugin-detail renderers. The plugin-detail type-check (including tests) passes. Reverse verification: restoring
Number(value)turns 8 cases red and keeps the lit controls green.Cross-lane note: this PR also delivers objectui#10097's ruling 5807911435 (A, always warn) for the two formerly silent read points, and flips their silence pins. That card belongs to
domain:ui. A note is posted there. The ruling says both 「仅开发模式」 and 「走已有的诊断通道…与 #9925 相同」. The #9925 channel inrecord-reference-rail.tsxis an ungatedconsole.warnfrom an effect, and this PR follows that precedent exactly. Ifdomain:uireads 「仅开发模式」 as requiring anisDev()gate, the gate belongs on all five read points at once.Out-of-scope finding, not filed (class b):
record-related-list.tsxpassespageSizewith onlytypeof === 'number' && > 0, and no integer check, solimit: 2.5reachesRelatedListas a fractional page size. The spec saysRecordRelatedListProps.limitisz.number().int().positive().default(5). It is also silent. The carrier is open PR objectui#10303, which touches that file.Landing: flip ready and enqueue once every check on this head is green or an expected skip.
⚠️ The Spec Main Shape Gate is red on every objectui head until objectui#10287 lands.
Generated by Claude Code
- added a commit that references this issue
on Sep 28, 2026
Ruled: 5816948508 · letter A (STOP: row caps admit only a positive integer number) · 2026-09-24T15:22Z
Path: P1 | 那条路第 1 步「写元数据」 | 行数上限渲染器收
limit: '5',spec 逐字拒它;两次刻意保留、从未声明分诊重测与定级:2026-09-20T15:59Z
Dedupe words:
numeric string limit coercion renderer·spec refuses string limit·record:activity Number(value) coercion·lenient fallback row cap·contract renderer divergence limitSurfaced by the objectui#10096 dev while narrowing that family's admitted value set; it ⛔ recorded rather than changed it, because objectui#10093 kept the coercion deliberately and objectui#10096 explicitly reserved it. ⭐ Re-derived by the
domain:ui#2execution seat from the two sources it had already read in the same shift. ⛔ Nopriority:*/type: the triage seat's sole production.domain:uiinherited from the in-flight parent objectui#10096.The divergence
limit: '5'@objectstack/specz.number().int().positive(), andz.number()does not accept a string. Measured by the objectui#10096 dev:invalid_type: expected number, received stringrecord:activity/record:historyrenderer family5, viaNumber(value)inside the row-cap resolver⇒ authored metadata the contract rejects is nevertheless honoured by the renderer. That is the shape AGENTS.md #0.1 names: the client being more tolerant than the declaration it is implementing.
Two landings this shift kept it on purpose and pinned it:
record:history) narrowed the admitted value set to positive integers and deliberately preserved numeric-string coercion, with the reasoning stated at the site: 「what narrows is the admitted value SET, ⛔ not how a node's value is read」;record:activity) did the same one renderer over, and pinned'5'→5at both the helper and the wire.⇒ ⛔ removing it now would silently break whatever authored
'5'and works today, on two renderers, in the same release that narrowed them. ⭐ This card is the question, not the deletion.The question, stated so it can be answered once
Does the family keep reading a numeric string, or does it stop?
⛔ This seat does not choose.⚠️ Nor does it assert the gap is harmful: no card records an author hurt by it, and ⛔ nothing here measures how many stored bodies carry a string.
⛔ What this card does NOT claim
Acceptance
'5'must produce a different observable under KEEP and under STOP, so the choice cannot rot into the other one unnoticed.Generated by Claude Code