lanekeep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # lanekeep
2
+
3
+ **Deterministic, AST-based architectural conformance checking for AI-generated and human-written code.**
4
+
5
+ [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
6
+
7
+ > **Status: early development.** Nothing is released yet and the CLI described below is not
8
+ > usable. The architecture is settled — see [`docs/architecture.md`](docs/architecture.md) — and
9
+ > the work is tracked as a sequence of milestones. Do not depend on this yet.
10
+
11
+ ---
12
+
13
+ ## What it is
14
+
15
+ lanekeep is not a linter in the ESLint sense. ESLint enforces language-level correctness.
16
+ lanekeep enforces *project-specific conventions* — the ones a language model has no way to infer
17
+ from the code it is shown, because they live in your team's heads and your reviewers' comments.
18
+
19
+ Every rule is a codified answer to **"the agent keeps doing this wrong."**
20
+
21
+ Rules are TypeScript programs, written in the same language as the code they inspect:
22
+
23
+ ```ts
24
+ import { defineRule } from 'lanekeep'
25
+
26
+ export default defineRule({
27
+ id: 'local/no-numeric-sizes',
28
+ severity: 'error',
29
+
30
+ card: {
31
+ message: 'Literal numeric size inside makeStyles',
32
+ remediation: 'Use theme.spacing.*, theme.borderRadius.* or theme.borders.*',
33
+ examples: { bad: 'padding: 12', good: 'padding: theme.spacing.md' },
34
+ },
35
+
36
+ // Matched in Rust, at native speed. Your code runs only on matches.
37
+ query: `
38
+ (pair
39
+ key: (property_identifier) @prop
40
+ value: [(number) (unary_expression operand: (number))] @value) @match
41
+ `,
42
+
43
+ check(ctx, m) {
44
+ if (!/^(padding|margin|gap|borderRadius)/.test(ctx.text(m.prop))) return
45
+ if (Number(ctx.text(m.value)) === 0) return
46
+
47
+ const call = ctx.closestAncestor(m.match, '(call_expression function: (identifier) @f)')
48
+ if (!call) return
49
+ if (!ctx.resolvesToImport(call.f, { module: '@rneui/themed', name: 'makeStyles' })) return
50
+
51
+ ctx.report(m.match)
52
+ },
53
+ })
54
+ ```
55
+
56
+ `check` is ordinary TypeScript. Loop, accumulate state, build data structures, read other files,
57
+ import shared helpers — there is no expressiveness ceiling and no DSL to learn beyond the query
58
+ that gates it.
59
+
60
+ ## Why it exists
61
+
62
+ An agent that writes code against your codebase will violate your conventions confidently and
63
+ repeatedly, because those conventions are invisible in the code it was shown. Telling it again in
64
+ the next prompt does not scale. Encoding the convention as a rule does.
65
+
66
+ That makes the design constraints unusual for a static analyzer:
67
+
68
+ - **It runs in the inner loop.** Agents and developers invoke it after every edit, so a cold run
69
+ on a couple of thousand files has a sub-second budget and a warm run has a sub-25ms one.
70
+ - **Its output is read by a machine.** Violations are sorted deterministically, because an agent
71
+ that reads the output twice must not see reordering as change.
72
+ - **Every rule carries its own fix.** `message`, `remediation` and `examples` are mandatory
73
+ fields, not documentation — they are the rule card that gets fed back to the agent.
74
+
75
+ ## How it stays fast with programmable rules
76
+
77
+ The usual problem with a native tool that runs JavaScript plugins is the boundary between them:
78
+ dispatching into JS once per AST node means tens of thousands of crossings per file.
79
+
80
+ lanekeep dispatches once per **query match** instead. The tree-sitter query runs in Rust across a
81
+ single shared parse; only matches reach your handler. That is typically two to three orders of
82
+ magnitude fewer crossings, and it is the reason a Rust engine still earns its place once rules are
83
+ TypeScript.
84
+
85
+ ```
86
+ discover paths (globs, gitignore-aware)
87
+ └─> for each file, in parallel:
88
+ cache key ──hit──> validate tracked deps ──> cached violations + facts
89
+ └─miss─> path and raw-text gates reject before any parse
90
+ └─> parse ─> match queries in Rust
91
+ └─> invoke the TypeScript handler, per match only
92
+ └─> reduce phase: cross-file rules consume facts only, never parse trees
93
+ └─> filter suppressions ─> sort ─> report
94
+ ```
95
+
96
+ A warm run with no changes executes no JavaScript at all — every file is a cache hit.
97
+
98
+ ## Installation
99
+
100
+ Not yet published. The distribution is built and tested — one npm package per platform plus a
101
+ launcher that resolves the right one, and every crate publishable to crates.io — but no
102
+ version has been released yet.
103
+
104
+ When it ships:
105
+
106
+ ```bash
107
+ npm install --save-dev lanekeep # or: cargo install lanekeep-cli
108
+ ```
109
+
110
+ A single static binary with the JavaScript engine compiled in. **Node.js is not required to
111
+ run lanekeep**, even though rules are written in TypeScript — it is required only to install
112
+ it from npm, where it picks which binary to fetch.
113
+
114
+ Prebuilt for macOS on Apple silicon, Linux on x86-64 and arm64, and Windows on x86-64.
115
+ Intel macOS is not prebuilt — `cargo install lanekeep-cli` builds it from source, and the
116
+ npm launcher says so rather than failing obscurely.
117
+
118
+ See [`docs/releasing.md`](docs/releasing.md) for how a release is cut.
119
+
120
+ ## What it looks like
121
+
122
+ ```
123
+ $ lanekeep check
124
+ src/also.ts:2:1 error [lanekeep/no-default-export] default export
125
+ → use a named export, so the symbol has one name every importer must use
126
+ src/bad.ts:2:1 error [lanekeep/no-default-export] default export
127
+ → use a named export, so the symbol has one name every importer must use
128
+
129
+ ✖ 2 error(s) across 2 file(s) checked
130
+ ```
131
+
132
+ Rules may offer a fix, applied with `--fix`:
133
+
134
+ ```
135
+ $ lanekeep check --fix
136
+ fixed 2 violation(s) in 2 file(s)
137
+ ```
138
+
139
+ Only fixes a rule marked as behavior-preserving are applied. Anything else is a suggestion —
140
+ shown, never written — because the cautious mistake costs a manual edit and the other one
141
+ rewrites your code silently.
142
+
143
+ Suppressions carry a mandatory reason and an optional expiry, and a directive that does not
144
+ work says so — a missing reason, a bare rule id, or an unreadable date is reported rather
145
+ than silently doing nothing:
146
+
147
+ ```ts
148
+ // lanekeep-ignore-next-line lanekeep/no-default-export reason: legacy entry point
149
+ export default parse
150
+ ```
151
+
152
+ ```
153
+ $ lanekeep check --report-unused-suppressions
154
+ ```
155
+
156
+ To start from nothing:
157
+
158
+ ```
159
+ $ lanekeep init # a config plus a first rule, both runnable
160
+ ```
161
+
162
+ To find out where a run spent its time — the split says whether the query or the code is
163
+ the problem:
164
+
165
+ ```
166
+ $ lanekeep check --profile
167
+ ```
168
+
169
+ To find out what a rule wants without opening its source:
170
+
171
+ ```
172
+ $ lanekeep explain lanekeep/no-default-export
173
+ $ lanekeep rules --json
174
+ ```
175
+
176
+ For fast feedback on what you touched:
177
+
178
+ ```
179
+ $ lanekeep check --staged # what is about to be committed
180
+ $ lanekeep check --since main # what changed against a ref
181
+ ```
182
+
183
+ Both are intersected with the config's `include`/`exclude`, and both skip cross-file rules —
184
+ a whole-corpus rule over a subset gives a wrong answer, not a smaller one, so they are
185
+ skipped and named on stderr rather than quietly producing one.
186
+
187
+ Exit `0` when clean, `1` when violations are found, `2` when the checker could not run —
188
+ a caller has to be able to tell "your code has problems" from "the tool is broken".
189
+ Four output formats: `human` (default), `json` (versioned, stable schema), `sarif` (GitHub
190
+ code scanning), and `agent` — token-minimal, grouped by rule rather than by file, with each
191
+ rule's card stated once instead of once per violation. Diagnostics always go to stderr, so
192
+ piping into a parser works even when something fails.
193
+
194
+ ## Documentation
195
+
196
+ | Document | Purpose |
197
+ | --- | --- |
198
+ | [`docs/architecture.md`](docs/architecture.md) | The full design: execution model, host API, cache, milestones |
199
+ | [`docs/built-in-rules.md`](docs/built-in-rules.md) | The rules lanekeep ships with, and their options |
200
+ | [`docs/cross-file-rules.md`](docs/cross-file-rules.md) | Writing a rule that needs a whole-corpus view |
201
+ | [`AGENTS.md`](AGENTS.md) | How to work in this repository — for coding agents and humans alike |
202
+ | [`CONTRIBUTING.md`](CONTRIBUTING.md) | Setup, commands, and the pull request process |
203
+ | [`SECURITY.md`](SECURITY.md) | Threat model and how to report a vulnerability |
204
+ | [`docs/releasing.md`](docs/releasing.md) | How a release is built, gated and published |
205
+
206
+ ## Security
207
+
208
+ lanekeep is meant to run as a pre-commit hook and inside CI, which makes it a supply-chain target.
209
+ Rules are executable code, so the posture is about confinement rather than absence:
210
+
211
+ - **No ambient authority.** Rules run in an embedded QuickJS sandbox and reach exactly the host
212
+ functions lanekeep exposes. `fs`, `process`, `child_process`, network and dynamic import are not
213
+ restricted — they do not exist in the context.
214
+ - **No network access.** Ever, in any mode, with no configuration that enables it.
215
+ - **Filesystem confinement.** Reads go through a tracked `ctx.readFile`, confined to the project
216
+ root. Writes happen only under `--fix`, only to matched files, only within reported ranges.
217
+ - **Bounded execution.** A per-invocation timeout, a 15-second global run budget and a per-runtime
218
+ memory ceiling, none disableable — a rule that hangs a pre-commit hook is indistinguishable from
219
+ a broken tool. Breaching any of them cancels the run and exits `2`, rather than reporting a
220
+ partial result as a clean one.
221
+ - **Deterministic by construction.** The sandbox withholds the clock and randomness, so a rule
222
+ cannot introduce nondeterminism even by accident.
223
+
224
+ This bounds blast radius and makes third-party rule sets reviewable. It is not a boundary against
225
+ someone who can already commit to the repository being checked. To report a vulnerability, see
226
+ [`SECURITY.md`](SECURITY.md).
227
+
228
+ ## Contributing
229
+
230
+ Contributions are welcome, particularly new built-in rules and new host API surface. Start
231
+ with [`CONTRIBUTING.md`](CONTRIBUTING.md) — `./scripts/setup-dev.sh` installs everything and
232
+ wires the git hooks.
233
+
234
+ All work ships as squashed pull requests with [Conventional Commits](https://www.conventionalcommits.org/)
235
+ titles. `main` is protected and takes no direct pushes.
236
+
237
+ ## License
238
+
239
+ Licensed under either of
240
+
241
+ - Apache License, Version 2.0 ([`LICENSE-APACHE`](LICENSE-APACHE) or
242
+ <http://www.apache.org/licenses/LICENSE-2.0>)
243
+ - MIT License ([`LICENSE-MIT`](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
244
+
245
+ at your option.
246
+
247
+ Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
248
+ this work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without
249
+ any additional terms or conditions.
package/bin/lanekeep ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Hand control to the platform binary.
4
+ *
5
+ * lanekeep's exit code is load-bearing — 0 clean, 1 violations, 2 could not run — so this
6
+ * wrapper's whole job is to pass it through unchanged, along with stdio and any signal that
7
+ * killed the child.
8
+ *
9
+ * A `spawnSync` with inherited stdio rather than an `execve`: replacing the process would be
10
+ * marginally cleaner on Unix, Node has no way to do it without a native dependency, and a
11
+ * native dependency in the package whose entire point is not needing one is the wrong trade.
12
+ * The cost is one idle Node process for the duration of a run.
13
+ */
14
+
15
+ const { spawnSync } = require('node:child_process')
16
+
17
+ const { resolveBinary } = require('../resolve.js')
18
+
19
+ let binary
20
+ try {
21
+ binary = resolveBinary()
22
+ } catch (error) {
23
+ process.stderr.write(`${error.message}\n`)
24
+ // 2, not 1: lanekeep could not run, which is not the same as finding violations. A
25
+ // pre-commit hook that read a broken install as "your code has problems" would be worse
26
+ // than useless — it would block on something the author cannot fix by editing code.
27
+ process.exit(2)
28
+ }
29
+
30
+ const result = spawnSync(binary, process.argv.slice(2), {
31
+ stdio: 'inherit',
32
+ windowsHide: true,
33
+ })
34
+
35
+ if (result.error) {
36
+ process.stderr.write(`lanekeep could not start: ${result.error.message}\n`)
37
+ process.exit(2)
38
+ }
39
+
40
+ if (result.signal) {
41
+ // Re-raised rather than turned into an exit code, so a caller that distinguishes a killed
42
+ // process from a failing one still can. Windows reports none, so this never fires there.
43
+ process.kill(process.pid, result.signal)
44
+ }
45
+
46
+ // `null` means the child died from a signal the platform did not name. The run did not
47
+ // finish, so 2 is the honest answer.
48
+ process.exit(result.status ?? 2)
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "lanekeep",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic, AST-based architectural conformance checking",
5
+ "license": "MIT OR Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/fmsouza/lanekeep.git"
9
+ },
10
+ "homepage": "https://github.com/fmsouza/lanekeep#readme",
11
+ "bin": {
12
+ "lanekeep": "bin/lanekeep"
13
+ },
14
+ "files": [
15
+ "bin/lanekeep",
16
+ "resolve.js",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "optionalDependencies": {
23
+ "@lanekeep/darwin-arm64": "0.1.0",
24
+ "@lanekeep/linux-arm64": "0.1.0",
25
+ "@lanekeep/linux-x64": "0.1.0",
26
+ "@lanekeep/win32-x64": "0.1.0"
27
+ }
28
+ }
package/resolve.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Find the platform binary this machine should run.
3
+ *
4
+ * lanekeep ships one npm package per platform and a launcher — this one — that depends on
5
+ * all of them as `optionalDependencies`. npm installs only the one whose `os` and `cpu` match,
6
+ * so a developer downloads one binary rather than five.
7
+ *
8
+ * **Node is not required to run lanekeep.** It is required only to install it this way. The
9
+ * binary has the JavaScript engine compiled in; this file exists to pick which binary.
10
+ */
11
+
12
+ const { existsSync } = require('node:fs')
13
+
14
+ /**
15
+ * The platform packages, keyed by what Node reports.
16
+ *
17
+ * Written out rather than composed from `${platform}-${arch}`, so a platform lanekeep does
18
+ * not publish for produces a message naming what is available instead of a confusing
19
+ * "cannot find module @lanekeep/sunos-sparc".
20
+ */
21
+ const PACKAGES = {
22
+ 'darwin-arm64': '@lanekeep/darwin-arm64',
23
+ 'linux-arm64': '@lanekeep/linux-arm64',
24
+ 'linux-x64': '@lanekeep/linux-x64',
25
+ 'win32-x64': '@lanekeep/win32-x64',
26
+ }
27
+
28
+ /** Where the binary sits inside a platform package. */
29
+ function binaryName(platform) {
30
+ return platform === 'win32' ? 'lanekeep.exe' : 'lanekeep'
31
+ }
32
+
33
+ /**
34
+ * The path to this machine's lanekeep binary.
35
+ *
36
+ * @throws if this platform has no package, or the package is missing.
37
+ */
38
+ function resolveBinary(platform = process.platform, arch = process.arch) {
39
+ const key = `${platform}-${arch}`
40
+ const pkg = PACKAGES[key]
41
+
42
+ if (!pkg) {
43
+ throw new Error(
44
+ `lanekeep does not ship a binary for ${key}\n` +
45
+ ` available: ${Object.keys(PACKAGES).sort().join(', ')}\n` +
46
+ ` build from source with: cargo install lanekeep-cli`,
47
+ )
48
+ }
49
+
50
+ let binary
51
+ try {
52
+ // Resolved through Node rather than joined by hand, so it works with npm, pnpm's
53
+ // symlinked store, and Yarn's zero-install layout — three arrangements that agree on
54
+ // `require.resolve` and on nothing else.
55
+ binary = require.resolve(`${pkg}/bin/${binaryName(platform)}`)
56
+ } catch {
57
+ throw new Error(
58
+ `lanekeep's binary for ${key} is not installed\n` +
59
+ ` expected the optional dependency ${pkg}\n` +
60
+ ` this usually means the install ran with --no-optional, or on a different platform\n` +
61
+ ` reinstall with: npm install lanekeep`,
62
+ )
63
+ }
64
+
65
+ if (!existsSync(binary)) {
66
+ throw new Error(`lanekeep's binary for ${key} is missing at ${binary}`)
67
+ }
68
+
69
+ return binary
70
+ }
71
+
72
+ module.exports = { PACKAGES, binaryName, resolveBinary }