A decode-only introspection library for Go's encoding/gob wire format. It reads arbitrary gob streams without requiring the original Go types and produces a structured AST and human-readable output.
ALWAYS check off each box as the tasks are completed.
When asked to implement a PRD.md:
- Read the full PRD file before spawning any agents
- Implement features in the order they appear — dependencies flow top to bottom
- Spawn each feature as an isolated Agent invocation
- Do not spawn the next Agent until the current one completes with passing tests
- After all features complete, run
go test ./...and report results
- Decode only. No encoding support. Do not add encoding functionality.
- Opaque type decoders don't leak inspected types through the AST. The Value AST (
types.go) must never exposetime.Time,big.Int,uuid.UUID, or similar through field types.OpaqueValue.Decodedisanyand holds formatted strings or primitive Go values only — never stdlib or third-party wrapper types. This lets consumers read the AST without depending on the original Go types.- Stdlib imports are allowed in
builtins.gowhen they aid correctness (e.g.,big.Float.GobDecode) or simplify formatting (e.g.,time.FixedZone+time.Format). The stdlib is always available to consumers, so using it internally adds no dependency burden. - Third-party imports of inspected types are forbidden. Never import
github.com/google/uuid,github.com/shopspring/decimal, or similar — reimplement their wire formats. This keepsgo.modminimal and protects decoders from the upstream type being removed, renamed, or having its wire format drift. decode.go,valuedecode.go, andwire.goshould remain free of inspected-type imports even from stdlib — those files decode the wire format itself, not opaque blobs, and have no reason to touch inspected types.- Presentation layers (
format.go,json.go) and test code may import inspected types when needed.
- Stdlib imports are allowed in
- Two-layer output: a structural
ValueAST that preserves all wire information, and aFormat()function for human-readable rendering. Never discard wire information in the AST to make formatting easier. - Extensible opaque decoding. Users register
DecoderFuncfunctions keyed by type name. Built-in decoders are pre-registered and can be overridden.
Read docs/architecture.md for the full design. In brief:
doc.go— package-level godoc commentdecode.go— stream reader, message framing, type/value dispatchvaluedecode.go— value decoding: primitives, structs, maps, slices, arrays, opaques, interfacestypes.go— Value AST node types (StructValue, MapValue, OpaqueValue, etc.)wire.go— wire format primitives (varint, type ID, wireType struct decoding)registry.go— DecoderFunc registry, RegisterDecoder, built-in registrationbuiltins.go— decoders for std lib and common third-party opaque typesformat.go— human-readable rendering of Value trees
- Standard Go conventions. Run
gofmt,go vet,staticcheck. - Error messages start lowercase, no trailing punctuation, and include context:
"decoding struct field %q: %w". - No panics in library code. All errors returned. Use
fmt.Errorfwith%wfor wrapping. Exception: thequerypackage's convenience functions (Get,All,Keys,AllSeq) panic on syntactically invalid path expressions, following the Goregexp.MustCompileconvention. UseParse+ path-typed functions (GetPath,AllPath, etc.) when you need error-based handling. - Comments on exported types and functions follow godoc conventions.
- Assume Go 1.26 or later: Avoid
interface{}in new code; useany. Use generics when appropriate. When helpful, use new with the new feature that allows its operand to be an expression, e.g.new(yearsSince(born)). - Prefer the
slicesandmapsstandard library packages over hand-rolled helpers. Useslices.Contains,slices.SortFunc,maps.Keys, etc. instead of writing equivalent loops or utility functions.
- Use
github.com/stretchr/testifyfor assertions. - Table-driven tests with
t.Run()subtests. - Separate success and failure test functions when it reduces complexity.
- Gob test fixtures are generated by a
testdata/generate.gohelper that encodes known values and writes.gobfiles. Seedocs/testing.mdfor the fixture generation strategy. - Test files live alongside the code they test. Fixture
.gobfiles live intestdata/. - Golden file tests for formatter output:
testdata/*.goldenfiles compared withtestifyassertions.
Read docs/wire-format.md for the full reference. Key points:
- Negative type IDs = type definitions. Positive = values.
- Type IDs are session-scoped. Build a fresh registry per stream.
- Bootstrap types (wireType, structType, etc.) have hardcoded IDs 16-23.
- Structs use sparse field encoding with deltas. Zero-valued fields are omitted.
- Non-struct top-level values are wrapped in a synthetic single-field struct.
Read docs/opaque-types.md. Key points:
TextMarshalerTblobs are always UTF-8 strings. One universal handler, no registry lookup.GobEncoderTandBinaryMarshalerTrequire per-type decoders.- Built-in decoders:
time.Time,math/big.Int,math/big.Float,math/big.Rat,uuid.UUID,shopspring/decimal.Decimal. - Unknown opaque types: store raw bytes in
OpaqueValue, format as hex with type name prefix.
- Document the binary format in
docs/opaque-types.md. - Implement the decoder function in
builtins.go. - Register it in the
init()ornewDefaultRegistry()function inregistry.go. - Add a test fixture in
testdata/generate.gothat encodes a value using the real type. - Add a decode test and a formatter golden test.
- Add the struct to
types.gowith thegobValue()method. - Handle it in
decode.go's value dispatch. - Handle it in
format.go's rendering switch. - Add tests for both decoding and formatting.
- The
debug.goin the Go standard library'sencoding/gobpackage is a useful reference implementation of a standalone gob reader. Study it but do not copy from it — write clean-room implementations. - Gob's varint encoding is NOT the same as
encoding/binaryvarints. Gob uses its own scheme (see wire-format.md). - Struct field deltas are 1-based and represent the gap from the previous field index, not absolute positions.
[]byteis a special case: it's a builtin type (ID 5), NOT a slice of uint8 with a sliceType definition.- Interface values contain an embedded type sequence — the decoder must handle recursive type definitions mid-value.
- Go 1.26+ removed TextMarshaler support from gob. Streams from older Go versions may still use
TextMarshalerT, but new streams encode those types as plain structs. - When a
GobEncodertype is encoded directly (not through an interface),CommonType.Nameis empty. Opaque decoder registry lookups by name only match interface-wrapped values. - The per-message size limit is 64 MiB (1<<26). The struct field count limit is 65536. Slice/map/array element count limit is 1<<30.