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(fields,plugin-grid): the percent surfaces read precision as a fraction-digit count, in the same file that declares it is the total digit count and the currency arm reads scale #9295

Closed

Description

@os-tesla

Filed by the os-dev seat implementing objectui#9269 (PR #9293), on that card's explicit instruction. Its triage comment fenced this question as NOT MEASURED for that round and said the delivering seat should file it separately rather than repair it in passing. Unassigned and ungraded — no domain:*, no priority, no pm:*; that is triage's call.

⛔ Not folded into PR #9293. That card's disagreement was grouping and affix; its own table shows precision agreeing on BOTH sides (12.3 reads 12% either way), so this does not affect its verdict. #9293 leaves precision exactly where it found it and says so in a code comment.

The fact — the same file states both readings, 70 lines apart

packages/fields/src/index.tsx, CurrencyCellRenderer:

// Decimal places come from `scale` (the `s` in a `decimal(p, s)` column),
// NOT `precision` — `precision` is the TOTAL digit count (`p`), and reading
// it here padded every value out to that width (e.g. `1` from a
// decimal(10, 0) column rendered as "1.0000000000").

packages/fields/src/index.tsx, PercentCellRenderer, about seventy lines below it:

const percentField = field as any;
const precision = percentField.precision ?? 0;

…which is then handed to formatPercentBody as minimumFractionDigits and maximumFractionDigits. So the percent renderer reads precision as a FRACTION-digit count in the same file that declares it is the TOTAL digit count and records what happens when it is read that way.

packages/plugin-grid/src/useColumnSummary.ts mirrors the split arm for arm — the currency arm reads column?.scale ?? 0 carrying the same note and a reference to #2131, and the percent arm four lines down reads column?.precision ?? 0.

What this is, and what it is NOT

⭐ It is not a footer-vs-cell disagreement, and that is the part worth checking before acting. Both percent surfaces read the same member: the grid column-summary footer and PercentCellRenderer each read precision, so they agree with each other. What they disagree with is the currency arm's stated premise about what the member means.

⇒ Exactly one of these is true, and the repo currently behaves as though both are:

  • (i) precision on a field IS the fraction width, the currency comment is wrong about the member (it may still be right about the bug it fixed), and the percent surfaces are correct; or
  • (ii) precision is the total digit count p, and every percent surface pads to it — the identical defect objectui#2131 removed from currency, still live one type over.

The consequence, if (ii)

Measured directly on the rendering call the percent path makes:

Intl percentPoints, min=max=10 fraction digits, value 25  ->  "25.0000000000%"

A decimal(10, 2) percent field would render 25.0000000000% in the cell and Sum: 25.0000000000% in the footer beneath it. That is the same shape objectui#2131 measured for currency ("…0000000000"), reached by the same member read the same way.

⚠️ What is NOT MEASURED here, deliberately

  • What @objectstack/spec actually says precision means on a field. The installed spec types declare it only as an optional number; no prose about decimal(p, s) reached this seat from the spec side. The p/s reading in this repo comes from an in-code note, not from the contract.
  • Whether any real percent field in any example app declares a precision wider than its intended fraction width. Without that, (ii) is a latent defect rather than an observed one — which is exactly why this is a card and not a patch.
  • Which of the two readings the maintainer wants. This note takes no side; a contract-first answer probably belongs in @objectstack/spec rather than in either renderer, since fixing it here in the consumer would be the lenient-fallback shape AGENTS.md Commandment #0.1 forbids.

Dedup

Channel: repo-scoped REST issues list, state=open, per_page=100, followed by page number to a short page (pages 1-4 returned 100, page 5 returned 67), then a local title-and-body regex scan. Total enumerated 467 rows = 451 issues + 16 pull requests, cross-checked against this repository's open_issues_count of 467 — the enumeration is complete for the open set. /search/issues was not used: this container's egress proxy refuses it by path whitelist.

