lintuition

module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: MIT

README

lintuition

CI Go Reference Release

System One linters for your Go code.

lintuition finds places where code says one thing and does another: log messages, test names, comments or metric Help that contradict the surrounding code.

It runs built-in linters and lets you add your own. You choose a classifier to interpret the wording; each linter checks its answers against facts extracted by Go analysis.

Our quickstart uses Jev. TypeSafe says it "returns typed decisions with calibrated probabilities" and lists a price of $42 per billion input tokens. In our showcase measurement, Jev caught all 18 marked findings in each of three runs at an estimated $0.0006 a run.

LLMs work too: an OpenAI-compatible API with logprobs support (including local models through Ollama), or Claude Code and Codex. See classifiers.

This function logs success before it knows whether the save worked:

log.Print("config saved to disk")
if err := store.SaveConfig(cfg); err != nil {
	return err
}
logs.go:15:2: success logged before SaveConfig has returned: "config saved to disk" (premature-success)

The compiler is fine with it, and so is golangci-lint.

A unit that does not match the value:

return fmt.Sprintf("compaction took %.0f ms", d.Seconds())
// text says milliseconds, the value is in seconds

A test whose name and assertion disagree:

func TestUnquoteRejectsUnbalancedQuote(t *testing.T) {
	_, err := Unquote(`"abc`)
	if err != nil {
		t.Fatal(err)
	}
}
// test name expects an error from Unquote, but the test fails when Unquote returns one

More examples below, and the full list of linters.

What it asks, sends and costs

  • Go code decides, not the model. Typed analysis finds the place and the facts: this log call runs before SaveConfig, this want is compared with the result of Expired. The model answers one or two fixed questions about one piece of text ("does this message claim that SaveConfig has already completed?"), and Go code compares the answer with the facts.
  • You can see what leaves your machine. The built-in linters send selected text (a message, a comment, a test name) and facts named by identifiers, not source files and not the values your code logs. lintuition run --dry-run --preview requests.jsonl ./... writes every request without sending any. With a local model and semantic.local-only: true, nothing leaves.
  • Unsure means quiet. Below a linter's confidence threshold it stays silent whichever way the answer leans. Code it cannot read, like a message built at run time, is counted as unsupported in the summary instead of being guessed at.
  • Measured cost, with a budget. On an earlier six-case suite, both Jev and Claude Code haiku caught all six defects. Jev took about a second at an estimated $0.0003 a run; Claude Code haiku took 66 seconds and reported about $0.13. semantic.budget.max-cost-usd stops new requests once reported spending reaches the limit; requests already in flight can take the total above it, and everything found so far is still reported. Valid cached answers are reused on the next run.

Findings are review hints with their evidence, not proofs.

Try it

go install github.com/ssgreg/lintuition/cmd/lintuition@latest

Or download a binary for Linux, macOS or Windows from the releases page.

See the output without a model, on the showcase with scripted answers:

git clone https://github.com/ssgreg/lintuition
cd lintuition/examples/showcase
lintuition run ./...

On your own code with Jev, from your project's root:

curl -fsSLo .lintuition.yml https://raw.githubusercontent.com/ssgreg/lintuition/main/.lintuition.quickstart.yml
export TYPESAFE_API_KEY=...
lintuition run --dry-run --preview requests.jsonl ./...   # what would be sent; nothing is
lintuition run ./...

The quickstart config exits 0 even when it finds something, so a first run in CI does not fail the build; remove run.issues-exit-code: 0 to make findings fail it.

No Jev key? Set up another classifier from classifiers.

Status: v0.x. Until v1.0, a minor release may change the config, the command line and the sdk API; CHANGELOG.md says what changed.

More examples

A table row copied without flipping want:

{name: "expired when the deadline has passed", passed: true, want: true},
{name: "not expired before the deadline", passed: false, want: true},
// case "not expired before the deadline" reads as want false, but the table sets true

A routine event logged as an error, and a real loss logged as info:

slog.Error("cache miss, loading from the database", "key", key)
// routine event logged at error level

