CARLOS rastrillo docs

๐Ÿค– vectors

amadan.net/rastrillo/rastrillo/vectors

The golden vectors that pin your app's JS derivation engine to its Go one.

Any derivation over sealed content runs client-side, but the sidecar, operator tools and tests want the same derivation in Go. So the engine exists twice, and two engines drifting apart is the most dangerous E2EE bug class there is: a wrong answer with nothing looking broken.

Treat this as a treaty file between the two engines. The key names in a vector's fields are part of the contract โ€” the JS suite consumes them by name, changing one means changing both sides in the same commit, and nothing mechanical checks the key sets agree. Time values round-trip through RFC 3339 to new Date(v.now), so put times in as time.Time and never as pre-formatted strings.

A Set is built by your own cmd/genvectors (scaffolded by rastrillo vectors -init), written to test/vectors.json by rastrillo vectors, and consumed by your test/parity.test.mjs through the vendored helper this package embeds.

Building a Set

type Set struct { /* ordered cases */ }
func New() *Set
func (s *Set) Add(name, why string, fields map[string]any)
func (s *Set) WriteTo(w io.Writer) (int64, error)

Start with New() for an empty Set, then Add your vectors in order. They stay ordered so the file reads in the order the rules were written, and so two runs over the same cases write identical bytes. name identifies the vector and why names the rule it pins; the JS test titles come out as "name โ€” why".

name and why are reserved for the envelope, and WriteTo refuses them as field keys.

WriteTo emits the set as a JSON array with two-space indent and a trailing newline, which is test/vectors.json's exact contract.

Normalisation reaches top-level field values only. A nil slice or nil map sitting directly in fields marshals as [] or {} and never null, because "no bests" has to look the same on both sides. It does not walk inside app-typed values: json tags, omitempty and custom marshalers would make a generic deep walk a lie, so nil-versus-empty in there stays your own discipline.

The JS helper

func JS() []byte

The vendored ES module helper โ€” loadVectors and canonical โ€” that scaffolded apps copy to test/vectors.mjs and own from then on, on the same terms as tokens.css and the shim.

loadVectors(path) reads and parses a vectors.json written by rastrillo vectors. Resolve the path from your suite's own URL with fileURLToPath(new URL("./vectors.json", import.meta.url)), so the suite does not care which directory node started in.

canonical(value) is the comparison rule. It sorts object keys recursively, drops undefined and null members, and drops scalar zeros โ€” 0, false, "" โ€” to match Go's omitempty.

That last part has a blind spot worth knowing. Without the zero-strip, Go dropping a zero field while JS computes it would fail the diff on encoder behaviour instead of arithmetic. With it, an explicit zero on one side and a missing field on the other compare equal. The parity template scaffolds a belt section covering that hole: put explicit-value assertions beside the loop and they catch what the rule misses.

Empty arrays stay [] on both sides, because rastrillo vectors supplies them from Go by normalising top-level nils.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.