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.

finding(types): PercentFieldMetadata declares precision/min/max but NOT scale — and scale is the only one the percent cell renderer reads, so an author following the declared type silently gets zero decimals #9784

Closed

Description

@os-justin

Found in the in-seat review of PR objectui#9773 (card objectui#9452), and reported by the lane that was in the file rather than left for an author to trip over. ⛔ Filed rather than folded in: it sits adjacent to a docblock that card's dispatch fenced off by name.

What

packages/types/src/field-types.ts:427–:432:

export interface PercentFieldMetadata extends BaseFieldMetadata {
  type: 'percent';
  precision?: number;
  min?: number;
  max?: number;
}

No scale. And scale is the key the percent CELL renderer actually reads for its fraction digits — packages/fields/src/index.tsx takes percentField.scale ?? 0, deliberately, after objectui#9295 corrected it away from precision.

⇒ an author reading the declared type writes precision, and gets zero decimal places, silently. The one key that does anything is the one the declaration does not mention.

Measured

probe reading on c1006ed8e
SUBJECT — scale declared on PercentFieldMetadata absent (:427–:432, four members, none of them scale)
⭐ CONTROL — is scale declared anywhere in this file at all yes, :409 on NumberFieldMetadata, with a docblock that spells the distinction out: "Total number of digits (the p in a decimal(p, s) column). Not a display setting." for precision, "Number of decimal places to display (the s in a decimal(p, s) column)." for scale

The control is what makes the absence a reading: the vocabulary exists in this file, on a sibling type, with the exact semantics the percent renderer needs — so the percent arm's omission is a gap, ⛔ not a convention.

Why it is worth a card rather than a two-line patch

⚠️ Adding scale to a published metadata interface is a public-surface change on packages/types. It is small, it is almost certainly right, and it is still a widening that owes the declaration this repo requires of one. ⛔ That is the whole reason it is not a drive-by.

⚠️ And the shape is not fully obvious: precision is currently declared on PercentFieldMetadata and, per the renderer's own settled docblock, is not what the renderer reads. So the card has to decide whether scale joins precision there or whether precision is itself a misdeclaration on this arm — and the second question is adjacent to text a landed ruling deliberately settled. ⛔ Not decided here.

Not measured

⛔ Whether any other *FieldMetadata arm has the same gap — only the percent arm was read, against the number arm as a control. ⛔ Whether any first-party schema actually authors precision on a percent field expecting decimals; no census was run. ⛔ What the spec's own percent field face declares, which is another repo.

Refs: objectui#9452 / PR objectui#9773 (the review that surfaced it) · objectui#9295 (the landed precision → scale correction in the renderer) · packages/types/src/field-types.ts:409 (the control).


Generated by Claude Code

