lanekeep 0.8.1 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +20 -14
  2. package/index.d.ts +299 -11
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  [![PyPI](https://img.shields.io/pypi/v/lanekeep?label=pypi)](https://pypi.org/project/lanekeep/)
8
8
  [![CI](https://github.com/fmsouza/lanekeep/actions/workflows/ci.yml/badge.svg)](https://github.com/fmsouza/lanekeep/actions/workflows/ci.yml)
9
9
  [![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
10
+ [![MSRV](https://img.shields.io/badge/MSRV-1.94-blue.svg)](Cargo.toml)
10
11
 
11
12
  lanekeep enforces the conventions that live in your team's heads and your reviewers' comments —
12
13
  the ones a language model cannot infer from the code it is shown. Every rule is a codified answer
@@ -67,20 +68,18 @@ Three things follow from who reads the output:
67
68
  the sandbox withholds the clock and randomness, so two runs over identical input produce
68
69
  byte-identical output. An agent reading it twice must not see reordering as change.
69
70
  - **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.
71
+ is measured in tens of milliseconds. The built-ins that ship as WebAssembly components are all
72
+ under 115 KB, so loading them is noise a 12.4 MiB compiled-TypeScript component that once
73
+ cost new TypeScript projects ~6.5 seconds on their first run was reverted for exactly that
74
+ reason.
75
+ [`docs/architecture.md`](docs/architecture.md) §15 has the ledger.
77
76
 
78
77
  **Rules are authored in TypeScript whatever language they check** — that is the form to start
79
78
  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
79
+ WebAssembly component, which is how four of the sixteen built-ins ship — two written in Rust
80
+ and two written in Go; the other twelve run as QuickJS modules, three of them checking five of
81
+ the six supported languages from a single source (every one but JavaScript). Every form reaches the same host API and is held to
82
+ the same limits, and a config names a rule rather than its implementation. **Configuration is neither** — `lanekeep.json` is
84
83
  plain data, so a Go, Python or Rust team never writes a `.ts` file except when authoring an
85
84
  actual rule.
86
85
 
@@ -90,9 +89,10 @@ actual rule.
90
89
  lanekeep check # the whole project
91
90
  lanekeep check --staged # only what is about to be committed
92
91
  lanekeep check --since main # only what changed against a ref
92
+ lanekeep check --file src/a.ts # exactly these files, repeatable, no git involved
93
93
  lanekeep check --watch # re-check on every change, until Ctrl-C
94
94
  lanekeep check --fix # apply the safe fixes, report what is left
95
- lanekeep check --profile # where the run spent its time, per rule
95
+ lanekeep check --profile # per rule: where the time went, and what it looked at
96
96
  lanekeep rules # what this project has configured
97
97
  lanekeep explain <rule-id> # one rule's card, without opening its source
98
98
  lanekeep server # LSP for an editor, or --protocol mcp for an agent host
@@ -183,7 +183,9 @@ Known gaps, stated rather than implied:
183
183
  - **Two of the three performance budgets in [`docs/architecture.md`](docs/architecture.md)
184
184
  §15 are not met.** The cold budget is; they are targets, and that section says by how much
185
185
  and where the remaining time goes.
186
- - **No type-aware analysis**, by design. Name resolution is syntactic see §1 non-goals.
186
+ - **No general type inference**, by design. Name resolution is syntactic; a rule that opts in
187
+ (`requires: ['types']`) gets a bounded within-file oracle that answers `undefined` rather than
188
+ guess — see §1 non-goals and §6.10.
187
189
 
188
190
  ## Documentation
189
191
 
@@ -204,7 +206,11 @@ In-repo, versioned with the code:
204
206
  | [`docs/architecture.md`](docs/architecture.md) | The full design: execution model, host API, cache, milestones |
205
207
  | [`docs/built-in-rules.md`](docs/built-in-rules.md) | The rules lanekeep ships with, and their options |
206
208
  | [`docs/cross-file-rules.md`](docs/cross-file-rules.md) | Writing a rule that needs a whole-corpus view |
207
- | [`docs/adr/`](docs/adr/) | Decision records: why the design is the way it is |
209
+ | [`docs/obligation-rules.md`](docs/obligation-rules.md) | Writing a rule that needs a resource released on every path |
210
+ | [`docs/type-aware-rules.md`](docs/type-aware-rules.md) | Writing a rule that needs to know what a value's type is |
211
+ | [`docs/authoring-rust-rules.md`](docs/authoring-rust-rules.md) | Writing a rule in Rust, shipped as a WebAssembly component |
212
+ | [`docs/authoring-go-rules.md`](docs/authoring-go-rules.md) | Writing a rule in Go |
213
+ | [`docs/authoring-python-rules.md`](docs/authoring-python-rules.md) | Writing a rule in Python |
208
214
  | [`CONTRIBUTING.md`](CONTRIBUTING.md) | Setup, commands, and the pull request process |
209
215
  | [`AGENTS.md`](AGENTS.md) | How to work in this repository — for coding agents and humans alike |
210
216
  | [`SECURITY.md`](SECURITY.md) | Threat model and how to report a vulnerability |
package/index.d.ts CHANGED
@@ -8,11 +8,13 @@
8
8
  * Node: `defineRule` and `defineConfig` are identity functions whose only job is to give the
9
9
  * compiler something to check against, and `RuleContext` is provided by lanekeep at run time.
10
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.
11
+ * Two members deviate from the world on purpose, and both are QuickJS-shaped: `today` is
12
+ * omitted from `RuleContext` because QuickJS exposes it as a conditional property rather than a
13
+ * callable, a shape this renderer cannot state honestly from the world; and `types` is added
14
+ * to `RuleContext` because `ctx.types` the bounded
15
+ * type oracle is QuickJS-only and has no presence in `world.wit` at all: a component rule
16
+ * cannot declare `requires`, so there is nothing for the world to say about it. Nothing else is
17
+ * added or omitted by hand.
16
18
  */
17
19
 
18
20
  /**
@@ -78,7 +80,23 @@ export interface RuleCard {
78
80
  }
79
81
  }
80
82
 
81
- /** Cheap rejections applied before a file is read or parsed. */
83
+ /**
84
+ * Cheap rejections applied before a file is read or parsed.
85
+ *
86
+ * A gate is declared, not derived: nothing here is computed from the rule's `query`, and
87
+ * nothing checks the two against each other. So a gate can change what the rule reports — a
88
+ * file it rejects is a file the rule never runs on, and a violation there is never found.
89
+ *
90
+ * A gate is neutral when it admits every file the rule would have reported on — yours to
91
+ * keep, and not something the engine can check. The safe way to keep it is to gate wider
92
+ * than the query, which is sufficient rather than necessary: a rule whose handler filters
93
+ * may gate far narrower and still be neutral. `--profile` prints, per rule, how many files
94
+ * each gate rejected and how many the rule actually parsed, which is where a suspected gate
95
+ * is settled. A nonzero `cached` means the columns to its right are
96
+ * incomplete for that run, since a cache hit returns before the content gates are consulted;
97
+ * pair `--profile` with `--no-cache` to read them. `path-gated` is unaffected, because a path
98
+ * gate runs before the cache is consulted at all.
99
+ */
82
100
  export interface Gates {
83
101
  /**
84
102
  * Glob patterns a file's path must match for the rule to consider it.
@@ -117,7 +135,13 @@ export interface Gates {
117
135
  fileNotContains?: string[]
118
136
  }
119
137
 
120
- /** A replacement a rule offers for a violation. */
138
+ /**
139
+ * A replacement a rule offers for a violation.
140
+ *
141
+ * A per-file offer alone: a fix names a node, and the reduce phase consumes facts and the
142
+ * file list and nothing else — there is no parse tree there for `node` to name, which is why
143
+ * a reduce report's options carry a message and never a fix.
144
+ */
121
145
  export interface Fix {
122
146
  /** The node whose text is replaced. */
123
147
  node: Node
@@ -141,6 +165,20 @@ export interface ReportOptions {
141
165
  fix?: Fix
142
166
  }
143
167
 
168
+ /** Options for a single reduce report. */
169
+ export interface ReduceReportOptions {
170
+ /** Overrides the card's `message` for this one violation. */
171
+ message?: string
172
+ /**
173
+ * Never present. A fix replaces a node's text, and the reduce phase has no parse tree —
174
+ * `node` has nothing to name, and both hosts throw on a supplied fix rather than drop it.
175
+ * Typed `never` rather than left absent so offering one fails to compile even for an
176
+ * object built before the call, which a fresh literal's excess-property check alone would
177
+ * admit.
178
+ */
179
+ fix?: never
180
+ }
181
+
144
182
  /**
145
183
  * A fact a rule emits for the reduce phase.
146
184
  *
@@ -191,6 +229,151 @@ export interface StructureFingerprint {
191
229
  nodes: number
192
230
  }
193
231
 
232
+ /**
233
+ * Where a name came from. Returned by {@link TypeApi.symbolOf} directly, and nested under a
234
+ * {@link TypeInfo} whose `symbol` field is set.
235
+ *
236
+ * There is deliberately no `file` naming the declaring path, even now that declarations are
237
+ * read across files. It would be where a package happens to be installed —
238
+ * `node_modules/@types/money/index.d.ts` on one machine and a workspace path on another —
239
+ * so a rule branching on it would give different answers for one program. `module` is the
240
+ * stable identity, and `exported` is the name that module declares it under.
241
+ */
242
+ export interface SymbolInfo {
243
+ /**
244
+ * The name as it appears at the use site, not at the declaration. For a renamed import —
245
+ * `import { Decimal as Money }` — this is the local alias `Money`. It is the spelling to
246
+ * quote in a message, because it is the spelling the reader has in front of them; compare
247
+ * `exported` when the question is which export of a module this is.
248
+ */
249
+ name: string
250
+ /**
251
+ * The name the module exports this under. For a named import — `import { Decimal }` and
252
+ * `import { Decimal as Money }` alike — this is `Decimal` when `m`'s declaration file is
253
+ * unreadable, and the name that file actually declares (following any re-export chain)
254
+ * when it is readable, which need not be `Decimal` at all. Copied even when nothing was
255
+ * renamed, so a comparison never needs a fallback to `name` that would silently accept
256
+ * every plain import if it were forgotten. For a default import this is the name the
257
+ * declaration file declares the default export under when that file is readable — `Big`
258
+ * for a package whose default export is `Big`, whatever the local binding is called — and
259
+ * falls back to the placeholder `default` only when the declaration is not readable.
260
+ * Absent for a namespace import, which binds the module object and has no single exported
261
+ * name, and absent for a local declaration, which was not imported at all.
262
+ */
263
+ exported?: string
264
+ /**
265
+ * The module it was imported from. Absent for a local declaration — that absence is what
266
+ * distinguishes an imported `Decimal` from a local class that happens to share the name.
267
+ */
268
+ module?: string
269
+ }
270
+
271
+ /**
272
+ * What the oracle established about an expression, from {@link TypeApi.typeOf}.
273
+ *
274
+ * At most one of `primitive`, `symbol` or `union` is set, matching which kind of type this
275
+ * is — a `union`'s members are already flattened one level and in canonical order. `text` is
276
+ * set alongside whichever it is, but it is **display-only**: what TypeScript itself would
277
+ * call the type, for a message a rule builds. Branch on `primitive` and `symbol`, never on
278
+ * `text`'s wording.
279
+ *
280
+ * All three can be unset at once: a *nominal* type whose name the resolver could not
281
+ * attribute — an unresolvable, global or ambient type such as `Date` used with no local
282
+ * declaration or import — carries only `text`. That is not a gap to code around; it is
283
+ * another shape of the same "I could not be sure" answer this whole surface is built on,
284
+ * the same posture `typeOf` itself takes by returning `undefined` rather than guessing. Do
285
+ * not assume the final branch of `if (primitive) … else if (symbol) … else` is unreachable
286
+ * — for this shape, it is not.
287
+ *
288
+ * There is deliberately no `complete` field on a *type*. Whether the oracle had a full view
289
+ * is a property of the file rather than of any one answer — `TypeApi.complete()` is where it
290
+ * is asked — and putting it here would invite a rule to read it per type and conclude
291
+ * something different on each.
292
+ */
293
+ export interface TypeInfo {
294
+ /** What TypeScript would call this type. Display-only — branch on the fields below instead. */
295
+ text: string
296
+ /** Set when this is a primitive — exactly the set TypeScript itself recognizes as one. */
297
+ primitive?: 'number' | 'string' | 'boolean' | 'bigint' | 'symbol' | 'null' | 'undefined'
298
+ /**
299
+ * Set when this is a named type and the oracle could resolve where the name came from.
300
+ * Absent on an unresolvable, global or ambient nominal type — see the interface doc above.
301
+ */
302
+ symbol?: SymbolInfo
303
+ /** Set when this is a union. */
304
+ union?: TypeInfo[]
305
+ }
306
+
307
+ /**
308
+ * The type oracle, reached through `ctx.types`.
309
+ *
310
+ * Every question can come back with no answer, and no answer is a first-class result rather
311
+ * than a failure to work around: the oracle is conservative on purpose, and it would rather
312
+ * say nothing than say something wrong, because a rule reporting on a wrong type accuses
313
+ * correct code. A rule is expected to check for `undefined` and quietly stay silent, the same
314
+ * posture the rest of the navigation surface already takes on a dead handle.
315
+ *
316
+ * It reads the file in front of it **and the declaration files that file imports**, through
317
+ * the same tracked, confined reads `ctx.readFile` uses. Nothing above the project root is read,
318
+ * ever — so point lanekeep at the workspace root, the directory `node_modules` lives in,
319
+ * rather than at a package inside it. `--config` does not move the root.
320
+ */
321
+ export interface TypeApi {
322
+ /**
323
+ * The type of the expression at `n`. `undefined` is that first-class no-answer, not a
324
+ * failure.
325
+ */
326
+ typeOf(n: Node): TypeInfo | undefined
327
+ /** Where the identifier at `n` was declared. `undefined` on the same terms as `typeOf`. */
328
+ symbolOf(n: Node): SymbolInfo | undefined
329
+ /**
330
+ * What calling the function at `n` yields.
331
+ *
332
+ * Separate from `typeOf` because a function declaration is not an expression. Accepts a
333
+ * call expression, a function-like declaration, or an identifier bound to one. An
334
+ * annotated signature answers its annotation; an unannotated one with a single `return`
335
+ * answers that expression's type; several returns answer their union when every member is
336
+ * known and `undefined` otherwise; a function with no `return` answers `undefined` rather
337
+ * than guessing `void`. Type arguments are dropped throughout — `useQuery<Balance[]>`
338
+ * answers by the result type's name, never by the argument.
339
+ */
340
+ returnTypeOf(n: Node): TypeInfo | undefined
341
+ /**
342
+ * Whether the type at `n` is the type `module` exports as `name`, or inherits from it.
343
+ *
344
+ * Nominal, never structural: a class reaches it through `extends` or `implements`, an
345
+ * interface through `extends`, an alias by being transparent, and a union only when *every*
346
+ * member does. A primitive answers `false` — but only when `module` and `name` resolve to a
347
+ * declaration; a target that does not resolve answers `undefined` whatever `n` is.
348
+ *
349
+ * **`undefined` is not `false`.** `false` means the walk completed and reached nothing;
350
+ * `undefined` means a link could not be read — an unresolvable import, a package that is
351
+ * not installed, a type with no symbol at all. A rule treating the two alike reports on
352
+ * code the oracle never saw.
353
+ */
354
+ isAssignableTo(n: Node, module: string, name: string): boolean | undefined
355
+ /**
356
+ * Whether every import in the file being checked resolved to something readable.
357
+ *
358
+ * `false` is the honest label on a partial view: some name in this file came from a module
359
+ * the oracle could not open, so an `undefined` anywhere in it may be ignorance rather than
360
+ * a considered answer. A rule that reports only on what it established does not need to
361
+ * ask; a rule that wants to say "I could not check this file" does.
362
+ *
363
+ * An import that resolves to a file which does not *parse* counts as unreadable too: the
364
+ * names outside the broken span still answer, the ones inside it come back `undefined`, and
365
+ * nothing on either answer says which. An import of something that is not code — a
366
+ * stylesheet, a JSON asset, an image — is not counted at all, since it is not a module the
367
+ * oracle reads.
368
+ *
369
+ * **The verdict is the whole file's, and a parse fault is the whole declaration file's.**
370
+ * One `ERROR` node anywhere in a fifty-thousand-line `@types` bundle makes every file that
371
+ * imports it `false`, however far that span is from the names the rule asked about. Silence
372
+ * is the safe direction; a narrower verdict is a refinement rather than a promise.
373
+ */
374
+ complete(): boolean
375
+ }
376
+
194
377
  /** A rule's RuleContext surface. */
195
378
  export interface RuleContext {
196
379
  readonly filePath: string
@@ -217,8 +400,19 @@ export interface RuleContext {
217
400
  emitFact(fact: Fact): void
218
401
  loc(n: Node): NodeLocation | undefined
219
402
  report(at: Node, message?: string | ReportOptions): void
220
- /** Facts emitted so far, optionally filtered by `kind`. */
221
- facts(kind?: string): EmittedFact[]
403
+ /**
404
+ * The type oracle, present only for a rule that declared `requires: ['types']`.
405
+ * Bounded by a fixed depth rather than by file: it follows an import into the
406
+ * declaration file that answers it, and the file after that.
407
+ *
408
+ * Typed as always present because there is no way to spell "present when
409
+ * this rule's own `requires` says so" as a type — so a rule that forgets the
410
+ * declaration still compiles. It finds out at the first call instead:
411
+ * `ctx.types` is `undefined` at run time, and `ctx.types.typeOf(...)` throws a
412
+ * `TypeError` rather than returning a quietly wrong answer. That loudness is
413
+ * deliberate.
414
+ */
415
+ types: TypeApi
222
416
  }
223
417
 
224
418
  /** A violation the reduce phase reports, which has no node to point at. */
@@ -235,7 +429,51 @@ export interface ReduceLocation {
235
429
  export interface ReduceContext {
236
430
  readonly files: string[]
237
431
  facts(kind?: string): EmittedFact[]
238
- report(at: ReduceLocation, message?: string | ReportOptions): void
432
+ /** Takes a message; a supplied `fix` throws — there is no node to attach one to. */
433
+ report(at: ReduceLocation, message?: string | ReduceReportOptions): void
434
+ }
435
+
436
+ /** Queries whose named captures drive the taint analysis. */
437
+ export type FlowSpec = {
438
+ /** Queries whose `@source` capture marks a tainted origin. */
439
+ sources: string[]
440
+ /** Queries whose `@sink` capture marks a forbidden destination. */
441
+ sinks: string[]
442
+ /** Queries whose `@sanitizer` capture clears taint from the value it produces. */
443
+ sanitizers?: string[]
444
+ }
445
+
446
+ /** One tainted flow: a source whose value reaches a sink with no intervening sanitizer. */
447
+ export type FlowPath = {
448
+ readonly source: Node
449
+ readonly sink: Node
450
+ /** The assignments and calls between source and sink, in flow order. One canonical path. */
451
+ readonly steps: readonly Node[]
452
+ }
453
+
454
+ /**
455
+ * A typestate obligation: an acquired resource must reach a release on every
456
+ * path out of `scope`. Requires `requires: ['dataflow']`.
457
+ */
458
+ export type ObligationSpec = {
459
+ /** Queries whose `@acquire` capture starts an obligation on the captured value. */
460
+ acquire: string[]
461
+ /** Queries whose `@release` capture discharges it. */
462
+ release: string[]
463
+ /**
464
+ * `'function'` — every path out of the enclosing function, `return`/`throw` included.
465
+ * `'block'` — every path out of the block the acquire is in.
466
+ */
467
+ scope: 'function' | 'block'
468
+ }
469
+
470
+ /** An acquire some path leaves undischarged, passed to `checkObligation`. */
471
+ export type UnmetObligation = {
472
+ readonly acquire: Node
473
+ /** The exit the value escapes through — a `return`, a `throw`, or the implicit end. */
474
+ readonly exit: Node
475
+ /** Whether any path *did* discharge it: partial coverage reads differently to none. */
476
+ readonly partial: boolean
239
477
  }
240
478
 
241
479
  /** A rule, as `defineRule` takes it. */
@@ -256,6 +494,15 @@ export interface Rule {
256
494
  * or Rust rule means it silently never fires.
257
495
  */
258
496
  language?: LanguageId | LanguageId[]
497
+ /**
498
+ * Host analyses this rule needs before it can run.
499
+ *
500
+ * Absent means none, which is every rule today. A rule declaring one the engine cannot
501
+ * provide is refused at load rather than run without it: an analysis that silently goes
502
+ * missing makes the rule report nothing, and a rule reporting nothing is indistinguishable
503
+ * from a codebase with nothing to report.
504
+ */
505
+ requires?: Array<'types' | 'dataflow'>
259
506
  /** How serious a violation is, before any config override. */
260
507
  severity: Severity
261
508
  /** What the rule tells whoever has to act on it. */
@@ -269,6 +516,11 @@ export interface Rule {
269
516
  * what keeps a JavaScript rule affordable. Write the narrowest query that captures what
270
517
  * you need; `check` then only refines.
271
518
  *
519
+ * Optional — required only when `check` is present. A flow-only rule (`checkFlow` with no
520
+ * `check`) declares no top-level query; its file gate comes from the union of its `flow`
521
+ * queries instead (see {@link FlowSpec}). The type cannot express that conditional, so a
522
+ * `check` rule missing `query` type-checks here and is refused by the config loader instead.
523
+ *
272
524
  * A single string applies to every declared language. An object maps each declared
273
525
  * language to its own query — required when the grammars do not share node vocabulary
274
526
  * (Python spells a call `call`, the other supported grammars say `call_expression`).
@@ -282,13 +534,29 @@ export interface Rule {
282
534
  * no backreferences or lookaround. `#is?`, `#is-not?`, `#set!`, or an operator the
283
535
  * binding does not know is refused at compile time.
284
536
  */
285
- query: string | Partial<Record<LanguageId, string>>
537
+ query?: string | Partial<Record<LanguageId, string>>
286
538
  /** A per-invocation budget overriding the default, in milliseconds. */
287
539
  timeout?: number
288
540
  /** Called once per query match. */
289
541
  check?(ctx: RuleContext, match: Match): void
290
542
  /** Called once per run, after every file, with facts only. */
291
543
  reduce?(ctx: ReduceContext): void
544
+ /**
545
+ * A taint-flow specification: queries whose captures mark tainted origins, forbidden
546
+ * destinations, and the calls that neutralize a value. Requires `requires: ['dataflow']`
547
+ * and a `checkFlow` handler; a rule declaring one without the other is refused at load.
548
+ */
549
+ flow?: FlowSpec
550
+ /** Called once per canonical tainted flow from a source to a sink. */
551
+ checkFlow?(ctx: RuleContext, path: FlowPath): void
552
+ /**
553
+ * A typestate obligation: queries whose captures acquire an obligation on a value and
554
+ * queries that discharge it. Requires `requires: ['dataflow']` and a `checkObligation`
555
+ * handler; a rule declaring one without the other is refused at load.
556
+ */
557
+ obligation?: ObligationSpec
558
+ /** Called once per value left with an unmet obligation at the end of its scope. */
559
+ checkObligation?(ctx: RuleContext, unmet: UnmetObligation): void
292
560
  }
293
561
 
294
562
  /** A lanekeep configuration, as `defineConfig` takes it. */
@@ -307,6 +575,13 @@ export interface Config {
307
575
  rule?: number
308
576
  /** Wall-clock, for the whole run. */
309
577
  global?: number
578
+ /**
579
+ * Host-side type-provider work, across the whole run.
580
+ *
581
+ * Analysis time rather than elapsed time: only what the provider spends building
582
+ * programs and answering requests is charged against it.
583
+ */
584
+ analysis?: number
310
585
  }
311
586
  /** Policy for suppression directives. All off by default. */
312
587
  suppressions?: {
@@ -317,6 +592,19 @@ export interface Config {
317
592
  /** Any whole-file directive is reported. */
318
593
  forbidFileScope?: boolean
319
594
  }
595
+ /** Which type oracle answers `ctx.types`. */
596
+ types?: {
597
+ /**
598
+ * `builtin` is lanekeep's own bounded oracle and needs no toolchain. `tsc` drives the
599
+ * project's own `typescript` package through a sidecar process: wider answers, a slower
600
+ * run, and a pre-commit hook that builds the project's program before it checks anything.
601
+ */
602
+ provider?: 'builtin' | 'tsc'
603
+ /** How to launch the sidecar. `tsc` only. */
604
+ command?: string[]
605
+ /** The `typescript` package the sidecar loads, resolved from the project root. `tsc` only. */
606
+ typescript?: string
607
+ }
320
608
  /** The rules to run, in order. */
321
609
  rules: Rule[]
322
610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lanekeep",
3
- "version": "0.8.1",
3
+ "version": "0.10.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.8.1",
27
- "@lanekeep/linux-arm64": "0.8.1",
28
- "@lanekeep/linux-x64": "0.8.1",
29
- "@lanekeep/win32-x64": "0.8.1"
26
+ "@lanekeep/darwin-arm64": "0.10.0",
27
+ "@lanekeep/linux-arm64": "0.10.0",
28
+ "@lanekeep/linux-x64": "0.10.0",
29
+ "@lanekeep/win32-x64": "0.10.0"
30
30
  },
31
31
  "main": "index.js",
32
32
  "types": "index.d.ts",