You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(plugin-form): a form input's step is derived from precision, the TOTAL digit count — a decimal(10, 0) field steps by 1e-10 instead of 1, and NumberField one layer down already moved to scale #9574
Surfaced by the os-dev delivering objectui#9295's patch round (PR objectui#9566) during a tree-wide sweep, and filed by the dispatching domain:ui seat (session_01KSd9P5u2Mf4p8g4n4SD4Fx, os-justin) — dev seats do not file on this lane. ⛔ Not graded and ⛔ not routed by me.
The fact — re-derived at source by this seat on origin/main
@objectstack/spec declares precision as "Total digits (non-negative integer)" and scale as "Decimal places (non-negative integer)" (packages/spec/src/data/field.zod.ts:1157-1158).
⇒ a decimal(10, 0) field — ten total digits, zero decimal places — gets a form input step of 1e-10 instead of 1. A decimal(10, 2) gets 1e-10 instead of 0.01. The step is the granularity the browser enforces on the spinner and on validation, so this is a control the user actually operates.
⭐ The repaired sibling is one layer down, and it states the rule
⇒ the identical defect was already recognised and fixed for the number widget. ObjectForm derives the same quantity from the wrong member, one layer up.
Relationship to the card that surfaced it
objectui#9295 moved the three display percent surfaces from precision to scale. This is a different layer — the form input's step — and outside that card's surface, which is why it is its own card and ⛔ was not folded into it.
⚠️ ⛔ Not a duplicate of objectui#9568 either: that one is the PercentField widget's readonly width and slider step, which sits inside the widget family's own adjudicated convention (objectui#4361). This one is ObjectForm's generic numeric step, and its governing precedent — NumberField — has already moved.
Claim: PM loop round 5
Session: session_01Xm4WFhEe5mwcgyqHjxR2hn
Branch: claude/issue-9574-objectform-step-scale
Worktree: objectui-issue-9574
Domain: domain:ui
Seat: domain:ui#3
File surface: packages/plugin-form/src/ (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: opus — the default judgment tier. ⚠️ The tier citation is HAND-DERIVED, not an instrument reading: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui --tier packages/plugin-form/src/ObjectForm.tsx, run for this claim at 2026-09-18T12:02Z, answers verbatim "dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'." Gate families are a property of the tree the process runs in, so this repo derives nothing there; objectui's gate surface is hand-read from its own package.json scripts and .github/workflows/.
Clause-②: no
Thread-read: 5713420207
Serial constraints cleared: none — the changed-file page of all 16 open PRs in this repo was read in one pass beginning 2026-09-18T11:57Z and NOT ONE touches packages/plugin-form/**. The triage comment on this card names two siblings carrying the same rule: objectui#9568 (packages/fields PercentField edit widget) is delivered and sits in PR objectui#9804, ready and green but unqueued; objectui#9575 (packages/plugin-list ObjectGallery) is priority:p3, unassigned, and is NOT dispatched with this one — three different files, three different faces, and no claim of this card asserts either sibling's behaviour.
{
"issue": 9574,
"status": "done",
"branch": "claude/issue-9574-objectform-step-scale",
"pr": "#9873",
"session": "session_01Xm4WFhEe5mwcgyqHjxR2hn",
"premise_still_valid": true,
"summary": "ObjectForm's auto-generated numeric form field now derives step from scale (decimal places) instead of precision (total digits): typeof field.scale === 'number' ? Math.pow(10, -field.scale) : 'any'. typeof because scale: 0 is a valid declaration meaning steps-by-1; 'any' rather than undefined for an undeclared scale because an absent step attribute is HTML's default of 1, which refuses every decimal - keeping undefined would have turned the fix into a new defect for fields declaring only precision. PREMISE GATE PASSES but the card's downstream framing is partly falsified and the PR body says so: measured through a real render, the registered field:* widgets DROP this key (metadata carrier is formField.field, and their DOM whitelist does not forward step), so on the ordinary route a decimal(10, 0) number field already stepped by 1 via NumberField's own derivation; the 1e-10 a user meets there on currency/percent comes from CurrencyField/PercentField, not from this file. The key IS live on the renderer's unregistered-widget fallback branch, where this file's value is the attribute the browser enforces - that is where the fix and its load-bearing pin act. Card assignee was os-sales (PM's, untouched); no second claim written; PR carries no needs:contract-review label (read at open time).",
"premise_checks": {
"spec_members": "CONFIRMED against the INSTALLED spec in this worktree, @objectstack/spec 17.4.0 (node_modules/@objectstack/spec/dist/ui/index.js, the Field schema block): precision: z.number().int().min(0).optional().describe(\"Total digits (non-negative integer)\") and scale: ... .describe(\"Decimal places (non-negative integer)\"). The same block states that #7501 made scale ENFORCED AT WRITE TIME via Number.isInteger(def.scale) && def.scale >= 0, and its neighbouring prose uses Field.number({ scale: 0, min: 1900 }) as a live example - so scale: 0 is an author-legal declaration, not an omission.",
"scale_reaches_the_site": "CONFIRMED. At the call site field is objectSchema.fields?.[name], and objectSchema is whatever dataSource.getObjectSchema() returned, typed any. The shipped adapter (packages/data-objectstack getObjectSchema) returns the server's schema OBJECT verbatim - it only normalizes the relational-target key and stamps ADR-0056 widget overrides - so no whitelist drops scale on the way in. objectui's own NumberFieldMetadata and PercentFieldMetadata also declare scale (CurrencyFieldMetadata does not - see findings). Positive control for the spec search: the same package and the same grep spell FieldSchema and percentScaleOf, both of which hit.",
"liveness_of_the_assignment": "MEASURED, and this is the half the card did not state. Ablation on the base (3a32a7c): the assignment deleted outright, mutation verified on disk (anchor hits 1 then 0, injected marker 1), re-render of the same six numeric fields byte-identical to the unablated run; file restored to its HEAD blob hash afterwards. Rendered faces on the base: number scale 0 -> step 1, number scale 2 -> step 0.01, number bare -> step any (all three from NumberField), currency precision 10 -> step 0.0000000001 and percent precision 10 -> step 0.0000000001 (both from the widgets' own Math.pow(10, -precision).toFixed(precision)). A field declaring a widget nothing registers routes to the renderer's fallback branch, and THERE this file's value reached the DOM as step=\"1e-10\" - the bare-number spelling being the lit control that tells this producer apart from the widgets' .toFixed spelling."
},
"percent_seam_objectui_9810": "Read as instructed, and treated as a hazard, not a ruling. Re-derived here: percentScaleOf in the installed spec confirms fraction storage unless max > 1; the spec's scale block confirms write-time enforcement. NOT re-derived: the objectql max_scale branch - @objectstack/objectql is NOT installed in this repo (only client, core, formula, lint, sdui-parser, spec, types are present under .pnpm), so that leg is a dead instrument here, with @objectstack/spec present as the lit control. Consequence for this change: on the ONLY route where this key reaches the DOM, the box holds the STORED number (the times-100 conversion lives in PercentField, which drops this key), so 10^-scale there is stored-decimal granularity - the quantity a write-time scale check judges. The base offered 1e-10 on that same box, finer than any reading of a declared scale would accept. So this change moves the percent affordance TOWARD the write path under either reading of #9810 and cannot begin offering a step max_scale rejects. Left untouched: PercentField and CurrencyField (packages/fields, fenced), and the stored-vs-displayed question itself. The new pin's percent line is scoped to producer UNIFORMITY and says in its own comment that a #9810 ruling re-prices it.",
"tests": "All at HEAD 4c44f71, in worktree objectui-issue-9574. NEW PIN packages/plugin-form/src/tests/objectFormNumericStep-9574.test.tsx: 3 passed. REVERSE VERIFICATION (fix committed first, old expression reinstated in place, on-disk mutation verified, then restored to the HEAD blob with git diff HEAD empty): Test Files 1 failed (1) | Tests 2 failed | 1 passed (3), with AssertionError: decimal(10, 0) steps by 1: expected '1e-10' to be '1' and AssertionError: undeclared scale => step=\"any\": expected '1e-10' to be 'any'. The third block stayed green and is LABELLED in the file as a chain pin that passes on both configurations, not as evidence for this card. PACKAGE SUITE (via the shared verify lock, slot objectui-9574): pnpm exec vitest run packages/plugin-form/ -> Test Files 98 passed (98), Tests 928 passed | 1 skipped (929), exit 0. TYPE-CHECK: turbo run type-check --filter=@object-ui/plugin-form --concurrency=2 -> Tasks: 13 successful, 13 total, exit 0; the package task reported cache miss, executing, so no cross-worktree cache hit stood in for it, and it runs tsc --noEmit && tsc -p tsconfig.test.json so the new pin is type-checked. LINT: eslint . in packages/plugin-form (the invocation turbo runs) -> 128 files, 0 errors, 853 warnings, exit 0; narrowed to this package on the stated ground that this config enables no type-aware linting (no parserOptions.project / projectService in eslint.config.js), so this diff cannot move any untouched file's verdict; repo-wide is CI's. GATES: check-changeset-presence OK (1 source file of 1 released package, 1 changeset), check-changeset-no-major OK, check-control-bytes OK (7936 tracked text files), check-new-cross-file-line-citations 0 new citation(s), check-vi-mock-specifiers OK, check-vi-mock-inherit OK. NOT MEASURED: the repo-wide pnpm lint / pnpm test farm and every other check:* family, declared to CI; scripts/pm/dispatch-gates.mjs refuses to derive for this repo by design, so the gate list is hand-derived from this repo's package.json and .github/workflows and is not authoritative. Exit codes were captured before any pipe.",
"mcp_calls": "0 - no MCP GitHub tool was called, read or write.",
"api_writes": "3 REST writes, all through the proxy with GITHUB_TOKEN: POST /repos/objectstack-ai/objectui/pulls (draft PR 9873), POST /repos//issues/9873/labels (additive, domain:ui + priority:p2; a first attempt returned 415 for a missing Content-Type and wrote nothing, the retry returned 200), POST /repos//issues/9574/comments (this report). Label readback after the write: ['domain:ui', 'plugin', 'priority:p2', 'tests'] - equals union(read, target), nothing stripped; plugin and tests are the auto-labeler's. Plus 2 git push (empty-branch probe, then the commit). No PATCH of the PR body. PR body read back after creation: byte-identical to what was sent apart from one trailing newline, attribution footer intact in session-URL form.",
"open_questions": [
{
"question": "Should ObjectForm keep deriving a numeric step at all, now that the registered widgets are measured to drop it and derive their own from the same member?",
"options": [
"A - keep it (this PR): the key stays correct and stays live for the unregistered-widget fallback, where it is the only granularity the browser gets.",
"B - delete it and tombstone the site, the way the neighbouring formField.maxLength case was handled: one derivation, owned by the widget.",
"C - leave both and publish step as a real form-field contract that widgets consult before deriving."
],
"recommendation": "A, and it is what this PR does. B was weighed and rejected on measurement, not taste: deleting the key drops the fallback branch's INPUT element to no step attribute, i.e. HTML's default of 1, which refuses decimals on precisely the fields that declare a scale - a new defect on the only route that reads the key. C widens a published surface for no pull. This is reported rather than acted on because B/C would be a contract decision, not a patch."
}
],
"out_of_scope_findings": [
"to file (3 classes, dedupe words) - class (a), reproducible defect with a measured face: CurrencyField derives its input step from precision (Math.pow(10, -precision).toFixed(precision)), the identical rule this card names, so a currency field declaring precision: 10, scale: 2 renders step 0.0000000001 instead of 0.01 - measured in this worktree through a real ObjectForm render. It is the sibling of objectui#9568 (PercentField) with no card of its own as far as this seat read (not deduped by me). Dedupe words: CurrencyField step precision scale / currency input step precision / Math.pow(10, -precision).toFixed / currency widget decimal places / fields widget step scale family.",
"to file (3 classes, dedupe words) - candidate class (c), metadata trap, NOT fully verified so filed as a question rather than a claim: @object-ui/types declares step on NumberFieldMetadata and NumberField reads it as an override that beats the derived value, but the installed @objectstack/spec FieldSchema declares NO step on the object-field surface (the only step members there are InlineGridColumn's 'Input step for numeric cells' and the slider node's). If that reading holds, an author who writes step on a platform field has it dropped at publish while the renderer-side type says it is honoured. Deliberately not copied into ObjectForm by this PR. Dedupe words: field step declared types not spec / NumberFieldMetadata step publish stripped / step field metadata spec FieldSchema / widget step override undeclared / declared-but-dropped step key.",
"noted, not filed: CurrencyFieldMetadata in @object-ui/types declares precision but no scale, while NumberFieldMetadata and PercentFieldMetadata declare both. Successor: whoever takes the CurrencyField finding above - it lands in the same file pair and cannot be done without touching this declaration. Not filed separately because it is one half of that finding, not a defect on its own.",
"noted, not filed: the form renderer's built-in input branch and its unregistered-widget fallback both spread the leftover field props onto the INPUT element, so any author-supplied numeric key on a hand-written FormSchema reaches the DOM there while the registered-widget route filters through a whitelist - two different answers to 'what may become an attribute', by route. Observation only, no measured defect. Successor: none - no PR or person is queued on packages/components/src/renderers/form/form.tsx as far as this seat read."
]
}
domain:ui seat 3 (session session_01Xm4WFhEe5mwcgyqHjxR2hn, seat post objectui#9800), 2026-09-18T12:49Z. Reviewed against this repository and the PR's own diff, ⛔ not against the report's narrative.
Path surface
GET /pulls/9873/files returns 3 paths: .changeset/9574-objectform-step-scale.md · packages/plugin-form/src/ObjectForm.tsx (34+/1-) · packages/plugin-form/src/__tests__/objectFormNumericStep-9574.test.tsx (added). ⇒ the declared file surface held, and ⛔ no governed surface is touched.
The change, and the one thing in it the card did not ask for
The expression at the card's anchor becomes typeof field.scale === 'number' ? Math.pow(10, -field.scale) : 'any'.
scale instead of precision is the card, and the rule it follows is the one NumberField.tsx:21 already states verbatim one layer down.
typeof instead of truthiness is required, not stylistic: scale: 0 is an author-legal declaration meaning steps by 1, and the old truthiness test would have dropped it.
⭐ The 'any' tail is not in the card, and it is accepted on measurement rather than taste: the old expression's else-branch was undefined, and undefined emits no step attribute, which is HTML's default of 1 — that refuses every decimal on exactly the fields declaring only a precision. Keeping the literal card would have turned this fix into a new defect. The new pin holds that case (undeclared scale => step="any"), and the reverse verification failed on it before the fix, which is what makes the pin load-bearing rather than decorative.
What the dev falsified in the card's framing, and why it is still a fix
Measured through a real render: the registered field:* widgets drop this key — their DOM whitelist does not forward step — so on the ordinary route a decimal(10, 0) number field already stepped by 1 via NumberField's own derivation, and the 1e-10 a user meets on currency/percent comes from those widgets, ⛔ not from this file. The key is live on the renderer's unregistered-widget fallback branch, where this file's value is the attribute the browser enforces, and that is where the fix and its pin act. The PR body says so. ⇒ the card's anchor was right and its reach was overstated; reporting that rather than quietly inheriting it is the right delivery.
The objectui#9810 addendum was handled as asked
Treated as a hazard, ⛔ not as a ruling. One leg could not be re-derived — @objectstack/objectql is not installed in this repo, so the max_scale branch is a dead instrument here, and the dev said so with @objectstack/spec as the lit control rather than claiming a reading. On the only route where this key reaches the DOM the box holds the stored number, so the change moves the percent affordance toward the write path under either reading of #9810 and ⛔ cannot begin offering a step a write-time scale check would reject. PercentField and CurrencyField stayed untouched, as fenced.
The dev's open question — this seat takes A, which is what the PR does
Keep the derivation. B (delete the key, one derivation owned by the widget) is refused on the dev's own measurement: deleting it drops the fallback branch to no step attribute, i.e. HTML's 1, a new defect on the only route that reads the key. C (publish step as a form-field contract widgets consult) widens a published surface for no measured pull. ⛔ Neither is authorised by this card, so neither is done, and ⛔ this is not escalated: "keep the existing behaviour, corrected" is the non-widening option and is the card.
⛔ One of the dev's out-of-scope findings is REFUTED here and was NOT filed
The report proposed a class (a) card: CurrencyField derives its input step from precision, the identical rule this card names. Re-derived at source before filing, and the claim does not hold:
packages/fields/src/widgets/CurrencyField.tsx:14-16, verbatim: "precision is a display width the caller resolved — either the field's authored precision or, when it declared none, the currency's own ISO 4217 minor-unit count (see the derivation at the call site, objectui#4361)."
and at :37-38: "No scale passed: this is a currency widget whose precision is a display width, so a precision: 0 amount keeps its separators rather than being read as an ordinal (objectui#4033)."
⇒ on the currency widget precision is a deliberately different quantity under an adjudicated convention (objectui#4361 / #4033 / ADR-0053), ⛔ not the spec's total-digit precision misread. A card filed on "the identical rule" would have been a card against a landed, reasoned convention — the same fence that kept objectui#9568 inside the widget family. The file's own docblock also records that the contradicting-precision question is already filed upstream in @objectstack/spec. ⛔ Not filed; recorded here so it is not re-derived.
The report's other candidate was filed, because its zero survived a lit control: objectui#9875 — NumberFieldMetadata declares step?: number while the protocol's field surface declares step nowhere (only the inline-grid column and the slider node), filed as a question with the unmeasured leg named.
Clause-②: no holds on the diff: one consumer is pulled back onto a member the contract already declares.
Flipping to ready. ⛔ It stops there: the queue call is refused by this session's auto-mode classifier ([Merge Without Review]).
The dev's open_questions entry is answered, and its residual has a home — filed so it is not left unreachable
domain:ui seat 3, 2026-09-18T13:03Z. The R6 half-state sweep filed H52 against this card: the newest os-dev-report (5730014732) carries one non-empty open_questions entry while the card carries no needs-user-decision, so a question could sit「well-formed and UNREACHABLE」. Answering that here rather than letting the row stand.
The question: 「Should ObjectForm keep deriving a numeric step at all, now that the registered widgets are measured to drop it and derive their own from the same member?」
Answered in the ACCEPT 5730217971, and ⛔ not escalated, because it did not need the maintainer: the seat takes A — keep it, which is what PR objectui#9873 does. It is the status quo corrected, ⛔ not a change. B (delete the key) is refused on the dev's own measurement — deleting it drops the unregistered-widget fallback branch to no step attribute, i.e. HTML's default of 1, a new defect on the only route that reads the key. C (publish step as a form-field contract widgets consult) widens a published surface, and ⛔ this card does not authorise that.
Where the residual lives, so it is in an inbox rather than in a closed report: the part of C that is a real open question — whether step is an authorable field key at all — is filed as objectui#9875 (NumberFieldMetadata declares step?: number; the protocol's field surface declares step nowhere but on the inline-grid column and the slider node). That card carries the unmeasured leg named on its own face.
⇒ ⛔ No needs-user-decision on this card: nothing here is owed to the maintainer. The question is discharged and its successor is named.
Left untouched: priority:p2, domain:ui — ownership, priority and outcome are not state claims.
The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
Activity
os-sales commentedon Sep 18, 2026
Claim: PM loop round 5⚠️ The tier citation is HAND-DERIVED, not an instrument reading:
Session:
session_01Xm4WFhEe5mwcgyqHjxR2hnBranch:
claude/issue-9574-objectform-step-scaleWorktree:
objectui-issue-9574Domain:
domain:uiSeat:
domain:ui#3File surface:
packages/plugin-form/src/(stop on breach; explain in the report)Container & model:
S,mode:subagent,model: opus— the default judgment tier.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectui --tier packages/plugin-form/src/ObjectForm.tsx, run for this claim at 2026-09-18T12:02Z, answers verbatim "dispatch-gates: REFUSING — asked for 'objectstack-ai/objectui', but this checkout is 'objectstack-ai/objectstack'." Gate families are a property of the tree the process runs in, so this repo derives nothing there; objectui's gate surface is hand-read from its ownpackage.jsonscripts and.github/workflows/.Clause-②: no
Thread-read: 5713420207
Serial constraints cleared: none — the changed-file page of all 16 open PRs in this repo was read in one pass beginning 2026-09-18T11:57Z and NOT ONE touches
packages/plugin-form/**. The triage comment on this card names two siblings carrying the same rule: objectui#9568 (packages/fieldsPercentField edit widget) is delivered and sits in PR objectui#9804, ready and green but unqueued; objectui#9575 (packages/plugin-listObjectGallery) ispriority:p3, unassigned, and is NOT dispatched with this one — three different files, three different faces, and no claim of this card asserts either sibling's behaviour.Generated by Claude Code
os-sales commentedon Sep 18, 2026
os-dev-report
{
"issue": 9574,
"status": "done",
"branch": "claude/issue-9574-objectform-step-scale",
"pr": "#9873",
"session": "session_01Xm4WFhEe5mwcgyqHjxR2hn",
"premise_still_valid": true,
"summary": "ObjectForm's auto-generated numeric form field now derives
stepfromscale(decimal places) instead ofprecision(total digits):typeof field.scale === 'number' ? Math.pow(10, -field.scale) : 'any'.typeofbecausescale: 0is a valid declaration meaning steps-by-1;'any'rather thanundefinedfor an undeclared scale because an absent step attribute is HTML's default of 1, which refuses every decimal - keepingundefinedwould have turned the fix into a new defect for fields declaring onlyprecision. PREMISE GATE PASSES but the card's downstream framing is partly falsified and the PR body says so: measured through a real render, the registeredfield:*widgets DROP this key (metadata carrier isformField.field, and their DOM whitelist does not forwardstep), so on the ordinary route adecimal(10, 0)number field already stepped by 1 via NumberField's own derivation; the1e-10a user meets there on currency/percent comes from CurrencyField/PercentField, not from this file. The key IS live on the renderer's unregistered-widget fallback branch, where this file's value is the attribute the browser enforces - that is where the fix and its load-bearing pin act. Card assignee wasos-sales(PM's, untouched); no second claim written; PR carries noneeds:contract-reviewlabel (read at open time).","premise_checks": {
"spec_members": "CONFIRMED against the INSTALLED spec in this worktree, @objectstack/spec 17.4.0 (node_modules/@objectstack/spec/dist/ui/index.js, the Field schema block):
precision: z.number().int().min(0).optional().describe(\"Total digits (non-negative integer)\")andscale: ... .describe(\"Decimal places (non-negative integer)\"). The same block states that #7501 madescaleENFORCED AT WRITE TIME viaNumber.isInteger(def.scale) && def.scale >= 0, and its neighbouring prose usesField.number({ scale: 0, min: 1900 })as a live example - soscale: 0is an author-legal declaration, not an omission.","scale_reaches_the_site": "CONFIRMED. At the call site
fieldisobjectSchema.fields?.[name], andobjectSchemais whateverdataSource.getObjectSchema()returned, typedany. The shipped adapter (packages/data-objectstackgetObjectSchema) returns the server's schema OBJECT verbatim - it only normalizes the relational-target key and stamps ADR-0056 widget overrides - so no whitelist dropsscaleon the way in. objectui's ownNumberFieldMetadataandPercentFieldMetadataalso declarescale(CurrencyFieldMetadata does not - see findings). Positive control for the spec search: the same package and the same grep spellFieldSchemaandpercentScaleOf, both of which hit.","liveness_of_the_assignment": "MEASURED, and this is the half the card did not state. Ablation on the base (3a32a7c): the assignment deleted outright, mutation verified on disk (anchor hits 1 then 0, injected marker 1), re-render of the same six numeric fields byte-identical to the unablated run; file restored to its HEAD blob hash afterwards. Rendered faces on the base: number scale 0 -> step 1, number scale 2 -> step 0.01, number bare -> step any (all three from NumberField), currency precision 10 -> step 0.0000000001 and percent precision 10 -> step 0.0000000001 (both from the widgets' own
Math.pow(10, -precision).toFixed(precision)). A field declaring a widget nothing registers routes to the renderer's fallback branch, and THERE this file's value reached the DOM asstep=\"1e-10\"- the bare-number spelling being the lit control that tells this producer apart from the widgets'.toFixedspelling."},
"percent_seam_objectui_9810": "Read as instructed, and treated as a hazard, not a ruling. Re-derived here:
percentScaleOfin the installed spec confirms fraction storage unlessmax > 1; the spec'sscaleblock confirms write-time enforcement. NOT re-derived: the objectqlmax_scalebranch -@objectstack/objectqlis NOT installed in this repo (only client, core, formula, lint, sdui-parser, spec, types are present under .pnpm), so that leg is a dead instrument here, with@objectstack/specpresent as the lit control. Consequence for this change: on the ONLY route where this key reaches the DOM, the box holds the STORED number (the times-100 conversion lives in PercentField, which drops this key), so10^-scalethere is stored-decimal granularity - the quantity a write-time scale check judges. The base offered1e-10on that same box, finer than any reading of a declared scale would accept. So this change moves the percent affordance TOWARD the write path under either reading of #9810 and cannot begin offering a stepmax_scalerejects. Left untouched: PercentField and CurrencyField (packages/fields, fenced), and the stored-vs-displayed question itself. The new pin's percent line is scoped to producer UNIFORMITY and says in its own comment that a #9810 ruling re-prices it.","tests": "All at HEAD 4c44f71, in worktree objectui-issue-9574. NEW PIN packages/plugin-form/src/tests/objectFormNumericStep-9574.test.tsx: 3 passed. REVERSE VERIFICATION (fix committed first, old expression reinstated in place, on-disk mutation verified, then restored to the HEAD blob with
git diff HEADempty):Test Files 1 failed (1) | Tests 2 failed | 1 passed (3), withAssertionError: decimal(10, 0) steps by 1: expected '1e-10' to be '1'andAssertionError: undeclared scale => step=\"any\": expected '1e-10' to be 'any'. The third block stayed green and is LABELLED in the file as a chain pin that passes on both configurations, not as evidence for this card. PACKAGE SUITE (via the shared verify lock, slot objectui-9574):pnpm exec vitest run packages/plugin-form/->Test Files 98 passed (98),Tests 928 passed | 1 skipped (929), exit 0. TYPE-CHECK:turbo run type-check --filter=@object-ui/plugin-form --concurrency=2->Tasks: 13 successful, 13 total, exit 0; the package task reportedcache miss, executing, so no cross-worktree cache hit stood in for it, and it runstsc --noEmit && tsc -p tsconfig.test.jsonso the new pin is type-checked. LINT:eslint .in packages/plugin-form (the invocation turbo runs) -> 128 files, 0 errors, 853 warnings, exit 0; narrowed to this package on the stated ground that this config enables no type-aware linting (no parserOptions.project / projectService in eslint.config.js), so this diff cannot move any untouched file's verdict; repo-wide is CI's. GATES: check-changeset-presence OK (1 source file of 1 released package, 1 changeset), check-changeset-no-major OK, check-control-bytes OK (7936 tracked text files), check-new-cross-file-line-citations0 new citation(s), check-vi-mock-specifiers OK, check-vi-mock-inherit OK. NOT MEASURED: the repo-widepnpm lint/pnpm testfarm and every othercheck:*family, declared to CI;scripts/pm/dispatch-gates.mjsrefuses to derive for this repo by design, so the gate list is hand-derived from this repo's package.json and .github/workflows and is not authoritative. Exit codes were captured before any pipe.","mcp_calls": "0 - no MCP GitHub tool was called, read or write.",
"api_writes": "3 REST writes, all through the proxy with GITHUB_TOKEN: POST /repos/objectstack-ai/objectui/pulls (draft PR 9873), POST /repos//issues/9873/labels (additive, domain:ui + priority:p2; a first attempt returned 415 for a missing Content-Type and wrote nothing, the retry returned 200), POST /repos//issues/9574/comments (this report). Label readback after the write: ['domain:ui', 'plugin', 'priority:p2', 'tests'] - equals union(read, target), nothing stripped;
pluginandtestsare the auto-labeler's. Plus 2git push(empty-branch probe, then the commit). No PATCH of the PR body. PR body read back after creation: byte-identical to what was sent apart from one trailing newline, attribution footer intact in session-URL form.","open_questions": [
{
"question": "Should ObjectForm keep deriving a numeric
stepat all, now that the registered widgets are measured to drop it and derive their own from the same member?","options": [
"A - keep it (this PR): the key stays correct and stays live for the unregistered-widget fallback, where it is the only granularity the browser gets.",
"B - delete it and tombstone the site, the way the neighbouring
formField.maxLengthcase was handled: one derivation, owned by the widget.","C - leave both and publish
stepas a real form-field contract that widgets consult before deriving."],
"recommendation": "A, and it is what this PR does. B was weighed and rejected on measurement, not taste: deleting the key drops the fallback branch's INPUT element to no step attribute, i.e. HTML's default of 1, which refuses decimals on precisely the fields that declare a scale - a new defect on the only route that reads the key. C widens a published surface for no pull. This is reported rather than acted on because B/C would be a contract decision, not a patch."
}
],
"out_of_scope_findings": [
"to file (3 classes, dedupe words) - class (a), reproducible defect with a measured face:
CurrencyFieldderives its input step fromprecision(Math.pow(10, -precision).toFixed(precision)), the identical rule this card names, so a currency field declaringprecision: 10, scale: 2renders step 0.0000000001 instead of 0.01 - measured in this worktree through a real ObjectForm render. It is the sibling of objectui#9568 (PercentField) with no card of its own as far as this seat read (not deduped by me). Dedupe words:CurrencyField step precision scale/currency input step precision/Math.pow(10, -precision).toFixed/currency widget decimal places/fields widget step scale family.","to file (3 classes, dedupe words) - candidate class (c), metadata trap, NOT fully verified so filed as a question rather than a claim:
@object-ui/typesdeclaressteponNumberFieldMetadataandNumberFieldreads it as an override that beats the derived value, but the installed @objectstack/spec FieldSchema declares NOstepon the object-field surface (the onlystepmembers there are InlineGridColumn's 'Input step for numeric cells' and the slider node's). If that reading holds, an author who writesstepon a platform field has it dropped at publish while the renderer-side type says it is honoured. Deliberately not copied into ObjectForm by this PR. Dedupe words:field step declared types not spec/NumberFieldMetadata step publish stripped/step field metadata spec FieldSchema/widget step override undeclared/declared-but-dropped step key.","noted, not filed:
CurrencyFieldMetadatain @object-ui/types declaresprecisionbut noscale, whileNumberFieldMetadataandPercentFieldMetadatadeclare both. Successor: whoever takes the CurrencyField finding above - it lands in the same file pair and cannot be done without touching this declaration. Not filed separately because it is one half of that finding, not a defect on its own.","noted, not filed: the form renderer's built-in
inputbranch and its unregistered-widget fallback both spread the leftover field props onto the INPUT element, so any author-supplied numeric key on a hand-written FormSchema reaches the DOM there while the registered-widget route filters through a whitelist - two different answers to 'what may become an attribute', by route. Observation only, no measured defect. Successor: none - no PR or person is queued on packages/components/src/renderers/form/form.tsx as far as this seat read."]
}
Generated by Claude Code
os-sales commentedon Sep 18, 2026
ACCEPT — objectui#9574, PR objectui#9873
domain:uiseat 3 (sessionsession_01Xm4WFhEe5mwcgyqHjxR2hn, seat post objectui#9800), 2026-09-18T12:49Z. Reviewed against this repository and the PR's own diff, ⛔ not against the report's narrative.Path surface
GET /pulls/9873/filesreturns 3 paths:.changeset/9574-objectform-step-scale.md·packages/plugin-form/src/ObjectForm.tsx(34+/1-) ·packages/plugin-form/src/__tests__/objectFormNumericStep-9574.test.tsx(added). ⇒ the declared file surface held, and ⛔ no governed surface is touched.The change, and the one thing in it the card did not ask for
The expression at the card's anchor becomes
typeof field.scale === 'number' ? Math.pow(10, -field.scale) : 'any'.scaleinstead ofprecisionis the card, and the rule it follows is the oneNumberField.tsx:21already states verbatim one layer down.typeofinstead of truthiness is required, not stylistic:scale: 0is an author-legal declaration meaning steps by 1, and the old truthiness test would have dropped it.'any'tail is not in the card, and it is accepted on measurement rather than taste: the old expression's else-branch wasundefined, andundefinedemits nostepattribute, which is HTML's default of1— that refuses every decimal on exactly the fields declaring only aprecision. Keeping the literal card would have turned this fix into a new defect. The new pin holds that case (undeclared scale => step="any"), and the reverse verification failed on it before the fix, which is what makes the pin load-bearing rather than decorative.What the dev falsified in the card's framing, and why it is still a fix
Measured through a real render: the registered
field:*widgets drop this key — their DOM whitelist does not forwardstep— so on the ordinary route adecimal(10, 0)number field already stepped by1viaNumberField's own derivation, and the1e-10a user meets on currency/percent comes from those widgets, ⛔ not from this file. The key is live on the renderer's unregistered-widget fallback branch, where this file's value is the attribute the browser enforces, and that is where the fix and its pin act. The PR body says so. ⇒ the card's anchor was right and its reach was overstated; reporting that rather than quietly inheriting it is the right delivery.The objectui#9810 addendum was handled as asked
Treated as a hazard, ⛔ not as a ruling. One leg could not be re-derived —
@objectstack/objectqlis not installed in this repo, so themax_scalebranch is a dead instrument here, and the dev said so with@objectstack/specas the lit control rather than claiming a reading. On the only route where this key reaches the DOM the box holds the stored number, so the change moves the percent affordance toward the write path under either reading of #9810 and ⛔ cannot begin offering a step a write-time scale check would reject.PercentFieldandCurrencyFieldstayed untouched, as fenced.The dev's open question — this seat takes A, which is what the PR does
Keep the derivation. B (delete the key, one derivation owned by the widget) is refused on the dev's own measurement: deleting it drops the fallback branch to no
stepattribute, i.e. HTML's1, a new defect on the only route that reads the key. C (publishstepas a form-field contract widgets consult) widens a published surface for no measured pull. ⛔ Neither is authorised by this card, so neither is done, and ⛔ this is not escalated: "keep the existing behaviour, corrected" is the non-widening option and is the card.⛔ One of the dev's out-of-scope findings is REFUTED here and was NOT filed
The report proposed a class (a) card:
CurrencyFieldderives its input step fromprecision, the identical rule this card names. Re-derived at source before filing, and the claim does not hold:packages/fields/src/widgets/CurrencyField.tsx:14-16, verbatim: "precisionis a display width the caller resolved — either the field's authoredprecisionor, when it declared none, the currency's own ISO 4217 minor-unit count (see the derivation at the call site, objectui#4361).":37-38: "Noscalepassed: this is a currency widget whoseprecisionis a display width, so aprecision: 0amount keeps its separators rather than being read as an ordinal (objectui#4033)."⇒ on the currency widget
precisionis a deliberately different quantity under an adjudicated convention (objectui#4361 / #4033 / ADR-0053), ⛔ not the spec's total-digitprecisionmisread. A card filed on "the identical rule" would have been a card against a landed, reasoned convention — the same fence that kept objectui#9568 inside the widget family. The file's own docblock also records that the contradicting-precisionquestion is already filed upstream in@objectstack/spec. ⛔ Not filed; recorded here so it is not re-derived.The report's other candidate was filed, because its zero survived a lit control: objectui#9875 —
NumberFieldMetadatadeclaresstep?: numberwhile the protocol's field surface declaresstepnowhere (only the inline-grid column and the slider node), filed as a question with the unmeasured leg named.Clause-②: noholds on the diff: one consumer is pulled back onto a member the contract already declares.Flipping to ready. ⛔ It stops there: the queue call is refused by this session's auto-mode classifier (
[Merge Without Review]).Generated by Claude Code
os-sales commentedon Sep 18, 2026
The dev's
open_questionsentry is answered, and its residual has a home — filed so it is not left unreachabledomain:uiseat 3, 2026-09-18T13:03Z. The R6 half-state sweep filed H52 against this card: the newestos-dev-report(5730014732) carries one non-emptyopen_questionsentry while the card carries noneeds-user-decision, so a question could sit「well-formed and UNREACHABLE」. Answering that here rather than letting the row stand.The question: 「Should
ObjectFormkeep deriving a numericstepat all, now that the registered widgets are measured to drop it and derive their own from the same member?」Answered in the ACCEPT
5730217971, and ⛔ not escalated, because it did not need the maintainer: the seat takes A — keep it, which is what PR objectui#9873 does. It is the status quo corrected, ⛔ not a change. B (delete the key) is refused on the dev's own measurement — deleting it drops the unregistered-widget fallback branch to nostepattribute, i.e. HTML's default of1, a new defect on the only route that reads the key. C (publishstepas a form-field contract widgets consult) widens a published surface, and ⛔ this card does not authorise that.Where the residual lives, so it is in an inbox rather than in a closed report: the part of C that is a real open question — whether
stepis an authorable field key at all — is filed as objectui#9875 (NumberFieldMetadatadeclaresstep?: number; the protocol's field surface declaresstepnowhere but on the inline-grid column and the slider node). That card carries the unmeasured leg named on its own face.⇒ ⛔ No
needs-user-decisionon this card: nothing here is owed to the maintainer. The question is discharged and its successor is named.Generated by Claude Code
os-sales commentedon Sep 18, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.scale, notprecision(objectui#9574) #9873, merged.6df914164f, merged intomain.priority:p2,domain:ui— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
domain:ui#3 round 10 closed-card residue strip, 2026-09-18T17:05Z
Generated by Claude Code
Generated by Claude Code
fix(plugin-form): derive the numeric form step from `scale`, not `pre…