🤖 Done. Branch shared/golden-vectors, pushed — commits a13df93..71fe556.
go test ./... && go vet ./... && gofmt -l . all clean. 18 tests, and the suite has been watched failing (below). No canary URL on this one: there is no deployable surface until web/1.
What landed
vectors/contra-v1.json — the contract. Argon2id at m=64MiB, t=3, p=1; all five contra/v1/* HKDF labels; the KCV; P-256 seed derivation including the rejection branch; Crockford Base32 with the O→0 / I,L→1 mappings and a strict pad rule; the 20-bit checksum with a repair case; AES-256-GCM under the AAD binding; and account_chain, the whole of "The shape" end to end so a diverging implementation can bisect rather than stare at one wrong key.vectors/gen/ — the generator, and vectors/README.md carrying the rule.internal/crypto/ — the Go side, and vectors_test.go consuming the file.
On the trap
The generator is C against OpenSSL 3.5, not Go, so no code written this session contributed a byte to the file it is tested against. Before generating anything, gen.c recomputes the published PHC Argon2id reference outputs, RFC 5869 cases 1 and 3, RFC 4231 case 1 and a NIST CAVP AES-256-GCM vector, and aborts on any mismatch — otherwise "independent implementation" would only mean the two are not the same, not that either is right. The hand-verification #6 asked for is that check, run on every generation rather than once by me.
Every fixture input is SHA-256("contra/v1/vectors/" || label), so the file regenerates from the source alone and nothing in it is or resembles a real secret.
The rejecting seed
A P-256 rejection is a ~2^-32 event, so the branch is unreachable by any seed anyone picks. vectors/gen/seedsearch.c found one at k = 10962785172 after about 1.1×10^10 candidates. Independently confirmed two ways — the openssl kdf CLI and RFC 5869 written out longhand against Python hashlib, which agree — that candidate_0 opens ffffffff6646… and so exceeds n. The witness is pinned in the Makefile; make search finds a new one if the contra/v1/ec-p256 label ever moves.
Watched failing
A canary nobody has seen fail is a canary nobody should trust, so I ran nine mutations. All nine turn the suite red: lowering the Argon2 time cost, typoing an HKDF label, cutting the KCV to 8 bytes, dropping the AAD length prefixes, dropping its domain tag, skipping the rejected EC candidate, accepting non-zero Base32 pad bits, widening the checksum to 24 bits, and dropping the O/I/L mappings.
Two guards are worth naming: the Argon2 parameters and the label set are read out of the file, so the constants cannot drift from the protocol and a label that loses its vector fails instead of passing quietly.
One thing that needs your eye — discussion 19
DESIGN.md names primitives without pinning wire formats, and vectors cannot exist without them. I chose and recorded six: HKDF mode and salt handling, the P-256 rejection loop, Crockford packing, the printed form and checksum, the AAD layout, and the Go module path. All are cheap to reverse now and expensive after web/3.
One is a real reconciliation rather than a gap-fill, so flag it if you disagree: DESIGN.md writes the AAD binding twice, as (account_id, object_type, object_id, version) and as (vault_id, item_id, version). I read the first field as a scope id — the account for account-scoped objects, the vault for vault-scoped ones — which makes both spellings the same rule. Item ciphertexts are then bound to their vault, and the object-type strings are namespaced so the two scopes cannot collide.
Moving on to web/1 (#7) next, claiming there.