Skip to content

Latest commit

 

History

History
103 lines (77 loc) · 7.26 KB

File metadata and controls

103 lines (77 loc) · 7.26 KB

Project: gobspect

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.

PRD.md Execution

ALWAYS check off each box as the tasks are completed.

When asked to implement a PRD.md:

  1. Read the full PRD file before spawning any agents
  2. Implement features in the order they appear — dependencies flow top to bottom
  3. Spawn each feature as an isolated Agent invocation
  4. Do not spawn the next Agent until the current one completes with passing tests
  5. After all features complete, run go test ./... and report results

Key Design Decisions

  • 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 expose time.Time, big.Int, uuid.UUID, or similar through field types. OpaqueValue.Decoded is any and 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.go when 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 keeps go.mod minimal and protects decoders from the upstream type being removed, renamed, or having its wire format drift.
    • decode.go, valuedecode.go, and wire.go should 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.
  • Two-layer output: a structural Value AST that preserves all wire information, and a Format() function for human-readable rendering. Never discard wire information in the AST to make formatting easier.
  • Extensible opaque decoding. Users register DecoderFunc functions keyed by type name. Built-in decoders are pre-registered and can be overridden.

Architecture

Read docs/architecture.md for the full design. In brief:

  • doc.go — package-level godoc comment
  • decode.go — stream reader, message framing, type/value dispatch
  • valuedecode.go — value decoding: primitives, structs, maps, slices, arrays, opaques, interfaces
  • types.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 registration
  • builtins.go — decoders for std lib and common third-party opaque types
  • format.go — human-readable rendering of Value trees

Code Style

  • 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.Errorf with %w for wrapping. Exception: the query package's convenience functions (Get, All, Keys, AllSeq) panic on syntactically invalid path expressions, following the Go regexp.MustCompile convention. Use Parse + 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; use any. 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 slices and maps standard library packages over hand-rolled helpers. Use slices.Contains, slices.SortFunc, maps.Keys, etc. instead of writing equivalent loops or utility functions.

Testing Conventions

  • Use github.com/stretchr/testify for 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.go helper that encodes known values and writes .gob files. See docs/testing.md for the fixture generation strategy.
  • Test files live alongside the code they test. Fixture .gob files live in testdata/.
  • Golden file tests for formatter output: testdata/*.golden files compared with testify assertions.

Wire Format

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.

Opaque Types

Read docs/opaque-types.md. Key points:

  • TextMarshalerT blobs are always UTF-8 strings. One universal handler, no registry lookup.
  • GobEncoderT and BinaryMarshalerT require 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.

Common Tasks

Adding a new built-in opaque decoder

  1. Document the binary format in docs/opaque-types.md.
  2. Implement the decoder function in builtins.go.
  3. Register it in the init() or newDefaultRegistry() function in registry.go.
  4. Add a test fixture in testdata/generate.go that encodes a value using the real type.
  5. Add a decode test and a formatter golden test.

Implementing a new Value node type

  1. Add the struct to types.go with the gobValue() method.
  2. Handle it in decode.go's value dispatch.
  3. Handle it in format.go's rendering switch.
  4. Add tests for both decoding and formatting.

Things to Watch Out For

  • The debug.go in the Go standard library's encoding/gob package 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/binary varints. 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.
  • []byte is 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 GobEncoder type is encoded directly (not through an interface), CommonType.Name is 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.