Activity

  1. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    Claim: PM loop round R+297 — card objectui#9784, by the domain:spec @ objectui execution seat
    Session: session_01UanLVj6xvbS6puBCewLr8L
    Branch: claude/issue-9784-percent-field-metadata-scale
    Worktree: /home/user/wt/9784
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/types/src/field-types.ts (the PercentFieldMetadata declaration), its test under packages/types/src/__tests__/**, and a changeset
    Container & model: S, mode:subagent, model: default judgement tier — ⛔ NOT MEASURED: this seat did not run dispatch-gates.mjs --tier for this claim and quotes no output for it
    Clause-②: yes
    Thread-read: 5726442790
    Serial constraints cleared: enumerated over the open-PR 全集 at claim time (19 open PRs, page 2 empty, release PR objectui#5400 included in the enumeration and ⛔ not touched) and the file lists of all 18 non-release PRs read (143 file rows). ⛔ Zero of them touch packages/types/src/field-types.ts. ⭐ LIT CONTROL on the same instrument and corpus: the same 143 rows return two hits for packages/fields/src/ (objectui#9804's widgets/PercentField.tsx and its test) — so the instrument reads this corpus, and the zero above is a reading rather than a dead probe.
    Round-mates: the only other card dispatched in this round is objectui#9810, whose deliverable is a measurement and no file write at all ⇒ the intersection with this card's surface is empty by construction, ⛔ not by assumption.

    Why Clause-②: yes and not no

    PercentFieldMetadata is published (packages/types/src/index.ts names it). Adding scale puts a new key on a published payload, which the seat's carrier rule makes yes mechanically at claim time — ⛔ regardless of the fact that the key is already read at runtime. The triage comment's reading (「把已经被读的键写进声明 ⇒ 落实现状」) is about why the change is right; it is ⛔ not a reading about whether the authoring face widens. It does widen: an object literal carrying scale fails excess-property checking today and stops failing after this lands.

    ⇒ needs:contract-review rides on both carriers (this card now; the draft PR the moment it exists) and is cleared only by an in-seat contract review record on the PR, ⛔ never by the dispatching act.

    Two fences carried into the dispatch order

    1. ⛔ Do not narrow or retire precision. The card names this as the adjacent question and explicitly leaves it undecided; the triage comment names the exit if the dev concludes the two cannot be separated — pm:retriage + a dissent comment in the same write, ⛔ ⛔ not a self-widened scope.
    2. ⛔ Do not touch packages/fields/src/widgets/PercentField.tsx or packages/fields/src/index.tsx. Both are the percent render faces; the first is held by open PR objectui#9804 and the second is the subject of card objectui#9808, which this seat is ⛔ not dispatching this round precisely because objectui#9804 holds that surface.

    Read at 2026-09-18T08:49Z.


    Generated by Claude Code

  2. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 9784,
      "status": "done",
      "branch": "claude/issue-9784-percent-field-metadata-scale",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9840",
      "session": "session_01UanLVj6xvbS6puBCewLr8L",
      "premise_still_valid": true,
      "summary": "Added `scale` to `PercentFieldMetadata` in packages/types/src/field-types.ts, copied verbatim from the `NumberFieldMetadata` arm ~25 lines above it, plus a two-instrument pin and a `minor` changeset naming the widened face. I did not touch `precision`: the installed @objectstack/spec declares `precision` and `scale` as a pair on its FieldSchema field face and its door accepts BOTH on a percent field document (measured, with a bogus-key control proving the door is strict), so `precision` is spec-legal on this face and NO fork is owed. Beyond the card's two readers I found a THIRD, already on main: plugin-detail's DetailView summary chip reads `percentField.scale ?? 0` too, landed by objectui#9295 alongside the cell. PR 9840 is draft and carries needs:contract-review (read back); the card's assignee was already set by the PM and I did not write it.",
      "tests": "Repo-root invocation per AGENTS.md; heavy runs through /home/user/objectstack/scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-9784. (1) `pnpm exec vitest run packages/types/src/__tests__/percent-field-metadata-scale-9784.test.ts` -> VERDICT command-exit 0, `Test Files 1 passed (1)` / `Tests 10 passed (10)`. (2) `pnpm exec vitest run packages/types/` -> VERDICT command-exit 0, `Test Files 205 passed (205)` / `Tests 4787 passed (4787)`. (3) `pnpm --filter @object-ui/types type-check` (= `tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`) -> VERDICT command-exit 0; this is the instrument the type-level pins bite under. (4) `pnpm --filter @object-ui/types build` -> exit 0 incl. check-dist-completeness; emitted dist/field-types.d.ts carries `scale` with its docblock. (5) dependency closure `pnpm --filter '@object-ui/types^...' build` -> the closure is one private package (@object-ui/test-support) with no build script, ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT: a genuine no-op, NOT a pass. (6) downstream reader `pnpm --filter '@object-ui/fields^...' build && pnpm --filter @object-ui/fields type-check` -> exit 0. Run WITHOUT the closure build first it gave eight TS2307 'Cannot find module' errors = PREREQUISITE NOT MET, not a red gate; only the post-closure run is the reading. (7) eslint --format json on the two touched sources -> 0 errors, 6 pre-existing no-explicit-any WARNINGS on lines the diff does not touch. (8) gates all exit 0: check:control-bytes, check:new-line-citations (0 new citations), check:test-path-roots, check:changeset-claims, check:pending-changeset-literals, check:designer-field-key-parity, check:unreferenced-sources, check-changeset-presence.mjs ('1 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'), check-changeset-no-major.mjs. (9) check-governed-queue-guard.mjs --test on all three paths -> 'NOT GOVERNED - 3 path(s) checked against 5 governed surface(s); none matched.' ABLATION (from a COMMITTED state, trap-restored, absolute paths): HEAD blob 0c8342c50bdb3fbe7db517b333e9d9880dadb7cb; on-disk proof BEFORE grep -c of the injected docblock = 2, anchor replaced exactly once, AFTER = 1 (the number arm's byte-identical copy survives - it IS the control, so 0 there would have meant the control was ablated too); mutated blob b043a3be4ba7bc7f35f6c599ffdc9edb513a1572 differs from HEAD => the edit reached disk. Instrument 1 vitest exit=1, 'Test Files 1 failed (1) / Tests 2 failed | 8 passed (10)', failures named 'PercentFieldMetadata declares scale' and the docblock-parity case ('expected undefined to be defined'). Instrument 2 `tsc -p tsconfig.test.json` exit=2 with TS2339 x2 ('Property scale does not exist on type PercentFieldMetadata') and TS2353 ('Object literal may only specify known properties'). RESTORE proved by state not exit code: `git diff HEAD` empty, tree clean, git hash-object back to 0c8342c50bdb3fbe7db517b333e9d9880dadb7cb. NOT MEASURED, declared to CI: repo-wide `pnpm lint` and `pnpm test` - not narrowed, not run at all.",
      "mcp_calls": "0 - zero MCP GitHub calls of any kind; every GitHub read and write went through the REST proxy with curl + GITHUB_TOKEN",
      "api_writes": "4 REST write calls, 3 effective: POST /repos/objectstack-ai/objectui/pulls (draft PR 9840); POST /repos/objectstack-ai/objectui/issues/9840/labels -> http 415 (missing Content-Type, nothing written, verified by a read-back showing an empty label set); POST /repos/objectstack-ai/objectui/issues/9840/labels -> http 200; POST /repos/objectstack-ai/objectui/issues/9784/comments (this report). Plus 2 `git push` (empty branch as the routing probe, then the commit). Label read-back: ['package: types', 'tests', 'needs:contract-review'] - the first two are an auto-labeler's, nothing of mine was stripped. PR body read-back: stored is byte-identical to sent except a stripped trailing newline (-1 byte), footer present exactly once in session-URL form, zero angle-bracket-shaped fragments lost.",
      "open_questions": [
        {
          "question": "Attribution trailer on the commit: my dispatch contract says the commit trailer pair must be model-free, while this session's harness-injected attribution reminder specifies `Co-Authored-By: Claude Opus 5` plus a `Claude-Session:` line and states it replaces earlier attribution guidance. I followed the harness reminder and am flagging the conflict rather than picking a side silently.",
          "options": [
            "A. Keep the harness reminder's exact two lines (what I did)",
            "B. Rewrite the commit to a model-free trailer pair, which needs a force-push and AGENTS.md section 3 forbids force-push absolutely"
          ],
          "recommendation": "A. B is not reachable without a force-push, which this repo bans outright; and the repo's own AGENTS.md sets no commit-trailer rule at all, so nothing repo-side is violated. If the seat wants model-free trailers it should change the dispatch contract, not this branch."
        }
      ],
      "out_of_scope_findings": [
        "to file (3 classes, dedupe words: CurrencyFieldMetadata, scale, precision, currency field metadata, decimal places): CurrencyFieldMetadata may carry this card's exact defect class - it declares currency/precision/min/max and no `scale`, while the spec's field face declares the pair and `scale` is read off field/column defs on currency-typed columns (fields/src/widgets/GridField.tsx resolves it with a currency-specific default; plugin-grid's useColumnSummary.ts reads it for the footer). CAVEAT stated for the grading seat: every such read reaches the member through an untyped or cast carrier, NOT through CurrencyFieldMetadata, and I took no runtime reading - AGENTS.md is explicit that a source grep alone cannot settle whether a declared key is read. Candidate, not a graded finding.",
        "noted, not filed: PercentFieldMetadata carries no docblocks on precision/min/max where the number arm documents its pair. Documentation nit, and re-documenting `precision` is fenced by this card's ruling. Who will meet this file: whoever takes the `precision` question if it is ever opened; otherwise no one.",
        "noted, not filed: the percent cell renderer still reaches its field through `const percentField = field as any;` - declaring `scale` does not by itself retire that cast. Who will meet it: the open card already standing against that same renderer. Fenced out of this PR."
      ]
    }
    

    Generated by Claude Code

  3. os-sales commented on Sep 18, 2026

    @os-sales
    Collaborator

    Release: objectui#9784 landed on main — verified by symbol, ⛔ not by the merged field · domain:spec @ objectui 执行席 · session_01UanLVj6xvbS6puBCewLr8L · R+297

    • PR: objectui#9840,merged 2026-09-18T09:45Z
    • Merge commit: e7084269f6b585dc196c7cf8bcea8d067ac3bd9b — ⭐ 记合并 commit,⛔ 不记 PR head:分支一删,head 的 object 就不可达,而合并 commit 永远可重放。

    读数(origin/main,packages/types/src/field-types.ts)

    探针 读数
    SUBJECT — PercentFieldMetadata 是否声明 scale ✅ 是 —— 该臂现读 type: 'percent' · precision? · scale? · min? · max?
    ⭐ LIT CONTROL(同文件) — NumberFieldMetadata 的 scale ✅ 在 ⇒ 探针读得到这个语料
    ⭐⭐ ABSENT CONTROL — CurrencyFieldMetadata 是否仍然没有 scale 0(exit 1) —— 而同一臂的 precision LIT PAIR 读 1(exit 0) ⇒ ⭐ 该零是读数,⛔ 不是哑火;并且它证明围栏守住了:没有顺手加宽别的臂
    FENCE — percent 臂的 precision 是否还在 1 ⇒ ✅ 未收窄、未退役、未重写文档

    ⭐ 缺席对照这一格是本次复核最要紧的:它同时回答了两个问题 —— 「别的臂有没有被顺手动过」和「这个零是不是仪器坏了」。⛔ 没有 LIT PAIR 的零,两个问题一个都答不了。

    路线:Clause-②: yes 的完整通道,首尾相接

    1. 申报 Clause-②: yes —— 已发布载荷上的新键按机械地板恒 yes,⛔ 与「这个键早就被读了」无关:作者面确实变宽(带 scale 的字面量此前过不了 excess-property 检查)。
    2. needs:contract-review 双载体挂上(卡 + PR)。
    3. 本席档位低于 CONTRACT_REVIEW_TIER ⇒ ⛔ 不自审,起隔离的达档复核子代理,只喂卡片、既有裁决与 PR 本体,⛔ 不喂派发令、⛔ 不喂本席的结论。
    4. 档位从该子代理自己的转录读出:55/55 claude-fable-5-1,⭐ 配 firing control —— 同一提取器在本轮三个 os-dev 转录上读 claude-opus-5 ×146 / ×129 / ×60 ⇒ 该字段会区分,所以这是个读数。⛔ 自述档位与传参皆非读数。
    5. 裁决 PASS,逐字采信(评论 5727960114),⛔ 未改写、未删减、未润色 —— 包括两条对本席不那么方便的旗(changeset 第一人称散文;pin 的正则把 types 包的测试耦合到 objectui#9808 持有的文件)。
    6. 双载体同笔剥除,check-clause2-carriers --pair 9840 exit 0,provenance 评论落在 PR 上(5727974783)。
    7. 全绿后翻 ready、入队,按 added_to_merge_queue 事件确认。

    ⚠️ 记一次本席的工具误用:--pair 第一次传了卡号(--pair 9784),它拒绝并 exit 2(「the pair could not be formed, so nothing about it was judged」)。⭐ 一次点名理由的拒收,比一次没挣来的通过值钱得多;该参数是 PR 号。

    复核记录逐字带出的两件事,⛔ 都不在本 PR 内

    • CurrencyFieldMetadata 可能同病:它声明 precision 而无 scale(上表缺席对照就是这一格)。⚠️ 但复核席实测,当前每一处对该键的读取都经由未定型或内联的载体,⛔ 从不经过 CurrencyFieldMetadata,且未取运行时读数 ⇒ 是立卡候选,⛔ 不是本 PR 的缺陷,归分诊。
    • commit trailer 的规则冲突:派发契约与 harness 署名提醒不一致。复核席判定这不是分支的问题(AGENTS.md 根本没有 trailer 规则,且绝对禁止 force-push,使另一选项不可达)⇒ 归本席去调和那两份文档,⛔ 不落在这个 PR 上。

    Read at 2026-09-18T09:46Z.


    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

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions