Your Linter Is a Query
mache parses your codebase into SQLite. A lint rule stops being a plugin you compile and becomes a SQL query you can read, diff, and ratchet.
Every linter is a stack of opinions compiled into a binary. Somebody decided what “too long” means, what counts as dead code, which patterns deserve a warning — and then froze those decisions into a tool you configure through whatever flags they remembered to expose. When you disagree, you write a plugin: learn the tool’s AST API, learn its visitor pattern, write a traversal in its extension language, keep it compiling as the tool evolves. The opinions are yours, but the medium fights you the whole way.
Last time I argued that code isn’t text and agents shouldn’t be handed raw bytes. Taking that seriously buys you something quieter too: once your codebase is structured data, a lint rule stops being a plugin. It’s a query.
The database was already there
mache build src/ out.db parses a source tree into a SQLite database of code structure: construct nodes, definitions, references, and — on backends that carry it — raw AST spans with byte and row positions. That database already powers the navigation story: callers, callees, blast radius.
But look at what it means that your code is now rows. “Which library files call log.Fatal?” isn’t a program anymore. It’s a SELECT with two JOINs. “Which test functions are longer than 120 lines?” is a WHERE clause on span arithmetic. Every structural question you’ve ever wanted to ask a codebase became expressible the moment the codebase became a table.
mache find-smells is the thin layer on top: it runs rules and reports each returned row as a finding — a file, a node, a span, a metric.
A rule you can read
A rule is one JSON file. Here’s the shape, trimmed from a real one that flags log.Fatal calls in library code:
{
"ID": "fatal_call_in_library",
"Description": "Calls to Fatal outside main.go and tests. log.Fatal in library code exits the process from a place the caller can't intercept — return an error instead. Heuristic caveat: a custom Fatalf method on your own logger type is a false positive.",
"Requires": ["node_refs", "nodes"],
"Severity": "warn",
"Tags": ["errors", "library-hygiene"],
"Query": "SELECT ... FROM node_refs r JOIN nodes n ON n.id = r.node_id WHERE r.token IN ('Fatal', 'Fatalf', 'Fatalln') ..."
}
Everything a linter usually hides is sitting in the open. The logic is SQL you can run by hand against the same database. The known false positives are documented inside the rule, where anyone deciding whether to trust a finding will actually see them. Severity is a field, not a compile-time constant. Thresholds are explicit and overridable per invocation. Custom rules drop into a directory and compose with the built-ins, and a rule that needs tables your backend doesn’t have skips itself instead of lying.
A code-review comment on a rule change is a code-review comment on a WHERE clause. That’s a different conversation from “the linter is complaining again.”
Debt that only goes down
Running rules against a real codebase surfaces the standard objection: hundreds of pre-existing findings, and nobody is going to stop shipping to fix them all. Most teams respond by turning the rule off, which is how linters die.
mache’s gate takes the other path: a ratchet. You bootstrap a baseline — a count of findings per rule, per file — and commit it. From then on, the gate fails only on findings above the baseline count, and the baseline is allowed to move in exactly one direction. Existing debt is tolerated; new debt is refused; every cleanup permanently lowers the ceiling.
If you’ve read the constraint post, you’ll recognize the shape — a machine-decidable predicate, not an opinion, and one that composes with every other gate in the pipeline. “The codebase must not get structurally worse” turns out to be enforceable, mechanically, one commit at a time. Findings export as SARIF, so the whole thing rides existing CI plumbing.
Write the question, not the plugin
The deeper shift is who can author a rule. A linter plugin asks you to learn a framework. A smell rule asks you to know SQL and describe what worries you — which also puts it squarely within reach of an agent. An agent that reads the rule’s description knows the rule’s blind spots; an agent that writes one produces an artifact a human can review in thirty seconds, because it’s a query, not a traversal.
Opinions argued in prose stay arguments. Opinions expressed as queries over shared structure become checkable, diffable, and — with a baseline — enforceable. That’s the same trade running through everything on the map: take the judgment out of the loop and leave the predicate.
mache is open source: github.com/agentic-research/mache. The starter rules — including the ones quoted here — live in examples/smell-rules/.