@konvert7/klint 0.8.0 → 0.9.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 +241 -150
  2. package/cli.ts +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,188 +1,298 @@
1
1
  # klint
2
2
 
3
- The bridge between vibe coding and agentic engineering.
3
+ Architecture and type-aware linting for TypeScript projects.
4
4
 
5
- ## Why
5
+ Biome and oxlint are excellent at syntax, style, and fast local correctness. klint handles the rules that need project context: layer boundaries, singleton ownership, forbidden patterns, custom repository policies, and type-aware checks that should block an AI agent or CI run before code lands.
6
6
 
7
- Biome and oxlint enforce syntax-level style. klint enforces architecture-level rules — the kind that require TypeScript's type graph, span multiple files, or encode constraints that an AI agent must not bypass. If a rule needs to know that `fetchUser()` returns `Promise<User>`, or that sync filesystem calls are banned inside async hooks, that's a klint rule.
7
+ Use klint when "please follow AGENTS.md" is not strong enough. Put the rule in `klint.yaml`, run `klint`, and make the constraint executable.
8
8
 
9
- Rules give your agent freedom. Without constraints, every decision is a risk. With klint, your agent knows exactly where it can move fast — and where it can't.
9
+ ## Install
10
10
 
11
- ## Architecture as Code
12
-
13
- klint's YAML config supports an `arch:` section that lets you define architectural rules declaratively — no code required.
14
-
15
- > **AGENTS.md tells the model what to do. Klint ensures it actually did.**
16
- >
17
- > Instructions in a prompt are a contract with no enforcement. A model that's drifting, context-starved, or just wrong will violate AGENTS.md silently and ship anyway. Klint makes the violation structurally impossible to land — the gate blocks it regardless of what the model thought it understood.
18
-
19
- ### Layers
11
+ klint currently runs on Bun.
20
12
 
21
- Define named file groups once, reference them everywhere:
22
-
23
- ```yaml
24
- arch:
25
- layers:
26
- core: ["src/hooks/lib/**", "src/tools/**"]
27
- skills: ["assets/skills/**"]
13
+ ```sh
14
+ bun add -d @konvert7/klint
28
15
  ```
29
16
 
30
- ### Import boundaries
17
+ Add a script:
31
18
 
32
- ```yaml
33
- arch:
34
- imports:
35
- # deny: block imports from one layer into another
36
- - from: skills
37
- deny: core
38
- message: "Skills must be self-contained and portable"
39
- severity: warn # optional, default: error
40
-
41
- # allow: whitelist mode — anything not listed is denied (npm/node: builtins always pass)
42
- - from: ["src/dao/**"]
43
- allow: ["src/dao/**", "src/prisma/**", "src/types/**"]
44
- message: "DAO may only import from dao, prisma, or types"
45
-
46
- # type-only: allow — import type {} is permitted even when value imports are denied
47
- - from: core
48
- deny: ["src/targets/**"]
49
- type-only: allow
50
- message: "Core must not depend on agent-specific code"
19
+ ```json
20
+ {
21
+ "scripts": {
22
+ "klint": "klint"
23
+ }
24
+ }
51
25
  ```
52
26
 
53
- ### Forbidden patterns
27
+ Then create `klint.yaml` in your project root.
54
28
 
55
- Block literal string patterns inside a scoped layer:
29
+ ## Quickstart
56
30
 
57
31
  ```yaml
58
- arch:
59
- forbidden:
60
- - pattern: "console.log("
61
- in: core
62
- message: "Leaks into the agent event stream — use the hook output API instead"
32
+ # klint.yaml
33
+ include: ["src", "!**/node_modules/**"]
63
34
 
64
- - pattern: "process.exit("
65
- in: ["src/hooks/lib/**"]
66
- message: "Library functions should return or throw, not terminate the process"
67
- ```
68
-
69
- ### Singleton locations
70
-
71
- Enforce that a pattern appears only in one designated file:
35
+ rules:
36
+ no-floating-promise: error
37
+ no-unguarded-json-parse: error
38
+ no-sync-in-async:
39
+ severity: error
40
+ include: ["src/hooks/**"]
72
41
 
73
- ```yaml
74
42
  arch:
75
- singleton:
76
- - pattern: "process.env.PAL_HOME"
77
- only: "src/hooks/lib/paths.ts"
78
- in: ["src/**"] # optional: limit scan scope
79
- message: "Use the paths module"
43
+ layers:
44
+ app: ["src/app/**"]
45
+ data: ["src/data/**"]
46
+ ui: ["src/components/**"]
80
47
 
81
- - pattern: "process.env.API_KEY"
82
- only: "src/lib/auth.ts"
83
- message: "Use the auth module"
48
+ imports:
49
+ - from: ui
50
+ deny: data
51
+ message: "UI components must go through app services, not data modules"
84
52
  ```
