Conversation
Adds Envelope plus Marshal/Unmarshal helpers and five sentinel errors in internal/relay/envelope.go, with exhaustive table-driven tests covering the round-trip opacity invariant and every documented rejection case. Frames stay opaque to the relay: json.RawMessage preserves the inner bytes verbatim (modulo JSON whitespace), and Marshal validates only syntax via json.Valid. Unmarshal collapses absent/empty/null conn_id and absent/null frame into ErrMissingConnID / ErrMissingFrame. Issue: #1 Testing: go test -race ./..., go vet ./..., make build all clean. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ilmoniemi
left a comment
There was a problem hiding this comment.
Code Review: #1
Decision: PASS
Findings
- [NIT] internal/relay/envelope.go:59-60 — Doc says Unmarshal returns
ErrMalformedEnvelope"wrapping the underlying decoder error". The implementation at line 70 usesfmt.Errorf("%w: %v", ErrMalformedEnvelope, err)— the json decoder error is included in the message text only, not wrapped in the unwrap chain. Soerrors.Is(err, ErrMalformedEnvelope)works, buterrors.As(err, &jsonSyntaxErr)would not. Either drop "wrapping" from the doc, or use Go 1.20+ multi-%w(fmt.Errorf("%w: %w", ErrMalformedEnvelope, err)) to make the doc accurate. Non-blocking. - [NIT] internal/relay/envelope.go:75 —
bytes.Equal(env.Frame, []byte("null"))works;string(env.Frame) == "null"would let you drop thebytesimport. Either is fine.
Summary
Faithful, minimal implementation of the spec. Envelope + Marshal + Unmarshal + five sentinel errors, stdlib-only, pure functions, no concurrency or I/O. Doc comments on every exported symbol, each naming the opacity invariant.
Validation hits the right boundaries:
- Marshal rejects empty
connIDand invalid-JSONframe(covering nil/empty/garbage/truncated in onejson.Validcall). - Unmarshal collapses absent/empty/
nullconn_idintoErrMissingConnIDand absent/nullframeintoErrMissingFrame— the explicitbytes.Equal(env.Frame, []byte("null"))check is necessary becausejson.RawMessageretains the literal four bytes for"frame": nulland would otherwise pass the length check. Good catch. - Inner-frame contents are never parsed on the read path, preserving the opacity invariant.
Tests cover every row of both rejection tables plus the round-trip with nested JSON, unicode, and a null value. Assertions go through errors.Is, not string compare. t.Parallel() used on outer tests and subtests. The opacity assertion uses json.Compact on both sides, which correctly implements the "modulo whitespace" caveat without flaking on encoder formatting. make vet, make test (with -race), and make build all clean.
The defensive error-wrap at envelope.go:49-53 is dead in practice (the only non-static field is a pre-validated json.RawMessage), but the in-line comment names exactly that, so it reads as deliberate rather than cargo-culted. Fine.
Adds knowledge index, feature doc, ADR-0001, project memory, and lessons covering the routing-envelope wrapper landed in #1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
What
Adds the canonical Go vocabulary for the relay↔binary routing envelope:
Envelopestruct withConnID stringandFrame json.RawMessagematching the wire shape{conn_id, frame}.Marshal(connID string, frame []byte) ([]byte, error)— validates non-empty conn id, validates inner-frame JSON syntax viajson.Valid, emits the envelope with the inner bytes preserved verbatim (modulo whitespace).Unmarshal(data []byte) (Envelope, error)— rejects malformed JSON, absent/empty/nullconn_id, and absent/nullframe.ErrEmptyConnID,ErrInvalidFrameJSON,ErrMalformedEnvelope,ErrMissingConnID,ErrMissingFrame) so downstream code branches viaerrors.Isinstead of string matching.The opacity invariant is enforced by
json.RawMessage: the relay never deserialises the inner frame on either side.Issue
Closes #1
Testing
internal/relay/envelope_test.go:null, arrays); asserts bytewise equality afterjson.Compactto honour the "modulo whitespace" caveat.errors.Is.make vet,make test(with-race),make buildall clean.Architecture compliance
internal/relay/next todoc.goper the spec; no edits tocmd/pyrycode-relay/main.go(binary still builds unchanged).encoding/json,errors,bytes,fmt).conn_idand absent/nullframeinto single sentinels per the spec's algorithm.🤖 Generated with Claude Code