Skip to content

the row-cap renderers read a numeric string the spec refuses outright — kept deliberately twice this week, never declared #10145

Description

@os-tesla

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 limit

Surfaced 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#2 execution seat from the two sources it had already read in the same shift. ⛔ No priority:* / type: the triage seat's sole production. domain:ui inherited from the in-flight parent objectui#10096.

The divergence

side what it does with limit: '5'
@objectstack/spec REFUSES — the member is z.number().int().positive(), and z.number() does not accept a string. Measured by the objectui#10096 dev: invalid_type: expected number, received string
the record:activity / record:history renderer family ACCEPTS — resolves it to 5, via Number(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.

⚠️ Why it is NOT simply「delete the coercion」

Two landings this shift kept it on purpose and pinned it:

  • objectui#10093 (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」;
  • objectui#10096 (record:activity) did the same one renderer over, and pinned '5' → 5 at 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?

  • KEEP ⇒ the divergence is deliberate and should be declared — at the sites and in the docs — so the next reader meets a decision rather than a leniency. ⚠️ And then the contract-vs-renderer gap is a standing fact somebody may later read as a bug again.
  • STOP ⇒ the renderers refuse what the contract refuses, and that is a behaviour change for authored metadata that works today, which wants a migration note rather than a quiet tightening.

⛔ 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

  • ⛔ Not that objectui#10093 or objectui#10096 did the wrong thing: both reserved this explicitly, which is why it has a number now instead of a silent edit.
  • ⛔ Not that the coercion is the same question as the loudness one (objectui#10097) or the non-integer one (objectui#10096). Those are about refusing and about telling; this is about which shapes are read at all.
  • ⛔ Not that other members with the same shape behave this way: unmeasured. ⚠️ If the answer is STOP, the sweep is part of it.

Acceptance

  • One answer, applied to every read point in the family, with the reasoning at the sites.
  • ⭐ Whichever way it goes, a pin that discriminates: '5' must produce a different observable under KEEP and under STOP, so the choice cannot rot into the other one unnoticed.
  • If STOP: a count of how many shapes in this family read a string, and a migration note. ⛔ Not a quiet tightening.

Generated by Claude Code

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 20, 2026
  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Re-measurement at source, replacing the deleted triage comment

    domain:spec @ objectui execution seat · session session_01877XiBYSaRCk2CU7cMSg3S · 2026-09-24T05:46Z · objectui origin/main 62597c5 · read with git show / git grep on origin/main, never the working tree. Maintainer instruction in this seat's session, verbatim: 「这些卡你自己分析和排查一下」.

    ⚠️ The clone is shallow: its oldest commit is 8ed4b95 at 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) and record-history.tsx:88-91 (normalizeHistoryLimit) both do Number(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,180 and record-history.rowLimitRefusal-10005.test.tsx:114.
    • Spec source: ui/component.zod.ts:1404 RecordActivity limit: z.number().int().positive().default(20) and :1822 RecordHistory z.number().int().positive().optional() both refuse a string.
    • ⭐ Beyond the card (checked in node): Number() also admits true→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 is yes (it publishes a wider accepted set than spec). Size S either way.

    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    待裁:行数上限(limit)要不要继续接受 '5' 这种字符串 —— domain:spec @ objectui 执行席落卡,2026-09-24T13:55Z,session session_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 仍拒字符串 —— 在 objectstack packages/spec/src/ui/component.zod.ts grep limit: 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

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Ruling: 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:19Z

    Director 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 limit as z.number().int().positive() on both record:activity and record: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) and normalizeHistoryLimit (record-history.tsx) admit only a positive integer number.
    • Every other value falls back to the default with a loud console.warn naming the block and the raw value. That includes '5', ' 5 ', '0x10', true and [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 limit drops 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:queue in this stroke. priority:p2 and domain:spec stand. Clause-②: no (it narrows).


    Generated by Claude Code

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    Claim: 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) and record-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-…md with 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. On origin/main, the Number(value) row-cap reads are record-history.tsx:89 and recordActivityFeed.ts:601 (2 hits in plugin-detail/src, not counting test prose). Open PRs in plugin-detail touch only record-related-list.tsx (objectui#10303) and record-details.tsx (objectui#10279). No overlap.

    Dispatch authority: director ruling 5816948508 (maintainer 「同意」), plus objectstack-ai/objectstack#19980 and 「任务很多,并发保持3」.

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    os-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

  7. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    ACCEPT: objectui#10145, delivered by PR objectui#10309 at head 5cdeedc. Session session_01877XiBYSaRCk2CU7cMSg3S, domain:spec @ objectui seat, reviewed against the diff and origin/main.

    Executes ruling 5816948508 (A, STOP) as written.

    • normalizeLimit (record:activity, record:chatter, record:discussion) and normalizeHistoryLimit (record:history) now admit only a typeof number positive integer. This is the same predicate the objectui#9925 read points use.
    • '5', ' 5 ', '0x10', true and [7] now drop to the default. 1.5, 0, -3, NaN and Infinity already did.
    • Each renderer emits one console.warn from an effect, keyed on the declaration, naming the block and the raw value with its type. undefined and null stay silent. This matches the channel in record-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 test typeof number.

    Spec: limit: z.number().int().positive() on RecordActivityProps, RecordHistoryProps and RecordChatterProps.feed, confirmed on 17.4.0 and on objectstack main.

    Clause-②: no. The change narrows, per the ruling. The changeset is minor on @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 in record-reference-rail.tsx is an ungated console.warn from an effect, and this PR follows that precedent exactly. If domain:ui reads 「仅开发模式」 as requiring an isDev() gate, the gate belongs on all five read points at once.

    Out-of-scope finding, not filed (class b): record-related-list.tsx passes pageSize with only typeof === 'number' && > 0, and no integer check, so limit: 2.5 reaches RelatedList as a fractional page size. The spec says RecordRelatedListProps.limit is z.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

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

Metadata

Metadata

Assignees

Labels

domain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanepriority:p2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions