Skip to content

The serialisation contract: money as strings, values presentation-ready #533

Description

@eaitbrahim

Spec: docs/superpowers/specs/2026-08-23-web-ui-rewrite-design.md § The data contract.

Prerequisite for every other issue in this milestone. It is split from the API endpoints deliberately: this is the rule everything inherits, and burying it inside a large endpoint issue would hide the dependency.

Rule 1 — money crosses the wire as strings, never as JSON numbers

JSON.parse yields IEEE-754 doubles. keel is Decimal-only for exactly this reason, and JSON's number type would silently undo it at the boundary.

"notional": { "value": "500.00", "display": "$500.00" }

A serialisation test fails the build if any monetary field emits a JSON number.

Rule 2 — values arrive presentation-ready; the client places, never derives

Not {"qty": "0.01", "price": "50000"} for the client to multiply. Every figure a user sees is computed by the Python that holds the rails.

This is what makes the invariant checkable rather than merely likely, and it is what lets tests/commands/test_console_thinness.py extend to cover the new layer as it covers the console and keel/web/ today. It is also why the client needs no decimal library — see the zero-dependency rule in the spec.

Rule 3 — semantic state is a field, not an inference

"pnl": { "value": "-12.34", "display": "▼ −$12.34", "state": "bad" }

The client must never decide "this is bad" by inspecting a sign — that is arithmetic by another name, and it puts a judgement back on the client. state also gives #532's glyph somewhere to come from.

Sorting is server-side

Tables sort by query parameter; Python orders with Decimal. On loopback the round trip is sub-millisecond, so there is nothing to optimise and no client arithmetic to audit.

An integer companion field scaled to cents was considered and rejected. Precision is per-productbase_increment varies by instrument, which is exactly what #514 and #517 were about — so a fixed 100× scale silently truncates anything finer than a cent, and a 1e8 scale caps a USD notional near Number.MAX_SAFE_INTEGER. Should instant client-side re-sorting ever be wanted, the field is named sort, is a plain JSON number, and is documented as ordering only — never displayed, never summed.

Scope

The shared serialiser and its tests. Endpoints are the next issue.

Acceptance

  • A serialiser turning the existing frozen report dataclasses into the payload shape above.
  • A test that fails if any monetary field serialises as a JSON number.
  • A test that the serialiser computes nothing — it reads what gather_status, build_insights_report and their siblings already return.
  • test_console_thinness.py extended to pin the new layer.
  • Decimal values round-trip through the string form without loss.
  • No sort field ships in this issue; if one is ever added it is documented as ordering-only.

Metadata

Metadata

Assignees

Labels

featureNew capability (groups under Features)prerequisiteBlocks other issues; do first

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions