How Geppetto runs linting, how to add custom go/analysis analyzers, and how turnsdatalint works
Linting is how we encode “team rules” that the Go compiler cannot enforce by itself. In Geppetto we combine standard linters (via golangci-lint) with project-specific checks implemented as go vet plugins using the go/analysis framework. This keeps checks fast, type-aware, and easy to run in CI and locally.
make lintmake lint is the blessed command for contributors. It is expected to:
golangci-lint for standard Go lintingcmd/geppetto-lint) via go vet -vettool=... ./...For custom analyzers only, use:
make linttool: builds and runs the bundled vettool (multichecker)make turnsdatalint: builds and runs the single analyzer tool (singlechecker)Geppetto packages analyzers under pkg/analysis/<name> (not internal/) so third-party projects can import them. We then provide two entrypoints:
cmd/<name> using singlechecker.Main(<name>.Analyzer)cmd/geppetto-lint using multichecker.Main(...)This allows downstream repos to either run Geppetto’s bundled tool directly, or embed Geppetto analyzers into their own multichecker alongside their own checks.
To add a new analyzer that scales with the bundling approach:
pkg/analysis/<name>/analyzer.go exporting var Analyzer *analysis.Analyzeranalysistest under pkg/analysis/<name>/testdata/src/...cmd/geppetto-lint/main.gocmd/<name>/main.go as a singlechecker wrapperpkg/doc/topics/*/testdata/reference/ so go vet ./... doesn’t pick them up during normal lint runsturnsdatalint is a fast go/analysis-based linter that enforces two conventions:
turns.PayloadKeyText, etc; no raw literals and no variables).turns.DataK, turns.TurnMetaK, turns.BlockMetaK) must only be called in key-definition files so the rest of the codebase reuses canonical key variables.In current Geppetto, Turn.Data, Turn.Metadata, and Block.Metadata are wrapper stores (not map[...]... fields), so direct indexing is not possible; typed-key access is done via key.Get/key.Set. The linter still applies typed-key index rules to any remaining map fields (notably Run.Metadata).
turnsdatalint enforces:
map[turns.RunMetadataKey]any): key expression must have type turns.RunMetadataKey (no raw string literals; no untyped string const identifiers)map[string]any): key must be a const string (no raw literals; no variables)turns.DataK/TurnMetaK/BlockMetaK calls are only allowed in generated canonical key files (geppetto/pkg/turns/keys_gen.go, geppetto/pkg/inference/engine/turnkeys_gen.go), app-level *_keys.go, and tests.Allowed examples (high-level):
b.Payload[turns.PayloadKeyText]run.Metadata[turns.RunMetaKeyTraceID]turns.KeyTurnMetaProvider.Get(t.Metadata) (wrapper store access via typed key)engine.KeyToolConfig.Set(&t.Data, engine.ToolConfig{Enabled: true}) (wrapper store access via typed key)Flagged examples (high-level):
b.Payload["text"] (raw string literal)k := turns.PayloadKeyText; b.Payload[k] (variable key)turns.DataK[string]("app", "some_key", 1) in a non-*_keys.go fileNote: b.Payload["text"] can compile because payload keys are strings; the linter exists specifically to force canonical const keys for stability and searchability.
turnsdatalint is implemented with golang.org/x/tools/go/analysis. It:
IndexExpr nodes that look like <something>.Payload[<key>] and enforces const string keys.turns.DataK/TurnMetaK/BlockMetaK and enforces they only appear in key-definition files.Run.Metadata in Geppetto).Pseudocode:
for each IndexExpr idx:
if idx.X is not SelectorExpr ".Data": continue
if selected field is not map[TurnDataKey]...: continue
if idx.Index has type TurnDataKey AND is not a raw literal and not an untyped-string const ident: ok
else: report diagnostic at '['
turnsdatalint supports flags for the named key types (fully-qualified):
-turnsdatalint.data-keytype-turnsdatalint.turn-metadata-keytype-turnsdatalint.block-metadata-keytype-turnsdatalint.run-metadata-keytypegithub.com/go-go-golems/geppetto/pkg/turns.<...> named typesThis is mainly useful if you reuse the analyzer logic for a different struct/map key type in another project.
If another repo uses Geppetto (for example it imports github.com/go-go-golems/geppetto/pkg/turns), it can reuse the analyzer in two standard ways:
This is the lowest-friction approach when you’re happy to run the analyzers that Geppetto bundles:
go build -o /tmp/geppetto-lint github.com/go-go-golems/geppetto/cmd/geppetto-lint@<version>
go vet -vettool=/tmp/geppetto-lint ./...
If you want to bundle Geppetto’s analyzers plus your own analyzers, create your own lint binary:
multichecker.Main(turnsdatalint.Analyzer, yourlint.Analyzer, ...)github.com/go-go-golems/geppetto/pkg/analysis/turnsdatalint