Repository navigation
module: exact JSON equality (cic-canonical-json/1); Plan no longer calls different values a noop - #42
Merged
Conversation
…lls different values a noop canonicalEqual re-encoded both sides through encoding/json, so every number became a float64: two different integers above 2^53 compared equal and Plan answered noop for a real change; a repeated object key was resolved to the last one; an invalid UTF-8 byte or a lone surrogate escape became U+FFFD. cic-canonical-json/1 is a strict parser and writer of its own: exact numbers (<digits>e<exponent>, so 1, 1.0 and 1e0 are one value and 2^53+1 differs from 2^53), keys sorted by UTF-8 bytes, a repeated key or lone surrogate refused, only the quote, backslash and controls escaped, bounds on input size, nesting, digits and exponent. Its expected results are language-neutral vectors in testdata/canonical_json_vectors.json. Plan refuses desired.data and observed.data that are not canonicalizable (validation) and compares values by the canonical form. The noop reproduction, the vectors, the bounds and a deterministic mutation fuzz are tests. Plan digests (M4a) are separate. --- [signing-metadata] key = cic-my-sign-key signature = vault:v1:MEQCIHNLToZ7X+thijfXoqS2ALOfTHOeGvZVK7hCOj3Q0uX+AiBB1LDOLk9O4Ne22lgxHEWRyoZKIJNXFmYUfoj1gpN/gw== hash-algorithm = sha256 digest = 9q1QUR2K4a+XpIoRGaT2dmnSIHZppO5opzHywkMyEUM= [certificate] -----BEGIN CERTIFICATE----- MIICBjCCAaygAwIBAgIUSnRMR6RPnEbg296XWPOqq/u5PCwwCgYIKoZIzj0EAwIw QzELMAkGA1UEBhMCSFUxGTAXBgNVBAoMEENlbnRyYWxJbmZyYUNvcmUxGTAXBgNV BAMMEENJQyBEZXZlbG9wZXIgQ0EwHhcNMjYwMzIwMTMyMjU5WhcNMjYxMjMxMTMy MjU5WjBFMQswCQYDVQQGEwJIVTEZMBcGA1UECgwQQ2VudHJhbEluZnJhQ29yZTEb MBkGA1UEAwwSR2Fib3IgWm9sdGFuIFNpbmtvMFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAEIG2CVmTfmLB9pLLclj7YmP2eedAjklpy4LGrU2ijoiy6Xqpuybv7OgJe i+ez31s65NEV8+X/ByeX1cstR988z6N8MHowCQYDVR0TBAIwADAdBgNVHQ4EFgQU yZN6AIX/TNnIJ9GwAa/NRN3ujHAwHwYDVR0jBBgwFoAUXn6CHYzPUqU4JVP8g+OS WeDYjhcwDgYDVR0PAQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEF BQcDBDAKBggqhkjOPQQDAgNIADBFAiEA+bFzXRoJ4PCQbhAAtpkcMjt0vNj5rEW0 lOMBGDNyaWkCIB1vmM7PcZzv/c9bIrxF5kqv6QXomouhByUfeNUTbpKW -----END CERTIFICATE-----
sinkog
marked this pull request as ready for review
November 8, 2026 12:11
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
canonicalEqualre-encoded both sides throughencoding/json, so every number became afloat64: two different integers above 2^53 compared equal andPlananswered noop for a real change (reproduced on a realPlancall:plan = "noop" with 0 operations). A repeated object key was resolved to the last one, and an invalid UTF-8 byte or a lone surrogate escape became U+FFFD, so distinct texts could collapse into one.This PR replaces it with
cic-canonical-json/1, a strict parser and writer of its own, and makesPlanuse it. It is the prerequisite the owner set before M4a (the plan re-derivation must not rest on a wrong equality).<digits>e<exponent>;1,1.0,1e0,10e-1are one value,2^53+1differs from2^53,0.1differs from0.10000000000000001, zero in any spelling is0.\u00xx, lower case) escaped, no HTML escaping; a lone or mismatched surrogate, invalid UTF-8, a BOM, raw control characters are refused.Planrefusesdesired.data/observed.datathat are not canonicalizable (validation,field_path: desired.data/observed.data) and compares values (whole payloads and field by field) by the canonical form.module/testdata/canonical_json_vectors.json(25 canonical outputs, 9 equal, 11 distinct, 40 refused incl. byte-level cases); the rules are indocs/design/specs/canonical-json.md(also why it is not JCS: ES6 float formatting is the loss this avoids).Evidence
okinstead ofvalidation); equivalence controls (whitespace, key order,1vs1.0, escapes) pass on both. An earlier RED run was invalid (non-hexschema_hashin my test setup) and was redone; only the rerun counts.observe_derivation_test, which usescanonicalEqual).{"a":2}instead of{"a":2e0}); the tests caught it, the vectors were corrected, the implementation was not changed for it.Limits / not in this PR
validateand the other operations are unchanged. The M4a digests (plan_digest,target_digest,execution_digest) are not here; this only fixes the equality they will rest on.Plannow refuses duplicate-key and over-bound payloads that it used to read leniently.module.wasmnot rebuilt or re-signed. No merge without explicit owner GO.🤖 Generated with Claude Code