Imported from eshu-hq/eshu (
go/internal/query/decode/AGENTS.md). Install upstream withnpx skills add eshu-hq/eshu --skill decode. Copyright stays with the author.
Agent instructions: decode
Read doc.go and README.md first. This package is one type, one
constructor, the shared default schema version and a *string deref; nearly
any change here is a contract change.
Invariants
Error()andUnwrap()MUST stay exported under those names: theerrorinterface anderrors.Is/errors.Asfind them by name.DefaultSchemaMajorVersionandDerefStringhave one copy, here. Do not re-fork either into a handler family; the per-family copies they replaced existed only because those families could not import root.UnwrapMUST return the underlying*factschema.DecodeError. Callers reach itsErrUnsupportedSchemaMajorsentinel viaerrors.Is/errors.As; wrapping or dropping it compiles fine and stops the sentinel matching.NewMUST classify an unrecognised error asinput_invalidrather than returning nil. A nil here reads to the caller as a successful decode, which turns a malformed fact into a row of empty strings in a user-facing response.
Common changes
Adding a field: add it to Error, populate it in New, and check whether any
caller formats the error into an operator-facing log, since Error() is what
they see.
Failure modes
- A caller builds
Error{FactKind: ..., FactID: ...}from the exported fields and never sets the wrapped error.Error()andUnwrap()are nil-guarded for exactly this, because the type's fields are exported. A panic here would surface as a 500 on a read path whose job is to degrade one bad fact gracefully. Unwrapreturning a typed nil rather than an untyped one. A nil*factschema.DecodeErrorinside anerrorinterface is not nil, soerrors.Iskeeps walking and callers testingUnwrap() != nilget the wrong answer. The guard returns a bare nil.- A caller treats a returned
nilfrom a decode helper as success.Newnever returns nil; if it ever did, a malformed fact would read as a decoded one and reach the response as a row of empty strings.
Anti-patterns
- Do not unexport
Error()orUnwrap(); the type stops being anerrorand the wrapped sentinel stops matching. - Do not add decoding logic here. This package holds the failure shape. The
factschemaDecode* seam does the decoding and the query layer decides what to do with a drop. - Do not add telemetry here. Whether a decode drop is logged, and at what level, is the calling read path's decision.
Changes needing ADR review
None specific to this package. It carries no wire contract, no persisted shape,
and no runtime behaviour of its own. A change to the Classification values it
records, however, follows the fact-kind contract rules in
.agents/skills/eshu-contract-rigor, because those values travel with the
payload contract rather than belonging to this package.
Verification
From go/: go test ./internal/query/... -count=1. The decode paths that
exercise this live in the root package's factschema_decode_*_test.go files.