← All notes

Work from

One format, two validators

Keeping Rust and TypeScript in agreement about which records are valid.

A record can have a valid canonical encoding and still be invalid for its intended type. It might be missing a required field, contain the wrong nested structure, or use a number outside the allowed range.

Grain already checked canonical DAG-CBOR. The next boundary was making its Rust and TypeScript implementations agree on whether those bytes represented a particular kind of record.

Make the requested type explicit

The existing validation operation gained an optional object_type selector. When supplied, it checks the selected type from the CDDL schema, including required fields, nested records, unions, fixed widths, and CID links.

Without the selector, callers retain the previous validation behavior. Adding stricter checks to every call would have changed the meaning of an existing operation, so typed validation is an explicit request.

Rust and TypeScript implement the same contract independently. The WASM build uses the Rust implementation; it does not add another independent interpretation of the format.

Share the awkward cases

The change adds 103 shared vectors across all 14 top-level types: 17 accepted records and 86 rejected inputs. Both implementations consume the same inputs and expected results.

The rejected cases matter as much as the valid examples. They exercise malformed links, numeric domains, nested shapes, limits, ordering, and uniqueness. A validator that accepts the happy path can still disagree with another implementation on the records that should never enter the system.

Error order is part of the contract too. If an input has invalid bytes and an unknown type selector, implementations must preserve the specified diagnostic precedence rather than report whichever problem their code happens to encounter first.

Check the evidence itself

Review caught a problem in the coverage tooling: a vector named as a positive case could count toward type coverage even if its expected result was rejection. That would make a coverage report look complete without proving a valid record could pass.

The guard now checks the expected outcome. A mutation check flips a positive vector to expect failure and verifies that it can no longer satisfy the coverage requirement.

Shared vectors make disagreements reproducible. Keeping the schema registries and coverage checks aligned with the specification helps prevent both implementations from agreeing on an incomplete contract.

Implementation and conformance vectors