slog.Info("events for the last hour are lost, the write to disk was refused")
// unintended loss logged at info level

A comment that slid onto the wrong constant:

const (
	// waiting for a free worker
	PhaseQueued Phase = iota
	// every block has been copied and verified
	PhaseCopying
	PhaseVerified
)
// comment describes PhaseVerified, not PhaseCopying

Every example on this page is real code in examples/showcase, together with one for each of the other linters.

Linters

How each one reads your code, what it sends and how it decides, with an example: docs/linters.md.

linter catches
logs premature-success a log line reports success before the call that can still fail
log-sensitive-field a structured log field that logs a secret (token, password, key material) as is
log-key-value-role a log key that names a different quantity than the variable logged under it
normal-event-at-error an expected routine event (cache miss, retry scheduled) logged at error level
severe-event-understated an unintended loss of data or work, or an outage, logged at debug or info
destructive-remediation a message that advises deleting, wiping or reinstalling without saying what is lost
errors sentinel-name-vs-text a sentinel error whose name and message describe different conditions
error-needs-type policy, off by default: a branchable condition (not found, already exists) returned as a plain string error
units, comments human-unit-contradiction a printf message names a different unit than the duration it prints
enum-comment-shift a comment in a const block that describes a neighbouring constant
doc-vs-signature a doc that promises a result or an error the function's signature does not have
read-only-promise a doc that promises no change while the body writes the receiver, a parameter or a package variable
tests test-name-vs-assertion a test named for a failure that asserts success, or the reverse
table-case-vs-expectation a table case whose name says the opposite of its boolean want
doc-vs-table a table case whose want contradicts the tested function's doc
metrics metric-type-vs-help Prometheus Help that describes a different kind of value than the metric records
nolint suppression-rationale a //nolint reason that explains something other than what the linter reports

Works like golangci-lint

lintuition runs next to golangci-lint, not instead of it. Its command line, config and output follow golangci-lint v2 where it supports the same thing.

lintuition run ./...                          # file:line:col: message (linter)
lintuition run --output.sarif.path out.sarif ./...
lintuition linters                            # what the config enables
lintuition config verify                      # unknown keys, linters and settings are errors
  • Config: .lintuition.yml, found from the working directory upwards, in the golangci-lint v2 shape plus a semantic section for the classifier. Every key with its default is in .lintuition.reference.yml.
  • Formats: text, json, sarif, checkstyle, code-climate, junit-xml and github-actions.
  • Suppressing: //nolint:premature-success // why it is fine here, with the same scopes as golangci-lint. The directive must name the linter and give a reason; a bare //nolint does not suppress.
  • Exit codes: findings exit 1 by default, or what run.issues-exit-code says; a clean run exits 0. A run that is incomplete (a package did not load, a request failed, a budget was reached) exits 2, whatever the findings.

This repository lints itself on every push to main: the workflow sends findings to GitHub code scanning and keeps the answer cache between runs.

What leaves the machine

semantic.payload decides what a request may carry:

policy sends
facts facts computed by code: identifiers, types, kinds, counts
prose (default) facts plus the text a person wrote: comments, metric Help, log messages, test names
source also Go source text; no built-in linter asks for it

A candidate that needs more than the policy allows is skipped whole and counted, never sent stripped. semantic.local-only: true refuses any backend that sends requests off the machine.

More

  • How the linters work, one section per linter
  • Classifiers: Jev, a local or remote LLM, Claude Code, Codex, budgets, and how they compare
  • Plugins: your own linters and classifiers, built into a custom binary

License

MIT

Directories

