← Back to PRs

#19401: fix(ui): prevent precision loss when coercing large numeric strings in config form

by Operative-001 open 2026-02-17 19:18 View on GitHub →
app: web-ui size: S trusted-contributor
## Problem When users edit config via the Control UI form mode, the form coercion logic silently corrupts Discord Snowflake IDs (and other large numeric strings). **Example:** - Input: `"805422583936421918"` (valid string snowflake) - Output: `805422583936421900` (corrupted number with precision loss) This breaks Discord DMs because the snowflake ID no longer matches. ## Root Cause The `coerceFormValues()` function in `ui/src/ui/controllers/config/form-coerce.ts` handles `anyOf/oneOf` schemas by trying to coerce strings to numbers when a number variant exists. Discord IDs are defined as `z.union([z.string(), z.number()])` which becomes a JSON Schema `anyOf` with string and number variants. When coercing `"805422583936421918"`: 1. Loop finds the `number` variant 2. `Number("805422583936421918")` → `805422583936421900` (JavaScript loses precision on 64-bit integers) 3. Returns the corrupted number instead of preserving the valid string ## Fix Before returning a coerced number, check if round-tripping preserves the value: ```typescript if (String(parsed) !== trimmed) { return value; // Keep original string — precision would be lost } ``` This ensures: - Large numeric strings (snowflakes, etc.) are preserved as strings - Small numbers that fit safely are still coerced to numbers for correct typing ## Testing Added test cases for: - Preserving large numeric strings (Discord snowflake) in anyOf union - Still coercing small numeric strings when precision is preserved Verified manually: ```javascript // Before fix: coerceFormValues({allowFrom: ["805422583936421918"]}, schema) // → {allowFrom: [805422583936421900]} // WRONG // After fix: coerceFormValues({allowFrom: ["805422583936421918"]}, schema) // → {allowFrom: ["805422583936421918"]} // CORRECT ``` Fixes #17802 <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR fixes a real precision-loss bug where Discord Snowflake IDs (and other 64-bit integers) were being silently corrupted when coerced through `Number()` in the config form. The root cause and approach are correct in intent. However, the precision guard introduced in `coerceNumberString` — `String(parsed) !== trimmed` — is overly broad. It fires correctly for large integers beyond `Number.MAX_SAFE_INTEGER` (e.g. `"805422583936421918"` → `"805422583936421900"`), but it also fires for lossless float normalizations like `"1.0"` → `1` (where `String(1)` is `"1"`, not `"1.0"`). This breaks the existing test case that coerces `"1.0"` to the number `1` in a `type: "number"` field. **Key issues:** - The `String(parsed) !== trimmed` check will cause the existing `"coerces string numbers to numbers based on schema"` test to fail for the `"1.0"` input, since `String(Number("1.0"))` is `"1"` not `"1.0"`. - The correct guard should target only the unsafe-integer case: `Number.isInteger(parsed) && !Number.isSafeInteger(parsed)`, which precisely catches 64-bit integer overflow without affecting float string normalization. - The two new test cases are well-constructed and do verify the intended behavior. <h3>Confidence Score: 2/5</h3> - This PR introduces a regression: the `String(parsed) !== trimmed` guard breaks coercion of valid float strings like `"1.0"` to numbers, which fails an existing test. - The fix correctly identifies the snowflake precision-loss problem and the overall approach is sound, but the chosen guard condition is too broad. It catches lossless representations like `"1.0"` → `1` as false positives, causing at minimum one existing test to fail (`cost.output` expected to be `1` from input `"1.0"`). The fix needs a targeted guard (`Number.isInteger(parsed) && !Number.isSafeInteger(parsed)`) before it can be safely merged. - `ui/src/ui/controllers/config/form-coerce.ts` lines 30–32 — the round-trip guard needs to be narrowed to only block unsafe integers. <sub>Last reviewed commit: f9df4e3</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs