Skip to content

Add conformance test infrastructure and cross-language decoder ports#2

Merged
Gityosan merged 14 commits into
mainfrom
claude/relaxed-johnson-GZ31j
May 31, 2026
Merged

Add conformance test infrastructure and cross-language decoder ports#2
Gityosan merged 14 commits into
mainfrom
claude/relaxed-johnson-GZ31j

Conversation

@Gityosan
Copy link
Copy Markdown
Owner

Summary

This PR completes the Graft binary format implementation by adding:

  1. Language-neutral test vectors (spec/golden/*.meta.json) — JSON sidecars describing expected decoded values with shared identity and cycles
  2. Cross-language decoder ports — Reference implementations in Python, Rust, and Go that decode Graft binaries and validate against .meta.json
  3. JS library enhancements — TypeExtension API, JSON bridge, CLI tools, and updated test suite

The conformance infrastructure ensures all language ports correctly restore shared references, cycles, and type semantics across 13 test vectors covering all major types.

Key Changes

Conformance Test Infrastructure

  • conformance/README.md — Contract specification for all language ports: how to decode, match against .meta.json, and verify shared identity/cycles
  • spec/golden/*.meta.json — Language-neutral expectations for 13 vectors (primitive, bigint, string, date, bytes, dataview, typedarray, regexp, url, symbol, map_set, cycles, error)
  • js/scripts/gen-golden.ts — Generator for .bin + .meta.json pairs from JS values

Language Ports (Zero External Dependencies)

  • Python (conformance/python/) — stdlib-only decoder + encoder + conformance runner
  • Rust (conformance/rust/) — Rc-based heap decoder with identity preservation, encoder, matcher
  • Go (conformance/go/) — Standard library decoder + encoder + conformance test
  • JS (conformance/js/run.ts) — Reference implementation that other ports model on

Each port implements:

  • Two-pass heap algorithm (FORMAT.md §4) for reference type identity restoration
  • Parallel-walk matcher that binds $ref indices on first sight and asserts pointer identity thereafter
  • Encoder round-trip validation (encode(decode(bytes)) == bytes)

JS Library Additions

  • TypeExtension API (js/src/extension.ts) — User-registered custom type handlers for class instances and domain types
  • EncodeOptions / DecodeOptions — Support for extensions and weak collection providers
  • JSON bridge (js/src/json-bridge.ts) — toJSON / fromJSON for human-friendly inspection and hand-editing of fixtures
  • CLI tools (js/src/cli.ts) — graft inspect and graft diff commands for binary analysis
  • Extended test suite — Tests for RegExp, extensions, JSON bridge, CLI, and error paths

Format Specification Updates

  • spec/FORMAT.md — Added RegExp (Tag 43) and DataView (Tag 44) to the tag table
  • js/src/format.ts — Added RegExp and DataView tags

CI/CD

  • .github/workflows/ci.yml — Added conformance test step that runs all language ports

Documentation

  • docs/RELEASING.md — Distribution strategy for JS library (JSR, npm, GitHub Releases) and language ports
  • CLAUDE.md — Updated project structure and implementation status

Implementation Details

Two-Pass Heap Algorithm (all ports):

  1. Pass 1: Read stream header, create empty placeholders for all reference types (Array, Object, Map, Set, Error, Symbol)
  2. Pass 2: Fill containers by resolving references to heap slots, preserving shared identity and cycles

Matcher Contract (all ports):

  • Bind each $ref index to decoded object on first encounter
  • Assert pointer identity on subsequent occurrences
  • Verify container entries positionally (property order is mandated by format)
  • Value-type leaves (Date, Bytes, RegExp, URL, DataView, TypedArray) carry no identity; match structurally

Encoder Determinism:

  • Depth-first pre-order interning
  • Identity dedup for reference types, value dedup for primitives
  • Guarantees encode(decode(golden)) == golden for all vectors

Testing

All 13 golden vectors pass conformance in all 4 language ports:

  • JS reference

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE

claude added 14 commits May 31, 2026 01:57
…cars

- RegExp (Tag 43): lossless source+flags, leaf node with shared identity
- encoder now rejects exotic objects (class instances, Error, URL, etc.)
  instead of silently writing them as empty Objects, keeping the
  encoder->file lossless guarantee honest (consistent with functions)
- gen-golden.ts emits a language-neutral .meta.json sidecar per vector,
  mirroring the decoded heap (refs for shared identity/cycles, tagged
  leaves) so other-language ports can assert structure; documented schema
  in conformance/README.md
- FORMAT.md updated as source of truth (RegExp row, reserved range 44-255,
  exotic-object guard rule)

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Add test/errors.test.ts covering every documented error path:
- encoder rejections: function (top-level & nested), class instance,
  Error, URL, Promise, DataView, plus exotic value nested in a container
- decoder rejections: bad magic, unsupported version, reserved/unknown
  tag, unknown TypedArray element type, truncated stream (EOF, header &
  mid-payload), and node count exceeding a safe integer
- keeps a positive control so the guard can't silently become a no-op

Consolidated the rejection cases out of roundtrip.test.ts into the new file.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
…ta.json

conformance/js/run.ts decodes every spec/golden/*.bin with the reference
decoder and asserts it against the .meta.json sidecar via the parallel-walk
algorithm from conformance/README.md §2:
- validates all leaf types, containers, and object symbol keys
- binds each $ref index on first sight and asserts identity afterwards, so
  shared references and cycles must be truly restored (not just copied)
- reports the failing path (e.g. $#0.flags#2) and exits non-zero

Wired in as the reference port other languages mirror:
- js: 'pnpm conformance' script; typechecked via tsconfig (rootDir bumped)
- CI runs it as a dedicated step
- conformance/js/README.md + docs updated to point at it

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
…y arrays

New encodable types (FORMAT.md updated as source of truth):
- Url (Tag 44): href string, fully reversible, shared identity preserved
- DataView (Tag 45): own tag distinct from TypedArray; stores viewed window
- Error (Tag 46): name + message + optional cause (child ref) + extra own
  enumerable props; builtin subclasses (TypeError, ...) restored by name;
  stack intentionally dropped (environment-derived). Custom Error subclasses
  now round-trip as base Error with their name preserved
- Boxed Number/String/Boolean: best-effort unwrap to the primitive

Closed silent-loss holes:
- arrays with non-index own enumerable properties now throw instead of
  dropping them (consistent with the exotic-object guard)
- fixed a latent decode bug where an object key of "__proto__" mutated the
  prototype instead of becoming an own property (found via property test);
  Object/Error property restore now uses defineProperty

Tooling:
- golden + .meta.json regenerated; added url/dataview/error vectors
- conformance runner matches the three new node types
- errors.test.ts updated: URL/Error/DataView no longer rejected; added
  array-extra-prop and __proto__ regression coverage

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Two independent, zero-dependency decoders + conformance runners that decode
every spec/golden/*.bin and match it against the .meta.json sidecar (the
parallel-walk algorithm from conformance/README.md §2), proving the format is
implementable outside JS.

Python (conformance/python/, stdlib only):
- decode.py: two-pass heap decoder with wrapper types + documented fallbacks
- run.py: matcher + runner; `python3 conformance/python/run.py`

Rust (conformance/rust/, no external crates):
- Rc-wrapped value graph so shared identity / cycles compare by pointer
- hand-rolled JSON parser for the sidecars (no serde -> builds offline)
- arbitrary-precision bigints kept as canonical decimal strings
- `cargo test` (one test over all vectors) or `cargo run`

Both pass all 13 golden vectors and were verified to fail loudly on a
tampered meta. CI gains conformance-python and conformance-rust jobs; root
conformance README documents the run commands.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
A standard-library-only Go decoder + conformance runner that decodes every
spec/golden/*.bin and matches it against the .meta.json sidecar (the
parallel-walk algorithm from conformance/README.md §2).

- decode.go: two-pass heap decoder; reference types are pointers so shared
  identity and cycles compare by address. math/big for arbitrary-precision
  integers, encoding/json for the sidecars -> no third-party modules.
- match.go: meta matcher; binds each $ref on first sight and asserts pointer
  identity afterwards; container entries matched positionally (also verifies
  property order).
- run.go + conformance_test.go: `go test ./...` or `go run .`.

Passes all 13 golden vectors; verified to fail loudly on a tampered meta
(including a broken shared-identity case). CI gains a conformance-go job; the
root conformance README lists all four ports (js, python, rust, go).

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Each port now ships an encoder that faithfully clones the reference JS
algorithm — depth-first pre-order interning, identity dedup for reference
types, value dedup for primitives, identical tag layout — and a round-trip
test asserting encode(decode(golden)) reproduces the original bytes exactly
for all 13 vectors. (Byte equality isn't required for conformance, but the
reference ports reproduce it; useful for generating fixtures in a non-JS
language that the JS reference reads back.)

- python: encode.py + roundtrip.py; decode.py now decodes BigInt to an
  int subclass so encode can re-emit the BigInt tag (matcher unaffected).
- rust: src/encode.rs + tests/roundtrip.rs; decimal-string bigint -> LEB128
  via long division (still no external crates).
- go: encode.go + TestEncoderRoundtrip; math/big for bigint magnitude.

All three pass decode conformance AND byte-identical round-trip (13x3).
CI: Python job gains a round-trip step; rust/go round-trips run under the
existing cargo test / go test. Docs updated.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Covers JSR, GitHub Releases (for the spec + golden vectors), CDN (jsDelivr
/gh), GitHub Packages, git-based installs, and per-ecosystem registries
(crates.io / PyPI / Go modules) with this repo's three artifact types in
mind, plus OIDC/Trusted-Publishing automation notes. Linked from README.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
…nd-rolled code

Per the maintainability direction, replace self-implemented pieces in the Rust
port with well-known crates that don't touch the wire format's byte
determinism:

- delete src/json.rs (~230-line hand-rolled JSON parser); parse .meta.json
  with serde_json (matcher now walks serde_json::Value)
- replace the manual decimal<->LEB128 long division with num-bigint for
  BigInt encode/decode

The byte-level format primitives (varint / ZigZag / float-LE / UTF-8) stay
hand-written since those define the format. Still passes all 13 decode +
13 byte-identical round-trip vectors; net ~150 fewer lines. README/CI notes
updated (Rust port is no longer dependency-free; CI fetches from crates.io).

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Extension registry — opt-in lossless support for values the core rejects
(class instances, domain types, Temporal, ...):
- new TypeExtension { name, match, encode, decode }; pass via
  encode(v,{types}) / decode(b,{types}). Tag.Custom=47 stores
  str(name)+surrogate_ref; the surrogate is any Graft value (interned, so it
  may nest Graft types and share identity).
- decode resolution reworked to be lazy + memoized: containers keep stable
  placeholders (cycles/shared refs unchanged), customs reconstruct once their
  surrogate is resolved; a cycle *through* a custom is detected and rejected.
- encode's 2nd arg now accepts EncodeOptions or a bare WeakProvider
  (back-compat).

Decode safety limits — reject node count > buffer length, a maxNodes cap,
out-of-range root, and out-of-range references (was an uncaught crash).

FORMAT.md gains §5.8 Custom + tag 47 (reserved -> 48-255). 93 JS tests pass;
golden/conformance unaffected (no vector uses Custom).

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Human-friendly, JSON.stringify-able view of a Graft value and its inverse, for
inspecting / hand-editing fixtures with ordinary JSON tooling. Non-JSON types
(undefined, bigint, Date, Map/Set, RegExp, URL, typed arrays, ArrayBuffer,
DataView, Error, symbols, NaN/-0/±Infinity, symbol-keyed objects) render as
tagged { "$graft": "<type>", ... } wrappers; plain JSON-ish data stays
natural. Lossy on identity (shared refs duplicated) and rejects cycles — for
exact transport use encode/decode. Exported from index. 103 JS tests pass.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
New Node CLI entry (src/cli.ts, bin: graft), kept out of the runtime bundle:
- graft inspect <file.bin>: cycle-safe readable tree (shared/cycle markers,
  typed leaves) + a type histogram counting distinct objects once.
- graft diff <a.bin> <b.bin>: value-graph differences as path-tagged lines
  (changed scalars, added/removed keys, length/type mismatches), exit 1 on
  any difference — handy for reviewing golden/fixture changes.

format/histogram/diffValues are exported and unit-tested; the entry only
auto-runs when invoked directly. tsdown builds a second cli.js entry.
README documents the JS library surface (extension types, decode hardening,
JSON bridge, CLI). 108 JS tests pass.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
Nothing is published yet, so collapse encode's dual second-arg (bare
WeakProvider | EncodeOptions, detected heuristically) into a single clean
options shape: encode(root, { provider?, types? }). Removes the isWeakProvider
guess. Callers updated to pass { provider }.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
…omment

- CLAUDE.md was a tasks 1-4 walkthrough that is long done; rewrite it as an
  accurate handoff: full tag set (0-47), JS features (extension registry,
  decode safety, JSON bridge, CLI), 4 conformance ports with byte-identical
  round-trip, current repo layout, constraints (V8-independent, zero runtime
  deps, byte determinism, conformance = test harness not packages, pre-release
  so breaking changes OK), commands, and the format-change workflow.
- conformance/rust/Cargo.toml comment claimed zero deps + a src/json.rs JSON
  parser; it now uses serde_json + num-bigint (src/json.rs deleted). Corrected.

https://claude.ai/code/session_01PMGbxdzQGTi6cuNipYznzE
@Gityosan Gityosan merged commit 7188950 into main May 31, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants