lanekeep 0.5.0 → 0.6.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 CHANGED
@@ -138,10 +138,13 @@ that gates it.
138
138
  **The card is not documentation.** `message`, `remediation` and `examples` are mandatory, because
139
139
  they are what gets fed back to whoever has to act on the violation — increasingly an agent.
140
140
 
141
- > **Editor types are not shipped yet.** `defineRule` and `defineConfig` resolve inside lanekeep's
142
- > sandbox at run time, so rules execute correctly, but there is no published package supplying
143
- > TypeScript definitions for the host API you will not get autocomplete on `ctx` today.
144
- > [`docs/architecture.md`](docs/architecture.md) §6 documents the full surface in the meantime.
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.
145
148
 
146
149
  ## Supported languages
147
150
 
package/builtin.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Types reached through the `typesVersions` mapping in `package.json`.
3
+ *
4
+ * That mapping points *every* specifier here, the bare `lanekeep` included — TypeScript's
5
+ * `"*"` pattern does not exclude the package root, and a narrower pattern would have to
6
+ * predict what future built-ins are called. So this file is a superset: it re-exports
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.
18
+ */
19
+ export * from './index'
20
+
21
+ import type { Rule } from './index'
22
+
23
+ declare const rule: Rule & ((options?: Record<string, unknown>) => Rule)
24
+ export default rule
package/index.d.ts ADDED
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Type definitions for authoring lanekeep rules.
3
+ *
4
+ * These describe the host API a rule reaches inside lanekeep's sandbox. Nothing here runs in
5
+ * Node: `defineRule` and `defineConfig` are identity functions whose only job is to give the
6
+ * compiler something to check against, and `RuleContext` is provided by lanekeep at run time.
7
+ *
8
+ * Every member below is asserted against the host's own registration in
9
+ * `crates/lanekeep-js/tests/host_types.rs`. A method that exists here and not there — or the
10
+ * reverse — fails that test, because a definition that drifts from the engine is worse than
11
+ * none: it produces confident autocomplete for something that does not exist.
12
+ */
13
+
14
+ /**
15
+ * A node in the parse tree.
16
+ *
17
+ * Deliberately opaque. Nodes cross into the sandbox as integer handles rather than objects,
18
+ * which is one of the one-way doors in the architecture — and the reason this is a branded
19
+ * type rather than `number` is that **the root node's handle is `0`**. Written as a plain
20
+ * number, `if (!node)` looks like a null check and silently discards the root, which is how
21
+ * a rule loses its whole top-level case without any error.
22
+ *
23
+ * Compare against `undefined` explicitly.
24
+ */
25
+ export type Node = number & { readonly __lanekeepNode: unique symbol }
26
+
27
+ /** How a name was introduced, as `ctx.bindingKind` reports it. */
28
+ export type BindingKind =
29
+ | 'import'
30
+ | 'const'
31
+ | 'let'
32
+ | 'var'
33
+ | 'param'
34
+ | 'function'
35
+ | 'class'
36
+ | 'catch-param'
37
+ | 'assignment'
38
+ | 'loop'
39
+ | 'context-manager'
40
+ | 'comprehension'
41
+ | 'type'
42
+ | 'receiver'
43
+ | 'type-param'
44
+ | 'module'
45
+ | 'trait'
46
+
47
+ /** A language lanekeep can parse. */
48
+ export type LanguageId = 'typescript' | 'tsx' | 'javascript' | 'python' | 'go' | 'rust'
49
+
50
+ /** How serious a violation is. */
51
+ export type Severity = 'error' | 'warn' | 'off'
52
+
53
+ /**
54
+ * The captures of one query match, keyed by capture name without the `@`.
55
+ *
56
+ * A capture that did not participate in the match is absent, which is why the values are
57
+ * optional — an alternation like `[(a) (b)] @x` binds `@x` either way, but two separate
58
+ * patterns capturing different names do not.
59
+ */
60
+ export type Match = Record<string, Node | undefined>
61
+
62
+ /**
63
+ * What a rule tells whoever has to act on the violation — increasingly an agent.
64
+ *
65
+ * Not documentation, and not optional. `remediation` is the field worth the effort: it should
66
+ * say what to do, not restate the problem.
67
+ */
68
+ export interface RuleCard {
69
+ /** What is wrong, in a few words. */
70
+ message: string
71
+ /** What to do about it. */
72
+ remediation: string
73
+ /** One example each way. */
74
+ examples: {
75
+ bad: string
76
+ good: string
77
+ }
78
+ }
79
+
80
+ /** Cheap rejections applied before a file is parsed. */
81
+ export interface Gates {
82
+ /**
83
+ * Literal substrings a file's raw bytes must contain. A file missing any one of them is
84
+ * never parsed.
85
+ *
86
+ * **This is an *and*, not an *or*.** A rule matching either of two tokens cannot express
87
+ * its gate as `['a', 'b']` — that rejects any file containing only one, which is usually
88
+ * most of them, and the rule then reports nothing while looking healthy. There is no `or`
89
+ * form; omit the gate when no single substring covers every case.
90
+ */
91
+ fileContains?: string[]
92
+ }
93
+
94
+ /** A replacement a rule offers for a violation. */
95
+ export interface Fix {
96
+ /** The node whose text is replaced. */
97
+ node: Node
98
+ /** What to replace it with. */
99
+ text: string
100
+ /**
101
+ * Whether the fix preserves behavior.
102
+ *
103
+ * Only a fix marked `true` is applied by `--fix`. Anything else is a suggestion — shown,
104
+ * never written — because the cautious mistake costs a manual edit and the other one
105
+ * rewrites someone's code silently.
106
+ */
107
+ safe?: boolean
108
+ }
109
+
110
+ /** Options for a single report. */
111
+ export interface ReportOptions {
112
+ /** Overrides the card's `message` for this one violation. */
113
+ message?: string
114
+ /** A replacement to offer. */
115
+ fix?: Fix
116
+ }
117
+
118
+ /**
119
+ * A fact a rule emits for the reduce phase.
120
+ *
121
+ * `kind` is required and must be non-empty, because it is what `ctx.facts('...')` filters on.
122
+ * A fact without one could never be retrieved, so emitting it is always a mistake — and a
123
+ * silent one, since the rule would look like it was working right up until `reduce` found
124
+ * nothing. lanekeep throws rather than accept it.
125
+ */
126
+ export interface Fact {
127
+ kind: string
128
+ [key: string]: unknown
129
+ }
130
+
131
+ /** A fact as `reduce` receives it, with the file that emitted it. */
132
+ export interface EmittedFact extends Fact {
133
+ /** Path of the file this came from, relative to the project root. */
134
+ file: string
135
+ }
136
+
137
+ /** What a rule's `check` handler reaches. */
138
+ export interface RuleContext {
139
+ /** Path of the file being checked, relative to the project root. */
140
+ readonly filePath: string
141
+ /** The whole file, as text. */
142
+ readonly fileText: string
143
+ /** The tree's root node. Its handle is `0` — see {@link Node}. */
144
+ readonly root: Node
145
+
146
+ /** The node's kind, as tree-sitter names it. */
147
+ kind(node: Node): string
148
+ /** The source text the node spans. */
149
+ text(node: Node): string
150
+ /** Whether this is a named node rather than an anonymous token. */
151
+ isNamed(node: Node): boolean
152
+ /** One-based line of the node's start. */
153
+ line(node: Node): number
154
+ /** One-based column of the node's start. */
155
+ column(node: Node): number
156
+
157
+ /** The node's parent, or `undefined` at the root. */
158
+ parent(node: Node): Node | undefined
159
+ /** Every child, including anonymous tokens. */
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
+ */
194
+ readFile(path: string): string | undefined
195
+ /** Whether a file exists, tracked the same way. */
196
+ fileExists(path: string): boolean
197
+
198
+ /** Emit a fact for the reduce phase. */
199
+ emitFact(fact: Fact): void
200
+ /** Facts emitted so far, optionally filtered by `kind`. */
201
+ facts(kind?: string): EmittedFact[]
202
+
203
+ /** Report a violation at a node. */
204
+ report(at: Node, message?: string | ReportOptions): void
205
+ }
206
+
207
+ /** A violation the reduce phase reports, which has no node to point at. */
208
+ export interface ReduceLocation {
209
+ /** Path relative to the project root. */
210
+ file: string
211
+ /** One-based. */
212
+ line?: number
213
+ /** One-based. */
214
+ column?: number
215
+ }
216
+
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
+ */
224
+ export interface ReduceContext {
225
+ /** Every file the run checked, relative to the project root. */
226
+ readonly files: string[]
227
+ /** Facts from every file, optionally filtered by `kind`. */
228
+ facts(kind?: string): EmittedFact[]
229
+ /** Report a violation against a file. */
230
+ report(at: ReduceLocation, message?: string | ReportOptions): void
231
+ }
232
+
233
+ /** A rule, as `defineRule` takes it. */
234
+ export interface Rule {
235
+ /**
236
+ * Namespaced identifier, as `namespace/name`.
237
+ *
238
+ * `local/` needs no declaration and `lanekeep/` is reserved for built-ins; any other
239
+ * namespace must be listed in the config's `namespaces`.
240
+ */
241
+ id: string
242
+ /**
243
+ * Which languages this rule applies to.
244
+ *
245
+ * **Defaults to `['typescript', 'tsx']`**, and this is the field most worth getting right
246
+ * on a rule for anything else. The grammar is chosen by the file, not by the rule, and a
247
+ * rule does not run on a file whose language it does not name — so omitting this on a Go
248
+ * or Rust rule means it silently never fires.
249
+ */
250
+ language?: LanguageId | LanguageId[]
251
+ /** How serious a violation is, before any config override. */
252
+ severity: Severity
253
+ /** What the rule tells whoever has to act on it. */
254
+ card: RuleCard
255
+ /** Cheap rejections before parsing. */
256
+ gates?: Gates
257
+ /**
258
+ * The tree-sitter query gating the handler.
259
+ *
260
+ * Rust matches it across a single shared parse and only matches reach `check`, which is
261
+ * what keeps a JavaScript rule affordable. Write the narrowest query that captures what
262
+ * you need; `check` then only refines.
263
+ */
264
+ query: string
265
+ /** A per-invocation budget overriding the default, in milliseconds. */
266
+ timeout?: number
267
+ /** Called once per query match. */
268
+ check?(ctx: RuleContext, match: Match): void
269
+ /** Called once per run, after every file, with facts only. */
270
+ reduce?(ctx: ReduceContext): void
271
+ }
272
+
273
+ /** A lanekeep configuration, as `defineConfig` takes it. */
274
+ export interface Config {
275
+ /** Globs selecting files to check, relative to the project root. */
276
+ include?: string[]
277
+ /** Globs removing files from that selection. */
278
+ exclude?: string[]
279
+ /** Rule-id namespaces this project uses beyond `local`. */
280
+ namespaces?: string[]
281
+ /** Override a rule's own severity, by id. */
282
+ severity?: Record<string, Severity>
283
+ /** Execution budgets, in milliseconds. */
284
+ timeouts?: {
285
+ /** Per rule invocation. */
286
+ rule?: number
287
+ /** Wall-clock, for the whole run. */
288
+ global?: number
289
+ }
290
+ /** The rules to run, in order. */
291
+ rules: Rule[]
292
+ }
293
+
294
+ /**
295
+ * Define a rule.
296
+ *
297
+ * An identity function. It exists so the compiler checks the object against {@link Rule}
298
+ * where it is written, rather than reporting a mismatch from wherever it is imported.
299
+ */
300
+ export declare function defineRule(rule: Rule): Rule
301
+
302
+ /**
303
+ * Define a configuration.
304
+ *
305
+ * An identity function, for the same reason as {@link defineRule}. Most projects will write
306
+ * `lanekeep.json` instead — configuration is data, and only rules need to be programs.
307
+ */
308
+ export declare function defineConfig(config: Config): Config
package/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // The runtime half of the authoring package.
2
+ //
3
+ // Rules never execute in Node — lanekeep evaluates them in its own sandbox, where `lanekeep`
4
+ // resolves to a host module rather than to this file. These exports exist so that a tool
5
+ // which *does* load a rule under Node (a bundler, a test runner, an editor's type server
6
+ // following the import) finds something coherent instead of a missing module.
7
+ //
8
+ // Identity functions, which is also what they are inside the sandbox: their entire job is to
9
+ // give the compiler a place to check the object against a type.
10
+ 'use strict'
11
+
12
+ const identity = (value) => value
13
+
14
+ module.exports = { defineRule: identity, defineConfig: identity }
15
+ module.exports.default = module.exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lanekeep",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Deterministic, AST-based architectural conformance checking",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -14,15 +14,27 @@
14
14
  "files": [
15
15
  "bin/lanekeep",
16
16
  "resolve.js",
17
+ "index.js",
18
+ "index.d.ts",
19
+ "builtin.d.ts",
17
20
  "README.md"
18
21
  ],
19
22
  "engines": {
20
23
  "node": ">=18"
21
24
  },
22
25
  "optionalDependencies": {
23
- "@lanekeep/darwin-arm64": "0.5.0",
24
- "@lanekeep/linux-arm64": "0.5.0",
25
- "@lanekeep/linux-x64": "0.5.0",
26
- "@lanekeep/win32-x64": "0.5.0"
26
+ "@lanekeep/darwin-arm64": "0.6.0",
27
+ "@lanekeep/linux-arm64": "0.6.0",
28
+ "@lanekeep/linux-x64": "0.6.0",
29
+ "@lanekeep/win32-x64": "0.6.0"
30
+ },
31
+ "main": "index.js",
32
+ "types": "index.d.ts",
33
+ "typesVersions": {
34
+ "*": {
35
+ "*": [
36
+ "builtin.d.ts"
37
+ ]
38
+ }
27
39
  }
28
40
  }