BUG-3202 (#1545) decodes fields_patch from its raw member and
field_overrides through models.FieldValues, to keep number literals. A
plain map member MERGED a repeated occurrence; a raw member keeps the
LAST, so a request repeating either answered 200 having discarded part
of the write.
A repeat is now refused with a 400 naming the member, and nothing is
written:
- models.RefuseRepeatedMember scans an object's top-level members and
matches names by encoding/json's own rule (exact, or its case fold),
so fields_patch followed by FIELDS_PATCH is a repeat. A test pins the
matcher against encoding/json itself.
- ItemUpdate.UnmarshalJSON refuses a repeated fields_patch; the move
request (now a named type) and the copy/preflight request refuse a
repeated field_overrides. The three handlers answer with the message
unwrapped; the copy door no longer flattens it to "Invalid JSON body".
- FieldValues refuses a second non-null decode itself, as the backstop
for a future struct that forgets the scan.
Out of scope: repeated keys INSIDE one fields object (BUG-2896), and
members that were already raw before #1545 (fields, tags), which kept
the last occurrence before and after.
* fix(fields): field writes keep every number's literal, so an integer above 2^53 is no longer rounded by a write that never named it (BUG-3202)
Every door that writes an item's fields decoded the blob into
map[string]any, turning each JSON number into a float64, so the re-encode
rounded integers above 2^53 (9007199254740993 stored as ...992). The stored
blob was decoded that way too, so a fields_patch to `status` rewrote every
large integer in the row.
- models.DecodeFieldsJSON / DecodeJSONKeepingNumbers decode with UseNumber.
They accept and refuse exactly what json.Unmarshal does (trailing data,
out-of-float64-range numbers); only the representation changes.
- Used at every fields decode that feeds a write: create, full-fields
update, the fields object on the wire, fields_patch (request and store
merge), the carried-blank-relation normalisation, the Append/Set metadata
helpers, move, bulk move, cross-workspace copy and its preflight, and the
move/copy field_overrides (models.FieldValues).
- items.validateFieldType accepts a json.Number that parses as a finite
number. items.coerceValue and the CLI's parseFieldFlag keep a numeric
string that is a JSON number literal as that literal.
- Read-only decoders are unchanged (display, hydration, guards).
Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m
* fix(fields): compare field values by value, so an equal number in another spelling is no change and two integers that share a float64 are (BUG-3202)
Field writes now keep a number's literal, so one value can be stored as
different text (1e3 vs 1000). Three places compared field values:
- item.updated's change detector compared the fields blob's TEXT whenever
it was not masking a string done key: an equal-value rewrite, or a
key-reorder-only rewrite, emitted item.updated.
- the activity `changes` diff and the BUG-3163 carry check compared
float64s: equal spellings were fine, but two different integers above
2^53 compared equal, so such a change went unrecorded and a changed
reserved value passed as carried.
models.CanonicalJSONNumbers gives every number one exact canonical
spelling. All three decode keeping literals and compare canonical forms.
Claude-Session: https://claude.ai/code/session_01A3rXV7GDLAhBe9mL4V6z2m