Path Synopsis
Package builtin registers lintuition's built-in linters and classifiers.
Package builtin registers lintuition's built-in linters and classifiers.
classifiers
agentcli
Package agentcli makes coding-agent command lines classifiers: Claude Code (`claude -p`) and the Codex CLI (`codex exec`).
Package agentcli makes coding-agent command lines classifiers: Claude Code (`claude -p`) and the Codex CLI (`codex exec`).
fake
Package fake is a deterministic, local classifier that answers from scripted rules.
Package fake is a deterministic, local classifier that answers from scripted rules.
jev
Package jev is a classifier backend for TypeSafe System One models (Jev), https://typesafe.ai.
Package jev is a classifier backend for TypeSafe System One models (Jev), https://typesafe.ai.
openai
Package openai is a classifier backend for OpenAI-compatible chat completion APIs that return token log probabilities: OpenAI itself, and local servers such as Ollama, llama.cpp, vLLM and LM Studio.
Package openai is a classifier backend for OpenAI-compatible chat completion APIs that return token log probabilities: OpenAI itself, and local servers such as Ollama, llama.cpp, vLLM and LM Studio.
Package cli is the lintuition command line.
Package cli is the lintuition command line.
cmd
lintuition command
Command lintuition is the lintuition CLI.
Command lintuition is the lintuition CLI.
internal
classify
Package classify turns candidates into classifier requests under the payload policy and checks what comes back.
Package classify turns candidates into classifier requests under the payload policy and checks what comes back.
config
Package config decodes and validates the lintuition configuration.
Package config decodes and validates the lintuition configuration.
custom
Package custom builds a lintuition binary with plugin linters and classifiers compiled in, the way golangci-lint's module plugin system does: a manifest lists Go modules, a generated main imports them next to the built-ins, and `go build` makes the binary.
Package custom builds a lintuition binary with plugin linters and classifiers compiled in, the way golangci-lint's module plugin system does: a manifest lists Go modules, a generated main imports them next to the built-ins, and `go build` makes the binary.
effects
Package effects reads what a function body does to state its caller can see.
Package effects reads what a function body does to state its caller can see.
engine
Package engine runs a lint: load, extract, ask, decide, report.
Package engine runs a lint: load, extract, ask, decide, report.
facts
Package facts holds typed extraction helpers shared by the built-in linters.
Package facts holds typed extraction helpers shared by the built-in linters.
httpx
Package httpx is the HTTP transport shared by the remote classifier backends: https only (plain http only to loopback), redirects never followed, retries on 429, 5xx and transport errors with Retry-After honoured, every retry cleared with the run's budget, a per-minute rate limit, and errors that never carry a request or response body.
Package httpx is the HTTP transport shared by the remote classifier backends: https only (plain http only to loopback), redirects never followed, retries on 429, 5xx and transport errors with Retry-After honoured, every retry cleared with the run's budget, a per-minute rate limit, and errors that never carry a request or response body.
load
Package load loads Go packages and runs the linters' analyzers over them.
Package load loads Go packages and runs the linters' analyzers over them.
report
Package report holds the canonical issue model, the issue processors (exclusions, severity, limits, sorting) and the output writers.
Package report holds the canonical issue model, the issue processors (exclusions, severity, limits, sorting) and the output writers.
tables
Package tables reads the rows of table-driven tests and binds each boolean expectation field to what the loop over the table compares it with.
Package tables reads the rows of table-driven tests and binds each boolean expectation field to what the loop over the table compares it with.
twins
Package twins runs lintuition over defect / fixed twins marked with `// want` comments and compares the findings with the marks.
Package twins runs lintuition over defect / fixed twins marked with `// want` comments and compares the findings with the marks.
linters
destructiveadvice
Package destructiveadvice finds an error or log message that tells its reader to delete, wipe, reset or reinstall something that holds data, without saying what is lost or how to keep it:
Package destructiveadvice finds an error or log message that tells its reader to delete, wipe, reset or reinstall something that holds data, without saying what is lost or how to keep it:
docsignature
Package docsignature finds a doc comment that promises a result the function's signature does not have, as when a function loses its results and its doc is not updated:
Package docsignature finds a doc comment that promises a result the function's signature does not have, as when a function loses its results and its doc is not updated:
docvstable
Package docvstable checks a table test's boolean expectation against the doc of the function it tests: a row whose situation, by the doc's own condition, gives the opposite of its want.
Package docvstable checks a table test's boolean expectation against the doc of the function it tests: a row whose situation, by the doc's own condition, gives the opposite of its want.
enumcomment
Package enumcomment finds a comment in a block of constants that describes a neighbour rather than the constant it is attached to, as when a constant is inserted or removed and the comments shift by one:
Package enumcomment finds a comment in a block of constants that describes a neighbour rather than the constant it is attached to, as when a constant is inserted or removed and the comments shift by one:
errorneedstype
Package errorneedstype is a policy linter: it finds a condition a caller would want to branch on, returned as a plain string error that the caller can only match by its text.
Package errorneedstype is a policy linter: it finds a condition a caller would want to branch on, returned as a plain string error that the caller can only match by its text.
humanunit
Package humanunit finds a printf-style message whose wording names one unit for a number while the code passes the number in another:
Package humanunit finds a printf-style message whose wording names one unit for a number while the code passes the number in another:
logkeyrole
Package logkeyrole finds a structured log field whose key names a different quantity or role than the variable logged under it, as when a line is copied and the value is changed but not the key:
Package logkeyrole finds a structured log field whose key names a different quantity or role than the variable logged under it, as when a line is copied and the value is changed but not the key:
logsensitive
Package logsensitive finds a log call whose structured fields log a secret as is:
Package logsensitive finds a log call whose structured fields log a secret as is:
metrictypevshelp
Package metrictypevshelp checks that a Prometheus metric's Help describes the kind of value its type records: a counter's Help a running total, a gauge's a current value, a histogram's or summary's a distribution.
Package metrictypevshelp checks that a Prometheus metric's Help describes the kind of value its type records: a counter's Help a running total, a gauge's a current value, a histogram's or summary's a distribution.
normalaterror
Package normalaterror finds an expected, routine event logged at error level, where it pages someone or hides the real errors among the noise:
Package normalaterror finds an expected, routine event logged at error level, where it pages someone or hides the real errors among the noise:
prematuresuccess
Package prematuresuccess finds a log line that reports an operation as done before the call that can still fail:
Package prematuresuccess finds a log line that reports an operation as done before the call that can still fail:
readonlypromise
Package readonlypromise finds a doc comment that promises a function changes nothing while its body writes state the caller can see:
Package readonlypromise finds a doc comment that promises a function changes nothing while its body writes state the caller can see:
sentinelname
Package sentinelname finds a sentinel error whose name and message describe different failure conditions, as when a declaration is copied and only one half is edited:
Package sentinelname finds a sentinel error whose name and message describe different failure conditions, as when a declaration is copied and only one half is edited:
severeunderstated
Package severeunderstated finds an unintended loss logged at debug or info level, where nobody looks:
Package severeunderstated finds an unintended loss logged at debug or info level, where nobody looks:
suppressionreason
Package suppressionreason checks the reason given in a nolint directive against what the suppressed linter reports:
Package suppressionreason checks the reason given in a nolint directive against what the suppressed linter reports:
tablecase
Package tablecase checks the rows of table-driven tests: a case whose name says one boolean outcome while its expectation field says the other, as when a row is copied and the want is not flipped.
Package tablecase checks the rows of table-driven tests: a case whose name says one boolean outcome while its expectation field says the other, as when a row is copied and the want is not flipped.
testnameassert
Package testnameassert finds a test whose name says one outcome of the call under test while its assertion on that call's error checks the other:
Package testnameassert finds a test whose name says one outcome of the call under test while its assertion on that call's error checks the other:
Package linttest runs lintuition over a directory of defect / fixed twins and checks the findings against `// want "regexp"` (or backquoted) comments, like analysistest does for diagnostics.
Package linttest runs lintuition over a directory of defect / fixed twins and checks the findings against `// want "regexp"` (or backquoted) comments, like analysistest does for diagnostics.
Package sdk is the small public contract between lintuition, its linters and its classifiers.
Package sdk is the small public contract between lintuition, its linters and its classifiers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL