eslint-plugin-ai-rules 0.2.1 → 0.3.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
@@ -2,61 +2,212 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/eslint-plugin-ai-rules)](https://www.npmjs.com/package/eslint-plugin-ai-rules)
4
4
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
- [![node >=18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](package.json)
6
- [![eslint ^9](https://img.shields.io/badge/eslint-%5E9-4B32C3)](https://eslint.org)
5
+ [![node >=20](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](package.json)
6
+ [![eslint 9.22+ / 10](https://img.shields.io/badge/eslint-%5E9.22%20%7C%7C%20%5E10-4B32C3)](https://eslint.org)
7
7
 
8
- Flat-config only ESLint plugin that bundles useful rules for AI-generated code. Ships a `recommended` preset with all rules enabled as `error` — zero config for consumers.
8
+ Flat-config-only ESLint plugin for AI-generated code. It catches filename/import drift and enforces a project structure you declare, with ready-to-use `frontend` and `backend` presets.
9
9
 
10
- > **ESLint 9+ only.** Uses flat config (`eslint.config.{ts,js}`) and `eslint/config` `defineConfig`. No legacy `.eslintrc` support.
10
+ > **ESLint 9.22+ or 10 only.** Uses flat config (`eslint.config.{js,mjs,ts}`). No legacy `.eslintrc` support.
11
11
 
12
- ## Features
12
+ ## Start here
13
13
 
14
- - **AI Rules rules** — catches typical AI drift: wrong filename casing, unsorted/multiline imports
15
- - **Bundled preset** — `aiRules.configs.recommended` enables custom + `import-x`, `@stylistic`, `typescript-eslint` rules out of the box
16
- - **Autofix** — `sort-imports` and `no-multiline-imports` are `fixable: code` (`eslint --fix`)
17
- - **Dual build** — `dist/index.js` (ESM) + `dist/index.cjs` (CJS) via `tsup`, single-file bundle, `type: module`
14
+ | If your project is... | Use |
15
+ |-----------------------|-----|
16
+ | Frontend only | `preset: 'frontend'` |
17
+ | Backend only | `preset: 'backend'` |
18
+ | Frontend + backend in one repo | two projects with different `rootFolder` |
19
+ | Neither frontend nor backend | `preset: 'other'` |
20
+ | Layered architecture | `relations` (controller → useCase → repository) |
18
21
 
19
- ## Requirements
22
+ Before installing, check:
20
23
 
21
- - Node `>=18` (`package.json` `engines`)
22
- - ESLint `^9.0.0` (peer) — flat config only
23
- - TypeScript `>=5.0.0` (peer)
24
+ - Node.js `>= 20`
25
+ - ESLint `^9.22.0` or `^10.0.0`
26
+ - TypeScript `>= 5.0.0 < 6.1.0`
27
+ - `typescript-eslint` configured as parser
28
+ - `tsconfig.json` if you use aliases like `@/*`
29
+ - `eslint-import-resolver-typescript` installed for TS alias resolution
24
30
 
25
- `@eslint/js`, `eslint-plugin-import-x`, `@stylistic/eslint-plugin`, `typescript-eslint`, `globals` are already `dependencies` — you don't need to install them.
31
+ ## Quick start
26
32
 
27
- ## Installation
33
+ Install:
28
34
 
29
35
  ```bash
30
- # npm
31
- npm i -D eslint-plugin-ai-rules eslint typescript
36
+ npm i -D eslint-plugin-ai-rules eslint typescript typescript-eslint eslint-import-resolver-typescript
37
+ ```
32
38
 
33
- # yarn
34
- yarn add -D eslint-plugin-ai-rules eslint typescript
39
+ Configure the project structure and let the plugin apply its recommended rules:
35
40
 
36
- # pnpm
37
- pnpm add -D eslint-plugin-ai-rules eslint typescript
41
+ ```ts
42
+ import { defineConfig } from 'eslint/config'
43
+ import aiRules from 'eslint-plugin-ai-rules'
44
+ import tseslint from 'typescript-eslint'
38
45
 
39
- # bun
40
- bun add -d eslint-plugin-ai-rules eslint typescript
46
+ export default defineConfig([
47
+ ...tseslint.configs.recommended,
48
+ ...aiRules.configs.projects([
49
+ { rootFolder: '.', preset: 'frontend' },
50
+ ]),
51
+ ])
41
52
  ```
42
53
 
43
- ## Quick Start
54
+ Monorepo with frontend and backend:
44
55
 
45
- ### `eslint.config.ts` (recommended) — you define `files`
56
+ ```ts
57
+ export default defineConfig([
58
+ ...tseslint.configs.recommended,
59
+ ...aiRules.configs.projects([
60
+ { rootFolder: 'apps/web', preset: 'frontend' },
61
+ { rootFolder: 'apps/api', preset: 'backend' },
62
+ ]),
63
+ ])
64
+ ```
46
65
 
47
- `recommended` is file-agnostic (only `plugins` + `settings` + `rules`) and does **not** configure a parser. Pair it with `typescript-eslint` (or `@typescript-eslint/parser`) and define `files`:
66
+ Project that is neither frontend nor backend:
48
67
 
49
68
  ```ts
69
+ export default defineConfig([
70
+ ...tseslint.configs.recommended,
71
+ ...aiRules.configs.projects([
72
+ { rootFolder: '.', preset: 'other' },
73
+ ]),
74
+ ])
75
+ ```
76
+
77
+ Run:
78
+
79
+ ```bash
80
+ npx eslint .
81
+ npx eslint --fix .
82
+ ```
83
+
84
+ ## Contents
85
+
86
+ - [Configuration warning](#configuration-warning)
87
+ - [Features](#features)
88
+ - [Requirements](#requirements)
89
+ - [Project structure](#project-structure)
90
+ - [Examples](#examples)
91
+ - [Architecture relations](#architecture-relations)
92
+ - [Expected diagnostics](#expected-diagnostics)
93
+ - [Preset reference](#preset-reference)
94
+ - [Structure DSL](#structure-dsl)
95
+ - [Limitations](#limitations)
96
+ - [Rules](#rules)
97
+ - [What runs automatically](#what-runs-automatically)
98
+ - [Recommended preset](#recommended-preset)
99
+ - [Troubleshooting](#troubleshooting)
100
+ - [Common mistakes](#common-mistakes)
101
+ - [Migrating from 0.2.x](#migrating-from-02x)
102
+ - [Development](#development)
103
+ - [Publishing](#publishing)
104
+ - [Legacy](#legacy)
105
+
106
+ ## Configuration warning
107
+
108
+ If you install the plugin and use `aiRules.configs.recommended` without configuring the project structure, the plugin prints a warning the first time it lints a file:
109
+
110
+ ```text
111
+ [eslint-plugin-ai-rules] Project structure validation is not configured.
112
+ Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).
113
+ If this project is not a frontend/backend, use preset: 'other' to disable this warning.
114
+ ```
115
+
116
+ The warning exists only to make sure the new structure rules are actually configured. It is printed once per process.
117
+
118
+ It disappears when you provide a valid configuration:
119
+
120
+ - `preset: 'frontend'`
121
+ - `preset: 'backend'`
122
+ - `preset: 'other'`
123
+ - a custom non-empty `structure`
124
+ - a `relations` configuration
125
+
126
+ A project containing only `rootFolder` and an empty `structure` is still considered unconfigured and will warn.
127
+
128
+ For CI or temporary runs, silence the warning without changing the config:
129
+
130
+ ```bash
131
+ AI_RULES_SILENT=1 npx eslint .
132
+ ```
133
+
134
+ The warning is still recommended as a setup check, so prefer `preset: 'other'` for projects that intentionally do not use structure validation.
135
+
136
+ ## Features
137
+
138
+ - **Project structure validation** — folder hierarchy, dynamic folders, folder/file casing, prefixes, suffixes and regex patterns.
139
+ - **Frontend/backend presets** — ready-to-use structures and architecture relations based on real projects.
140
+ - **Custom structure** — declare exactly how your project must be organized.
141
+ - **Architecture relations** — enforce which layers can import which other layers, including global vs page-local scope.
142
+ - **Monorepo friendly** — one `rootFolder` per project, including nested roots.
143
+ - **Autofix** — `sort-imports` and `no-multiline-imports` are fixable.
144
+ - **Bundled rules** — ships with useful `import-x`, `@stylistic`, `boundaries` and `@typescript-eslint` rules.
145
+ - **ESM + CJS** — `dist/index.js` and `dist/index.cjs`, with correct type declarations for both.
146
+
147
+ ## Requirements
148
+
149
+ | Tool | Version |
150
+ |------|---------|
151
+ | Node.js | `>= 20` |
152
+ | ESLint | `^9.22.0 || ^10.0.0` |
153
+ | TypeScript | `>= 5.0.0 < 6.1.0` |
154
+ | ESLint resolver | `eslint-import-resolver-typescript` `^4.4.5` |
155
+
156
+ Runtime dependencies are `@stylistic/eslint-plugin`, `eslint-plugin-import-x`, `eslint-plugin-boundaries` and `typescript-eslint`.
157
+
158
+ `eslint-import-resolver-typescript` is a peer dependency used by `relations` to resolve TS aliases. npm installs peers automatically; with Yarn 1 or pnpm, install it explicitly.
159
+
160
+ ## Project structure
161
+
162
+ Each project definition has this shape:
163
+
164
+ ```ts
165
+ interface IProjectDefinition {
166
+ rootFolder: string
167
+ preset?: 'frontend' | 'backend' | 'other'
168
+ structure?: IProjectStructure
169
+ relations?: IRelationsConfig
170
+ }
171
+ ```
172
+
173
+ - `rootFolder` is the project root, usually the folder containing `package.json`.
174
+ - `rootFolder` is relative to the current working directory.
175
+ - Absolute paths inside the current working directory are also accepted.
176
+ - Nested roots are supported; the most specific root wins.
177
+ - Duplicate roots are rejected.
178
+ - `preset` provides a base structure.
179
+ - `structure` is merged on top of the preset.
180
+ - `relations` enables architecture dependency policies with `eslint-plugin-boundaries`.
181
+ - `preset: 'other'` disables structure validation unless a custom `structure` is provided.
182
+
183
+ `aiRules.configs.projects([...])` generates one flat config per project. Each config:
184
+
185
+ - scopes `files` to the project root;
186
+ - sets `settings['ai-rules'].project`;
187
+ - enables `ai-rules/project-structure`;
188
+ - adds `ignores` from `ignoredPaths`;
189
+ - includes the recommended rules;
190
+ - disables `ai-rules/filename-pascal-case`, because file naming is handled by `project-structure`;
191
+ - when `relations` is present, registers `boundaries` and enables `boundaries/dependencies`.
192
+
193
+ ## Examples
194
+
195
+ ### Frontend only
196
+
197
+ ```ts
198
+ import { defineConfig } from 'eslint/config'
50
199
  import aiRules from 'eslint-plugin-ai-rules'
51
200
  import tseslint from 'typescript-eslint'
52
201
 
53
- export default [
202
+ export default defineConfig([
54
203
  ...tseslint.configs.recommended,
55
- { files: ['**/*.{ts,tsx}'], ...aiRules.configs.recommended[0] },
56
- ]
204
+ ...aiRules.configs.projects([
205
+ { rootFolder: '.', preset: 'frontend' },
206
+ ]),
207
+ ])
57
208
  ```
58
209
 
59
- Alternative with `defineConfig` (ESLint >= 9.22):
210
+ ### Frontend and backend in the same repository
60
211
 
61
212
  ```ts
62
213
  import { defineConfig } from 'eslint/config'
@@ -65,125 +216,762 @@ import tseslint from 'typescript-eslint'
65
216
 
66
217
  export default defineConfig([
67
218
  ...tseslint.configs.recommended,
68
- { files: ['**/*.{ts,tsx}'], extends: aiRules.configs.recommended },
219
+ ...aiRules.configs.projects([
220
+ { rootFolder: 'packages/web', preset: 'frontend' },
221
+ { rootFolder: 'packages/api', preset: 'backend' },
222
+ ]),
69
223
  ])
70
224
  ```
71
225
 
72
- ### `eslint.config.js` / `eslint.config.mjs`
226
+ ### Backend with preset (copy-paste)
73
227
 
74
- ```js
228
+ ```ts
229
+ import { defineConfig } from 'eslint/config'
75
230
  import aiRules from 'eslint-plugin-ai-rules'
76
231
  import tseslint from 'typescript-eslint'
77
232
 
78
- export default [
233
+ export default defineConfig([
79
234
  ...tseslint.configs.recommended,
80
- { files: ['**/*.{js,ts,tsx}'], ...aiRules.configs.recommended[0] },
81
- ]
235
+ ...aiRules.configs.projects([
236
+ {
237
+ rootFolder: '.',
238
+ preset: 'backend',
239
+ },
240
+ ]),
241
+ ])
82
242
  ```
83
243
 
84
- ### With overrides
244
+ The backend preset includes strict architecture relations. With this config:
85
245
 
86
246
  ```ts
87
- import aiRules from 'eslint-plugin-ai-rules'
88
- import { defineConfig } from 'eslint/config'
247
+ // src/http/controllers/ListProducts.ts
248
+ import { listProducts } from '@/use-case/product/List' // allowed
249
+ import { productRepository } from '@/repositories/ProductRepository' // blocked
250
+ ```
251
+
252
+ To override a preset relation, declare only the layer/rule you want to change:
89
253
 
254
+ ```ts
255
+ relations: {
256
+ rules: [
257
+ { from: 'service', allow: ['generated', 'env'], allowSameLayer: false },
258
+ ],
259
+ }
260
+ ```
261
+
262
+ ### Opting out with `other`
263
+
264
+ ```ts
90
265
  export default defineConfig([
91
- { files: ['**/*.{ts,tsx}'], ...aiRules.configs.recommended[0] },
92
- {
93
- // turn off filename check for tests or generated code
94
- files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', 'scripts/**/*'],
95
- rules: {
96
- 'ai-rules/filename-pascal-case': 'off',
266
+ ...tseslint.configs.recommended,
267
+ ...aiRules.configs.projects([
268
+ { rootFolder: '.', preset: 'other' },
269
+ ]),
270
+ ])
271
+ ```
272
+
273
+ ### Custom structure
274
+
275
+ ```ts
276
+ export default defineConfig([
277
+ ...tseslint.configs.recommended,
278
+ ...aiRules.configs.projects([
279
+ {
280
+ rootFolder: 'apps/api',
281
+ preset: 'backend',
282
+ structure: {
283
+ folders: {
284
+ src: {
285
+ folders: {
286
+ 'use-case': {
287
+ folders: {
288
+ '[feature]': {
289
+ files: [
290
+ {
291
+ case: 'PascalCase',
292
+ suffix: 'UseCase',
293
+ extensions: ['.ts'],
294
+ },
295
+ ],
296
+ },
297
+ },
298
+ },
299
+ },
300
+ },
301
+ },
302
+ },
303
+ },
304
+ ]),
305
+ ])
306
+ ```
307
+
308
+ ### Use case with a suffix
309
+
310
+ ```ts
311
+ structure: {
312
+ folders: {
313
+ src: {
314
+ folders: {
315
+ 'use-case': {
316
+ folders: {
317
+ '[feature]': {
318
+ files: [
319
+ {
320
+ case: 'PascalCase',
321
+ suffix: 'UseCase',
322
+ extensions: ['.ts'],
323
+ },
324
+ ],
325
+ },
326
+ },
327
+ },
328
+ },
97
329
  },
98
330
  },
99
- {
100
- // or disable a bundled rule
101
- rules: {
102
- '@stylistic/semi': 'off',
331
+ }
332
+ ```
333
+
334
+ ### Controller with a suffix
335
+
336
+ ```ts
337
+ structure: {
338
+ folders: {
339
+ src: {
340
+ folders: {
341
+ http: {
342
+ folders: {
343
+ controllers: {
344
+ folders: {
345
+ '[feature]': {
346
+ files: [
347
+ {
348
+ case: 'PascalCase',
349
+ suffix: 'Controller',
350
+ extensions: ['.ts'],
351
+ },
352
+ ],
353
+ },
354
+ },
355
+ },
356
+ },
357
+ },
358
+ },
103
359
  },
104
360
  },
105
- ])
361
+ }
106
362
  ```
107
363
 
108
- Run:
364
+ ### Dynamic folder with prefix, suffix and regex
109
365
 
110
- ```bash
111
- npx eslint .
112
- npx eslint --fix . # autofix for sort-imports / no-multiline-imports
366
+ ```ts
367
+ structure: {
368
+ folders: {
369
+ '[module]': {
370
+ name: {
371
+ prefix: 'feature-',
372
+ suffix: '-module',
373
+ pattern: '^feature-[a-z-]+-module$',
374
+ },
375
+ files: ['index.ts'],
376
+ },
377
+ },
378
+ }
113
379
  ```
114
380
 
115
- ## Configuration
381
+ Valid: `feature-users-module/index.ts`.
116
382
 
117
- ### `recommended` preset
383
+ Invalid: `users-module/index.ts`, `feature-users/index.ts`, `feature-Users-module/index.ts`.
118
384
 
119
- `src/configs/recommended.ts` exports `defineConfig([...])` with **only** `plugins` + `settings` + `rules` (file-agnostic) — you define `files`/`ignores`/`languageOptions`:
385
+ ### Adding files to a preset
120
386
 
121
- - `plugins: { '@stylistic': stylistic, 'import-x': importX, 'ai-rules': plugin }`
122
- - `settings: { 'import-x/resolver-next': ... }` with `extensions: ['.ts','.tsx','.d.ts','.js','.jsx']`
123
- - Rules (all `error` unless noted):
387
+ ```ts
388
+ structure: {
389
+ folders: {
390
+ src: {
391
+ additionalFiles: [
392
+ { case: 'PascalCase', suffix: 'Service', extensions: ['.ts'] },
393
+ ],
394
+ },
395
+ },
396
+ }
397
+ ```
124
398
 
125
- | Rule | Default | Notes |
126
- |------|---------|-------|
127
- | `ai-rules/filename-pascal-case` | `error` | File-agnostic: checks every file ESLint lints. See below |
128
- | `ai-rules/sort-imports` | `error` | Groups + length desc, collapses multiline |
129
- | `ai-rules/no-multiline-imports` | `error` | Single-line imports |
130
- | `import-x/no-duplicates` | `error` | |
131
- | `import-x/no-default-export` | `off` | |
132
- | `max-len` | `off` | |
133
- | `@stylistic/semi` | `['error','never']` | No semicolons |
134
- | `@typescript-eslint/consistent-type-imports` | `['error',{prefer:'type-imports'}]` | |
135
- | `@typescript-eslint/no-unused-vars` | `['error',{argsIgnorePattern:'^_',varsIgnorePattern:'^_'}]` | |
136
- | `@typescript-eslint/naming-convention` | `['error', {interface:'PascalCase' prefix I}, {typeAlias:'PascalCase' prefix T}]` | Disable per-project for `**/*.d.ts` via your own override if needed |
399
+ ### Replacing preset files
137
400
 
138
- `recommended` has no `files`/`ignores` — add them in your `eslint.config.*` (see Quick Start). This repo's `eslint.config.mjs` adds `globalIgnores`, `files: ['**/*.{ts,tsx}']` with `extends: [js.configs.recommended, tseslint.configs.recommended]` + `globals.browser` locally.
401
+ ```ts
402
+ structure: {
403
+ folders: {
404
+ src: {
405
+ files: ['index.ts'],
406
+ },
407
+ },
408
+ }
409
+ ```
410
+
411
+ ### Ignoring a preset subtree
412
+
413
+ ```ts
414
+ structure: {
415
+ folders: {
416
+ e2e: { ignore: true },
417
+ },
418
+ }
419
+ ```
420
+
421
+ ### Ignoring paths globally
422
+
423
+ ```ts
424
+ structure: {
425
+ ignoredPaths: ['dist/**', 'coverage/**', 'src/services/generated/**'],
426
+ }
427
+ ```
139
428
 
140
- ### Custom setup (without `recommended`)
429
+ ### Manual settings without the factory
430
+
431
+ Advanced users can configure the rule directly:
141
432
 
142
433
  ```ts
143
434
  import { plugin } from 'eslint-plugin-ai-rules'
144
- import { defineConfig } from 'eslint/config'
145
435
 
146
- export default defineConfig([
436
+ export default [
147
437
  {
148
- files: ['**/*.{ts,tsx}'],
438
+ files: ['src/**/*.ts'],
149
439
  plugins: { 'ai-rules': plugin },
440
+ settings: {
441
+ 'ai-rules': {
442
+ project: {
443
+ rootFolder: '.',
444
+ preset: 'other',
445
+ structure: {
446
+ folders: {
447
+ src: {
448
+ files: [{ case: 'PascalCase', extensions: ['.ts'] }],
449
+ },
450
+ },
451
+ },
452
+ },
453
+ },
454
+ },
150
455
  rules: {
151
- 'ai-rules/sort-imports': 'error',
152
- 'ai-rules/no-multiline-imports': 'warn',
153
- 'ai-rules/filename-pascal-case': 'off',
456
+ 'ai-rules/project-structure': 'error',
154
457
  },
155
458
  },
459
+ ]
460
+ ```
461
+
462
+ Multiple projects can be passed directly through `settings['ai-rules'].projects`. The rule chooses the most specific root for each file.
463
+
464
+ ## Architecture relations
465
+
466
+ `relations` enforces which layers can import which other layers. It complements `structure`: `structure` validates paths and names, `relations` validates import direction.
467
+
468
+ ```ts
469
+ interface IRelationsRule {
470
+ from: string
471
+ allow?: string[]
472
+ deny?: string[]
473
+ allowSameLayer?: boolean
474
+ allowTypeImports?: boolean
475
+ message?: string
476
+ }
477
+
478
+ interface IRelationsScope {
479
+ capture: string
480
+ layers: string[]
481
+ }
482
+
483
+ interface IRelationsConfig {
484
+ layers: Record<string, string | string[]>
485
+ rules: IRelationsRule[]
486
+ allowTypeImports?: boolean
487
+ scope?: IRelationsScope
488
+ }
489
+ ```
490
+
491
+ Example for a backend:
492
+
493
+ ```ts
494
+ export default defineConfig([
495
+ ...tseslint.configs.recommended,
496
+ ...aiRules.configs.projects([
497
+ {
498
+ rootFolder: 'apps/backend',
499
+ preset: 'other',
500
+ relations: {
501
+ layers: {
502
+ controller: 'src/http/controllers',
503
+ useCase: 'src/use-case',
504
+ repository: 'src/repositories',
505
+ service: 'src/services',
506
+ helper: 'src/helpers',
507
+ },
508
+ rules: [
509
+ { from: 'controller', allow: ['useCase', 'helper'] },
510
+ { from: 'useCase', allow: ['repository', 'service', 'helper'] },
511
+ { from: 'repository', allow: ['helper'] },
512
+ { from: 'service', allow: ['repository', 'helper'] },
513
+ { from: 'helper', allow: [] },
514
+ ],
515
+ },
516
+ },
517
+ ]),
156
518
  ])
157
519
  ```
158
520
 
159
- You can also import rules individually:
521
+ Semantics:
522
+
523
+ - Layers are folder paths relative to `rootFolder`.
524
+ - `allow` means the layer can import only the listed layers.
525
+ - `deny` means the layer cannot import the listed layers.
526
+ - `allow` and `deny` are mutually exclusive per rule.
527
+ - `allowSameLayer` defaults to `true`. Set it to `false` to block imports within the same layer.
528
+ - External packages and Node core modules are allowed automatically.
529
+ - Local files that do not match any layer are ignored by default.
530
+ - Only one rule per `from` layer is allowed.
531
+ - `scope` allows comparing a captured value, such as the current page, to restrict local imports to the same page.
532
+
533
+ Presets already include relation rules. You only need to declare `relations` when you want to add or override layers and rules:
160
534
 
161
535
  ```ts
162
- import { filenamePascalCase, sortImports, noMultilineImports } from 'eslint-plugin-ai-rules'
536
+ aiRules.configs.projects([
537
+ {
538
+ rootFolder: 'apps/backend',
539
+ preset: 'backend',
540
+ relations: {
541
+ rules: [
542
+ { from: 'service', allow: ['generated', 'env'], allowSameLayer: false },
543
+ ],
544
+ },
545
+ },
546
+ ])
163
547
  ```
164
548
 
165
- ## Rules
549
+ The preset backend is strict: controllers only use use-cases plus infrastructure, use-cases cannot access database/helpers directly, repositories cannot import repositories, and services cannot import services. Structural exceptions exist for `generated` and `database/schema`. Service subfolders are generic modules with their own scope.
166
550
 
167
- Brief overview. See source in `src/rules/` for full logic.
551
+ The preset frontend uses global layers plus page-local scope. Global layers cannot import page-local files. Page-local files can import global layers and files from the same page only.
168
552
 
169
- ### `ai-rules/filename-pascal-case`
553
+ ### Backend preset relations
554
+
555
+ | Layer | Can import | Same layer |
556
+ |-------|------------|------------|
557
+ | controller | useCase, http, generated, helper, env | no |
558
+ | http | useCase, helper, generated | no |
559
+ | useCase | repository, service, serviceModule, generated | yes |
560
+ | repository | database, databaseSchema | no |
561
+ | service | generated, env | no |
562
+ | generated | — | yes |
563
+ | serviceModule | generated, env | yes, within the same module |
564
+ | database | databaseSchema, env | no |
565
+ | databaseSchema | — | yes |
566
+ | helper | — | no |
567
+ | env | — | no |
568
+ | types | — | no |
569
+
570
+ `generated` and `databaseSchema` are structural exceptions because they contain internal files that import each other. Any `src/services/<service-kebab>/` folder is a generic service module: files inside the same module can import each other, different modules cannot.
571
+
572
+ The strict backend preset will report existing violations such as:
573
+
574
+ - use-cases importing `database` or `helpers`;
575
+ - services importing other services;
576
+ - repositories importing other repositories, including `Setup`.
577
+
578
+ These violations are intentional and should be fixed during migration.
579
+
580
+ ### Frontend preset relations
581
+
582
+ | Layer | Can import | Same layer |
583
+ |-------|------------|------------|
584
+ | components | components, hooks, contexts, services, generated, utils, helpers, constants, env | yes |
585
+ | hooks | hooks, contexts, services, generated, utils, helpers, constants, env | yes |
586
+ | contexts | services, generated, utils, helpers, constants, env | no |
587
+ | services | generated, utils, helpers, constants, env | no |
588
+ | generated | — | yes |
589
+ | utils | helpers, constants, env | no |
590
+ | helpers | utils, constants, env | no |
591
+ | constants | — | no |
592
+ | env | — | no |
593
+ | page | globals + same-page locals | yes (same page) |
594
+ | pageComponent | globals + same-page locals (including page main) | yes |
595
+ | pageHook | hooks/contexts/services/generated/utils/helpers/constants/env + same-page local hooks/services/contexts/helpers | yes |
596
+ | pageService | services, generated, utils, helpers, constants, env + same-page pageHelper | no |
597
+ | pageContext | contexts, services, generated, utils, helpers, constants, env + same-page pageService/pageHelper | no |
598
+ | pageHelper | helpers, utils, constants, env | no |
599
+
600
+ Local layers can import global layers. Global layers cannot import page-local layers. `pageHook` cannot import `pageComponent`.
601
+
602
+ `utils` and `helpers` can import each other. If this creates undesirable cycles, remove one direction.
603
+
604
+ ### Same-layer and page scope
605
+
606
+ ```ts
607
+ relations: {
608
+ layers: {
609
+ pageComponent: 'src/pages/*/components',
610
+ pageHook: 'src/pages/*/hooks',
611
+ page: 'src/pages/*',
612
+ },
613
+ scope: {
614
+ capture: 'page',
615
+ layers: ['pageComponent', 'pageHook', 'page'],
616
+ },
617
+ rules: [
618
+ {
619
+ from: 'pageComponent',
620
+ allow: ['pageHook'],
621
+ allowSameLayer: true,
622
+ },
623
+ ],
624
+ }
625
+ ```
626
+
627
+ A file from `src/pages/orders` can import another file from `src/pages/orders`, but not from `src/pages/users`.
628
+
629
+ ### Type-only imports
630
+
631
+ Type-only imports follow the same restrictions by default:
632
+
633
+ ```ts
634
+ import type { TUser } from '@/repositories/UserRepository' // blocked
635
+ ```
636
+
637
+ Allow them globally:
638
+
639
+ ```ts
640
+ relations: {
641
+ allowTypeImports: true,
642
+ layers: { /* ... */ },
643
+ rules: [/* ... */],
644
+ }
645
+ ```
646
+
647
+ Or for a specific layer:
648
+
649
+ ```ts
650
+ {
651
+ from: 'controller',
652
+ allow: ['useCase'],
653
+ allowTypeImports: true,
654
+ }
655
+ ```
656
+
657
+ ### Deny rules and messages
658
+
659
+ ```ts
660
+ relations: {
661
+ layers: {
662
+ controller: 'src/http/controllers',
663
+ repository: 'src/repositories',
664
+ },
665
+ rules: [
666
+ {
667
+ from: 'controller',
668
+ deny: ['repository'],
669
+ message: 'Controllers must use use-cases instead of repositories.',
670
+ },
671
+ ],
672
+ }
673
+ ```
674
+
675
+ ### Aliases and TypeScript
170
676
 
171
- Enforces `PascalCase` for file names. The rule is file-agnostic: it validates every file ESLint decides to lint, following the flat-config `files`/`ignores` defined in the consumer's config (same pattern as other filename rules).
677
+ When the project root has a `tsconfig.json`, the generated config uses `eslint-import-resolver-typescript`, so aliases such as `@/repositories/UserRepository` are resolved automatically.
172
678
 
173
- - **Type:** `suggestion`, no autofix, `messageId: invalid`
174
- - **Ignores:** `index.{ts,tsx,js,jsx}`, `*.d.ts`, `*.css`, dotfiles
175
- - **Checks:** `path.basename` from `getPhysicalFilename()` / `context.filename`, `^[A-Z][a-zA-Z0-9]*$`, `kebab/snake -> PascalCase` conversion
176
- - **Options:**
177
- - `ignore` — array of regex strings matched against the basename and the path relative to `cwd` (same idea as `unicorn/filename-case`). Use it for tool-mandated names like `*.config.*`, tests/specs, generated or vendored code.
178
- - `ignoreMiddleExtensions` — `boolean`, default `true`. When `true`, only the base name before the first dot is validated, so `ValidationRanges.test.ts` is valid and `my-component.test.ts` is renamed to `MyComponent.test.ts`. When `false`, every dot-separated segment must be PascalCase, so `ValidationRanges.test.ts` is renamed to `ValidationRanges.Test.ts` (same idea as `unicorn/filename-case`).
679
+ Make sure the resolver is installed at the project level:
680
+
681
+ ```bash
682
+ npm i -D eslint-import-resolver-typescript
683
+ ```
684
+
685
+ For JS projects without `tsconfig.json`, the Node resolver is used.
686
+
687
+ `relations` uses `eslint-plugin-boundaries` under the hood and enables `boundaries/dependencies`.
688
+
689
+ ## Expected diagnostics
690
+
691
+ Configuration warning:
692
+
693
+ ```text
694
+ [eslint-plugin-ai-rules] Project structure validation is not configured.
695
+ Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).
696
+ If this project is not a frontend/backend, use preset: 'other' to disable this warning.
697
+ ```
698
+
699
+ Structure violation:
700
+
701
+ ```text
702
+ src/pages/MyFeature/Dashboard.tsx
703
+ error Folder "MyFeature" inside "src/pages" must match kebab-case. ai-rules/project-structure
704
+ ```
705
+
706
+ Relation violation:
707
+
708
+ ```text
709
+ src/http/controllers/ListProducts.ts
710
+ error Dependencies to elements of type "repository" are not allowed in elements of type "controller". boundaries/dependencies
711
+ ```
712
+
713
+ Custom relation message:
714
+
715
+ ```ts
716
+ {
717
+ from: 'controller',
718
+ deny: ['repository'],
719
+ message: 'Controllers must use use-cases instead of repositories.',
720
+ }
721
+ ```
722
+
723
+ ## Preset reference
724
+
725
+ ### Frontend
726
+
727
+ ```text
728
+ <root>/
729
+ ├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
730
+ │ tsconfig*.json, .env*, Dockerfile, entrypoint.sh, nginx.conf,
731
+ │ index.html, AGENTS.md, README.md, LICENSE
732
+ ├── src/
733
+ │ ├── App.tsx, AppRoutes.tsx, main.tsx, index.css, vite-env.d.ts
734
+ │ ├── components/*.tsx
735
+ │ ├── components/ui/*.{ts,tsx}
736
+ │ ├── constants/*.ts
737
+ │ ├── contexts/*.tsx
738
+ │ ├── env/index.ts
739
+ │ ├── helpers/*.ts
740
+ │ ├── hooks/*.ts
741
+ │ ├── pages/*.tsx
742
+ │ ├── pages/<feature-kebab>/
743
+ │ │ ├── *.tsx, helpers.ts, schema.ts, types.ts
744
+ │ │ ├── components/*.tsx
745
+ │ │ ├── hooks/*.ts
746
+ │ │ ├── services/*.ts
747
+ │ │ ├── contexts/*.tsx
748
+ │ │ ├── helpers/*.ts
749
+ │ │ └── tabs/<tab-kebab>/
750
+ │ │ ├── *.tsx
751
+ │ │ └── <section-kebab>/
752
+ │ │ ├── *.tsx, *.ts
753
+ │ │ ├── components/*.tsx
754
+ │ │ └── contents/
755
+ │ │ ├── *.tsx
756
+ │ │ └── components/*.tsx
757
+ │ ├── services/generated/** (ignored)
758
+ │ ├── services/utils/*.ts
759
+ │ └── utils/*.ts
760
+ ├── e2e/
761
+ │ ├── .auth/** (ignored)
762
+ │ ├── fixtures/*
763
+ │ ├── pages/*.ts
764
+ │ ├── specs/*.spec.ts
765
+ │ └── support/*.ts
766
+ │ └── api/*.ts
767
+ ├── scripts/*.ts
768
+ ├── scripts/types/** (ignored)
769
+ ├── public/** (ignored)
770
+ └── .github/**, .vscode/** (ignored)
771
+ ```
772
+
773
+ ### Backend
774
+
775
+ ```text
776
+ <root>/
777
+ ├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
778
+ │ tsconfig*.json, .env*, Dockerfile, .dockerignore,
779
+ │ AGENTS.md, README.md, LICENSE
780
+ ├── src/
781
+ │ ├── index.ts, Server.ts, Schemas.ts
782
+ │ ├── env/index.ts
783
+ │ ├── helpers/*.ts
784
+ │ ├── @types/*.d.ts
785
+ │ ├── database/
786
+ │ │ ├── Connection.ts
787
+ │ │ ├── schema/index.ts, *.ts
788
+ │ │ ├── seeds/index.ts
789
+ │ │ └── migrations/** (ignored)
790
+ │ ├── repositories/Setup.ts, *.ts
791
+ │ ├── http/
792
+ │ │ ├── Routes.ts
793
+ │ │ ├── controllers/Health.ts
794
+ │ │ ├── controllers/<feature-kebab>/
795
+ │ │ │ ├── *.ts
796
+ │ │ │ └── <resource-kebab>/*.ts
797
+ │ │ ├── hooks/*.ts
798
+ │ │ └── types/*.d.ts
799
+ │ ├── services/*.ts
800
+ │ ├── services/<service-kebab>/index.ts, *.ts
801
+ │ ├── services/generated/** (ignored)
802
+ │ └── use-case/
803
+ │ ├── Errors.ts
804
+ │ └── <feature-kebab>/
805
+ │ ├── *.ts
806
+ │ └── <resource-kebab>/*.ts
807
+ ├── test/*.test.ts, GlobalSetup.ts, Setup.ts, ProvidedContext.d.ts
808
+ ├── test/Helpers/*.ts
809
+ ├── docs/** (ignored)
810
+ ├── scripts/*.ts
811
+ ├── uploads/** (ignored)
812
+ └── .github/**, .vscode/** (ignored)
813
+ ```
814
+
815
+ ### Other
816
+
817
+ `preset: 'other'` has an empty structure. It disables validation and suppresses the configuration warning. If you provide a `structure`, it validates exactly what you declared.
818
+
819
+ ## Structure DSL
179
820
 
180
821
  ```ts
181
- // my-component.ts ❌ Rename to "MyComponent.ts"
182
- // ValidationRanges.test.ts ✅ (ignoreMiddleExtensions: true, default)
183
- // ValidationRanges.test.ts ❌ Rename to "ValidationRanges.Test.ts" (ignoreMiddleExtensions: false)
184
- // MyComponent.ts ✅
822
+ type TProjectPreset = 'frontend' | 'backend' | 'other'
823
+
824
+ type TNameCase = 'PascalCase' | 'camelCase' | 'kebab-case' | 'snake_case'
825
+
826
+ interface INameRule {
827
+ case?: TNameCase
828
+ prefix?: string
829
+ suffix?: string
830
+ pattern?: string
831
+ }
832
+
833
+ type TFileRule =
834
+ | string
835
+ | ({ extensions: string[] } & INameRule)
836
+
837
+ interface IStructureNode {
838
+ name?: INameRule
839
+ files?: TFileRule[]
840
+ additionalFiles?: TFileRule[]
841
+ folders?: Record<string, IStructureNode>
842
+ ignore?: boolean
843
+ }
844
+
845
+ interface IProjectStructure extends IStructureNode {
846
+ ignoredPaths?: string[]
847
+ }
185
848
  ```
186
849
 
850
+ ### Folder keys
851
+
852
+ Literal keys match exactly:
853
+
854
+ ```ts
855
+ folders: {
856
+ components: { files: ['*.tsx'] },
857
+ }
858
+ ```
859
+
860
+ Dynamic keys use brackets:
861
+
862
+ ```ts
863
+ folders: {
864
+ '[feature]': {
865
+ name: { case: 'kebab-case' },
866
+ files: ['*.tsx'],
867
+ },
868
+ }
869
+ ```
870
+
871
+ - Only one dynamic folder is allowed per level.
872
+ - `name` applies to dynamic folders.
873
+ - A static folder key cannot have a `name` rule.
874
+ - A dynamic folder without `name` accepts any folder name.
875
+
876
+ ### File rules
877
+
878
+ Strings match exact names or globs:
879
+
880
+ ```ts
881
+ files: [
882
+ 'index.ts',
883
+ '*.spec.ts',
884
+ '*.config.{js,mjs,cjs,ts}',
885
+ ]
886
+ ```
887
+
888
+ Supported glob syntax: `*`, `**`, `?` and `{a,b}`.
889
+
890
+ Object rules require `extensions` and validate the base name:
891
+
892
+ ```ts
893
+ files: [
894
+ {
895
+ extensions: ['.ts', '.tsx'],
896
+ case: 'PascalCase',
897
+ },
898
+ {
899
+ extensions: ['.ts'],
900
+ case: 'PascalCase',
901
+ suffix: 'UseCase',
902
+ },
903
+ {
904
+ extensions: ['.ts'],
905
+ case: 'PascalCase',
906
+ prefix: 'Use',
907
+ },
908
+ {
909
+ extensions: ['.ts'],
910
+ pattern: '^(Create|Read|Update|Delete|List)[A-Z][A-Za-z0-9]*$',
911
+ },
912
+ ]
913
+ ```
914
+
915
+ - `extensions` also supports compound extensions: `.d.ts`, `.test.mjs`, `.spec.ts`.
916
+ - `case`, `prefix`, `suffix` and `pattern` apply to the base name after removing the extension.
917
+ - A file is valid when it matches at least one rule completely.
918
+
919
+ ### Merge behavior
920
+
921
+ | Field | Behavior |
922
+ |-------|----------|
923
+ | `folders` | recursive merge by key; new keys are added |
924
+ | `files` | replaces the preset files at that node |
925
+ | `additionalFiles` | appends to the preset files |
926
+ | `name` | fields override the preset |
927
+ | `ignore` | overrides the preset |
928
+ | `ignoredPaths` | union of preset and custom paths |
929
+
930
+ ## Limitations
931
+
932
+ - Structure validation only runs for files ESLint actually lints.
933
+ - Required empty folders are not detected.
934
+ - Glob matching supports `*`, `**`, `?` and `{a,b}` only.
935
+ - Nested roots are supported, but duplicate roots are rejected.
936
+ - Relation layers must point to folders, not individual files.
937
+ - Scoped layers must use patterns with a wildcard, such as `src/pages/*`.
938
+ - TypeScript aliases in `relations` require the project `tsconfig.json`.
939
+ - Type-only imports follow the same restrictions by default.
940
+ - The backend preset is strict and will flag existing imports that violate AGENTS.md.
941
+ - The frontend preset allows `utils` and `helpers` to import each other, which can create cycles.
942
+ - ESLint versions below 9.22 are not supported.
943
+
944
+ ## Rules
945
+
946
+ ### `ai-rules/project-structure`
947
+
948
+ Validates the configured hierarchy, folder names and file names.
949
+
950
+ - Type: `problem`
951
+ - No autofix
952
+ - Reads `settings['ai-rules'].project` or `settings['ai-rules'].projects`
953
+ - Messages: `folderNotAllowed`, `folderNameMismatch`, `fileNotAllowed`, `fileNameMismatch`
954
+ - Ignores files outside the project root, `ignoredPaths` and `ignore: true` subtrees
955
+
956
+ ### `ai-rules/project-setup`
957
+
958
+ Prints the configuration warning described above.
959
+
960
+ - Type: `suggestion`
961
+ - Enabled as `warn` in `recommended`
962
+ - Does not report lint messages
963
+ - Warns once per process
964
+ - Suppressed by `preset: 'other'`, `frontend`, `backend` or a non-empty custom structure
965
+
966
+ ### `ai-rules/filename-pascal-case`
967
+
968
+ Enforces PascalCase for file names.
969
+
970
+ - Ignores `index.{ts,tsx,js,jsx}`, `*.d.ts`, `*.css` and dotfiles.
971
+ - Options:
972
+ - `ignore`: regex strings matched against the basename and relative path.
973
+ - `ignoreMiddleExtensions`: defaults to `true`, so `ValidationRanges.test.ts` is valid.
974
+
187
975
  ```ts
188
976
  'ai-rules/filename-pascal-case': [
189
977
  'error',
@@ -194,86 +982,224 @@ Enforces `PascalCase` for file names. The rule is file-agnostic: it validates ev
194
982
  ]
195
983
  ```
196
984
 
197
- To scope or disable it, use your own config (as this repo does in `eslint.config.mjs`):
985
+ `projects()` disables this rule because `project-structure` enforces file names.
986
+
987
+ ### `ai-rules/no-multiline-imports`
988
+
989
+ Disallows multiline import declarations. Fixable.
198
990
 
199
991
  ```ts
200
- import { defineConfig } from 'eslint/config'
201
- import { recommended } from './src/configs/recommended'
992
+ // before
993
+ import {
994
+ b,
995
+ a,
996
+ } from 'lodash'
997
+
998
+ // after --fix
999
+ import { b, a } from 'lodash'
1000
+ ```
1001
+
1002
+ ### `ai-rules/sort-imports`
1003
+
1004
+ Sorts imports by group and line length descending, collapsing multiline imports. Fixable.
1005
+
1006
+ Groups:
1007
+
1008
+ 1. Directives (`'use client'`, `'use server'`, `'use strict'`)
1009
+ 2. External packages (`react` first, then by line length desc)
1010
+ 3. Internal imports (`.`, `@/`)
1011
+ 4. Re-exports
1012
+
1013
+ ## Bundled rules from other plugins
1014
+
1015
+ `recommended` also registers and applies these rules. They existed before project structure support and remain part of the default setup.
202
1016
 
1017
+ | Rule | Default | Purpose |
1018
+ |------|---------|---------|
1019
+ | `import-x/no-duplicates` | `error` | Prevent importing the same module twice |
1020
+ | `import-x/no-default-export` | `off` | Default exports are allowed |
1021
+ | `max-len` | `off` | No line length limit |
1022
+ | `@stylistic/semi` | `['error', 'never']` | No semicolons |
1023
+ | `@typescript-eslint/consistent-type-imports` | `['error', { prefer: 'type-imports' }]` | Prefer `import type` |
1024
+ | `@typescript-eslint/no-unused-vars` | `['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]` | Ignore unused values prefixed with `_` |
1025
+ | `@typescript-eslint/naming-convention` | interfaces `I` prefix, type aliases `T` prefix | Enforce `IUser` and `TUser` style |
1026
+
1027
+ When `relations` is configured, `projects()` also registers `eslint-plugin-boundaries` and enables `boundaries/dependencies` with the generated layer policies.
1028
+
1029
+ The plugin also configures the `import-x` resolver with:
1030
+
1031
+ ```ts
1032
+ extensions: ['.ts', '.tsx', '.d.ts', '.js', '.jsx']
1033
+ ```
1034
+
1035
+ Override any bundled rule after the preset:
1036
+
1037
+ ```ts
203
1038
  export default defineConfig([
204
- ...recommended,
205
- { files: ['src/**/*'], rules: { 'ai-rules/filename-pascal-case': 'off' } },
1039
+ ...aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' }]),
1040
+ {
1041
+ rules: {
1042
+ '@stylistic/semi': ['error', 'always'],
1043
+ 'import-x/no-default-export': 'off',
1044
+ },
1045
+ },
206
1046
  ])
207
1047
  ```
208
1048
 
209
- ### `ai-rules/no-multiline-imports`
1049
+ ## What runs automatically
1050
+
1051
+ | You use | What is enabled |
1052
+ |---------|-----------------|
1053
+ | `aiRules.configs.recommended` | bundled rules + configuration warning |
1054
+ | `aiRules.configs.projects([...])` | recommended rules + `project-structure` |
1055
+ | `projects([...])` with `preset: 'frontend' \| 'backend'` | preset structure + preset relations |
1056
+ | `projects([...])` with `structure` | structure validation |
1057
+ | `projects([...])` with `relations` | `boundaries/dependencies` |
1058
+ | `projects([...])` with both | structure + relations |
1059
+
1060
+ `projects()` does **not** configure the TypeScript parser. Pair it with `typescript-eslint` as shown in the examples.
1061
+
1062
+ ## Recommended preset
1063
+
1064
+ `aiRules.configs.recommended` is file-agnostic. It provides:
1065
+
1066
+ - plugins: `@stylistic`, `import-x`, `@typescript-eslint`, `ai-rules`
1067
+ - `settings['import-x/resolver-next']`
1068
+ - the rule set documented above
1069
+
1070
+ It does **not** configure:
1071
+
1072
+ - `files`
1073
+ - `ignores`
1074
+ - parser
1075
+ - language options
1076
+ - globals
1077
+
1078
+ Pair it with `typescript-eslint` or `@typescript-eslint/parser` and define `files` in your config. `aiRules.configs.projects([...])` already does the file scoping for you.
1079
+
1080
+ If you want the recommended rules without project structure validation, use `projects([{ rootFolder: '.', preset: 'other' }])`.
210
1081
 
211
- Disallows multiline `ImportDeclaration`.
1082
+ ## Troubleshooting
1083
+
1084
+ ### The configuration warning keeps appearing
212
1085
 
213
- - **Type:** `layout`, `fixable: code`, `messageId: multilineImport`
214
- - **Fix:** collapses `{`, `}`, `,` and whitespace to single line: `import { b, a } from 'lodash'`
1086
+ Use a valid project configuration:
215
1087
 
216
1088
  ```ts
217
- // ❌
218
- import {
219
- b,
220
- a
221
- } from 'lodash'
1089
+ ...aiRules.configs.projects([
1090
+ { rootFolder: '.', preset: 'frontend' },
1091
+ ])
1092
+ ```
222
1093
 
223
- // ✅ after --fix
224
- import { b, a } from 'lodash'
1094
+ Or explicitly opt out:
1095
+
1096
+ ```ts
1097
+ ...aiRules.configs.projects([
1098
+ { rootFolder: '.', preset: 'other' },
1099
+ ])
225
1100
  ```
226
1101
 
227
- Note: `ai-rules/sort-imports` also collapses multiline; both fixers may run, but `sort-imports` handles the full `Program` sort.
1102
+ A project with only `rootFolder` and an empty `structure` still warns.
228
1103
 
229
- ### `ai-rules/sort-imports`
1104
+ For temporary runs or CI, use:
1105
+
1106
+ ```bash
1107
+ AI_RULES_SILENT=1 npx eslint .
1108
+ ```
1109
+
1110
+ ### `project-structure` is not running
1111
+
1112
+ Make sure you are using `aiRules.configs.projects([...])` or manually setting `settings['ai-rules'].project` and enabling `ai-rules/project-structure`.
230
1113
 
231
- Sorts imports by group and line-length descending, collapsing multiline imports.
1114
+ ### Type-aware rule error
232
1115
 
233
- - **Type:** `layout`, `fixable: code` (`Program` fixer), `messageId: sortRequired`
234
- - **Groups (in order, each separated by one blank line):**
235
- 1. Directives (`'use client'`, `'use server'`, `'use strict'`)
236
- 2. External (`react` first, then other externals sorted by `line.length` desc → `localeCompare`)
237
- 3. Internal (`.`, `@/` sorted same)
238
- 4. Re-exports (`export { ... } from`, `export * from` sorted same)
239
- - **Rest:** code after imports, joined with `\n\n\n` (two blank lines between imports and code)
240
- - **Directives:** preserved at top while `canAcceptDirectives` is true
1116
+ `@typescript-eslint/consistent-type-imports` needs the TypeScript parser. Pair the plugin with:
241
1117
 
242
1118
  ```ts
243
- // ❌ before --fix
244
- import { local } from './local'
245
- import { a } from 'lodash'
246
- import React from 'react'
1119
+ ...tseslint.configs.recommended
1120
+ ```
1121
+
1122
+ ### Missing `dist` before lint
247
1123
 
248
- // ✅ after --fix
249
- import React from 'react'
250
- import { a } from 'lodash'
1124
+ This repository imports the local build from `dist`. Consumers should import the published package:
251
1125
 
252
- import { local } from './local'
1126
+ ```ts
1127
+ import aiRules from 'eslint-plugin-ai-rules'
253
1128
  ```
254
1129
 
255
- Supports side-effect `import 'polyfill'`, `import type`, `export * from`, `export { ... } from`.
1130
+ ### File ignored because outside of base path
256
1131
 
257
- ## TypeScript
1132
+ Run ESLint from the project root, or configure the correct `basePath`.
258
1133
 
259
- Plugin itself uses `module: ESNext` + `moduleResolution: bundler` (`tsconfig.json`) and **no `.js` extensions** in source imports (e.g., `from './plugin'`). Output is bundled to single `dist/index.js` via `tsup`, so consumers don't need to handle internal paths. For your project, any TS config works; the preset sets `languageOptions.globals: globals.browser` and `import-x/resolver-next`.
1134
+ ### Flat config only
260
1135
 
261
- ## Troubleshooting
1136
+ This plugin exports flat config arrays. It does not support legacy `.eslintrc`.
262
1137
 
263
- **Missing `dist` before lint (repo uses `eslint.config.mjs` → `dist`)**
1138
+ ### Filename rule too strict
1139
+
1140
+ Use the `ignore` option or disable `ai-rules/filename-pascal-case` for tool-mandated files:
1141
+
1142
+ ```ts
1143
+ {
1144
+ files: ['*.config.*', 'scripts/**/*'],
1145
+ rules: {
1146
+ 'ai-rules/filename-pascal-case': 'off',
1147
+ },
1148
+ }
264
1149
  ```
265
- Error: Cannot find module './dist/index.js'
1150
+
1151
+ ## Common mistakes
1152
+
1153
+ ### Using `recommended` without `projects()`
1154
+
1155
+ You get the configuration warning and no project structure validation. Use `projects([...])` or explicitly opt out with `preset: 'other'`.
1156
+
1157
+ ### Forgetting the TypeScript parser
1158
+
1159
+ `aiRules.configs.recommended` does not configure `languageOptions.parser`. Pair it with `tseslint.configs.recommended` or a parser.
1160
+
1161
+ ### Aliases are not enforced in relations
1162
+
1163
+ Make sure the project has `tsconfig.json` with `paths`, and install `eslint-import-resolver-typescript`.
1164
+
1165
+ ### A relation layer does not match
1166
+
1167
+ Layers must point to folders, not files:
1168
+
1169
+ ```ts
1170
+ layers: {
1171
+ controller: 'src/http/controllers', // correct
1172
+ // controller: 'src/http/controllers/*.ts', // avoid
1173
+ }
266
1174
  ```
267
- Fix: `npm run build` first. This repo's `eslint.config.mjs` imports from `dist` (no `jiti`), so `prelint` runs `build` automatically (`package.json` `prelint`).
268
1175
 
269
- **File ignored because outside of base path**
270
- You passed an absolute path outside the config's `basePath`. Run `npx eslint .` from project root or use `npx eslint --no-config-lookup -c ./eslint.config.js ./src/file.ts` with correct cwd.
1176
+ ### Type-only imports are blocked
1177
+
1178
+ That is the default. Set `allowTypeImports: true` globally or per rule.
1179
+
1180
+ ### An external package or unknown file is not blocked
1181
+
1182
+ `relations` only checks known local layers. External packages, Node core modules, same-layer imports and files outside any layer are allowed.
1183
+
1184
+ ### Duplicate or overlapping roots
1185
+
1186
+ Nested roots are supported, but duplicate roots are rejected. Use the most specific configuration for each project.
1187
+
1188
+ ## Migrating from 0.2.x
1189
+
1190
+ 1. Replace `aiRules.configs.recommended` with `aiRules.configs.projects([...])`.
1191
+ 2. Choose a preset: `frontend`, `backend`, or `other`.
1192
+ 3. Set `rootFolder` to the folder containing `package.json`.
1193
+ 4. Add `relations` if you want layer boundaries.
1194
+ 5. Keep pairing with `typescript-eslint`.
271
1195
 
272
- **Flat config only**
273
- This plugin exports flat config arrays. It does not support legacy `.eslintrc`. Use `eslint.config.{js,ts}`.
1196
+ If you intentionally do not want structure validation:
274
1197
 
275
- **Filename rule too strict for tests/configs**
276
- The rule checks every linted file. Test/spec files with a PascalCase base (e.g. `ValidationRanges.test.ts`) pass by default via `ignoreMiddleExtensions: true`. For tool-mandated names like `*.config.*`, generated or vendored code, pass the `ignore` option (`{ ignore: ['\\.config\\.', '^generated/'] }`) or disable `ai-rules/filename-pascal-case` for those files via an override (see Quick Start).
1198
+ ```ts
1199
+ ...aiRules.configs.projects([
1200
+ { rootFolder: '.', preset: 'other' },
1201
+ ])
1202
+ ```
277
1203
 
278
1204
  ## Development
279
1205
 
@@ -282,60 +1208,104 @@ git clone <repo>
282
1208
  cd eslint-ai-guard
283
1209
  npm install
284
1210
 
285
- npm run typecheck # tsc --noEmit
286
- npm test # node:test + dist build
287
- npm run lint # eslint .
1211
+ npm run typecheck # tsc --noEmit
1212
+ npm test # node:test against dist
1213
+ npm run lint # eslint .
288
1214
  npm run lint -- --fix
289
- npm run build # tsup -> dist/index.js + dist/index.cjs + dist/index.d.ts
290
- npm run dev # tsup --watch
291
- npm run verify # typecheck + test + lint + pack
292
- npm pack --dry-run # verify files: dist only
1215
+ npm run build # tsup -> ESM + CJS + declarations
1216
+ npm run dev # tsup --watch
1217
+ npm run verify # typecheck + test + lint + pack
1218
+ npm run test:coverage # c8 coverage with thresholds
1219
+ npm run test:consumer # install packed tarball in a temp project
1220
+ npm run test:package # publint + arethetypeswrong
1221
+ npm run test:release # consumer + package checks
1222
+ npm pack --dry-run # verify published files
293
1223
  ```
294
1224
 
295
- ### Project structure
1225
+ `test:consumer` and `test:package` are release checks and are not part of `verify`. They may use the network.
1226
+
1227
+ The consumer smoke test covers structure validation, relations with aliases and type-only imports, warning/suppression, ESM and CJS. It accepts `ESLINT_VERSION`:
296
1228
 
1229
+ ```bash
1230
+ ESLINT_VERSION=9.22.0 npm run test:consumer
1231
+ ESLINT_VERSION='^10.0.0' npm run test:consumer
297
1232
  ```
298
- eslint-ai-guard/ # package: eslint-plugin-ai-rules
1233
+
1234
+ CI runs `verify`, coverage and consumer checks on Node 20, 22 and 24.
1235
+
1236
+ ### Project structure
1237
+
1238
+ ```text
1239
+ eslint-ai-guard/
299
1240
  ├── src/
300
- │ ├── index.ts # re-exports plugin + recommended
301
- │ ├── plugin.ts # plugin.meta + rules
1241
+ │ ├── index.ts
1242
+ │ ├── plugin.ts
302
1243
  │ ├── configs/
303
- │ │ ├── recommended.ts # flat-config preset (bundled rules)
1244
+ │ │ ├── recommended.ts
1245
+ │ │ ├── projects.ts
304
1246
  │ │ └── index.ts
1247
+ │ ├── types/
1248
+ │ │ └── structure.ts
1249
+ │ ├── structures/
1250
+ │ │ ├── buildRelationsConfig.ts
1251
+ │ │ ├── hasStructureRules.ts
1252
+ │ │ ├── mergeRelations.ts
1253
+ │ │ ├── mergeStructure.ts
1254
+ │ │ ├── validateRelations.ts
1255
+ │ │ ├── validateStructure.ts
1256
+ │ │ └── presets/
1257
+ │ │ ├── frontend.ts
1258
+ │ │ ├── backend.ts
1259
+ │ │ ├── other.ts
1260
+ │ │ ├── helpers.ts
1261
+ │ │ └── index.ts
1262
+ │ ├── utils/
305
1263
  │ └── rules/
306
1264
  │ ├── FilenamePascalCase.ts
307
1265
  │ ├── NoMultilineImports.ts
308
- │ ├── SortImports.ts # also exports getSortedContent
1266
+ │ ├── ProjectSetup.ts
1267
+ │ ├── ProjectStructure.ts
1268
+ │ ├── SortImports.ts
309
1269
  │ └── index.ts
310
- ├── test/ # node:test suites (run against dist)
311
- ├── eslint/ # deprecated original implementations
312
- ├── eslint.config.mjs # dogfooding: ...aiRules.configs.recommended from dist
313
- ├── tsconfig.json # ESNext/bundler, no .js extensions
314
- ├── tsup.config.ts # entry src/index.ts, format cjs+esm, dts, splitting:false
315
- ├── package.json # type module, exports ., files [dist]
316
- └── dist/ # build output (gitignored)
1270
+ ├── test/
1271
+ ├── scripts/
1272
+ │ └── TestConsumer.mjs
1273
+ ├── .github/
1274
+ │ └── workflows/
1275
+ │ └── ci.yml
1276
+ ├── eslint/
1277
+ ├── CHANGELOG.md
1278
+ ├── eslint.config.mjs
1279
+ ├── tsconfig.json
1280
+ ├── tsup.config.ts
1281
+ ├── package.json
1282
+ └── dist/
317
1283
  ```
318
1284
 
319
1285
  ### Adding a new rule
320
1286
 
321
- 1. Create `src/rules/MyRule.ts` (`Rule.RuleModule`, `meta.messages`, `fixable` if needed)
322
- 2. Export it from `src/rules/index.ts`
323
- 3. Register in `src/plugin.ts` (`rules: { 'my-rule': myRule }`)
324
- 4. Enable in `src/configs/recommended.ts` (`'ai-rules/my-rule': 'error'`)
325
- 5. Add a `RuleTester` case in `test/` (add a fixture if needed)
326
- 6. `npm run verify`
1287
+ 1. Create `src/rules/MyRule.ts`.
1288
+ 2. Export it from `src/rules/index.ts`.
1289
+ 3. Register it in `src/plugin.ts`.
1290
+ 4. Add it to `src/configs/recommended.ts` if it should be enabled by default.
1291
+ 5. Add tests in `test/`.
1292
+ 6. Run `npm run verify`.
327
1293
 
328
- ### Publishing
1294
+ ## Publishing
329
1295
 
330
1296
  ```bash
1297
+ npm run verify
1298
+ npm run test:coverage
1299
+ npm run test:release
331
1300
  npm version patch|minor|major
332
- npm publish --access public # prepublishOnly runs verify (typecheck+test+lint+pack)
333
- # or manually: npm run verify && npm publish --access public
1301
+ npm publish --access public
334
1302
  ```
335
1303
 
1304
+ `prepublishOnly` runs `verify` and refuses to publish with any uncommitted, staged or untracked changes.
1305
+
336
1306
  ## Legacy
337
1307
 
338
- `eslint/` keeps the three original rule files before packaging (deprecated). Source of truth is now `src/rules/`.
1308
+ `eslint/` keeps the three original rule implementations before packaging. Source of truth is `src/rules/`.
339
1309
 
340
1310
  ## License
341
1311