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-product — base_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
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.parseyields IEEE-754 doubles. keel isDecimal-only for exactly this reason, and JSON's number type would silently undo it at the boundary.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.pyextend to cover the new layer as it covers the console andkeel/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
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.
statealso 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-product —
base_incrementvaries 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 nearNumber.MAX_SAFE_INTEGER. Should instant client-side re-sorting ever be wanted, the field is namedsort, 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
gather_status,build_insights_reportand their siblings already return.test_console_thinness.pyextended to pin the new layer.Decimalvalues round-trip through the string form without loss.sortfield ships in this issue; if one is ever added it is documented as ordering-only.