Skip to main content

Deterministic, AST-based architectural conformance checking

Project description

lanekeep

Deterministic, AST-based architectural conformance checking for AI-generated and human-written code.

License: MIT OR Apache-2.0

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, remediation and examples are 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

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

lanekeep-0.4.0-py3-none-win_amd64.whl (2.7 MB view details)

Uploaded Python 3Windows x86-64

lanekeep-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.7 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

lanekeep-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.4 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

lanekeep-0.4.0-py3-none-macosx_11_0_arm64.whl (2.4 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

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

Hashes for lanekeep-0.4.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 c3e73a3e63bebcccd0be1b8c1f962472b6de8642c41884463cf074ec9ce162e8
MD5 29b5df8f57e3a4d008deb0aa59d6d494
BLAKE2b-256 648fc5624313ec709ce083e87f6ccf1b934d4176d754b2d9a80d8ae44db0cfe0

See more details on using hashes here.

File details

Details for the file lanekeep-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for lanekeep-0.4.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e5d2bced16150b66f05189a94da464c70e7455615b232acfcc4d2afb1383d63f
MD5 abdebf6dea18823b7e3b0fd46a3b8497
BLAKE2b-256 e4e34f0c04c07bd2041b847e471880d564f56a5273fcddfd64ea14686e4c97a5

See more details on using hashes here.

File details

Details for the file lanekeep-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for lanekeep-0.4.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 2f4babd4dfd6cb1b9eeb8133d34ec27dda035ebdca02f657a279040cf5a4d012
MD5 9ef33c452e6822c8708a7a9264aacf5f
BLAKE2b-256 2492eac84596a3498f37c090da0fe70f685a6f691d75db90c9ed0fa4959af230

See more details on using hashes here.

File details

Details for the file lanekeep-0.4.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for lanekeep-0.4.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a0ec384f27e364e2aa4241b283cb85ff1f55be1cc6d09c307ed999882465b9c6
MD5 cb44a2ce0953aa2c9fd4866999974c08
BLAKE2b-256 aad397337e222782d28f07758c1dfdc42ef674bd2229c9397a06ae4fb5456029

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page