# `AgentBlueprintProtocol.Canonicalization`
[🔗](https://github.com/baselabs/agent_blueprint_protocol/blob/v0.1.1/lib/agent_blueprint_protocol/canonicalization.ex#L1)

RFC 8785 JSON Canonicalization Scheme over the closed tagged algebra
produced by `AgentBlueprintProtocol.Json`.

This is the package's one and only encoder: object members are sorted by
UTF-16 code unit (`sort_key/1`), strings use the §3.2.2.2 escape set, and
floats serialize exactly as ECMAScript §7.1.12.1 (including the Note 2
shortest round-trip rule) — digits from `:erlang.float_to_binary/2` with
`[:short]`, notation re-formatted per ECMA-262. Output is UTF-8 with no
whitespace between tokens.

Fail-closed guards on the encode path (a value can reach the encoder
without ever passing the decoder): invalid UTF-8 and lone surrogates deny
with `:invalid_encoding`; duplicate object names deny with
`:duplicate_member`; a directly-constructed tagged integer above the
I-JSON bound `±(2^53−1)` denies with `:integer_magnitude` — decode can no
longer produce one (the integer window float-tags above-bound lexemes), but a
hand-built value still fails closed so the package never emits bytes an
ECMAScript peer would serialize differently. The
runtime cannot materialize NaN or ±Infinity floats (arithmetic raises,
parsing and the external term format reject them), so no non-finite value
can reach this module on a stock BEAM.

`verify/2` enforces the interchange contract: bytes are canonical only if
decode followed by re-encode reproduces them exactly; anything else
(whitespace, member reordering, needless escapes, non-ES6 number lexemes)
is `{:error, :non_canonical_bytes}`. Digests are computed over exactly
these verified bytes by the digest layer.

The decoder's integer window (the integer-window amendment, 2026-08-21) closes the
former encode/verify asymmetry: a pure-digit lexeme above `±(2^53−1)`
decodes as `{:float, f}` iff `number/1` reproduces it, so canonical
pure-digit output for integral floats from 2^53 up to 10^21 (e.g.
`9007199254740992`, `295147905179352830000` — RFC 8785 Appendix B) now
round-trips through decode and `verify/2`. Above-bound lexemes that are
not a double's canonical ES6 spelling deny decode with
`:number_not_double_expressible`, which `verify/2` passes through.

Errors are **value-free**: a reason names a category, never the offending
bytes or values.
Canonical bytes are a serialization fact — encoding never authorizes anything.

# `reason`

```elixir
@type reason() ::
  :non_canonical_bytes
  | :integer_magnitude
  | AgentBlueprintProtocol.Json.reason()
```

# `encode`

```elixir
@spec encode(
  AgentBlueprintProtocol.Json.value(),
  AgentBlueprintProtocol.Bounds.t() | map()
) ::
  {:ok, binary()} | {:error, reason()}
```

Encode `value` (the tagged algebra) to RFC 8785 canonical JSON under
`bounds` (a `Bounds` struct or tighten-only overrides, defaulting to the
profile maxima). The `bytes` ceiling bounds the OUTPUT size.

# `number`

```elixir
@spec number(float()) :: {:ok, binary()}
```

Serialize `float` per ECMA-262 §7.1.12.1 (Note 2 included): `-0.0` is
`"0"`, integral values below 10^21 print as integers, `1e+21` and beyond
(and below 10^-6) use exponential notation.

# `sort_key`

```elixir
@spec sort_key(binary()) :: binary()
```

The RFC 8785 §3.2.3 member sort key: `name` as big-endian UTF-16 code
units. Erlang binary order over these keys is exactly the RFC's unsigned
code-unit comparison, shorter-prefix-first. `name` must be valid UTF-8.

# `verify`

```elixir
@spec verify(binary(), AgentBlueprintProtocol.Bounds.t() | map()) ::
  {:ok, AgentBlueprintProtocol.Json.value()} | {:error, reason()}
```

Verify that `input` is already in canonical form: decode under `bounds`,
re-encode, and require byte equality. Returns the decoded value on
success, `{:error, :non_canonical_bytes}` on any mismatch, and passes
decoder errors through unchanged (so every `Json` decode reason —
`:number_not_double_expressible` included — can surface here).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