85
53
 
86
- ### Agent integration
54
+ Run it:
87
55
 
88
- Wire `--json` into your Stop hook so violations are machine-readable:
56
+ ```sh
57
+ bun run klint
58
+ ```
89
59
 
90
- ```typescript
91
- // .agents/hooks/klint.ts
92
- import { runHook } from "./run-hook";
93
- const exitCode = runHook(["bun", "klint/cli.ts", "--json"]);
94
- process.exit(exitCode);
60
+ For machines and hooks:
61
+
62
+ ```sh
63
+ bun run klint -- --json
95
64
  ```
96
65
 
97
- On errors the hook exits 2 (blocking) and emits:
66
+ `--json` emits structured violations and exits `2` when errors are present.
98
67
 
99
68
  ```json
100
69
  {
101
70
  "violations": [
102
71
  {
103
72
  "rule": "arch/imports",
104
- "file": "assets/skills/telos/tools/update-telos.ts",
105
- "line": 23,
106
- "severity": "warn",
107
- "message": "Skills must be self-contained and portable",
73
+ "file": "src/components/Profile.tsx",
74
+ "line": 3,
75
+ "severity": "error",
76
+ "message": "UI components must go through app services, not data modules",
108
77
  "fix": null
109
78
  }
110
79
  ],
111
- "summary": { "errors": 0, "warnings": 1 }
80
+ "summary": { "errors": 1, "warnings": 0 }
112
81
  }