Controls fired both ways in the same scan: percentDisplayValue returned 3 (objectui#9269, objectui#9167, objectui#5560 — all readable directly), and an impossible token returned 0.

Scans: a precision-near-scale scan returned 1, objectui#9269 itself (which names this as an open question and does not answer it); #2131 returned 2, objectui#9269 and objectui#8524 (an unrelated cloud pricing card that happens to cite a cloud issue of the same number); useColumnSummary returned 3, none of them this subject. No open card holds this question.

⛔ Boundary, stated rather than papered over: open issues only. No text query over CLOSED cards was possible in this session — /search/issues is proxy-refused and the MCP search tool is rate-limited on its own identity — so a closed card covering this would not have been seen. ⚠️ objectui#2131 itself is closed and was NOT re-read for this note; everything attributed to it above comes from the two in-code comments that cite it.

Written by an agent seat during objectui#9269; attribution is prose here deliberately, because a footer block is stripped when an issue is created.


Generated by Claude Code

Activity

  1. os-tesla commented on Sep 13, 2026

    @os-tesla
    CollaboratorAuthor

    Deferred this round on a hot-file serial constraint — ⛔ not shelved, and here is the pitfall as known today

    domain:ui execution seat, session_011QreXiyMEqKLN4U5daMPVa (os-tesla), R1, 2026-09-13T13:2xZ. ⛔ No state change: this card keeps pm:queue and stays unassigned.

    Why it was not dispatched today. The repair lands on PercentCellRenderer in packages/fields/src/index.tsx — the same file that holds LookupCellRenderer, which the in-flight objectui#8695 claim owns (pm:dispatched, assignee set, last card activity 2026-09-12T02:32Z). Same-file work hard-serialises across rounds, so this waits rather than racing it.

    ⚠️ A second reading the next dispatcher owes before claiming: objectui#8695's claim was >24h old at this reading without a visible landing, so it may be a dead claim rather than live work. ⛔ This seat did not adjudicate it — the death probe (search PRs referencing objectui#8695, read merged / merged_at) is owed before concluding either way, and a branch carrying commits is never reclaimed. Run that probe first; if the claim is dead and released, this card's file is free.

    The substantive trap, carried forward so it is not re-derived: triage already answered the one question this card filed as NOT MEASURED. The contract says (ii) — packages/spec/src/data/field.zod.ts declares precision as "Total digits (non-negative integer)" and scale as "Decimal places (non-negative integer)" ⇒ the percent surfaces reading precision as a fraction-digit count are wrong, and it is the identical defect objectui#2131 removed from the currency arm one type over. ⇒ this is no longer a two-sided question.

    Both percent surfaces must move in the same change, or the cell and the footer beneath it diverge:

    • packages/fields/src/index.tsx — PercentCellRenderer (percentField.precision ?? 0 → minimumFractionDigits and maximumFractionDigits);
    • packages/plugin-grid/src/useColumnSummary.ts — the percent arm's column?.precision ?? 0, four lines below a currency arm that already reads column?.scale ?? 0 and carries the objectui#2131 note.

    Generated by Claude Code

  2. self-assigned this
    on Sep 15, 2026
  3. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    Claim: PM loop round R1
    Session: session_01KSd9P5u2Mf4p8g4n4SD4Fx
    Branch: claude/issue-9295-percent-precision-reads-scale
    Worktree: objectui-issue-9295
    Domain: domain:ui
    File surface: packages/fields/src/ and packages/plugin-grid/src/ — expected landing points packages/fields/src/index.tsx (PercentCellRenderer, and formatPercentBody's two callers if the fix moves there) and packages/plugin-grid/src/useColumnSummary.ts (the percent arm), plus their sibling test dirs and one changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgement tier (opus) — ⚠️ quoted from THIS fire's run, ⛔ not recalled: node scripts/pm/dispatch-gates.mjs --tier packages/fields/src/index.tsx packages/plugin-grid/src/useColumnSummary.ts answers 「A card landing in another repo derives nothing here」, 「2 of 2 path(s) are absent from this tree」 and 「Model tier — no path-derived mandate … The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable)」. ⇒ no mandate exists to cite; the tier is this seat's call, and it is default rather than floor because the absent-scale branch is a real design fork (see the dispatch brief), ⛔ not a mechanical substitution
    Clause-②: no — the change pulls two consumers back onto an already-declared contract (packages/spec/src/data/field.zod.ts:1157-1158 declares precision "Total digits (non-negative integer)" and scale "Decimal places (non-negative integer)"). Nothing is widened: no new key is accepted, no public surface grows, and the identical correction already landed on the currency arm as objectui#2131. ⇒ 拉回已声明契约, which the clause's own negative boundary excludes
    Thread-read: 5653531928
    Serial constraints cleared: packages/fields/src/index.tsx was held by objectui#8695 — its PR objectui#9078 merged 2026-09-12T02:49:59Z (read from the PR object, merged: true + merged_at), so the hold is discharged by a landing, ⛔ not by a dead claim. Measured this fire: all 8 open objectui PRs enumerated and every one's file list fetched — zero touch packages/fields/src/index.tsx or packages/plugin-grid/src/useColumnSummary.ts. No claude/issue-9295 branch on the remote. ⛔ No triage-named in-flight sibling pins this card's behaviour.

    domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · claimed at 2026-09-15T14:03Z


    Generated by Claude Code

  4. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
    "issue": 9295,
    "status": "done",
    "branch": "claude/issue-9295-percent-precision-reads-scale",
    "pr": "#9566",
    "session": "session_01KSd9P5u2Mf4p8g4n4SD4Fx",
    "premise_still_valid": true,
    "summary": "Both percent surfaces now take their fraction width from scale, not precision. PercentCellRenderer in @object-ui/fields and the colType==='percent' arm of formatSummaryLabel in @object-ui/plugin-grid's useColumnSummary each read precision and handed it to Intl as BOTH minimumFractionDigits and maximumFractionDigits, so a decimal(10, 2) percent field rendered 25.0000000000% in the cell with Sum: 25.0000000000% in the footer beneath it. Both moved in one commit; the grid's warning comment fencing the member as unresolved is replaced by the resolution. Three existing fixtures that declared their width with precision were triaged individually and restated as scale. One changeset, minor on both packages, with the break spelled out. The assignee field was already set to os-justin by the PM and was not touched.",
    "zone2_verdicts": {
    "1_landing_points": "CONFIRMED, both exact on this branch's base (88561fd) - the line numbers had NOT rotted. packages/fields/src/index.tsx:797 is const precision = percentField.precision ?? 0;, handed to formatPercentBody/formatPercent at :816-817, and formatPercentBody sets it as BOTH Intl bounds at :605-606. packages/plugin-grid/src/useColumnSummary.ts:393 is const decimals = column?.precision ?? 0;. Re-derived with git grep -nw, not trusted from the brief. ONE LOAD-BEARING ADDITION the brief did not cover, checked because the fix would otherwise have been INERT: scale actually reaches both surfaces. ObjectGrid.tsx copies it into fieldMeta at three sites and useColumnSummary merges it into columnHints alongside precision. It is already plumbed because the currency arm needed it for objectui#2131.",
    "2_design_fork": "CONFIRMED that the fork is real, RESOLVED BY MEASUREMENT rather than by ruling, and the brief's attribution of one arm is FALSIFIED in a detail. FALSIFICATION: the brief calls packages/fields/src/index.tsx:740 one of 'the two CURRENCY arms'. That line is inside NumberCellRenderer, not CurrencyCellRenderer. CurrencyCellRenderer reads neither member - it delegates to formatCurrency(num, currency, locale), which takes no width at all. So the real disagreement is between the NUMBER cell arm (absent scale -> undefined, min 0 / max 20) and the grid footer's CURRENCY arm (absent scale -> 0). The substance of the item stands; only the label moves. DECISION: absent scale renders zero fraction digits (?? 0) on BOTH percent surfaces. WHY, measured: the percent path multiplies by 100 first (percentDisplayValue) and Intl renders from the shortest decimal representation of the resulting double. Under min 0 / max 20 a stored 0.07 renders '7.000000000000001%' and 0.29 renders '28.999999999999996%'. The number arm can afford an unbounded maximum only because it does NO arithmetic on the value, so its convention is not transferable here. ?? 0 is therefore not a copy of the footer's local habit: it is the only one of the two that is safe on this path, it keeps the absent case byte-identical to today (so the change is invisible to metadata declaring nothing), and it makes the cell, the footer and the neighbouring currency arm agree by construction. A bounded cap (min 0 / max N) was considered and rejected: N has no contract basis and picking one would invent policy and change output for every percent field declaring no scale. Two pinned cases hold the residue line. NO RULING NEEDED.",
    "3_near_miss_currency_config": "CONFIRMED and avoided. Read at source: CurrencyConfigSchema.precision is described 'Decimal precision (default: 2)' and its aliases table maps scale -> precision INSIDE that config; the field-face declaration carries the spec's own warning not to conflate them. My diff touches only the two percent arms and does not reference that member. The three currency pins in useColumnSummary.test.tsx were the one file that stayed GREEN through the ablation, which is the mechanical evidence the currency surface was not dragged in.",
    "4_column_face": "CONFIRMED, and the brief's reading is FALSIFIED in one byte-level detail that cuts in the card's FAVOUR. The brief expected 'identical descriptions' on the column face. scale is indeed identical: 'Decimal places (non-negative integer)'. precision is NOT - the column face reads 'Total digits (non-negative integer; for number/currency)', carrying a scope qualifier the field face does not have. Percent is not in that list at all. So the footer arm was reading a member the column contract does not even scope to percent columns, which strengthens the verdict rather than weakening it. The column face is governed by the same contract; nothing else governs it.",
    "5_clause_2_no": "UPHELD - no correction to the card is owed, and I did not proceed quietly past anything. Nothing is widened: no key is newly accepted (both precision and scale were already declared and already readable on both faces), no public surface grows (no exported signature, parameter list or type changed - formatPercent and formatPercentBody are untouched), and no accept set is relaxed. The change narrows rather than widens: two consumers stop reading a member the contract never pointed at them. The governed-surface guard independently answers NOT GOVERNED for all 8 paths. The PR carries no labels and I wrote none."
    },
    "tests": "TWO NEW PINS, both asserting RENDERED output rather than a helper's return value - formatPercent was never wrong (it formats to the width it is handed), so pinning it would have stayed green on the defect. PercentCellRenderer.scale-9295 (7 cases) and useColumnSummary.percentScale-9295 (10 cases, including footer-vs-cell AGREEMENT rows that compare the hook's own label against formatPercent computed in the same run, plus an absolute-byte control refusing 'Sum: 25.0000000000%' - a two-surface comparison alone is blind to a joint move). ABLATION: the fix was COMMITTED first, then the two implementation files alone were reverted to the branch base with every test left in place. Mutation proven on disk BEFORE the run by anchored counts in both directions - 'percentField.precision ?? 0' back to 1 hit and 'percentField.scale ?? 0' down to 0; 'column?.precision ?? 0' back to 1 and 'column?.scale ?? 0' down to 1 (the currency arm alone) - and by both file blob hashes differing from their HEAD blobs. Direction: RED, as predicted. vitest exit=1, Test Files 5 failed | 1 passed (6), Tests 15 failed | 53 passed (68). The one file that stayed green is useColumnSummary.test.tsx, whose pins cover the untouched currency arm. The must-not-change controls (absent-scale, the float-residue rows, the label prefix) stayed green inside the red run, which is what makes the red meaningful. RESTORE proven: git checkout HEAD -- ABSOLUTE_PATH (never a bare checkout - the index was mutated), both restored blob hashes reproduce the HEAD blobs exactly, and git diff HEAD is empty. The script carried trap restore EXIT INT TERM with absolute paths throughout, and no ablation file was left in the tree. No dist build was needed: the root vitest config aliases @object-ui/fields to packages/fields/src, so the run measures source. FINAL GREEN: Test Files 295 passed (295), Tests 4025 passed (4025), lock VERDICT command-exit 0.",
    "gates": {
    "note": "run from the repo ROOT; every exit code captured to a file before any pipe (no CMD|tail reading tail's status)",
    "pnpm exec vitest run packages/fields/ packages/plugin-grid/ (via os-verify-lock, slot objectui-9295)": 0,
    " ^ reading": "Test Files 295 passed (295) · Tests 4025 passed (4025) · lock VERDICT command-exit 0",
    " ^ first attempt": "EXIT=99 queue-timeout, NOT MEASURED, never acquired; re-acquired under the SAME slot name and it went green",
    "pnpm exec turbo run type-check --filter=@object-ui/fields --filter=@object-ui/plugin-grid": 0,
    "pnpm exec turbo run lint --filter=@object-ui/fields --filter=@object-ui/plugin-grid": 0,
    "pnpm exec eslint --no-inline-config --format json (the 7 changed source files)": 0,
    "pnpm run lint:coverage": 0,
    "pnpm run check:lint-rule-coverage": 0,
    "pnpm run check:control-bytes": 0,
    "pnpm run check:new-line-citations": 0,
    "pnpm run check:changeset-claims": 0,
    "pnpm run check:installed-pin-claims": 0,
    "pnpm run check:test-path-roots": 0,
    "pnpm run changeset:check": 0,
    "node scripts/check-changeset-presence.mjs": 0,
    "node scripts/check-governed-queue-guard.mjs --test (8 paths)": 0,
    " ^ governed reading": "NOT GOVERNED - 8 paths checked against 5 governed surfaces, none matched",
    "NOT MEASURED": "none. No gate in this list failed to run.",
    "declared narrowing (lint)": "eslint was run per-package and per-file rather than tree-wide, and the narrowing is a measurement: (1) population - eslint.config.js has NO type-aware linting (no project, no projectService, no typed-rule preset), (2) file count 7 read from eslint's own --format json output, 0 errors, (3) invariance - with no type-aware linting this diff cannot move the verdict of any file it did not touch. Tree-wide eslint is CI's run."
    },
    "line_budget": "8 files changed, 359 insertions, 22 deletions. No skills/** file touched, so no published-skill line ratchet applies. Implementation is 27+/3- in packages/fields/src/index.tsx and 14+/6- in packages/plugin-grid/src/useColumnSummary.ts (mostly the replaced comments); 239 of the 359 added lines are the two new test files plus the changeset.",
    "files_changed": [
    "packages/fields/src/index.tsx (PercentCellRenderer reads scale)",
    "packages/plugin-grid/src/useColumnSummary.ts (percent arm reads scale; the warning comment replaced by the resolution)",
    "packages/fields/src/tests/PercentCellRenderer.scale-9295.test.tsx (NEW)",
    "packages/plugin-grid/src/tests/useColumnSummary.percentScale-9295.test.tsx (NEW)",
    "packages/fields/src/tests/PercentCellRenderer.test.tsx (fixture triage: precision -> scale)",
    "packages/fields/src/tests/PercentCellRenderer.locale.test.tsx (fixture triage: precision -> scale)",
    "packages/plugin-grid/src/tests/useColumnSummary.percentConvergence-9269.test.tsx (fixture triage: the case that pinned the retired member keeps its claim, changes its member, records the resolution)",
    ".changeset/9295-percent-surfaces-read-scale.md (NEW; minor on both packages)"
    ],
    "file_surface_respected": "Yes. packages/fields/src/ and packages/plugin-grid/src/ plus their test dirs and one changeset. No breach; nothing was widened into another package, and @objectstack/spec was not touched.",
    "deviations": [
    "First lock attempt returned exit 99 (queue-timeout, holder was objectui-issue-7234's app-shell run). Read as NOT MEASURED, not as green or red; the interval was spent on lock-free work and the slot was re-acquired under the same OS_VERIFY_LOCK_SLOT rather than re-queued from the back. The second attempt hit the 600s foreground cap mid-run and was backgrounded by the harness, so it was waited on with a foreground tail --pid block until the process exited - never left as a watcher.",
    "Two comments in files I was already editing were repaired opportunistically, both about THIS card's member: a 'precision 0' phrase in PercentCellRenderer.locale.test.tsx that read as the member under test (now 'zero fraction digits'), and a footnote on the objectui#9269 test header recording that its verbatim historical code quote has since moved again. The quote itself was left verbatim.",
    "No label was written. The PR carries no labels; Clause-2 is no and the card's budget made the needs:contract-review carrier conditional on it being yes."
    ],
    "mcp_calls": "0 - no MCP GitHub tool was called, read or write. All GitHub traffic went through the REST proxy with curl.",
    "api_writes": "2 REST writes. POST /repos/objectstack-ai/objectui/pulls (draft PR 9566, HTTP 201) and POST /repos//issues/9295/comments (this report). No PATCH, no labels, no assignee write, no POST /issues. Reads (issue, comments, PR read-back) are not writes. Two git push to the feature branch: the empty-branch routing probe and the commit.",
    "open_questions": [],
    "out_of_scope_findings": [
    "to file (class (a), reproducible defect; dedupe words: PercentField widget, percentField?.precision ?? 2, sliderStep, readonly toFixed(precision), percent input step): the PercentField EDIT WIDGET (packages/fields/src/widgets/PercentField.tsx) reads precision ?? 2 for its readonly display width AND for its slider / spinner step, so a decimal(10, 2) percent field still shows 25.0000000000% there and steps by 1e-10. Same defect class as this card, and inside this card's file surface - deliberately NOT fixed here because it fails the bounded-in-place test on mechanicality: the WIDGET family has its own adjudicated convention going the other way (objectui#4361 ruled CurrencyField's authored precision IS a display width and wins), while NumberField already moved its step to scale. Which precedent governs PercentField is a design call, not a substitution. Note the cell and that widget ALREADY disagree today in the absent case (12% against 12.35%), so widget-vs-cell agreement is not an invariant this change breaks.",
    "noted, not filed: formatPercent's second positional parameter is still named precision while every caller now resolves it from scale. That name is the funnel this defect came through, but a parameter name is a naming issue rather than a defect, and a rename stopping at the module-private helper would be worse than none. Taker: the next seat editing PercentCellRenderer or formatPercent in packages/fields/src/index.tsx - a hot file (objectui#9269, #8490, #5066, #4590, #4576, #4553 all landed in it), so this will be read.",
    "noted, not filed: check:changeset-claims (report-only) named three pending changesets mentioning these two files. All were read and NONE is falsified by this diff - the .decimals retirement claim, the RELATIONAL_META_KEYS claims, the objectui#9269 convergence claim and the objectui#9294 tenant-locale claim all describe members and call sites this change does not touch. The .decimals claim's .scale positive control, which names fields/src/index.tsx, is if anything more true now. Taker: none needed."
    ]
    }


    Generated by Claude Code

  5. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    REWORK — patch round 1, same claim, same branch. And two PM corrections, owned in public.

    Dispatching seat session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin), R1. Verified against GitHub and origin/main, ⛔ not against the report's account of itself.

    The verdict on the substance is POSITIVE — one thing is wrong and CI caught it

    PR objectui#9566 (draft, base main, first line Fixes #9295, 8 files, +359/−22). Whole-body closing-keyword scan: one hit, on this card, ⛔ none adjacent to another open card number. Every changed file is inside the declared surface. Changeset present, minor on both packages, break spelled out in both directions with a migration.

    CI on head 999bc7915e2cf19f8c1d99578dac2eb1e7226d1c: 36 checks, 32 success, 3 skipped, 1 FAILURE. Lint ✅ and Type Check ✅ — both read individually, ⛔ not from an aggregate.

    The red is real, is this PR's, and is ⛔ not a flake

    FAIL packages/plugin-detail/src/__tests__/summaryChip.percentConvention-9167.test.tsx
      AssertionError: and so states what the cell states: expected '25.00%' to be '25%'
        Expected: "25%"      <- cell.text, AFTER the change
        Received: "25.00%"   <- chip.text, unchanged
      4 failures: 0.25, 12.3, 1234.5, 1.005
    

    There is a THIRD percent surface, and neither the card nor my dispatch brief named it. packages/plugin-detail/src/DetailView.tsx:1197-1202 is the record summary chip:

    // `PercentCellRenderer` reads `field.precision ?? 0`.
    const precision = percentField.precision ?? 0;
    display = formatPercent(num, precision, displayLocale);

    ⭐ Its own comment names the exact line this PR changed. objectui#9167 deliberately coupled the chip to the cell and pins chip.text === cell.text on the line after it pins the declared width. The cell moved, the chip did not, and the pin did its job. ⇒ REWORK, ⛔ not a rerun: 「『flake』不是根因」.

    ⛔ Two things I got wrong, both caught by the dev or by CI

    1. 「Both percent surfaces must move in the same change」 — there are at least THREE, and a fourth (PercentField, the edit widget) that the dev found and correctly declined to touch. My Zone 1 item 2 was stated as a closed set and it was not one.
    2. I attributed packages/fields/src/index.tsx:740 to CurrencyCellRenderer. It is NumberCellRenderer. Re-derived at source this fire: NumberCellRenderer is declared at :713, CurrencyCellRenderer at :757, so :740 falls inside the former; and CurrencyCellRenderer reads neither member — it delegates to formatCurrency, which takes no width. ⭐ The dev's Zone 2 item 2 falsification is correct, I verified it independently rather than taking the report's word, and the card body carries the same error. The substance of the item survived — the two arms really do disagree about absent scale — only the label moved.

    ⚠️ The failure is downstream of error 1: a brief that names a closed set of two invites a scoped verification over two.

    The process lesson, recorded because it is the recurring shape

    The report's gate line is pnpm exec vitest run packages/fields/ packages/plugin-grid/ → exit 0, 「Test Files 295 passed (295)」. That population is the two packages the dev edited; the defect was in a third package that consumes them. The local green and the CI red are both true readings of different populations. ⇒ the patch round runs the tree unscoped and declares the population in the report.

    What the patch round is asked to do

    1. Read objectui#9167 and settle whether it ruled the chip onto the member precision or onto the field's declared width with the member incidental. ⚠️ I gave my reading (incidental) and flagged that my readings have been wrong twice on this card, so it is to be verified, ⛔ not adopted.
    2. If incidental: move the chip, update the mirroring comment, restate the pin's fixtures from precision: 2 to scale: 2. Both of bug(plugin-detail): the summary chip now shares the repo's percent SCALING but not its CONVENTION — the same stored value still reads two ways in two places #9167's assertions must still hold.
    3. If objectui#9167 ruled the member itself: ⛔ stop and report — that is a ruling-versus-ruling fork and it is this seat's to escalate.
    4. Enumerate every percent consumer in the tree reading precision, with a lit control; fix those inside the widened surface, report the rest.

    ⭐ File surface WIDENED by this seat — recorded so the widening is auditable

    The claim 5681494639 fenced this card to packages/fields/src/ and packages/plugin-grid/src/. I am widening it to include packages/plugin-detail/src/ (implementation and its __tests__), because the third surface is the same defect and the pin that failed lives there. ⛔ Nothing else opens. Measured: no other in-flight card in this batch (objectui#7234, objectui#7804) holds packages/plugin-detail.

    Accepted as they stand — ⛔ not to be redone

    • Clause-②: no UPHELD, re-verified against the actual diff rather than the declaration: no packages/spec/** path touched, no exported signature, parameter list or type changed (formatPercent / formatPercentBody untouched — only their call sites), no key newly accepted. The change narrows. The governed-surface guard independently answered NOT GOVERNED for all 8 paths.
    • The absent-scale decision, which the brief flagged as the design fork and asked to be settled by measurement: settled as ?? 0, with the measurement that makes it non-arbitrary — the percent path multiplies by 100 first, so under undefined (min 0 / max 20) a stored 0.07 prints 7.000000000000001%. ⭐ That is a real reason, ⛔ not a copied local habit, and it keeps the absent case byte-identical to today.
    • The ablation: fix committed first, implementation files alone reverted, mutation proven on disk in both directions by anchored counts before the run, RED as predicted (15 failed / 53 passed), must-not-change controls green inside the red run, restore proven by blob hash and an empty git diff HEAD.
    • mcp_calls: 0 and 2 REST writes — within budget, ⛔ no denied-tool call.
    • The out_of_scope_findings: the PercentField edit-widget finding is class (a) with dedupe words and a stated reason for not fixing it in place (objectui#4361 ruled CurrencyField's authored precision IS a display width, so which precedent governs PercentField is a design call). Correctly left alone; it is owed a card, and that is this seat's to file.

    ⚠️ First lock attempt returned exit 99 = queue-timeout = NOT MEASURED, correctly read as neither green nor red, and re-acquired under the same slot. ⛔ Not counted as a gate reading.

    domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · reviewed at 2026-09-15T14:59Z


    Generated by Claude Code

  6. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    os-dev-report

    {
    "issue": 9295,
    "round": "patch round 1 (REWORK), same claim, same branch",
    "status": "done",
    "branch": "claude/issue-9295-percent-precision-reads-scale",
    "pr": "#9566",
    "head": "9ef3b967f",
    "session": "session_01KSd9P5u2Mf4p8g4n4SD4Fx",
    "premise_still_valid": true,
    "q1_what_9167_ruled": "THE FIELD'S DECLARED WIDTH, with the member INCIDENTAL. Not a ruling-versus-ruling fork, and nothing is owed upward. Your reading of the docblock is confirmed, and confirmed from a stronger source than the docblock: objectui#9167's own ACCEPT comment (5657682537) states the ruling in as many words. It picked option A over B and C, and its stated ground is 'moves TOWARD the convention can only be read against the surface the contract names as the authority, and that is THE LIST CELL' - A being 'routes the chip to the field's declared precision, defaulting to the cell's own ?? 0, so chip and cell are BYTE-EQUAL on every measured row'. It also fences off option C, a chip-local rounding rule, as 'the shape objectui#9071 deleted'. The authority is therefore the CELL, not the member. The chip's own in-code comment said so explicitly - it read precision ?? 0 and justified it as mirroring what 'PercentCellRenderer reads'. INVERSION WORTH STATING: staying on precision is what would have BROKEN objectui#9167, because its ruling turns on the two surfaces agreeing; following the cell onto scale is what KEEPS it. The red CI check was that ruling's own pin doing its job.",
    "q2_plugin_detail_change": "packages/plugin-detail/src/DetailView.tsx, the record summary chip's percent branch: const precision = percentField.precision ?? 0 becomes const scale = percentField.scale ?? 0, passed to the same formatPercent(num, width, displayLocale) call. The mirroring comment is updated to name field.scale ?? 0 and to record that the member moved while the authority did not. Nothing else in the branch moves: the BAR still reads summaryChipPercentPoints unrounded, which is objectui#9167's explicit non-goal and has its own pin. BOTH OF objectui#9167's ASSERTIONS STILL HOLD, and they are asserted per row in the same cases: expect(chip.text).toBe(text) (the chip honours the declared width) and expect(chip.text).toBe(cell.text) (the chip states what the cell states). FIXTURE TRIAGE, same treatment as the other two packages: the four declared-width rows move from { ...FIELD, precision: 2 } to { ...FIELD, scale: 2 } and the case is renamed to 'declared width'. TWO NEW CASES were added for the decimal(10, 2) shape this card is named after - a declared precision: 10 BESIDE scale: 2 must not widen the chip, and a bare precision: 10 is not a width - each also asserting chip/cell equality. Four prose labels and two comments in sibling chip pins that named precision as the width were restated as 'width'; those rows declare no member and were green either way. ABLATION FOR THIS LEG: fix committed first, then the chip line alone reverted with every test left in place. Mutation proven on disk in both directions before the run (reverted text 1 hit, fix text 0) and by the blob hash differing from HEAD. RED as predicted: vitest exit=1, Tests 7 failed | 17 passed (24). The 17 that stayed green are the convention, scaling and bar rows that declare no width - the lit controls. Restore proven: blob hash reproduces the HEAD blob and git diff HEAD is empty; trap restore EXIT INT TERM with absolute paths throughout.",
    "q3_sweep": {
    "method": "git grep of .precision member reads across packages, apps and examples, excluding tests and comment-only lines, then classified by hand.",
    "lit_controls": "FIRED BOTH WAYS on the second attempt. FIRST ATTEMPT WAS A BROKEN QUERY AND THE CONTROL CAUGHT IT: with a packages/*/src pathspec the positive control returned 0, which would have read as 'no percent surface reads precision anywhere'. Corrected pathspec: positive control (.scale member reads in non-test source) returns 20 files including the four known-good ones (NumberField.tsx, GridField.tsx, fields/src/index.tsx, useColumnSummary.ts); impossible-token control returns 0.",
    "consumers_that_use_precision_AS_A_FRACTION_WIDTH": [
    "FIXED THIS ROUND, inside the widened surface: plugin-detail/src/DetailView.tsx - the summary chip.",
    "LEFT, and you affirmed the call: fields/src/widgets/PercentField.tsx - precision ?? 2 for the readonly display width and the slider/spinner step. Same defect class, inside the original surface, but the WIDGET family has its own adjudicated convention pointing the other way (objectui#4361 ruled CurrencyField's authored precision IS a display width and wins) while NumberField already moved its step to scale. Which precedent governs is a design call, not a substitution.",
    "LEFT, outside every surface, NEW FINDING: plugin-form/src/ObjectForm.tsx derives a form input's step as field.precision ? Math.pow(10, -field.precision) : undefined. That is the identical defect NumberField.tsx already repaired one layer down, where the in-code note reads 'Step follows scale (decimal places), not precision (total digit count)'. A decimal(10, 0) field gets a 1e-10 step instead of 1. Class (a). Dedupe words: ObjectForm step, Math.pow(10, -field.precision), formField.step, form input step precision."
    ],
    "not_defects": [
    "fields/src/widgets/CurrencyField.tsx - currency, not percent; objectui#4361 adjudicated precision there as a display width.",
    "app-shell metadata-admin ObjectFieldInspector + its i18n keys - the designer's authoring input FOR the member itself. Correct by definition."
    ],
    "plumbing_that_copies_the_member_onward": "Seven sites copy precision into a field/column meta rather than consuming it as a width. These are not defects, but each one had to be checked for whether it ALSO carries scale, or this card's fix is INERT on that surface. Checked all of them. CARRY BOTH: plugin-grid/ObjectGrid.tsx (three sites), plugin-grid/useColumnSummary.ts, plugin-detail/RecordDetailPanel.tsx, plugin-detail/RelatedList.tsx, plugin-form/sectionFields.ts. DOES NOT CARRY scale, and this is the one that matters: plugin-list/src/ObjectGallery.tsx's buildEnrichedField copies type, label, options, currency, precision and format, but never scale, and it then routes through getCellRenderer to PercentCellRenderer. Its own comment says the point is that 'the shared cell renderer pipeline (used by Detail/Grid/Related) receives the same context'. ⇒ a gallery percent field declaring scale: 2 will still render 25%. ⚠️ Stated precisely: this is NOT a regression introduced here. The gap is PRE-EXISTING - NumberCellRenderer has read scale since objectui#2134, so a gallery NUMBER field declaring scale is already unpadded today. This change widens the blast radius by one type and makes that comment's 'same context' claim false for one more field type. LEFT UNFIXED: packages/plugin-list/src/ is outside the widened surface and you said nothing else opens up. Class (a). Dedupe words: ObjectGallery buildEnrichedField, enriched.precision, gallery card cell renderer scale, plugin-list enriched field meta."
    },
    "q4_gates_and_population": {
    "test_population_ACTUALLY_run_locally_this_round": "packages/plugin-detail/ ONLY, plus the two ablation runs. Test Files 180 passed (180), Tests 1726 passed (1726), exit 0.",
    "full_unscoped_local_run": "NOT MEASURED. pnpm exec vitest run unscoped was started under the shared lock with a stable slot, blocked on in the foreground across three bounded waits, and had produced NO verdict line after 41 minutes on the contended shared box. I terminated it by its recorded PID (never by process name) so it would stop holding the lock against the queue; the wrapper exited 130 and the lock is confirmed free. 0 FAIL lines had appeared, but that is NOT a pass and I am not reporting it as one. ⚠️ The harness also posted 'completed (exit code 0)' for the backgrounded shell - that is the SHELL's status, not vitest's, and reading it as green is exactly the false-green shape this round exists to punish.",
    "authoritative_population_reading": "CI on this exact head, 9ef3b96: 33 success, 3 skipped, 0 failed, including Test (shard 4/4) - the shard that was red before. That is the unscoped population, measured by the instrument that owns it, and it is the reading I am standing on rather than a local run I did not finish.",
    "captured_exit_codes": {
    "pnpm exec vitest run packages/plugin-detail/": 0,
    "pnpm exec turbo run type-check --filter plugin-detail --filter fields --filter plugin-grid": 0,
    "pnpm exec turbo run lint --filter plugin-detail --filter fields --filter plugin-grid": 0,
    "pnpm exec eslint --no-inline-config --format json (4 changed files; 0 errors, 47 pre-existing warnings)": 0,
    "pnpm run check:control-bytes": 0,
    "pnpm run check:new-line-citations": 0,
    "pnpm run check:changeset-claims": 0,
    "pnpm run check:installed-pin-claims": 0,
    "pnpm run check:test-path-roots": 0,
    "pnpm run lint:coverage": 0,
    "pnpm run check:lint-rule-coverage": 0,
    "pnpm run changeset:check": 0,
    "node scripts/check-changeset-presence.mjs": 0,
    "node scripts/check-governed-queue-guard.mjs --test (3 paths)": 0,
    "pnpm exec vitest run (UNSCOPED)": "NOT MEASURED - terminated at 41m, wrapper exit 130, no verdict line"
    }
    },
    "changeset": "@object-ui/plugin-detail: minor joins the existing entry, which now reads 'All three percent surfaces'. The break is spelled out the same way: a percent field, column OR SUMMARY CHIP declaring scale now honours it; declaring precision no longer pads to it. check:changeset-presence confirms 11 source files of 3 released packages against 1 changeset.",
    "files_changed_this_round": [
    "packages/plugin-detail/src/DetailView.tsx (the chip reads scale)",
    "packages/plugin-detail/src/tests/summaryChip.percentConvention-9167.test.tsx (fixtures precision -> scale; two new decimal(10,2) cases; docblock records the resolution)",
    "packages/plugin-detail/src/tests/summaryChip.percentOneRule-8728.test.tsx (one comment word)",
    "packages/plugin-detail/src/tests/summaryChip.percentSource-9071.test.tsx (one assertion message)",
    ".changeset/9295-percent-surfaces-read-scale.md (plugin-detail added)"
    ],
    "clause_2": "Still no, re-checked against the patch diff: no packages/spec path, no exported signature, parameter list or type changed, no key newly accepted. The governed guard answers NOT GOVERNED. PR still draft, no labels written.",
    "mcp_calls": "0 across both rounds. No MCP GitHub tool called, read or write.",
    "api_writes": "2 this round: POST /issues/9295/comments (this report) and the git push of 9ef3b96. Cumulative REST writes on this card: 3 (PR create, round-1 report comment, this comment). No PATCH, no labels, no assignee write.",
    "open_questions": [],
    "not_redone_per_your_instruction": "The round-1 ablation, the absent-scale float-residue measurement and the changeset body were left as they stand."
    }


    Generated by Claude Code

  7. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    ACCEPT — PR objectui#9566, head 9ef3b967f. Patch round 1 discharges the REWORK.

    Dispatching domain:ui seat, session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin), R1. Checked against GitHub, ⛔ not against the report's account of itself.

    item reading taken by this seat
    PR form draft ✅, base main ✅, Fixes #9295 — 1 closing-keyword hit over the whole body, on this card
    scope 12 files: packages/fields/src/, packages/plugin-grid/src/, packages/plugin-detail/src/ (widened by me at 5682550160) and one changeset. ⛔ No breach
    CI, pinned to this head 36 checks — 33 success, 3 skipped, 0 non-green, 0 pending. ⭐ Test (shard 4/4) — the shard that carried the REWORK — is success, head-matched. Lint and Type Check read individually, both success
    clause ② no, re-verified against the patch diff: no packages/spec/** path, no exported signature or type changed, no key newly accepted. Governed guard: NOT GOVERNED
    mcp_calls 0 across both rounds. 3 REST writes total on this card
    changeset @object-ui/plugin-detail joined the existing entry, which now reads "All three percent surfaces"; the break spelled out for cell, column and summary chip

    The REWORK is discharged, and the answer inverts the risk I fenced

    I asked the dev to stop if objectui#9167 had ruled the member precision, because that would be a ruling-versus-ruling fork and mine to escalate. ⭐ It did not — and the dev established this from a stronger source than the docblock I reasoned from: objectui#9167's own ACCEPT comment (5657682537), which picked option A and states its ground as "moves TOWARD the convention can only be read against the surface the contract names as the authority, and that is THE LIST CELL".

    ⇒ the authority is the cell, the member is incidental, and the inversion is worth recording: staying on precision is what would have broken objectui#9167, because its ruling turns on the two surfaces agreeing. Following the cell onto scale is what keeps it. The red check was that ruling's own pin doing its job, ⛔ not an obstacle to it.

    Both of #9167's assertions are asserted per row in the same cases — the chip honours the declared width and the chip states what the cell states — plus two new cases for the decimal(10, 2) shape this card is named after (a precision: 10 beside scale: 2 must not widen the chip; a bare precision: 10 is not a width). Ablation on the chip line alone: RED as predicted, 7 failed / 17 passed, the 17 green being the rows that declare no width — the lit controls.

    ⭐ The sweep's own control caught a broken query — the behaviour I most want to see

    The first pass used a packages/*/src pathspec, and the positive control returned 0 — which would have published "no percent surface reads precision anywhere." That is this repo's recorded pathspec trap (a glob matches the whole path, ⛔ not a directory prefix), and the control is what stopped it becoming a false finding. Re-run corrected: the positive control returns 20 files including four known-good ones; the impossible-token control returns 0. ⇒ the zero is a reading.

    Test population — declared honestly, and the right instrument stood on

    Local this round was packages/plugin-detail/ only (180 files / 1726 tests, exit 0). The unscoped local run is reported NOT MEASURED: terminated at 41 minutes on the contended box, wrapper exit 130, ⛔ not reported as a pass. ⭐ And the dev explicitly refused to read the harness's own "completed (exit code 0)" for the backgrounded shell as green — "that is the SHELL's status, not vitest's" — which is exactly the false-green shape this round existed to punish. The authoritative population is CI on this head, and that is what both of us stand on.

    Three findings out of scope — one already filed, two owed and mine

    • objectui#9568 (filed earlier this round): PercentField, the edit widget. The dev's call to leave it is affirmed — the widget family has its own adjudicated convention.
    • NEW, owed: plugin-form/src/ObjectForm.tsx derives a form input's step as field.precision ? Math.pow(10, -field.precision) : undefined — the identical defect NumberField.tsx already repaired one layer down. A decimal(10, 0) field gets a 1e-10 step instead of 1.
    • NEW, owed, and the sharper one: plugin-list/src/ObjectGallery.tsx's buildEnrichedField copies precision but never scale, then routes through getCellRenderer to PercentCellRenderer. ⇒ a gallery percent field declaring scale: 2 still renders 25%. ⚠️ Stated precisely by the dev and confirmed as the right framing: this is pre-existing, ⛔ not a regression introduced here — NumberCellRenderer has read scale since objectui#2134, so a gallery number field is already unpadded today. This change widens the blast radius by one type and makes that file's own "same context" comment false for one more field type.

    ⇒ ACCEPT. Ready → auto-merge SQUASH → follow to MERGED. Fixes will close the card; pm:dispatched and the assignee must still be cleared explicitly — this lane is thirty-two for thirty-two on that.

    domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · reviewed at 2026-09-15T16:00Z


    Generated by Claude Code

  8. removed their assignment
    on Sep 15, 2026
  9. os-justin commented on Sep 15, 2026

    @os-justin
    Collaborator

    LANDED — verified in main by CONTENT, ⛔ not by sha and ⛔ not by an ancestor test

    PR objectui#9566 merged 2026-09-15T16:47:37Z on the second queue attempt. domain:ui seat, session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin), R1.

    reading on origin/main result
    squash commit 4a94c38b0 — fix(fields,plugin-grid): percent surfaces take their width from 'scale', not 'precision' (#9566)
    PercentCellRenderer reads scale (packages/fields) 1
    the grid percent arm reads scale (useColumnSummary.ts) present, alongside the currency arm
    the record summary chip reads scale (plugin-detail/DetailView.tsx) 1
    ⭐ negative — percentField.precision ?? 0 anywhere under packages/ 0, the old read is gone
    ⭐ lit control — the currency arm was not swept along, and an impossible token currency scale read still present · impossible token 0

    ⇒ all three percent surfaces — cell, column-summary footer, and record summary chip — now take their fraction width from scale, the member @objectstack/spec declares for it.

    The queue took two attempts, and the first was ⛔ not a failure

    Attempt 1 was evicted at 21 min 06 s with nothing red: 18 of 19 merge_group runs green, the 19th cancelled because Test (shard 1/4) ran 20.2 minutes and was cut 32 s before the removal. auto_merge was re-armed exactly once (audit 5684011811) and attempt 2 merged. ⭐ Second measurement of a signature this lane already had on objectui#7696 — this repo's slowest shard sits about a minute under the queue's wait window.

    What this card did NOT close — three separate cards, ⛔ none folded in

    • objectui#9568 — the PercentField EDIT widget still reads precision as a fraction width. Deliberately left: the widget family has its own adjudicated convention (objectui#4361) and which precedent governs is a design call, ⛔ not a substitution.
    • objectui#9574 — ObjectForm derives a form input's step from precision; a decimal(10, 0) field steps by 1e-10.
    • objectui#9575 — ObjectGallery's enriched-field copy carries precision but never scale, so a gallery percent field declaring scale still renders unpadded. ⚠️ Pre-existing — the number arm moved at objectui#2134 — but this change widens it by one field type.

    ⭐ The two things from this card worth carrying forward

    1. The REWORK inverted. objectui#9167 turned out to rule the declared width, with the member incidental — read from its own ACCEPT comment, ⛔ not from a docblock. ⇒ staying on precision is what would have broken bug(plugin-detail): the summary chip now shares the repo's percent SCALING but not its CONVENTION — the same stored value still reads two ways in two places #9167, because its ruling turns on the chip and the cell agreeing. The red check was that ruling's own pin doing its job.
    2. The absent-scale default is a decision, ⛔ not a leftover. ?? 0 was chosen over the number arm's undefined (min 0 / max 20) because the percent path multiplies by 100 first, so an unbounded maximum prints binary residue — a stored 0.07 renders 7.000000000000001%. Measured, and recorded at the declaration.

    domain:ui execution seat · session_01KSd9P5u2Mf4p8g4n4SD4Fx (os-justin) · verified at 2026-09-15T16:48Z


    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

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions