Skip to content

core: write every number in the report in its plain form - #158

Merged
jbpros merged 3 commits into
mainfrom
core/plain-numbers
Oct 7, 2026
Merged

jbpros merged 3 commits into
mainfrom
core/plain-numbers

Conversation

@jbpros

@jbpros jbpros commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

OPA keeps a number as it was written, so the same policy and input could give different report bytes depending on how a number was typed, or on which implementation ran it. Fixes #152.

With this input:

{"items": [{"id": 1.0, "n": 1.50, "xs": [2.0, 3e0, 2.5e-3]}, {"id": 1e0, "n": -0.0, "xs": []}]}

params {"p": 2.50}, and checks n == 1.50, n in range -0.0 to 1e1, n == $$params.p and every xs[] == 9.0, main writes 1.50, 1.0, 1e0, 3e0, 2.5e-3, -0.0 and 2.50 across the checks, $refs, inputs, subject ids, failed_items and $unique_ids. The branch writes 1.5, 1, 3, 0.0025, 0, 2.5 and 10, and $unique_ids gives [1] whichever spelling comes first.

How: after building the report, ergo writes it with json.marshal and looks for a number written in another form. Most reports have none, and they're returned as they are. Otherwise each number is rewritten with the same printer expressions use, and the text is read back. Two exceptions:

  • A number a 64-bit float can't hold, like 1e400, stays as written. core: say how a number from the input or params that a 64-bit float can't hold is reported #89 is still open, and writing 1e1000000 out in full would make a million-character string.
  • A report built from something JSON can't hold, like a set in a Rego input or a policy whose requirement names aren't strings, is left as built. Rewriting it would turn keys into strings and separate rows from their requirement.

Changes the report: numbers in checks, $refs, inputs, subject ids, failed_items and violations are written in their plain form.

What I ran

  • Every check in AGENTS.md, plus Wasm (Docker, OPA 1.19.0) and the JS runtime script: 898/898 each. OPA 1.20.2 in Docker passes fmt and the tests.

  • Three of the six new tests fail on main. The others pin what stays the same: 1e400 copied as written, and reports built from values JSON can't hold. Each guard in the new code has a test that fails when it's removed.

  • One gotcha: comparing json.marshal of two reports whose inputs differ only in number spelling doesn't work as a test, because OPA caches function calls by value and 1.50 == 1.5. Both calls return the same cached report. The tests compare against literal JSON instead.

  • All 1,683 conformance cases from the core/conformance-suite branch give the same report by value on main and on this branch. About 20 of them have a non-plain number in the expected report, so they'll need regenerating after this lands.

  • sdlc-policies, pr-reviewer and the server's npm-bump tests: the same 9 failures on main and on this branch (from earlier PRs), nothing new. npm-bump passes on both.

  • Speed, best of 5 runs on 3,000 subjects: 1.93s on main and 2.04s here when every row has a number to respell, 1.96s and 2.00s when none does. The whole test suite takes 11.4s against 11.2s on main.

  • Tests cover the change, and the checks in AGENTS.md pass

  • REFERENCE.md and README.md are updated if behaviour changed

  • If the report changes for an existing policy, the description has a line starting with Changes the report:

🤖 Generated with Claude Code

OPA keeps a number as it was written, so `1.50` in the input came out as
`1.50` in the report, while JavaScript or Rust write `1.5`. The same
policy and input could give different bytes depending on how a number
was typed, and a repeated id like `1.0` and `1e0` was listed with
whichever spelling came first.

ergo now writes every number in the report the way expressions already
do: `1.50`, `-0.0` and `1e2` come out as `1.5`, `0` and `100`. A number
a 64-bit float can't hold, like `1e400`, is kept as written, since #89
is still open. A report holding something JSON can't, like a set from a
Rego input, is left as built, so its rows still name their requirement.

Changes the report: numbers in checks, `$refs`, `inputs`, subject ids,
`failed_items` and violations are written in their plain form.

Fixes #152

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros jbpros added core Relates to the ergo library or format themselves. impacts-ergo-reports Impacts ergo report schema. Careful design attention needed. labels Oct 7, 2026
jbpros and others added 2 commits October 7, 2026 22:41
Most reports have no number written in another form, so ergo now looks
for one in the JSON first and only rewrites the numbers when it finds
one. Values JSON can't hold are spotted from the input, the params and
the policy rather than by comparing the whole report after the rewrite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbpros
jbpros marked this pull request as ready for review October 7, 2026 21:01
@jbpros
jbpros merged commit 8ace25c into main Oct 7, 2026
5 checks passed
@jbpros
jbpros deleted the core/plain-numbers branch October 7, 2026 21:01
jbpros added a commit that referenced this pull request Oct 7, 2026
Extracts the cases again after #155, #156 and #158, leaving out policies
that aren't objects, since their reports use numbers as keys. The Rust
port now writes every number in its plain form, as #158 does, and its
runner compares number text exactly to check it. generate.sh lets OPA
write each file's text, because jq rewrote numbers like 0.000000123 as
1.23E-7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Relates to the ergo library or format themselves. impacts-ergo-reports Impacts ergo report schema. Careful design attention needed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: write every number in the report in its plain form

1 participant