113
82
  ```
114
83
 
115
- The agent reads the structured violations and fixes them before the session can close.
84
+ ## What Klint Catches
85
+
86
+ klint is for rules that are too project-specific or context-heavy for a formatter.
87
+
88
+ | Problem | Example |
89
+ |---------|---------|
90
+ | Layer boundaries | `src/components/**` must not import from `src/data/**` |
91
+ | Singleton ownership | `process.env.API_KEY` may only appear in `src/lib/auth.ts` |
92
+ | Forbidden patterns | `console.log(` is blocked inside hook libraries |
93
+ | Type-aware mistakes | Promise-returning calls cannot be silently discarded |
94
+ | Repository policy | Custom `klint.rules.ts` rules run with the same severity/config system |
116
95
 
117
- ---
96
+ Rules give agents room to move quickly because the unsafe moves are blocked structurally.
118
97
 
119
- ## Usage
98
+ ## CLI
120
99
 
121
100
  ```sh
122
- bun klint/cli.ts [--config <dir>] [--rules <file>] [--fix] [--json]
101
+ klint [--config <dir>] [--rules <file>] [--fix] [--json]
102
+ klint install-skill [--agents <list>] [--symlink | --copy]
123
103
  ```
124
104
 
125
105
  | Flag | Description |
126
106
  |------|-------------|
127
107
  | `--config <dir>` | Directory containing `klint.yaml` or `klint.config.json` (default: cwd) |
128
- | `--rules <file>` | Path to custom rules file (default: auto-discovered — see below) |
108
+ | `--rules <file>` | Path to custom rules file (default: `<configDir>/klint.rules.ts` when present) |
129
109
  | `--fix` | Apply auto-fixes for fixable violations in-place |
130
- | `--json` | Emit structured JSON to stdout (for agent/CI consumption) |
110
+ | `--json` | Emit structured JSON to stdout for agents and CI |
131
111
 
132
- If `--rules` is omitted, klint looks for `klint.rules.ts` next to the config file. If it exists it is loaded automatically; if it doesn't, no custom rules are used.
112
+ `klint install-skill` installs the bundled rule-authoring skill for agent environments:
113
+
114
+ ```sh
115
+ klint install-skill --agents claude,opencode,cursor,codex --copy
116
+ ```
133
117
 
134
118
  ## Configuration
135
119
 
136
- **`klint.yaml`** — lives at your project root alongside `biome.json` and `knip.json`:
120
+ `klint.yaml` lives at your project root alongside tools like `biome.json`, `tsconfig.json`, and `knip.json`.
137
121
 
138
122
  ```yaml
139
123
  # yaml-language-server: $schema=./klint.schema.yaml
140
124
 
141
- include: ["src", "klint", "!**/node_modules/**"]
125
+ include: ["src", "!**/node_modules/**"]
142
126
  plugins: [sonar]
127
+
143
128
  rules:
144
129
  no-unguarded-json-parse: error
145
- no-sync-in-async:
146
- severity: error
147
- include: ["src/hooks/**"]
148
130
  no-floating-promise: error
149
- my-custom-rule: warn
131
+ no-misused-promises: error
132
+ sonar/prefer-string-replaceall: warn
133
+ my-custom-rule:
134
+ severity: error
135
+ include: ["src/server/**"]
150
136
 
151
137
  arch:
152
138
  layers:
153
- core: ["src/hooks/lib/**", "src/tools/**"]
139
+ server: ["src/server/**"]
140
+ client: ["src/client/**"]
141
+
154
142
  imports:
155
- - from: ["assets/skills/**"]
156
- deny: ["src/**"]
157
- message: "Skills must be self-contained"
158
- severity: warn
143
+ - from: client
144
+ deny: server
145
+ message: "Client code must not import server-only modules"
159
146
  ```
160
147
 
161
- `include` — glob patterns selecting which `.ts` files to lint.
162
- `plugins` — named rule bundles (`"sonar"`) that apply a default set of rules.
163
- `rules` — map of rule name → `"error" | "warn" | "off"` or an options object with `severity` and/or `include`.
164
- `arch` — declarative architecture constraints (see Architecture as Code above).
148
+ `include` selects `.ts` files to lint. Patterns support `**` globs and `!` negation.
165
149
 
166
- A `klint.config.json` fallback is supported for backwards compatibility.
150
+ `plugins` enables bundled rule groups. Today, `sonar` provides focused code-quality rules.
151
+
152
+ `rules` maps rule names to `"error"`, `"warn"`, `"off"`, or an options object with `severity` and/or `include`.
153
+
154
+ `arch` declares architecture constraints without writing a custom rule.
155
+
156
+ `klint.config.json` is still supported for backwards compatibility.
157
+
158
+ ## Architecture as Code
159
+
160
+ The `arch:` section turns project boundaries into executable policy.
161
+
162
+ ### Layers
163
+
164
+ Define named file groups once, then reference them from import rules:
165
+
166
+ ```yaml
167
+ arch:
168
+ layers:
169
+ core: ["src/core/**", "src/lib/**"]
170
+ features: ["src/features/**"]
171
+ adapters: ["src/adapters/**"]
172
+ ```
173
+
174
+ ### Import Boundaries
175
+
176
+ Use `deny` to block one layer from importing another:
177
+
178
+ ```yaml
179
+ arch:
180
+ imports:
181
+ - from: features
182
+ deny: adapters
183
+ message: "Features must use core ports, not adapter implementations"
184
+ severity: error
185
+ ```
186
+
187
+ Use `allow` for whitelist mode. Anything not listed is denied:
188
+
189
+ ```yaml
190
+ arch:
191
+ imports:
192
+ - from: ["src/data/**"]
193
+ allow: ["src/data/**", "src/db/**", "src/types/**"]
194
+ message: "Data modules may only depend on data, db, and types"
195
+ ```
196
+
197
+ Allow type-only imports when runtime imports are forbidden:
198
+
199
+ ```yaml
200
+ arch:
201
+ imports:
202
+ - from: core
203
+ deny: adapters
204
+ type-only: allow
205
+ message: "Core may reference adapter types, but not adapter values"
206
+ ```
207
+
208
+ ### Forbidden Patterns
209
+
210
+ Block literal string patterns inside a scoped layer:
211
+
212
+ ```yaml
213
+ arch:
214
+ forbidden:
215
+ - pattern: "console.log("
216
+ in: ["src/hooks/**"]
217
+ message: "Hook output must go through the structured output API"
218
+
219
+ - pattern: "process.exit("
220
+ in: ["src/lib/**"]
221
+ message: "Libraries should return or throw, not terminate the process"
222
+ ```
223
+
224
+ ### Singleton Locations
225
+
226
+ Enforce that a pattern appears only in one designated file:
227
+
228
+ ```yaml
229
+ arch:
230
+ singleton:
231
+ - pattern: "process.env.API_KEY"
232
+ only: "src/lib/auth.ts"
233
+ in: ["src/**"]
234
+ message: "Use the auth module"
235
+ ```
167
236
 
168
237
  ## Built-in Rules
169
238
 
170
239
  | Rule | Type-aware | Description |
171
- |------|-----------|-------------|
240
+ |------|------------|-------------|
172
241
  | `no-unguarded-json-parse` | No | `JSON.parse()` called outside a try/catch |
173
- | `no-sync-in-async` | No | Sync filesystem calls (`readFileSync` etc.) inside async functions |
174
- | `no-floating-promise` | **Yes** | Promise-returning call whose result is discarded |
175
- | `no-misused-promises` | **Yes** | Async function passed where a sync callback is expected |
242
+ | `no-sync-in-async` | No | Sync filesystem calls inside async functions |
243
+ | `no-floating-promise` | Yes | Promise-returning call whose result is discarded |
244
+ | `no-misused-promises` | Yes | Async function passed where a sync callback is expected |
245
+ | `no-async-predicate` | No | Async predicate passed to array filtering/search methods |
246
+ | `no-date-equality` | No | Date values compared by object identity |
247
+ | `no-optional-chain-on-non-nullable` | Yes | Optional chaining on values TypeScript knows are non-nullable |
248
+ | `no-object-in-template` | Yes | Object values interpolated into template strings |
249
+ | `no-nested-template-literals` | No | Template literals nested inside template literals |
250
+ | `no-consecutive-array-push` | No | Multiple consecutive `push()` calls that should be grouped |
251
+ | `no-string-match` | No | `String#match()` usage better expressed with clearer APIs |
252
+
253
+ ## Plugins
254
+
255
+ Enable plugin rules with `plugins: [sonar]`, then override individual severities in `rules` when needed.
256
+
257
+ | Rule | Description |
258
+ |------|-------------|
259
+ | `sonar/prefer-string-replaceall` | Prefer `replaceAll()` when replacing every string occurrence |
260
+ | `sonar/prefer-string-raw-regexp` | Prefer `String.raw` for regex strings where escaping is easy to misread |
261
+ | `sonar/prefer-string-raw` | Prefer `String.raw` for escape-heavy strings |
262
+ | `sonar/prefer-nullish-coalescing-assign` | Prefer `??=` for nullish assignment patterns |
263
+ | `sonar/no-single-char-class` | Avoid regex character classes with a single character |
264
+ | `sonar/prefer-at` | Prefer `.at()` for relative index access |
265
+
266
+ ## Agent and CI Integration
267
+
268
+ Agent hooks should run klint with `--json` so violations are machine-readable.
269
+
270
+ ```ts
271
+ // .agents/hooks/klint.ts
272
+ import { runHook } from "./run-hook";
273
+
274
+ const exitCode = runHook(["bun", "run", "klint", "--", "--json"]);
275
+ process.exit(exitCode);
276
+ ```
277
+
278
+ In CI, run the same command you use locally:
279
+
280
+ ```sh
281
+ bun install
282
+ bun run klint
283
+ ```
284
+
285
+ The important contract is simple: klint exits `0` when the project is clean, `2` when blocking errors exist, and can emit JSON for agents that need structured feedback.
176
286
 
177
287
  ## Custom Rules
178
288
 
179
- Create `klint.rules.ts` at your project root and export a `Record<string, KlintRule>` as default. Each key is the rule name:
289
+ Create `klint.rules.ts` next to `klint.yaml` and export a `Record<string, KlintRule>` as default. Each key is the rule name.
180
290
 
181
291
  ```ts
182
292
  import { relative } from "node:path";
183
- import type { KlintRule } from "./klint/core/types";
293
+ import type { KlintRule } from "@konvert7/klint/core/types";
184
294
 
185
- const myCustomRule: KlintRule = {
295
+ const noForbiddenPattern: KlintRule = {
186
296
  check({ files, root, fileContents }, violations) {
187
297
  for (const file of files) {
188
298
  const lines = (fileContents.get(file) ?? "").split("\n");
@@ -191,7 +301,7 @@ const myCustomRule: KlintRule = {
191
301
  violations.push({
192
302
  file: relative(root, file),
193
303
  line: i + 1,
194
- message: "Explain what's wrong and how to fix it.",
304
+ message: "Explain what is wrong and how to fix it.",
195
305
  });
196
306
  }
197
307
  }
@@ -200,25 +310,24 @@ const myCustomRule: KlintRule = {
200
310
  };
201
311
 
202
312
  export default {
203
- "my-custom-rule": myCustomRule,
313
+ "no-forbidden-pattern": noForbiddenPattern,
204
314
  };
205
315
  ```
206
316
 
207
- All exported rules run at `"error"` severity by default. Override severity or scope them via `rules` in `klint.yaml` — the same mechanism as built-in rules:
317
+ All exported custom rules run at `"error"` severity by default. Override severity or scope through `rules`:
208
318
 
209
319
  ```yaml
210
320
  rules:
211
- my-custom-rule: warn
212
- my-scoped-rule:
213
- severity: error
214
- include: ["src/hooks/**"]
321
+ no-forbidden-pattern:
322
+ severity: warn
323
+ include: ["src/server/**"]
215
324
  ```
216
325
 
217
- No separate registration step — everything exported from `klint.rules.ts` is picked up automatically.
326
+ No separate registration step is required.
218
327
 
219
- ### Auto-fix support
328
+ ### Auto-fix Support
220
329
 
221
- Add a `fix` field to a violation to make it auto-fixable with `--fix`. The fix replaces a line range with new text:
330
+ Add a `fix` field to a violation to make it auto-fixable with `--fix`. Fixes replace a line range with new text:
222
331
 
223
332
  ```ts
224
333
  violations.push({
@@ -233,21 +342,22 @@ violations.push({
233
342
  });
234
343
  ```
235
344
 
236
- ### Type-aware rules
345
+ ### Type-aware Rules
237
346
 
238
- For rules that need TypeScript's type checker, use `walkAst` from `klint/core/ast`:
347
+ For rules that need the TypeScript AST, use `walkAst`:
239
348
 
240
349
  ```ts
241
350
  import ts from "typescript";
242
- import { walkAst } from "./klint/core/ast";
351
+ import { walkAst } from "@konvert7/klint/core/ast";
352
+ import type { KlintRule } from "@konvert7/klint/core/types";
243
353
 
244
- const myTypeAwareRule: KlintRule = {
245
- check({ files, root, fileContents }, violations) {
354
+ const noSpecificCall: KlintRule = {
355
+ check({ files, fileContents }, violations) {
246
356
  for (const file of files) {
247
357
  const content = fileContents.get(file) ?? "";
248
- walkAst(file, content, (node, src) => {
358
+ walkAst(file, content, (node) => {
249
359
  if (ts.isCallExpression(node)) {
250
- // inspect node using the TypeScript AST
360
+ // Inspect node with the TypeScript AST.
251
361
  }
252
362
  });
253
363
  }
@@ -255,32 +365,13 @@ const myTypeAwareRule: KlintRule = {
255
365
  };
256
366
  ```
257
367
 
258
- ## Scoped includes
259
-
260
- Any rule can be restricted to a file subset via the `include` option. Patterns support `**` globs and negation with `!`:
261
-
262
- ```yaml
263
- no-sync-in-async:
264
- severity: error
265
- include: ["src/hooks/**", "!src/hooks/scripts/**"]
266
- ```
368
+ ## How It Fits
267
369
 
268
- ## Architecture
370
+ klint is not a replacement for Biome, oxlint, ESLint, knip, or TypeScript itself.
269
371
 
270
- ```
271
- klint/
272
- cli.ts — CLI entry point; discovers config + rules, reports violations
273
- core/
274
- types.ts — KlintRule, KlintConfig, ArchConfig, Violation, RuleEntry
275
- runner.ts — runKlint(); resolves files, dispatches rules, calls arch engine
276
- arch.ts — runArchRules(); AST import scanner, layers/imports/forbidden/singleton
277
- ast.ts — walkAst(), createProgram(), nearestFunctionIsAsync(), isInsideTry()
278
- fixer.ts — applyFixes(); bottom-up line-range patch with overlap detection
279
- rules/
280
- index.ts — BUILT_IN_RULES registry
281
- ...
282
- tests/
283
- ...
284
- ```
372
+ - Use TypeScript for type correctness.
373
+ - Use Biome or oxlint for fast syntax and style checks.
374
+ - Use knip for unused files and exports.
375
+ - Use klint for architecture, agent constraints, project policy, and custom type-aware checks.
285
376
 
286
- The `klint/` directory is intentionally decoupled from the rest of the codebase — no imports cross the boundary in either direction. When it has enough rules, it ships as a standalone package.
377
+ The goal is a small, sharp gate: the rules your project cannot afford to leave as prose.
package/cli.ts CHANGED
@@ -283,7 +283,7 @@ function printHelp(): void {
283
283
  "klint — agent harness for TypeScript architecture rules",
284
284
  "",
285
285
  "Usage: klint [--config <dir>] [--rules <file>] [--fix] [--json]",
286
- " klint install-skill [--out <path>]",
286
+ " klint install-skill [--agents <list>] [--symlink | --copy]",
287
287
  "",
288
288
  " --config <dir> directory containing klint.yaml or klint.config.json (default: cwd)",
289
289
  " --rules <file> custom rules file (default: <configDir>/klint.rules.ts if present)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konvert7/klint",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Architecture-as-Code linter for TypeScript. Enforce layer boundaries, import rules, and singleton patterns in YAML.",
5
5
  "type": "module",
6
6
  "bin": {