lanekeep 0.6.1 → 0.7.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 +113 -294
- package/builtin.d.ts +7 -15
- package/index.d.ts +47 -80
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -12,220 +12,77 @@ lanekeep enforces the conventions that live in your team's heads and your review
|
|
|
12
12
|
the ones a language model cannot infer from the code it is shown. Every rule is a codified answer
|
|
13
13
|
to **"the agent keeps doing this wrong."**
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
dependency.
|
|
15
|
+
It ships as a single static binary with no runtime dependency.
|
|
17
16
|
|
|
18
17
|
---
|
|
19
18
|
|
|
20
|
-
##
|
|
19
|
+
## Languages
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Each guide covers installing lanekeep in that ecosystem, configuring it, the built-in rules that
|
|
22
|
+
apply, a worked custom rule, and the name-resolution behavior specific to that language.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
| Language | Extensions | Install | Guide |
|
|
25
|
+
| --- | --- | --- | --- |
|
|
26
|
+
| Go | `.go` | `go get -tool github.com/fmsouza/lanekeep/cmd/lanekeep` | **[Go](https://github.com/fmsouza/lanekeep/wiki/Go)** |
|
|
27
|
+
| Python | `.py`, `.pyi` | `pip install lanekeep` | **[Python](https://github.com/fmsouza/lanekeep/wiki/Python)** |
|
|
28
|
+
| Rust | `.rs` | `cargo install lanekeep-cli` | **[Rust](https://github.com/fmsouza/lanekeep/wiki/Rust)** |
|
|
29
|
+
| TypeScript / JavaScript | `.ts`, `.mts`, `.cts`, `.tsx`, `.js`, `.mjs`, `.cjs`, `.jsx` | `npm install --save-dev lanekeep` | **[TypeScript and JavaScript](https://github.com/fmsouza/lanekeep/wiki/TypeScript-and-JavaScript)** |
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<details>
|
|
31
|
-
<summary>Python, Go, Homebrew, cargo, or a raw binary</summary>
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
pip install lanekeep # Python
|
|
35
|
-
go get -tool github.com/fmsouza/lanekeep/cmd/lanekeep # Go
|
|
36
|
-
brew install fmsouza/tap/lanekeep # macOS / Linux, system-wide
|
|
37
|
-
cargo install lanekeep-cli # from source
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
Or download from the [releases page](https://github.com/fmsouza/lanekeep/releases).
|
|
41
|
-
|
|
42
|
-
</details>
|
|
31
|
+
`brew install fmsouza/tap/lanekeep` works anywhere, as does a binary from the
|
|
32
|
+
[releases page](https://github.com/fmsouza/lanekeep/releases). Every channel delivers the same
|
|
33
|
+
build, so the bytes are identical whichever you pick.
|
|
43
34
|
|
|
44
|
-
|
|
35
|
+
Whatever the project, the first two commands are the same:
|
|
45
36
|
|
|
46
37
|
```bash
|
|
47
|
-
|
|
38
|
+
lanekeep init # detects the project and writes a config plus a starter rule
|
|
39
|
+
lanekeep check
|
|
48
40
|
```
|
|
49
41
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
```
|
|
53
|
-
lanekeep.json # what to check, and with which rules
|
|
54
|
-
lanekeep/rules/<starter>.ts # a worked example you can edit
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
It detects whether the project is Go, Python or TypeScript and scaffolds accordingly — the
|
|
58
|
-
right glob, a starter rule in that language, and a built-in worth having on.
|
|
59
|
-
|
|
60
|
-
**3. Check:**
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
npx lanekeep check
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
src/payment.ts:12:3 error [local/no-debugger] debugger statement
|
|
68
|
-
→ remove it before committing
|
|
69
|
-
|
|
70
|
-
✖ 1 error(s) across 1 file(s) checked
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
> **If it says `0 file(s) checked`**, nothing matched the config's `include`. The scaffold starts
|
|
74
|
-
> with `src/**/*.{ts,tsx}` — widen it to wherever your code actually lives.
|
|
75
|
-
|
|
76
|
-
That is the whole loop. Everything below is detail.
|
|
77
|
-
|
|
78
|
-
---
|
|
42
|
+
New here? **[Getting Started](https://github.com/fmsouza/lanekeep/wiki/Getting-Started)** is about
|
|
43
|
+
a minute, end to end.
|
|
79
44
|
|
|
80
45
|
## What it is
|
|
81
46
|
|
|
82
47
|
lanekeep is not a linter in the ESLint sense. ESLint enforces language-level correctness; lanekeep
|
|
83
|
-
enforces *project-specific* conventions. The two
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
ctx.report(m.call)
|
|
122
|
-
},
|
|
123
|
-
})
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
**Rules are TypeScript whatever they check** — that is one embedded language, not a JavaScript
|
|
127
|
-
bias. Rules need to be *programs*, because the conventions worth enforcing are too specific for
|
|
128
|
-
any fixed vocabulary of predicates, and one language keeps the sandbox, the cache and the host
|
|
129
|
-
API single-implementation.
|
|
130
|
-
|
|
131
|
-
**Configuration is not TypeScript.** `lanekeep.json` is plain data, so a Go or Python team never
|
|
132
|
-
writes a `.ts` file except when authoring an actual rule.
|
|
133
|
-
|
|
134
|
-
`check` is ordinary TypeScript. Loop, accumulate state, build data structures, read other files,
|
|
135
|
-
import shared helpers — there is no expressiveness ceiling and no DSL to learn beyond the query
|
|
136
|
-
that gates it.
|
|
137
|
-
|
|
138
|
-
**The card is not documentation.** `message`, `remediation` and `examples` are mandatory, because
|
|
139
|
-
they are what gets fed back to whoever has to act on the violation — increasingly an agent.
|
|
140
|
-
|
|
141
|
-
**Editor types ship with the npm package.** `npm install --save-dev lanekeep` gives you the
|
|
142
|
-
binary *and* TypeScript definitions for the whole host API, so `ctx` autocompletes and a typo'd
|
|
143
|
-
method is a compile error rather than a rule that throws in the sandbox. They are checked
|
|
144
|
-
against the engine's own registration, so they cannot drift from what actually exists.
|
|
145
|
-
|
|
146
|
-
A Go, Python or Rust project that wants them can add the npm package as a dev dependency
|
|
147
|
-
purely for authoring — nothing about the checker needs Node.
|
|
148
|
-
|
|
149
|
-
## Supported languages
|
|
150
|
-
|
|
151
|
-
Each guide covers installing lanekeep in that ecosystem, what to put in the config, which
|
|
152
|
-
built-in rules apply, a worked custom rule, and the resolution behavior specific to it.
|
|
153
|
-
|
|
154
|
-
| Language | Guide | Extensions |
|
|
155
|
-
| --- | --- | --- |
|
|
156
|
-
| Go | **[Go guide](https://github.com/fmsouza/lanekeep/wiki/Go)** | `.go` |
|
|
157
|
-
| Python | **[Python guide](https://github.com/fmsouza/lanekeep/wiki/Python)** | `.py`, `.pyi` |
|
|
158
|
-
| Rust | **[Rust guide](https://github.com/fmsouza/lanekeep/wiki/Rust)** | `.rs` |
|
|
159
|
-
| TypeScript / JavaScript | **[TypeScript and JavaScript guide](https://github.com/fmsouza/lanekeep/wiki/TypeScript-and-JavaScript)** | `.ts`, `.mts`, `.cts`, `.tsx`, `.js`, `.mjs`, `.cjs`, `.jsx` |
|
|
160
|
-
|
|
161
|
-
Every one carries syntactic binding resolution, so a rule can ask where a name came from rather
|
|
162
|
-
than matching text — `ctx.bindingKind`, `ctx.resolvesToImport` and `ctx.isShadowed` answer for
|
|
163
|
-
all of them.
|
|
164
|
-
|
|
165
|
-
**The grammar is chosen by the file, not by the rule.** A rule declares which languages it
|
|
166
|
-
applies to and does not run on files of any other, defaulting to `['typescript', 'tsx']` when it
|
|
167
|
-
says nothing. That default is the one thing to get right on a non-TypeScript rule: omit
|
|
168
|
-
`language` on a Go rule and it silently never fires.
|
|
169
|
-
|
|
170
|
-
## Configuration
|
|
171
|
-
|
|
172
|
-
`lanekeep.json`, at the project root. `lanekeep init` writes one for you, matched to the
|
|
173
|
-
project it finds.
|
|
174
|
-
|
|
175
|
-
```json
|
|
176
|
-
{
|
|
177
|
-
"$schema": "https://raw.githubusercontent.com/fmsouza/lanekeep/main/schema/lanekeep.schema.json",
|
|
178
|
-
|
|
179
|
-
"include": ["**/*.go"],
|
|
180
|
-
"exclude": ["**/*_test.go"],
|
|
181
|
-
|
|
182
|
-
"rules": [
|
|
183
|
-
"lanekeep/no-package-init",
|
|
184
|
-
{ "rule": "lanekeep/no-restricted-imports", "options": { "restrictions": [
|
|
185
|
-
{ "module": "database/sql", "from": ["!internal/store/**"], "reason": "go through the store package" }
|
|
186
|
-
] } },
|
|
187
|
-
"./lanekeep/rules/no-fmt-println.ts"
|
|
188
|
-
]
|
|
189
|
-
}
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
A string uses a rule as it comes; the object form calls it with options. `$schema` is what
|
|
193
|
-
gives you **completion and validation in your editor with nothing installed** — VS Code and
|
|
194
|
-
most others read it directly.
|
|
195
|
-
|
|
196
|
-
**Rules are TypeScript, configuration is not.** A rule is a program, and that is the point of
|
|
197
|
-
the tool; saying which rules to run is data. A Go or Python team should not have to write a
|
|
198
|
-
`.ts` file to do the second, which is why the config is JSON and only the rules are not.
|
|
199
|
-
|
|
200
|
-
Rule ids are namespaced. `lanekeep/` is reserved for built-ins and `local/` needs no
|
|
201
|
-
declaration; any other prefix must be listed in `namespaces`, so a typo in an id is an error
|
|
202
|
-
rather than a rule that silently never runs.
|
|
203
|
-
|
|
204
|
-
Ten rules ship built in — four for TypeScript and JavaScript, two each for Python, Go and Rust. See
|
|
205
|
-
[`docs/built-in-rules.md`](docs/built-in-rules.md) for what each one checks and its options.
|
|
206
|
-
|
|
207
|
-
<details>
|
|
208
|
-
<summary>Configuring in TypeScript instead</summary>
|
|
209
|
-
|
|
210
|
-
`lanekeep.config.ts` still works, and is the better choice when the config computes something
|
|
211
|
-
or shares a preset across repositories — composition is then ordinary `import`, with no
|
|
212
|
-
bespoke `extends` mechanism to learn.
|
|
213
|
-
|
|
214
|
-
```ts
|
|
215
|
-
import { defineConfig } from 'lanekeep'
|
|
216
|
-
import noDefaultExport from 'lanekeep/no-default-export'
|
|
217
|
-
import noDebugger from './lanekeep/rules/no-debugger'
|
|
218
|
-
|
|
219
|
-
export default defineConfig({
|
|
220
|
-
include: ['src/**/*.{ts,tsx}'],
|
|
221
|
-
rules: [noDefaultExport, noDebugger],
|
|
222
|
-
})
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Both formats compile to the same thing before anything reads them, so they cannot differ in
|
|
226
|
-
behavior. `lanekeep.json` wins if a project somehow has both.
|
|
227
|
-
|
|
228
|
-
</details>
|
|
48
|
+
enforces *project-specific* conventions. The two barely overlap, and lanekeep replaces neither your
|
|
49
|
+
linter nor your formatter.
|
|
50
|
+
|
|
51
|
+
A rule is a **program**, not a configuration entry. It declares a
|
|
52
|
+
[tree-sitter query](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/1-syntax.html)
|
|
53
|
+
that Rust matches at native speed, and a handler that runs only on matches — where it can loop,
|
|
54
|
+
accumulate state, read other files and ask where a name came from.
|
|
55
|
+
|
|
56
|
+
That matters because the conventions worth enforcing are the ones specific enough that nobody
|
|
57
|
+
else would ever write them, which is exactly the population a fixed vocabulary of predicates
|
|
58
|
+
fails. See **[Writing Rules](https://github.com/fmsouza/lanekeep/wiki/Writing-Rules)** for the
|
|
59
|
+
anatomy and the full host API, and each language guide for a worked example in that language.
|
|
60
|
+
|
|
61
|
+
Three things follow from who reads the output:
|
|
62
|
+
|
|
63
|
+
- **Every rule carries its own fix.** `message`, `remediation` and `examples` are mandatory
|
|
64
|
+
fields, not documentation — they are the card fed back to whoever has to act on the violation,
|
|
65
|
+
increasingly an agent.
|
|
66
|
+
- **Output is deterministic.** Violations are always sorted by `(ruleId, file, line, column)`, and
|
|
67
|
+
the sandbox withholds the clock and randomness, so two runs over identical input produce
|
|
68
|
+
byte-identical output. An agent reading it twice must not see reordering as change.
|
|
69
|
+
- **It runs in the inner loop.** Agents and developers invoke it after every edit, so a warm run
|
|
70
|
+
is measured in tens of milliseconds — for a config whose rules are all TypeScript modules. A
|
|
71
|
+
rule that ships as a compiled component has to be loaded first, and the four TypeScript
|
|
72
|
+
built-ins share a 12.4 MiB one: a config naming all four of them costs **about 6.5 seconds on a
|
|
73
|
+
project's first run** and **about 0.2 seconds on every run after it** — the component is
|
|
74
|
+
deserialized once per run, not once per rule — and leaves 33 MiB in `.lanekeep`.
|
|
75
|
+
`lanekeep init` scaffolds one of those four, so that is what a new TypeScript project meets
|
|
76
|
+
first. [`docs/architecture.md`](docs/architecture.md) §15 has the table and what is owed.
|
|
77
|
+
|
|
78
|
+
**Rules are authored in TypeScript whatever language they check** — that is the form to start
|
|
79
|
+
from, and it is the one most teams already have someone who writes. A rule may also be a
|
|
80
|
+
WebAssembly component, which is how eight of the ten built-ins ship — two written in Rust, two
|
|
81
|
+
written in Go, and four compiled ahead of time from the same TypeScript they were already
|
|
82
|
+
written in. Every form reaches the same host API and is held to the same limits, and a config
|
|
83
|
+
names a rule rather than its implementation. **Configuration is neither** — `lanekeep.json` is
|
|
84
|
+
plain data, so a Go, Python or Rust team never writes a `.ts` file except when authoring an
|
|
85
|
+
actual rule.
|
|
229
86
|
|
|
230
87
|
## Using it
|
|
231
88
|
|
|
@@ -238,97 +95,100 @@ lanekeep check --fix # apply the safe fixes, report what is left
|
|
|
238
95
|
lanekeep check --profile # where the run spent its time, per rule
|
|
239
96
|
lanekeep rules # what this project has configured
|
|
240
97
|
lanekeep explain <rule-id> # one rule's card, without opening its source
|
|
98
|
+
lanekeep server # LSP for an editor, or --protocol mcp for an agent host
|
|
241
99
|
```
|
|
242
100
|
|
|
243
|
-
`--staged` and `--since` are intersected with the config's `include`/`exclude`, and both **skip
|
|
244
|
-
cross-file rules** — a whole-corpus rule over a subset gives a wrong answer rather than a smaller
|
|
245
|
-
one, so they are skipped and named on stderr instead of quietly producing one.
|
|
246
|
-
|
|
247
|
-
**Fixes.** Only a fix its rule marked as behavior-preserving is applied. Anything else is a
|
|
248
|
-
suggestion — shown, never written — because the cautious mistake costs a manual edit and the other
|
|
249
|
-
one rewrites your code silently.
|
|
250
|
-
|
|
251
|
-
**Suppressions** carry a mandatory reason and an optional expiry. A directive that does not work
|
|
252
|
-
says so, rather than silently doing nothing:
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
// lanekeep-ignore-next-line lanekeep/no-default-export reason: legacy entry point
|
|
256
|
-
export default parse
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
Run `lanekeep check --report-unused-suppressions` to find the ones that no longer silence
|
|
260
|
-
anything.
|
|
261
|
-
|
|
262
|
-
**Output.** `--format` takes `human` (default), `json` (versioned, stable schema), `sarif` (GitHub
|
|
263
|
-
code scanning) and `agent` — token-minimal, grouped by rule rather than by file, with each card
|
|
264
|
-
stated once instead of once per violation. Diagnostics always go to stderr, so piping into a
|
|
265
|
-
parser works even when something fails.
|
|
266
|
-
|
|
267
101
|
**Exit codes:** `0` clean, `1` violations found, `2` the checker could not run. A caller has to be
|
|
268
102
|
able to tell "your code has problems" from "the tool is broken". `--warn-only` reports violations
|
|
269
103
|
but exits `0`, for a phased rollout.
|
|
270
104
|
|
|
271
|
-
|
|
105
|
+
**Output formats** via `--format`: `human` (default), `json` (versioned, stable schema), `sarif`
|
|
106
|
+
(GitHub code scanning), and `agent` — token-minimal, grouped by rule rather than by file. Diagnostics
|
|
107
|
+
always go to stderr, so piping into a parser works even when something fails.
|
|
272
108
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
lanekeep server # LSP, for any editor
|
|
277
|
-
lanekeep server --protocol mcp # MCP, for an agent host
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
MCP exposes three tools — `lanekeep_check`, `lanekeep_rules`, `lanekeep_explain` — so an agent
|
|
281
|
-
can ask what it broke and what the rule wants without shelling out and parsing text.
|
|
109
|
+
**Fixes** are applied only when the rule marked them behavior-preserving; anything else is shown
|
|
110
|
+
and never written. **Suppressions** carry a mandatory reason and an optional expiry, and a directive
|
|
111
|
+
that does not work says so rather than silently doing nothing.
|
|
282
112
|
|
|
283
|
-
|
|
284
|
-
|
|
113
|
+
Configuration reference, CI recipes, editor setup and the MCP tool list are in the
|
|
114
|
+
[wiki](https://github.com/fmsouza/lanekeep/wiki).
|
|
285
115
|
|
|
286
116
|
## How it stays fast with programmable rules
|
|
287
117
|
|
|
288
118
|
The usual problem with a native tool that runs JavaScript plugins is the boundary between them:
|
|
289
119
|
dispatching into JS once per AST node means tens of thousands of crossings per file.
|
|
290
120
|
|
|
291
|
-
lanekeep dispatches once per **query match** instead. The
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
TypeScript.
|
|
121
|
+
lanekeep dispatches once per **query match** instead. The query runs in Rust across a single shared
|
|
122
|
+
parse; only matches reach your handler. That is typically two to three orders of magnitude fewer
|
|
123
|
+
crossings, and it is the reason a Rust engine still earns its place once rules are programs.
|
|
295
124
|
|
|
296
125
|
```
|
|
297
126
|
discover paths (globs, gitignore-aware)
|
|
298
127
|
└─> for each file, in parallel:
|
|
299
128
|
cache key ──hit──> validate tracked deps ──> cached violations + facts
|
|
300
129
|
└─miss─> path and raw-text gates reject before any parse
|
|
301
|
-
└─> parse ─> match queries in Rust
|
|
302
|
-
└─> invoke the
|
|
130
|
+
└─> parse once ─> match queries in Rust
|
|
131
|
+
└─> invoke the handler, per match only
|
|
303
132
|
└─> reduce phase: cross-file rules consume facts only, never parse trees
|
|
304
133
|
└─> filter suppressions ─> sort ─> report
|
|
305
134
|
```
|
|
306
135
|
|
|
307
136
|
A warm run with no changes executes no JavaScript at all — every file is a cache hit.
|
|
308
137
|
|
|
309
|
-
|
|
310
|
-
and randomness, so two runs over identical input produce byte-identical output. An agent reading
|
|
311
|
-
the output twice must not see reordering as change.
|
|
312
|
-
|
|
313
|
-
## Installing without a package manager
|
|
138
|
+
## Platforms
|
|
314
139
|
|
|
315
140
|
Prebuilt for macOS on Apple silicon, Linux on x86-64 and arm64, and Windows on x86-64. The Linux
|
|
316
141
|
binaries are built against **glibc 2.17**, so they run on anything from RHEL 7 onwards.
|
|
317
142
|
|
|
143
|
+
**No runtime is required to run lanekeep.** Node, Python or Go is needed only to install it from
|
|
144
|
+
that ecosystem, where it picks which binary to fetch. Nothing is pulled in as a dependency any of
|
|
145
|
+
those ways.
|
|
146
|
+
|
|
318
147
|
Intel macOS is not prebuilt — `cargo install lanekeep-cli` builds it from source, and both the npm
|
|
319
148
|
launcher and the Homebrew formula say so rather than failing obscurely.
|
|
320
149
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
150
|
+
## Security
|
|
151
|
+
|
|
152
|
+
lanekeep is meant to run as a pre-commit hook and inside CI, which makes it a supply-chain target.
|
|
153
|
+
Rules are executable code, so the posture is about confinement rather than absence:
|
|
154
|
+
|
|
155
|
+
- **No ambient authority.** A TypeScript rule runs in an embedded QuickJS sandbox and a
|
|
156
|
+
WebAssembly rule under wasmtime; both reach exactly the host functions lanekeep exposes. `fs`,
|
|
157
|
+
`process`, `child_process`, network and dynamic import are not restricted — they do not exist in
|
|
158
|
+
the context. A component imports one interface and is refused at load if it imports another.
|
|
159
|
+
- **No network access.** Ever, in any mode, with no configuration that enables it.
|
|
160
|
+
- **Filesystem confinement.** Reads go through a tracked host call, confined to the project root.
|
|
161
|
+
Writes happen only under `--fix`, only to matched files, only within reported ranges.
|
|
162
|
+
- **Bounded execution.** A per-invocation timeout, a global run budget and a per-runtime memory
|
|
163
|
+
ceiling, none disableable — a rule that hangs a pre-commit hook is indistinguishable from a
|
|
164
|
+
broken tool. Breaching any of them cancels the run and exits `2`, rather than reporting a partial
|
|
165
|
+
result as a clean one.
|
|
166
|
+
- **Deterministic by construction.** The sandbox withholds the clock and randomness, so a rule
|
|
167
|
+
cannot introduce nondeterminism even by accident.
|
|
168
|
+
|
|
169
|
+
This bounds blast radius and makes third-party rule sets reviewable. It is not a boundary against
|
|
170
|
+
someone who can already commit to the repository being checked. To report a vulnerability, see
|
|
171
|
+
[`SECURITY.md`](SECURITY.md).
|
|
172
|
+
|
|
173
|
+
## Project status
|
|
174
|
+
|
|
175
|
+
**Released and usable**, on every channel in the table above — one build feeding all of them.
|
|
176
|
+
|
|
177
|
+
It is **0.x**, and this repository treats that as semver does: a minor bump may break a public Rust
|
|
178
|
+
API. Rule authors are insulated from that — host API methods and the config shape are additive —
|
|
179
|
+
but pin a version if you embed the crates.
|
|
180
|
+
|
|
181
|
+
Known gaps, stated rather than implied:
|
|
324
182
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
183
|
+
- **Two of the three performance budgets in [`docs/architecture.md`](docs/architecture.md)
|
|
184
|
+
§15 are not met.** The cold budget is; they are targets, and that section says by how much
|
|
185
|
+
and where the remaining time goes.
|
|
186
|
+
- **No type-aware analysis**, by design. Name resolution is syntactic — see §1 non-goals.
|
|
328
187
|
|
|
329
188
|
## Documentation
|
|
330
189
|
|
|
331
|
-
**The [wiki](https://github.com/fmsouza/lanekeep/wiki) is the place to start** — it is task-shaped
|
|
190
|
+
**The [wiki](https://github.com/fmsouza/lanekeep/wiki) is the place to start** — it is task-shaped
|
|
191
|
+
and organized by language.
|
|
332
192
|
|
|
333
193
|
| Page | Purpose |
|
|
334
194
|
| --- | --- |
|
|
@@ -336,7 +196,6 @@ caches it. Set `LANEKEEP_BINARY` to an already-installed lanekeep and it fetches
|
|
|
336
196
|
| [Configuration](https://github.com/fmsouza/lanekeep/wiki/Configuration) | `lanekeep.json`, every field |
|
|
337
197
|
| [Writing Rules](https://github.com/fmsouza/lanekeep/wiki/Writing-Rules) | Rule anatomy and the full host API |
|
|
338
198
|
| [CI and Editors](https://github.com/fmsouza/lanekeep/wiki/CI-and-Editors) | Pre-commit, GitHub Actions, LSP, MCP |
|
|
339
|
-
| [Go](https://github.com/fmsouza/lanekeep/wiki/Go) · [Python](https://github.com/fmsouza/lanekeep/wiki/Python) · [Rust](https://github.com/fmsouza/lanekeep/wiki/Rust) · [TypeScript and JavaScript](https://github.com/fmsouza/lanekeep/wiki/TypeScript-and-JavaScript) | Per-language guides |
|
|
340
199
|
|
|
341
200
|
In-repo, versioned with the code:
|
|
342
201
|
|
|
@@ -352,46 +211,6 @@ In-repo, versioned with the code:
|
|
|
352
211
|
| [`docs/releasing.md`](docs/releasing.md) | How a release is built, gated and published |
|
|
353
212
|
| [`CHANGELOG.md`](CHANGELOG.md) | What changed, per release |
|
|
354
213
|
|
|
355
|
-
## Security
|
|
356
|
-
|
|
357
|
-
lanekeep is meant to run as a pre-commit hook and inside CI, which makes it a supply-chain target.
|
|
358
|
-
Rules are executable code, so the posture is about confinement rather than absence:
|
|
359
|
-
|
|
360
|
-
- **No ambient authority.** Rules run in an embedded QuickJS sandbox and reach exactly the host
|
|
361
|
-
functions lanekeep exposes. `fs`, `process`, `child_process`, network and dynamic import are not
|
|
362
|
-
restricted — they do not exist in the context.
|
|
363
|
-
- **No network access.** Ever, in any mode, with no configuration that enables it.
|
|
364
|
-
- **Filesystem confinement.** Reads go through a tracked `ctx.readFile`, confined to the project
|
|
365
|
-
root. Writes happen only under `--fix`, only to matched files, only within reported ranges.
|
|
366
|
-
- **Bounded execution.** A per-invocation timeout, a global run budget and a per-runtime memory
|
|
367
|
-
ceiling, none disableable — a rule that hangs a pre-commit hook is indistinguishable from a
|
|
368
|
-
broken tool. Breaching any of them cancels the run and exits `2`, rather than reporting a partial
|
|
369
|
-
result as a clean one.
|
|
370
|
-
- **Deterministic by construction.** The sandbox withholds the clock and randomness, so a rule
|
|
371
|
-
cannot introduce nondeterminism even by accident.
|
|
372
|
-
|
|
373
|
-
This bounds blast radius and makes third-party rule sets reviewable. It is not a boundary against
|
|
374
|
-
someone who can already commit to the repository being checked. To report a vulnerability, see
|
|
375
|
-
[`SECURITY.md`](SECURITY.md).
|
|
376
|
-
|
|
377
|
-
## Project status
|
|
378
|
-
|
|
379
|
-
**Released and usable.** The current version is on [crates.io](https://crates.io/crates/lanekeep-cli),
|
|
380
|
-
[npm](https://www.npmjs.com/package/lanekeep), [PyPI](https://pypi.org/project/lanekeep/), Homebrew,
|
|
381
|
-
and as a Go module — one build feeding every channel, so the bytes are identical whichever you use.
|
|
382
|
-
|
|
383
|
-
It is **0.x**, and this repository treats that as semver does: a minor bump may break a public Rust
|
|
384
|
-
API. Rule authors are insulated from that — `ctx` methods and the config shape are additive — but
|
|
385
|
-
pin a version if you embed the crates.
|
|
386
|
-
|
|
387
|
-
Known gaps, stated rather than implied:
|
|
388
|
-
|
|
389
|
-
- **No editor types for rule authors yet** (above).
|
|
390
|
-
- **The performance budgets in [`docs/architecture.md`](docs/architecture.md) §15 are not met.**
|
|
391
|
-
They are targets, and that document says by how much and what the levers are. The tool is fast;
|
|
392
|
-
the numbers are simply ambitious.
|
|
393
|
-
- **No type-aware analysis**, by design. Binding resolution is syntactic — see §1 non-goals.
|
|
394
|
-
|
|
395
214
|
## Contributing
|
|
396
215
|
|
|
397
216
|
Contributions are welcome, particularly new built-in rules and new host API surface. Start with
|
package/builtin.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Types
|
|
2
|
+
* Types for the importable built-in subpaths — the module built-ins, reached through the
|
|
3
|
+
* per-name `exports`/`typesVersions` entries `crates/lanekeep-package-gen` generates from
|
|
4
|
+
* `COMPONENT_RULES`. A component built-in has no entry there, so importing one is a compile
|
|
5
|
+
* error rather than a default export that lies.
|
|
3
6
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* everything `index.d.ts` has, and adds the default export a built-in subpath needs.
|
|
8
|
-
*
|
|
9
|
-
* A `declare module 'lanekeep/*'` block inside `index.d.ts` would have been the obvious way
|
|
10
|
-
* to do this and does nothing at all: a `declare module` inside a file that has its own
|
|
11
|
-
* imports or exports is module augmentation, not an ambient declaration, so TypeScript
|
|
12
|
-
* ignores it and the import stays unresolved. That failed silently until a compile test
|
|
13
|
-
* caught it.
|
|
14
|
-
*
|
|
15
|
-
* The default covers both shapes a built-in can take, because which one it is cannot be known
|
|
16
|
-
* from the specifier: a rule taking options is a factory — `noRestrictedImports({ ... })` —
|
|
17
|
-
* and one taking none is the rule itself.
|
|
7
|
+
* The default covers the two shapes an importable built-in can take, because which one it is
|
|
8
|
+
* cannot be known from the specifier: a rule taking options is a factory —
|
|
9
|
+
* `noRestrictedImports({ ... })` — and one taking none is the rule itself.
|
|
18
10
|
*/
|
|
19
11
|
export * from './index'
|
|
20
12
|
|
package/index.d.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type definitions for authoring lanekeep rules.
|
|
3
3
|
*
|
|
4
|
+
* **Generated from `crates/lanekeep-wasm/wit/world.wit` by `crates/lanekeep-types-gen`.** Do not
|
|
5
|
+
* edit by hand — run `just generate-index-dts` and commit the result.
|
|
6
|
+
*
|
|
4
7
|
* These describe the host API a rule reaches inside lanekeep's sandbox. Nothing here runs in
|
|
5
8
|
* Node: `defineRule` and `defineConfig` are identity functions whose only job is to give the
|
|
6
9
|
* compiler something to check against, and `RuleContext` is provided by lanekeep at run time.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The world is the single source of truth for every member the renderer emits straight from it.
|
|
11
|
+
* Two members deviate from the world on purpose, and both are QuickJS-shaped: `today` is omitted
|
|
12
|
+
* from `RuleContext` because QuickJS exposes it as a conditional property rather than a callable,
|
|
13
|
+
* a shape this renderer cannot state honestly from the world; and `facts` is added to
|
|
14
|
+
* `RuleContext` because QuickJS hands a per-file rule `facts` that the world declares only on
|
|
15
|
+
* `reduce-context`. Nothing else is added or omitted by hand.
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* A node in the parse tree.
|
|
16
20
|
*
|
|
17
21
|
* Deliberately opaque. Nodes cross into the sandbox as integer handles rather than objects,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* a rule loses its whole top-level case without any error.
|
|
22
|
-
*
|
|
23
|
-
* Compare against `undefined` explicitly.
|
|
22
|
+
* and the reason this is a branded type rather than `number` is that **the root node's handle
|
|
23
|
+
* is `0`** — written as a plain number, `if (!node)` looks like a null check and silently
|
|
24
|
+
* discards the root. Compare against `undefined` explicitly.
|
|
24
25
|
*/
|
|
25
26
|
export type Node = number & { readonly __lanekeepNode: unique symbol }
|
|
26
27
|
|
|
@@ -134,74 +135,49 @@ export interface EmittedFact extends Fact {
|
|
|
134
135
|
file: string
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* A node's location: the file, line and column `ctx.loc` returns.
|
|
140
|
+
*
|
|
141
|
+
* `line` and `column` are required here, unlike on `ReduceLocation`: `ctx.loc` either
|
|
142
|
+
* resolves the node and returns all three together, or the node does not resolve and the
|
|
143
|
+
* call returns `undefined` entirely — there is no partial state to leave room for.
|
|
144
|
+
*/
|
|
145
|
+
export interface NodeLocation {
|
|
146
|
+
/** Path relative to the project root. */
|
|
147
|
+
file: string
|
|
148
|
+
/** One-based. */
|
|
149
|
+
line: number
|
|
150
|
+
/** One-based. */
|
|
151
|
+
column: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** A rule's RuleContext surface. */
|
|
138
155
|
export interface RuleContext {
|
|
139
|
-
/** Path of the file being checked, relative to the project root. */
|
|
140
156
|
readonly filePath: string
|
|
141
|
-
/** The whole file, as text. */
|
|
142
157
|
readonly fileText: string
|
|
143
|
-
/** The tree's root node. Its handle is `0` — see {@link Node}. */
|
|
144
158
|
readonly root: Node
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
children(node: Node): Node[]
|
|
161
|
-
/** Named children only. */
|
|
162
|
-
namedChildren(node: Node): Node[]
|
|
163
|
-
/** Every ancestor, innermost first. */
|
|
164
|
-
ancestors(node: Node): Node[]
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Whether an identifier resolves to a given import.
|
|
168
|
-
*
|
|
169
|
-
* Handles aliasing, so `import { makeStyles as ms }` resolves correctly. This is the call
|
|
170
|
-
* that separates a rule from a grep: a text match both misses the alias and fires on a
|
|
171
|
-
* local of the same name.
|
|
172
|
-
*
|
|
173
|
-
* @param name Which export. Omit to match the module regardless of which name was taken.
|
|
174
|
-
*/
|
|
175
|
-
resolvesToImport(node: Node, module: string, name?: string): boolean
|
|
176
|
-
/** Whether an identifier came from a module matching this glob. */
|
|
177
|
-
isImportedFrom(node: Node, pattern: string): boolean
|
|
178
|
-
/** How the name was introduced, or `undefined` when it does not resolve. */
|
|
179
|
-
bindingKind(node: Node): BindingKind | undefined
|
|
180
|
-
/** Whether an outer binding of the same name is hidden by this one. */
|
|
181
|
-
isShadowed(node: Node): boolean
|
|
182
|
-
|
|
183
|
-
/** Run a query inside a subtree. */
|
|
184
|
-
querySubtree(node: Node, query: string): Match[]
|
|
185
|
-
/** The nearest ancestor matching a query, with its captures. */
|
|
186
|
-
closestAncestor(node: Node, query: string): Match | undefined
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Read another file, relative to the project root.
|
|
190
|
-
*
|
|
191
|
-
* Tracked: the read becomes part of the cache key, so a change to that file invalidates
|
|
192
|
-
* this one's result. Confined to the project root; `undefined` when absent or outside.
|
|
193
|
-
*/
|
|
159
|
+
kind(n: Node): string | undefined
|
|
160
|
+
text(n: Node): string | undefined
|
|
161
|
+
isNamed(n: Node): boolean
|
|
162
|
+
line(n: Node): number | undefined
|
|
163
|
+
column(n: Node): number | undefined
|
|
164
|
+
parent(n: Node): Node | undefined
|
|
165
|
+
children(n: Node): Node[]
|
|
166
|
+
namedChildren(n: Node): Node[]
|
|
167
|
+
ancestors(n: Node): Node[]
|
|
168
|
+
resolvesToImport(n: Node, module: string, name?: string): boolean
|
|
169
|
+
isImportedFrom(n: Node, pattern: string): boolean
|
|
170
|
+
bindingKind(n: Node): BindingKind | undefined
|
|
171
|
+
isShadowed(n: Node): boolean
|
|
172
|
+
querySubtree(n: Node, query: string): Match[]
|
|
173
|
+
closestAncestor(n: Node, query: string): Match | undefined
|
|
194
174
|
readFile(path: string): string | undefined
|
|
195
|
-
/** Whether a file exists, tracked the same way. */
|
|
196
175
|
fileExists(path: string): boolean
|
|
197
|
-
|
|
198
|
-
/** Emit a fact for the reduce phase. */
|
|
199
176
|
emitFact(fact: Fact): void
|
|
177
|
+
loc(n: Node): NodeLocation | undefined
|
|
178
|
+
report(at: Node, message?: string | ReportOptions): void
|
|
200
179
|
/** Facts emitted so far, optionally filtered by `kind`. */
|
|
201
180
|
facts(kind?: string): EmittedFact[]
|
|
202
|
-
|
|
203
|
-
/** Report a violation at a node. */
|
|
204
|
-
report(at: Node, message?: string | ReportOptions): void
|
|
205
181
|
}
|
|
206
182
|
|
|
207
183
|
/** A violation the reduce phase reports, which has no node to point at. */
|
|
@@ -214,19 +190,10 @@ export interface ReduceLocation {
|
|
|
214
190
|
column?: number
|
|
215
191
|
}
|
|
216
192
|
|
|
217
|
-
/**
|
|
218
|
-
* What a rule's `reduce` handler reaches.
|
|
219
|
-
*
|
|
220
|
-
* Deliberately smaller than {@link RuleContext}: **the reduce phase never touches parse
|
|
221
|
-
* trees.** Facts are small and serializable, which is what keeps cross-file rules parallel
|
|
222
|
-
* and cacheable — handing a tree to `reduce` would make the whole corpus resident.
|
|
223
|
-
*/
|
|
193
|
+
/** A rule's ReduceContext surface. */
|
|
224
194
|
export interface ReduceContext {
|
|
225
|
-
/** Every file the run checked, relative to the project root. */
|
|
226
195
|
readonly files: string[]
|
|
227
|
-
/** Facts from every file, optionally filtered by `kind`. */
|
|
228
196
|
facts(kind?: string): EmittedFact[]
|
|
229
|
-
/** Report a violation against a file. */
|
|
230
197
|
report(at: ReduceLocation, message?: string | ReportOptions): void
|
|
231
198
|
}
|
|
232
199
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Deterministic, AST-based architectural conformance checking",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"node": ">=18"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@lanekeep/darwin-arm64": "0.
|
|
27
|
-
"@lanekeep/linux-arm64": "0.
|
|
28
|
-
"@lanekeep/linux-x64": "0.
|
|
29
|
-
"@lanekeep/win32-x64": "0.
|
|
26
|
+
"@lanekeep/darwin-arm64": "0.7.0",
|
|
27
|
+
"@lanekeep/linux-arm64": "0.7.0",
|
|
28
|
+
"@lanekeep/linux-x64": "0.7.0",
|
|
29
|
+
"@lanekeep/win32-x64": "0.7.0"
|
|
30
30
|
},
|
|
31
31
|
"main": "index.js",
|
|
32
32
|
"types": "index.d.ts",
|