shape

TypeScript ↔ Go ↔ Rust parity#

Shape has three implementations. TypeScript is canonical—it defines the behaviour. The Go and Rust ports match it exactly for validation outcomes, produced values, and error message text. The page keeps its Go-centred shape, since the Go port came first and its divergences are the longer list; the Rust port's own section is at the end.

How parity is enforced#

Two gates, and they do different jobs.

The shared corpus#

A language-neutral conformance corpus lives in test/ as a set of .tsv files. Each row is a (spec, input) → output | error case whose expected column is computed from the canonical TypeScript build. All three implementations run every row:

A row's output is compared JSON-normalized (so numeric widths and absent properties don't cause spurious mismatches). Its error column holds the complete expected message and is compared exactly—a substring check cannot see a wrong separator, a wrong error order or an extra error, and those are precisely the ways two implementations drift apart.

The differential harness#

The corpus only covers rows someone thought to write, and for a long time it had no negative case for Type() or Rest()—so a Type() that asserted nothing and a Rest() that validated nothing passed it.

test/differential/ is the wider net. It generates thousands of (spec, input) pairs, runs every one through all three implementations, and diffs the JSON Schema export, the declarative JSON export, the export of the shape that JSON reads back as, the verdict, the produced value and the exact error text, each port against the canonical build:

make diff        # sampled report, both ports
make diff-full   # every mismatch
make diff-rs     # the Rust port alone

Run it after any behaviour change, and promote anything it finds into a corpus row so the committed gate keeps it closed.

What is guaranteed to match#

Intentional divergences#

Some differences are inherent to Go and are unlikely ever to close.

Error metadata#

All three languages produce the same message text, and the Go FieldError and Rust FieldError carry the fields of TypeScript's ErrDesc (why, check, mark, path, pathArr, …). The codes differ for the built-in checks: TypeScript reports a bound, One/Some/All and Exact as why: "check" with mark 4000 and the builder's name in check, while Go and Rust report the builder's own code and mark (why: "Min", mark 4011; why: "One", mark 4030). The corpus compares text only, so this is a documented divergence rather than a gated one.

Contributing changes#

Because TypeScript is canonical, a behaviour change starts there:

  1. Change ts/src/shape.ts and add/adjust a case in test/gen-compat.js.
  2. Regenerate the corpus: node test/gen-compat.js.
  3. Run make test—all three languages must pass the regenerated corpus.
  4. Run make diff—both ports must agree with TypeScript on every generated case.
  5. If a port diverges, fix the port to match.

A divergence caused by a TypeScript bug is fixed in TypeScript first: never "fix" it by changing a port to match, and never change TypeScript to match a port without deciding that the TypeScript behaviour is wrong.

The Rust port#

The Rust port (rs/) is held to the same two gates: every corpus row (none skipped; make test-rs sets SHAPE_RS_STRICT=1, which turns a skipped row into a failure) and every differential case. Two of the Go divergences listed earlier do not apply to it:

Its own divergences, each deliberate: