Uh oh!
There was an error while loading. Please reload this page.
feat(objectql): enforce number field scale by rejection, never rounding - #8322
Conversation
A declared `scale` was documentation, not a constraint: the number branch
of record-validator tested only `def.min`/`def.max`, so `scale: 0`
accepted 11.5 and stored it verbatim through both the REST create route
and the CSV import route.
Per the maintainer ruling of 2026-08-11 (issue comment 5250623270), an
over-scale value is now REFUSED — 400 VALIDATION_FAILED with field code
`max_scale` and constraint `{ scale, actual }` — symmetric with
min_value/max_value. Never rounded: silent rounding is silently altering
data. New writes only; stored legacy values are not migrated.
- spec: `max_scale` joins the closed FieldErrorCode catalog (ADR-0114 D2)
and the built-in validation message catalog in all four locales
- objectql: scale branch after min/max; decimal places measured from the
number's canonical string form (exponent-safe, no overflow)
- tests: unit pins (repro declaration, negative pins for integers /
min/max / unconstrained fields, exponent forms, malformed declarations),
CSV import leg, dry-run parity, direct create-route 400 envelope
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRyCo-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 109 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
) The group table in content/docs/api/error-catalog.mdx enumerates the closed FieldErrorCode catalog exhaustively (27 members pre-change, one per group row); max_scale joins the bounded-ranges row after max_value, matching the enum's own ordering. The three example lines above the table are illustrative (3 of 27 codes, mirroring the errors.zod.ts doc examples) and stay as-is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDTnVvsgA6cUZ4xFVtPZRy
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7501
What
A
numberfield's declaredscalewas documentation, not a constraint: the record validator's number branch tested onlydef.min/def.max, soscale: 0accepted11.5and stored it verbatim through both the REST create route and the CSV import route.Per the maintainer ruling of 2026-08-11 (issue comment 5250623270): an over-scale value is now refused —
400 VALIDATION_FAILEDwith field codemax_scaleandconstraint: { scale, actual }— symmetric withmin_value/max_value. Never rounded: silent rounding is silently altering data. Validation applies to new writes only; already-stored legacy values are not migrated or re-judged.Where it landed
packages/objectql/src/validation/record-validator.ts— thescalebranch aftermin/maxin the number branch, plus adecimalPlacesOfhelper that measures decimal places from the number's canonical string form (exponent-safe:1e-7is 7 places,1.2e+3is 0; no multiply-overflow false rejections). A malformed declaration (negative or non-integerscale) stays unenforced rather than getting invented semantics — tightening the declaration itself at authoring time is filed as spec:Field.scaleaccepts meaningless declarations (2.5,-1) — now that scale is enforced, malformed declarations should be refused at authoring time #8321 (that issue is not addressed here and remains open).packages/spec/src/api/errors.zod.ts—max_scalejoins the closed field-level catalog (ADR-0114 D2), in the bounded-rangemax_*family. This is a declared widening of the card's file surface: the catalog is closed and validated on the way out (FieldErrorSchema.code), so a first-class symmetric refusal cannot be expressed from insiderecord-validator.tsalone; the catalog's own doc says a new constraint kind "adds a member here".packages/spec/src/system/validation-message.ts—max_scalemessage templates in all four platform locales (en / zh-CN / ja-JP / es-ES), so the refusal is born fully localized.content/docs/references/api/errors.mdx— regenerated (gen:docs);api-surfaceverified current after rebuild.One branch covers every leg the issue measured: REST create/update, CSV/JSON import real write, and the import dry run (it asks the engine for its verdict via
validateData, so preview and write agree by construction).Pins
400+VALIDATION_FAILED+fields[0].code === 'max_scale'+constraint { scale: 0, actual: 1 }(import-integration test, real sqlite backend).max_scaleand stores nothing — neither11.5nor a rounded12(the assertion that distinguishes the ruling from the rejected rounding option); a sibling in-range row still writes. Dry run reports the same verdict and message.scale: 0still accepts integers (the repro's own declaration); values within a non-zero scale still write;min/maxbehaviour unchanged and checked first; a field with no declaredscaleaccepts any precision; malformed declarations stay unenforced.Verification
pnpm --filter @objectstack/objectql test— 196 files / 3461 tests greenpnpm --filter @objectstack/spec test— 388 files / 10249 tests greenpnpm --filter @objectstack/rest test— 109 files / 1812 tests green'max_scale'in the validator type-checks only against the rebuilt spec.d.ts(stale-dist false green impossible); reverse-verified by substituting a non-member literal →TS2345red, restored from commit.origin/main, predicted red): exactly the 6 rejection-dependent pins failed; all negative pins stayed green. Restored from commit.dispatch-gates.mjsderived from the actual diff (adr-anchors, authz-resolver, changeset-gate-self-tests, docs-audit-scope, filter-alias-parity, i18n, merge-driver, meta-type-normalized, objectui-changeset, quick-reference-counts, release-body, role-word, spec-parsed-alias, type-source-resolution, query-options-erasure, type-check-coverage) — all green.check:i18nrequired building the CLI first (prerequisite, not a finding).check:generated: 2 artifacts proved stale (api-surface via rebuild + docs), regenerated exactly those; all 13 now current.Deliberately not in this PR
min/maximport message the issue's secondary observation records — ruled out of this card (「same file is not the same card」); it stays recorded on the A number field's declaredscaleis never enforced — values with more decimals are accepted and stored verbatim (min/max on the same field are enforced) #7501 thread per triage.scaledeclarations — spec:Field.scaleaccepts meaningless declarations (2.5,-1) — now that scale is enforced, malformed declarations should be refused at authoring time #8321 remains open.Generated by Claude Code