kanonak-codec 3.2.0

Kanonak codec runtime (Rust port). Builds the canonical input model from typed nodes + an embedded CodecSchema and content-addresses it via kanonak-canonical.
Documentation
# kanonak-codec (Rust)

The generic, ontology-independent Kanonak codec runtime — the Rust port of
`@kanonak-protocol/codec`. Given a `CodecSchema` (the per-package metadata a generated
typed SDK embeds) and a set of typed nodes, it builds the canonical input model
and content-addresses it via [`kanonak-canonical`](../../kanonak-canonical/rust),
producing the exact content hash the `kanonak hash` CLI emits. It also
(de)serializes the normalized-JSON wire form.

A **node** is a plain JSON object (`serde_json::Map<String, serde_json::Value>`):
the `$`-envelope (`$type`, `$id`, optional `$extra`, …) plus alias-collapsed
local-name fields. A generated typed model serializes to one.

## API

```rust
use kanonak_codec::{
    build_package, canonical_form, content_hash, deserialize, enum_member, serialize, type_matches,
    Node,
};

// nodes: &[Node], schema/pkg: &serde_json::Value
let hash = content_hash(&nodes, &schema, &pkg)?;   // "sha256:..."
let form = canonical_form(&nodes, &schema, &pkg)?; // {"subjects":[...]}
let wire = serialize(&node, &schema)?;              // normalized-JSON Node, numbers in their JSON forms
let node = deserialize(&wire, &schema)?;            // typed Node (unmodeled -> $extra)
let is_app = type_matches(&node, "example.com/vocab@5.1.0/Application", &schema)?; // bool
let member = enum_member(&schema, "example.com/vocab@5.0.0/north");                // Option<EnumMemberMatch>
```

- `build_package` — the canonical input model (subject per node + synthesized
  package-wrapper subject). The seam every other entrypoint shares.
- `content_hash` / `canonical_form` — delegate to `kanonak-canonical`.
- `serialize` / `deserialize` — the normalized-JSON wire form. `serialize` writes modeled fields
  (nulls dropped) then `$extra` spread as siblings (modeled wins a collision; no `$extra` key on the
  wire); `deserialize` keeps `$`-keys and `$type`-modeled fields top-level and collects everything
  else into `$extra` for lossless round-trip. From 3.0.0, each integer and decimal value travels in
  its JSON form (kanonak-protocol 4.11.0's `numbers-in-json`, runtime#54), through lists and embedded
  values. In the node, `integer`, its four sign-restricted forms and `decimal` are a string of their
  canonical digits, since a `serde_json::Value` holds no integer past 64 bits; `long`,
  `unsignedLong` and `int` and narrower are numbers. An embedded value whose class the schema doesn't
  hold is carried as given. `deserialize` judges the `Value`s it's given: without serde_json's
  `arbitrary_precision`, a number such as `1.0000000000000000001` is already the `f64` `1.0`, so a
  caller that must refuse it parses with that feature. A JSON number past ±(2^53 − 1) for a wide integer, a number for a decimal, a string for a narrow integer, or a value outside its datatype's range is refused, ending `[json-number-form]`, as `codec-vectors-json-numbers.json` pins in every port.
- The serde binding (3.0.0), for a generated struct's fields, so serde_json reads and writes them as
  `serialize` and `deserialize` do:
  - `Integer<D>`, an `integer` or one of its sign-restricted forms held as its canonical digits, where `D` is
    `XsdInteger`, `NonNegativeInteger`, `PositiveInteger`, `NonPositiveInteger` or `NegativeInteger`, whose range
    it checks;
  - `Long` and `UnsignedLong`, over `i64` and `u64`, written as strings;
  - `Decimal`, held as its canonical digits and read from a string only.

  `int` and narrower take `i32` and the rest, which serde_json already refuses a string, a fraction or overflow
  for. Each refusal of these types ends `[json-number-form]`.
- `type_matches` — whether a node (any member of `$types`, else its `$type`) is
  an instance of a class: the class itself, or a subclass whose schema entry
  lists it in the optional `ancestors` field (every superclass, transitively,
  by versioned URI; canonicalization-inert). Classes compare by versionless
  identity; a type the schema cannot read never matches. A typed instance asks
  the same through `KanonakNode::type_matches` on its envelope.
- `enum_member` — the enumeration member a `{"$ref": ...}` names, as an
  `EnumMemberMatch { enum_type, uri, member }`, or `None` ("not this schema's").

**Reading earlier compatible versions (runtime#28).** A node typed at an
earlier compatible version of the schema's own package still reads: the exact
versioned class first, else the same class at the highest version for which
`kanonak_canonical::is_readable_by(written, schema_version)` holds.
`serialize`, `deserialize`, `type_matches` and `enum_member` share that one lookup, and the
decoded node keeps the `$type` it was written with. Rejections end in a
bracketed kind — `[unknown-type]`, `[newer-version]`, `[other-major]`,
`[other-minor-line]`. Hashing stays EXACT-VERSION only: a content hash is
computed over the producer's versioned URIs, so a node (or embedded value)
whose class the schema has only at a compatible later version fails with
`[hash-needs-exact-version]` rather than producing a different hash.
Hashing also refuses a statement the schema doesn't declare, an `$extra` entry or a key the class doesn't model, with `[hash-undeclared-statement]`: a codec holds one package's schema and can't type another package's predicate, so such a node is hashed with the SDK, over the graph. Serializing and deserializing still carry it (codec 2.0.0, runtime#37).

**Reference values (runtime#6).** A `{"$ref": uri}` must address a named
resource (`publisher/package[@version]/name`); a `$ref` containing a URI
fragment (`#`) is rejected at any depth by canonicalization, `serialize` and
`deserialize` alike, with an error ending in `[fragment-reference]`.

An object property carries a reference (`{"$ref": ...}`) or an embedded node,
gated by `codec-vectors-embedded.json`. Malformed input fails loudly
(`CodecError`); no fallbacks.

## Test

```sh
cargo test
```

Runs the shared golden vectors in `../vectors/` (`codec-vectors*.json`,
including the compatibility file `codec-vectors-compat.json`).