Deterministic, AST-based architectural conformance checking
Project description
lanekeep
Deterministic, AST-based architectural conformance checking for AI-generated and human-written code.
Status: early development. Nothing is released yet and the CLI described below is not usable. The architecture is settled — see
docs/architecture.md— and the work is tracked as a sequence of milestones. Do not depend on this yet.
What it is
lanekeep is not a linter in the ESLint sense. ESLint enforces language-level correctness. lanekeep enforces project-specific conventions — the ones a language model has no way to infer from the code it is shown, because they live in your team's heads and your reviewers' comments.
Every rule is a codified answer to "the agent keeps doing this wrong."
Rules are TypeScript programs, written in the same language as the code they inspect:
import { defineRule } from 'lanekeep'
export default defineRule({
id: 'local/no-numeric-sizes',
severity: 'error',
card: {
message: 'Literal numeric size inside makeStyles',
remediation: 'Use theme.spacing.*, theme.borderRadius.* or theme.borders.*',
examples: { bad: 'padding: 12', good: 'padding: theme.spacing.md' },
},
// Matched in Rust, at native speed. Your code runs only on matches.
query: `
(pair
key: (property_identifier) @prop
value: [(number) (unary_expression operand: (number))] @value) @match
`,
check(ctx, m) {
if (!/^(padding|margin|gap|borderRadius)/.test(ctx.text(m.prop))) return
if (Number(ctx.text(m.value)) === 0) return
const call = ctx.closestAncestor(m.match, '(call_expression function: (identifier) @f)')
if (!call) return
if (!ctx.resolvesToImport(call.f, { module: '@rneui/themed', name: 'makeStyles' })) return
ctx.report(m.match)
},
})
check is ordinary TypeScript. Loop, accumulate state, build data structures, read other files,
import shared helpers — there is no expressiveness ceiling and no DSL to learn beyond the query
that gates it.
Why it exists
An agent that writes code against your codebase will violate your conventions confidently and repeatedly, because those conventions are invisible in the code it was shown. Telling it again in the next prompt does not scale. Encoding the convention as a rule does.
That makes the design constraints unusual for a static analyzer:
- It runs in the inner loop. Agents and developers invoke it after every edit, so a cold run on a couple of thousand files has a sub-second budget and a warm run has a sub-25ms one.
- Its output is read by a machine. Violations are sorted deterministically, because an agent that reads the output twice must not see reordering as change.
- Every rule carries its own fix.
message,remediationandexamplesare mandatory fields, not documentation — they are the rule card that gets fed back to the agent.
How it stays fast with programmable rules
The usual problem with a native tool that runs JavaScript plugins is the boundary between them: dispatching into JS once per AST node means tens of thousands of crossings per file.
lanekeep dispatches once per query match instead. The tree-sitter query runs in Rust across a single shared parse; only matches reach your handler. That is typically two to three orders of magnitude fewer crossings, and it is the reason a Rust engine still earns its place once rules are TypeScript.
discover paths (globs, gitignore-aware)
└─> for each file, in parallel:
cache key ──hit──> validate tracked deps ──> cached violations + facts
└─miss─> path and raw-text gates reject before any parse
└─> parse ─> match queries in Rust
└─> invoke the TypeScript handler, per match only
└─> reduce phase: cross-file rules consume facts only, never parse trees
└─> filter suppressions ─> sort ─> report
A warm run with no changes executes no JavaScript at all — every file is a cache hit.
Installation
Pick whichever matches the project you are adding it to:
npm install --save-dev lanekeep # Node
pip install lanekeep # Python
brew install fmsouza/tap/lanekeep # macOS and Linux, system-wide
cargo install lanekeep-cli # from source
For a Go project, pin it in go.mod alongside your other tools:
go get -tool github.com/fmsouza/lanekeep/cmd/lanekeep
then go tool lanekeep check ./.... Go can only install and pin things written in Go, so
that package is a small launcher which fetches the real binary on first use, verifies it
against the release's published checksums, and caches it. Set LANEKEEP_BINARY to an
already-installed lanekeep and it fetches nothing.
Or download a binary from the releases page.
A single static binary with the JavaScript engine compiled in. No runtime is required to run lanekeep, even though rules are written in TypeScript — Node, Python or Go is needed only to install it from that ecosystem, where it picks which binary to fetch. Nothing is pulled in as a dependency any of those ways.
Prebuilt for macOS on Apple silicon, Linux on x86-64 and arm64, and Windows on x86-64. The
Linux binaries are built against glibc 2.17, so they run on anything from RHEL 7 onwards.
Intel macOS is not prebuilt — cargo install lanekeep-cli builds it from source, and both the
npm launcher and the Homebrew formula say so rather than failing obscurely.
See docs/releasing.md for how a release is cut.
What it looks like
$ lanekeep check
src/also.ts:2:1 error [lanekeep/no-default-export] default export
→ use a named export, so the symbol has one name every importer must use
src/bad.ts:2:1 error [lanekeep/no-default-export] default export
→ use a named export, so the symbol has one name every importer must use
✖ 2 error(s) across 2 file(s) checked
Rules may offer a fix, applied with --fix:
$ lanekeep check --fix
fixed 2 violation(s) in 2 file(s)
Only fixes a rule marked as behavior-preserving are applied. Anything else is a suggestion — shown, never written — because the cautious mistake costs a manual edit and the other one rewrites your code silently.
Suppressions carry a mandatory reason and an optional expiry, and a directive that does not work says so — a missing reason, a bare rule id, or an unreadable date is reported rather than silently doing nothing:
// lanekeep-ignore-next-line lanekeep/no-default-export reason: legacy entry point
export default parse
$ lanekeep check --report-unused-suppressions
To start from nothing:
$ lanekeep init # a config plus a first rule, both runnable
To find out where a run spent its time — the split says whether the query or the code is the problem:
$ lanekeep check --profile
To find out what a rule wants without opening its source:
$ lanekeep explain lanekeep/no-default-export
$ lanekeep rules --json
For fast feedback on what you touched:
$ lanekeep check --staged # what is about to be committed
$ lanekeep check --since main # what changed against a ref
Both are intersected with the config's include/exclude, and both skip cross-file rules —
a whole-corpus rule over a subset gives a wrong answer, not a smaller one, so they are
skipped and named on stderr rather than quietly producing one.
Exit 0 when clean, 1 when violations are found, 2 when the checker could not run —
a caller has to be able to tell "your code has problems" from "the tool is broken".
Four output formats: human (default), json (versioned, stable schema), sarif (GitHub
code scanning), and agent — token-minimal, grouped by rule rather than by file, with each
rule's card stated once instead of once per violation. Diagnostics always go to stderr, so
piping into a parser works even when something fails.
Documentation
| Document | Purpose |
|---|---|
docs/architecture.md |
The full design: execution model, host API, cache, milestones |
docs/built-in-rules.md |
The rules lanekeep ships with, and their options |
docs/cross-file-rules.md |
Writing a rule that needs a whole-corpus view |
AGENTS.md |
How to work in this repository — for coding agents and humans alike |
CONTRIBUTING.md |
Setup, commands, and the pull request process |
SECURITY.md |
Threat model and how to report a vulnerability |
docs/releasing.md |
How a release is built, gated and published |
Security
lanekeep is meant to run as a pre-commit hook and inside CI, which makes it a supply-chain target. Rules are executable code, so the posture is about confinement rather than absence:
- No ambient authority. Rules run in an embedded QuickJS sandbox and reach exactly the host
functions lanekeep exposes.
fs,process,child_process, network and dynamic import are not restricted — they do not exist in the context. - No network access. Ever, in any mode, with no configuration that enables it.
- Filesystem confinement. Reads go through a tracked
ctx.readFile, confined to the project root. Writes happen only under--fix, only to matched files, only within reported ranges. - Bounded execution. A per-invocation timeout, a 15-second global run budget and a per-runtime
memory ceiling, none disableable — a rule that hangs a pre-commit hook is indistinguishable from
a broken tool. Breaching any of them cancels the run and exits
2, rather than reporting a partial result as a clean one. - Deterministic by construction. The sandbox withholds the clock and randomness, so a rule cannot introduce nondeterminism even by accident.
This bounds blast radius and makes third-party rule sets reviewable. It is not a boundary against
someone who can already commit to the repository being checked. To report a vulnerability, see
SECURITY.md.
Contributing
Contributions are welcome, particularly new built-in rules and new host API surface. Start
with CONTRIBUTING.md — ./scripts/setup-dev.sh installs everything and
wires the git hooks.
All work ships as squashed pull requests with Conventional Commits
titles. main is protected and takes no direct pushes.
License
Licensed under either of
- Apache License, Version 2.0 (
LICENSE-APACHEor http://www.apache.org/licenses/LICENSE-2.0) - MIT License (
LICENSE-MITor http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lanekeep-0.4.0-py3-none-win_amd64.whl.
File metadata
- Download URL: lanekeep-0.4.0-py3-none-win_amd64.whl
- Upload date:
- Size: 2.7 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c3e73a3e63bebcccd0be1b8c1f962472b6de8642c41884463cf074ec9ce162e8
|
|
| MD5 |
29b5df8f57e3a4d008deb0aa59d6d494
|
|
| BLAKE2b-256 |
648fc5624313ec709ce083e87f6ccf1b934d4176d754b2d9a80d8ae44db0cfe0
|
File details
Details for the file lanekeep-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: lanekeep-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.7 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5d2bced16150b66f05189a94da464c70e7455615b232acfcc4d2afb1383d63f
|
|
| MD5 |
abdebf6dea18823b7e3b0fd46a3b8497
|
|
| BLAKE2b-256 |
e4e34f0c04c07bd2041b847e471880d564f56a5273fcddfd64ea14686e4c97a5
|
File details
Details for the file lanekeep-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: lanekeep-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 2.4 MB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f4babd4dfd6cb1b9eeb8133d34ec27dda035ebdca02f657a279040cf5a4d012
|
|
| MD5 |
9ef33c452e6822c8708a7a9264aacf5f
|
|
| BLAKE2b-256 |
2492eac84596a3498f37c090da0fe70f685a6f691d75db90c9ed0fa4959af230
|
File details
Details for the file lanekeep-0.4.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: lanekeep-0.4.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.4 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0ec384f27e364e2aa4241b283cb85ff1f55be1cc6d09c307ed999882465b9c6
|
|
| MD5 |
cb44a2ce0953aa2c9fd4866999974c08
|
|
| BLAKE2b-256 |
aad397337e222782d28f07758c1dfdc42ef674bd2229c9397a06ae4fb5456029
|