eslint-plugin-ai-rules 0.2.1 → 0.3.1

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
+ ```
38
+
39
+ Configure the project structure and let the plugin apply its recommended rules:
40
+
41
+ ```ts
42
+ import { defineConfig } from 'eslint/config'
43
+ import aiRules from 'eslint-plugin-ai-rules'
44
+ import tseslint from 'typescript-eslint'
45
+
46
+ export default defineConfig([
47
+ ...tseslint.configs.recommended,
48
+ ...aiRules.configs.projects([
49
+ { rootFolder: '.', preset: 'frontend' },
50
+ ]),
51
+ ])
52
+ ```
53
+
54
+ Monorepo with frontend and backend:
55
+
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
+ ```
65
+
66
+ Project that is neither frontend nor backend:
67
+
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.
32
135
 
33
- # yarn
34
- yarn add -D eslint-plugin-ai-rules eslint typescript
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`.
35
157
 
36
- # pnpm
37
- pnpm add -D eslint-plugin-ai-rules eslint typescript
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.
38
159
 
39
- # bun
40
- bun add -d eslint-plugin-ai-rules eslint typescript
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
+ }
41
171
  ```
42
172
 
43
- ## Quick Start
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.
44
182
 
45
- ### `eslint.config.ts` (recommended) — you define `files`
183
+ `aiRules.configs.projects([...])` generates one flat config per project. Each config:
46
184
 
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`:
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
48
196
 
49
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,768 @@ 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
+ ```
89
251
 
252
+ To override a preset relation, declare only the layer/rule you want to change:
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/shared/services/generated/**'],
426
+ }
427
+ ```
428
+
429
+ ### Manual settings without the factory
139
430
 
140
- ### Custom setup (without `recommended`)
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',
457
+ },
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
+ ]),
518
+ ])
519
+ ```
520
+
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:
534
+
535
+ ```ts
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
+ ],
154
544
  },
155
545
  },
156
546
  ])
157
547
  ```
158
548
 
159
- You can also import rules individually:
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.
550
+
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.
552
+
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
+ All global frontend layers live under `src/shared` (for example `src/shared/components`). Page-local layers remain under `src/pages/*`.
583
+
584
+ | Layer | Can import | Same layer |
585
+ |-------|------------|------------|
586
+ | components | components, hooks, contexts, services, generated, utils, helpers, constants, env | yes |
587
+ | hooks | hooks, contexts, services, generated, utils, helpers, constants, env | yes |
588
+ | contexts | services, generated, utils, helpers, constants, env | no |
589
+ | services | generated, utils, helpers, constants, env | no |
590
+ | generated | — | yes |
591
+ | utils | helpers, constants, env | no |
592
+ | helpers | utils, constants, env | no |
593
+ | constants | — | no |
594
+ | env | — | no |
595
+ | page | globals + same-page locals | yes (same page) |
596
+ | pageComponent | globals + same-page locals (including page main) | yes |
597
+ | pageHook | hooks/contexts/services/generated/utils/helpers/constants/env + same-page local hooks/services/contexts/helpers | yes |
598
+ | pageService | services, generated, utils, helpers, constants, env + same-page pageHelper | no |
599
+ | pageContext | contexts, services, generated, utils, helpers, constants, env + same-page pageService/pageHelper | no |
600
+ | pageHelper | helpers, utils, constants, env | no |
601
+
602
+ Local layers can import global layers. Global layers cannot import page-local layers. `pageHook` cannot import `pageComponent`.
603
+
604
+ `utils` and `helpers` can import each other. If this creates undesirable cycles, remove one direction.
605
+
606
+ ### Same-layer and page scope
160
607
 
161
608
  ```ts
162
- import { filenamePascalCase, sortImports, noMultilineImports } from 'eslint-plugin-ai-rules'
609
+ relations: {
610
+ layers: {
611
+ pageComponent: 'src/pages/*/components',
612
+ pageHook: 'src/pages/*/hooks',
613
+ page: 'src/pages/*',
614
+ },
615
+ scope: {
616
+ capture: 'page',
617
+ layers: ['pageComponent', 'pageHook', 'page'],
618
+ },
619
+ rules: [
620
+ {
621
+ from: 'pageComponent',
622
+ allow: ['pageHook'],
623
+ allowSameLayer: true,
624
+ },
625
+ ],
626
+ }
163
627
  ```
164
628
 
165
- ## Rules
629
+ A file from `src/pages/orders` can import another file from `src/pages/orders`, but not from `src/pages/users`.
166
630
 
167
- Brief overview. See source in `src/rules/` for full logic.
631
+ ### Type-only imports
168
632
 
169
- ### `ai-rules/filename-pascal-case`
633
+ Type-only imports follow the same restrictions by default:
170
634
 
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).
635
+ ```ts
636
+ import type { TUser } from '@/repositories/UserRepository' // blocked
637
+ ```
172
638
 
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`).
639
+ Allow them globally:
179
640
 
180
641
  ```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 ✅
642
+ relations: {
643
+ allowTypeImports: true,
644
+ layers: { /* ... */ },
645
+ rules: [/* ... */],
646
+ }
185
647
  ```
186
648
 
649
+ Or for a specific layer:
650
+
651
+ ```ts
652
+ {
653
+ from: 'controller',
654
+ allow: ['useCase'],
655
+ allowTypeImports: true,
656
+ }
657
+ ```
658
+
659
+ ### Deny rules and messages
660
+
661
+ ```ts
662
+ relations: {
663
+ layers: {
664
+ controller: 'src/http/controllers',
665
+ repository: 'src/repositories',
666
+ },
667
+ rules: [
668
+ {
669
+ from: 'controller',
670
+ deny: ['repository'],
671
+ message: 'Controllers must use use-cases instead of repositories.',
672
+ },
673
+ ],
674
+ }
675
+ ```
676
+
677
+ ### Aliases and TypeScript
678
+
679
+ 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.
680
+
681
+ Make sure the resolver is installed at the project level:
682
+
683
+ ```bash
684
+ npm i -D eslint-import-resolver-typescript
685
+ ```
686
+
687
+ For JS projects without `tsconfig.json`, the Node resolver is used.
688
+
689
+ `relations` uses `eslint-plugin-boundaries` under the hood and enables `boundaries/dependencies`.
690
+
691
+ ## Expected diagnostics
692
+
693
+ Configuration warning:
694
+
695
+ ```text
696
+ [eslint-plugin-ai-rules] Project structure validation is not configured.
697
+ Configure it with aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' | 'backend' }]).
698
+ If this project is not a frontend/backend, use preset: 'other' to disable this warning.
699
+ ```
700
+
701
+ Structure violation:
702
+
703
+ ```text
704
+ src/pages/MyFeature/Dashboard.tsx
705
+ error Folder "MyFeature" inside "src/pages" must match kebab-case. ai-rules/project-structure
706
+ ```
707
+
708
+ Relation violation:
709
+
710
+ ```text
711
+ src/http/controllers/ListProducts.ts
712
+ error Dependencies to elements of type "repository" are not allowed in elements of type "controller". boundaries/dependencies
713
+ ```
714
+
715
+ Custom relation message:
716
+
717
+ ```ts
718
+ {
719
+ from: 'controller',
720
+ deny: ['repository'],
721
+ message: 'Controllers must use use-cases instead of repositories.',
722
+ }
723
+ ```
724
+
725
+ ## Preset reference
726
+
727
+ ### Frontend
728
+
729
+ ```text
730
+ <root>/
731
+ ├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
732
+ │ tsconfig*.json, .env*, Dockerfile, entrypoint.sh, nginx.conf,
733
+ │ index.html, AGENTS.md, README.md, LICENSE
734
+ ├── src/
735
+ │ ├── App.tsx, AppRoutes.tsx, main.tsx, index.css, vite-env.d.ts
736
+ │ ├── shared/
737
+ │ │ ├── components/
738
+ │ │ │ ├── *.tsx
739
+ │ │ │ └── ui/*.{ts,tsx}
740
+ │ │ ├── constants/*.ts
741
+ │ │ ├── contexts/*.tsx
742
+ │ │ ├── env/index.ts
743
+ │ │ ├── helpers/*.ts
744
+ │ │ ├── hooks/*.ts
745
+ │ │ ├── services/
746
+ │ │ │ ├── generated/** (ignored)
747
+ │ │ │ └── utils/*.ts
748
+ │ │ └── utils/*.ts
749
+ │ └── pages/
750
+ │ ├── *.tsx
751
+ │ └── <feature-kebab>/
752
+ │ ├── *.tsx, helpers.ts, schema.ts, types.ts
753
+ │ ├── components/*.tsx
754
+ │ ├── hooks/*.ts
755
+ │ ├── services/*.ts
756
+ │ ├── contexts/*.tsx
757
+ │ ├── helpers/*.ts
758
+ │ └── tabs/<tab-kebab>/
759
+ │ ├── *.tsx
760
+ │ └── <section-kebab>/
761
+ │ ├── *.tsx, *.ts
762
+ │ ├── components/*.tsx
763
+ │ └── contents/
764
+ │ ├── *.tsx
765
+ │ └── components/*.tsx
766
+ ├── e2e/
767
+ │ ├── .auth/** (ignored)
768
+ │ ├── fixtures/*
769
+ │ ├── pages/*.ts
770
+ │ ├── specs/*.spec.ts
771
+ │ └── support/*.ts
772
+ │ └── api/*.ts
773
+ ├── scripts/*.ts
774
+ ├── scripts/types/** (ignored)
775
+ ├── public/** (ignored)
776
+ └── .github/**, .vscode/** (ignored)
777
+ ```
778
+
779
+ ### Backend
780
+
781
+ ```text
782
+ <root>/
783
+ ├── root files: package.json, lockfiles, *.config.{js,mjs,cjs,ts},
784
+ │ tsconfig*.json, .env*, Dockerfile, .dockerignore,
785
+ │ AGENTS.md, README.md, LICENSE
786
+ ├── src/
787
+ │ ├── index.ts, Server.ts, Schemas.ts
788
+ │ ├── env/index.ts
789
+ │ ├── helpers/*.ts
790
+ │ ├── @types/*.d.ts
791
+ │ ├── database/
792
+ │ │ ├── Connection.ts
793
+ │ │ ├── schema/index.ts, *.ts
794
+ │ │ ├── seeds/index.ts
795
+ │ │ └── migrations/** (ignored)
796
+ │ ├── repositories/Setup.ts, *.ts
797
+ │ ├── http/
798
+ │ │ ├── Routes.ts
799
+ │ │ ├── controllers/Health.ts
800
+ │ │ ├── controllers/<feature-kebab>/
801
+ │ │ │ ├── *.ts
802
+ │ │ │ └── <resource-kebab>/*.ts
803
+ │ │ ├── hooks/*.ts
804
+ │ │ └── types/*.d.ts
805
+ │ ├── services/*.ts
806
+ │ ├── services/<service-kebab>/index.ts, *.ts
807
+ │ ├── services/generated/** (ignored)
808
+ │ └── use-case/
809
+ │ ├── Errors.ts
810
+ │ └── <feature-kebab>/
811
+ │ ├── *.ts
812
+ │ └── <resource-kebab>/*.ts
813
+ ├── test/*.test.ts, GlobalSetup.ts, Setup.ts, ProvidedContext.d.ts
814
+ ├── test/Helpers/*.ts
815
+ ├── docs/** (ignored)
816
+ ├── scripts/*.ts
817
+ ├── uploads/** (ignored)
818
+ └── .github/**, .vscode/** (ignored)
819
+ ```
820
+
821
+ ### Other
822
+
823
+ `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.
824
+
825
+ ## Structure DSL
826
+
827
+ ```ts
828
+ type TProjectPreset = 'frontend' | 'backend' | 'other'
829
+
830
+ type TNameCase = 'PascalCase' | 'camelCase' | 'kebab-case' | 'snake_case'
831
+
832
+ interface INameRule {
833
+ case?: TNameCase
834
+ prefix?: string
835
+ suffix?: string
836
+ pattern?: string
837
+ }
838
+
839
+ type TFileRule =
840
+ | string
841
+ | ({ extensions: string[] } & INameRule)
842
+
843
+ interface IStructureNode {
844
+ name?: INameRule
845
+ files?: TFileRule[]
846
+ additionalFiles?: TFileRule[]
847
+ folders?: Record<string, IStructureNode>
848
+ ignore?: boolean
849
+ }
850
+
851
+ interface IProjectStructure extends IStructureNode {
852
+ ignoredPaths?: string[]
853
+ }
854
+ ```
855
+
856
+ ### Folder keys
857
+
858
+ Literal keys match exactly:
859
+
860
+ ```ts
861
+ folders: {
862
+ components: { files: ['*.tsx'] },
863
+ }
864
+ ```
865
+
866
+ Dynamic keys use brackets:
867
+
868
+ ```ts
869
+ folders: {
870
+ '[feature]': {
871
+ name: { case: 'kebab-case' },
872
+ files: ['*.tsx'],
873
+ },
874
+ }
875
+ ```
876
+
877
+ - Only one dynamic folder is allowed per level.
878
+ - `name` applies to dynamic folders.
879
+ - A static folder key cannot have a `name` rule.
880
+ - A dynamic folder without `name` accepts any folder name.
881
+
882
+ ### File rules
883
+
884
+ Strings match exact names or globs:
885
+
886
+ ```ts
887
+ files: [
888
+ 'index.ts',
889
+ '*.spec.ts',
890
+ '*.config.{js,mjs,cjs,ts}',
891
+ ]
892
+ ```
893
+
894
+ Supported glob syntax: `*`, `**`, `?` and `{a,b}`.
895
+
896
+ Object rules require `extensions` and validate the base name:
897
+
898
+ ```ts
899
+ files: [
900
+ {
901
+ extensions: ['.ts', '.tsx'],
902
+ case: 'PascalCase',
903
+ },
904
+ {
905
+ extensions: ['.ts'],
906
+ case: 'PascalCase',
907
+ suffix: 'UseCase',
908
+ },
909
+ {
910
+ extensions: ['.ts'],
911
+ case: 'PascalCase',
912
+ prefix: 'Use',
913
+ },
914
+ {
915
+ extensions: ['.ts'],
916
+ pattern: '^(Create|Read|Update|Delete|List)[A-Z][A-Za-z0-9]*$',
917
+ },
918
+ ]
919
+ ```
920
+
921
+ - `extensions` also supports compound extensions: `.d.ts`, `.test.mjs`, `.spec.ts`.
922
+ - `case`, `prefix`, `suffix` and `pattern` apply to the base name after removing the extension.
923
+ - A file is valid when it matches at least one rule completely.
924
+
925
+ ### Merge behavior
926
+
927
+ | Field | Behavior |
928
+ |-------|----------|
929
+ | `folders` | recursive merge by key; new keys are added |
930
+ | `files` | replaces the preset files at that node |
931
+ | `additionalFiles` | appends to the preset files |
932
+ | `name` | fields override the preset |
933
+ | `ignore` | overrides the preset |
934
+ | `ignoredPaths` | union of preset and custom paths |
935
+
936
+ ## Limitations
937
+
938
+ - Structure validation only runs for files ESLint actually lints.
939
+ - Required empty folders are not detected.
940
+ - Glob matching supports `*`, `**`, `?` and `{a,b}` only.
941
+ - Nested roots are supported, but duplicate roots are rejected.
942
+ - Relation layers must point to folders, not individual files.
943
+ - Scoped layers must use patterns with a wildcard, such as `src/pages/*`.
944
+ - TypeScript aliases in `relations` require the project `tsconfig.json`.
945
+ - Type-only imports follow the same restrictions by default.
946
+ - The backend preset is strict and will flag existing imports that violate AGENTS.md.
947
+ - The frontend preset allows `utils` and `helpers` to import each other, which can create cycles.
948
+ - ESLint versions below 9.22 are not supported.
949
+
950
+ ## Rules
951
+
952
+ ### `ai-rules/project-structure`
953
+
954
+ Validates the configured hierarchy, folder names and file names.
955
+
956
+ - Type: `problem`
957
+ - No autofix
958
+ - Reads `settings['ai-rules'].project` or `settings['ai-rules'].projects`
959
+ - Messages: `folderNotAllowed`, `folderNameMismatch`, `fileNotAllowed`, `fileNameMismatch`
960
+ - Ignores files outside the project root, `ignoredPaths` and `ignore: true` subtrees
961
+
962
+ ### `ai-rules/project-setup`
963
+
964
+ Prints the configuration warning described above.
965
+
966
+ - Type: `suggestion`
967
+ - Enabled as `warn` in `recommended`
968
+ - Does not report lint messages
969
+ - Warns once per process
970
+ - Suppressed by `preset: 'other'`, `frontend`, `backend` or a non-empty custom structure
971
+
972
+ ### `ai-rules/filename-pascal-case`
973
+
974
+ Enforces PascalCase for file names.
975
+
976
+ - Ignores `index.{ts,tsx,js,jsx}`, `*.d.ts`, `*.css` and dotfiles.
977
+ - Options:
978
+ - `ignore`: regex strings matched against the basename and relative path.
979
+ - `ignoreMiddleExtensions`: defaults to `true`, so `ValidationRanges.test.ts` is valid.
980
+
187
981
  ```ts
188
982
  'ai-rules/filename-pascal-case': [
189
983
  'error',
@@ -194,86 +988,224 @@ Enforces `PascalCase` for file names. The rule is file-agnostic: it validates ev
194
988
  ]
195
989
  ```
196
990
 
197
- To scope or disable it, use your own config (as this repo does in `eslint.config.mjs`):
991
+ `projects()` disables this rule because `project-structure` enforces file names.
992
+
993
+ ### `ai-rules/no-multiline-imports`
994
+
995
+ Disallows multiline import declarations. Fixable.
198
996
 
199
997
  ```ts
200
- import { defineConfig } from 'eslint/config'
201
- import { recommended } from './src/configs/recommended'
998
+ // before
999
+ import {
1000
+ b,
1001
+ a,
1002
+ } from 'lodash'
202
1003
 
1004
+ // after --fix
1005
+ import { b, a } from 'lodash'
1006
+ ```
1007
+
1008
+ ### `ai-rules/sort-imports`
1009
+
1010
+ Sorts imports by group and line length descending, collapsing multiline imports. Fixable.
1011
+
1012
+ Groups:
1013
+
1014
+ 1. Directives (`'use client'`, `'use server'`, `'use strict'`)
1015
+ 2. External packages (`react` first, then by line length desc)
1016
+ 3. Internal imports (`.`, `@/`)
1017
+ 4. Re-exports
1018
+
1019
+ ## Bundled rules from other plugins
1020
+
1021
+ `recommended` also registers and applies these rules. They existed before project structure support and remain part of the default setup.
1022
+
1023
+ | Rule | Default | Purpose |
1024
+ |------|---------|---------|
1025
+ | `import-x/no-duplicates` | `error` | Prevent importing the same module twice |
1026
+ | `import-x/no-default-export` | `off` | Default exports are allowed |
1027
+ | `max-len` | `off` | No line length limit |
1028
+ | `@stylistic/semi` | `['error', 'never']` | No semicolons |
1029
+ | `@typescript-eslint/consistent-type-imports` | `['error', { prefer: 'type-imports' }]` | Prefer `import type` |
1030
+ | `@typescript-eslint/no-unused-vars` | `['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }]` | Ignore unused values prefixed with `_` |
1031
+ | `@typescript-eslint/naming-convention` | interfaces `I` prefix, type aliases `T` prefix | Enforce `IUser` and `TUser` style |
1032
+
1033
+ When `relations` is configured, `projects()` also registers `eslint-plugin-boundaries` and enables `boundaries/dependencies` with the generated layer policies.
1034
+
1035
+ The plugin also configures the `import-x` resolver with:
1036
+
1037
+ ```ts
1038
+ extensions: ['.ts', '.tsx', '.d.ts', '.js', '.jsx']
1039
+ ```
1040
+
1041
+ Override any bundled rule after the preset:
1042
+
1043
+ ```ts
203
1044
  export default defineConfig([
204
- ...recommended,
205
- { files: ['src/**/*'], rules: { 'ai-rules/filename-pascal-case': 'off' } },
1045
+ ...aiRules.configs.projects([{ rootFolder: '.', preset: 'frontend' }]),
1046
+ {
1047
+ rules: {
1048
+ '@stylistic/semi': ['error', 'always'],
1049
+ 'import-x/no-default-export': 'off',
1050
+ },
1051
+ },
206
1052
  ])
207
1053
  ```
208
1054
 
209
- ### `ai-rules/no-multiline-imports`
1055
+ ## What runs automatically
210
1056
 
211
- Disallows multiline `ImportDeclaration`.
1057
+ | You use | What is enabled |
1058
+ |---------|-----------------|
1059
+ | `aiRules.configs.recommended` | bundled rules + configuration warning |
1060
+ | `aiRules.configs.projects([...])` | recommended rules + `project-structure` |
1061
+ | `projects([...])` with `preset: 'frontend' \| 'backend'` | preset structure + preset relations |
1062
+ | `projects([...])` with `structure` | structure validation |
1063
+ | `projects([...])` with `relations` | `boundaries/dependencies` |
1064
+ | `projects([...])` with both | structure + relations |
212
1065
 
213
- - **Type:** `layout`, `fixable: code`, `messageId: multilineImport`
214
- - **Fix:** collapses `{`, `}`, `,` and whitespace to single line: `import { b, a } from 'lodash'`
1066
+ `projects()` does **not** configure the TypeScript parser. Pair it with `typescript-eslint` as shown in the examples.
1067
+
1068
+ ## Recommended preset
1069
+
1070
+ `aiRules.configs.recommended` is file-agnostic. It provides:
1071
+
1072
+ - plugins: `@stylistic`, `import-x`, `@typescript-eslint`, `ai-rules`
1073
+ - `settings['import-x/resolver-next']`
1074
+ - the rule set documented above
1075
+
1076
+ It does **not** configure:
1077
+
1078
+ - `files`
1079
+ - `ignores`
1080
+ - parser
1081
+ - language options
1082
+ - globals
1083
+
1084
+ 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.
1085
+
1086
+ If you want the recommended rules without project structure validation, use `projects([{ rootFolder: '.', preset: 'other' }])`.
1087
+
1088
+ ## Troubleshooting
1089
+
1090
+ ### The configuration warning keeps appearing
1091
+
1092
+ Use a valid project configuration:
215
1093
 
216
1094
  ```ts
217
- // ❌
218
- import {
219
- b,
220
- a
221
- } from 'lodash'
1095
+ ...aiRules.configs.projects([
1096
+ { rootFolder: '.', preset: 'frontend' },
1097
+ ])
1098
+ ```
222
1099
 
223
- // ✅ after --fix
224
- import { b, a } from 'lodash'
1100
+ Or explicitly opt out:
1101
+
1102
+ ```ts
1103
+ ...aiRules.configs.projects([
1104
+ { rootFolder: '.', preset: 'other' },
1105
+ ])
225
1106
  ```
226
1107
 
227
- Note: `ai-rules/sort-imports` also collapses multiline; both fixers may run, but `sort-imports` handles the full `Program` sort.
1108
+ A project with only `rootFolder` and an empty `structure` still warns.
228
1109
 
229
- ### `ai-rules/sort-imports`
1110
+ For temporary runs or CI, use:
230
1111
 
231
- Sorts imports by group and line-length descending, collapsing multiline imports.
1112
+ ```bash
1113
+ AI_RULES_SILENT=1 npx eslint .
1114
+ ```
1115
+
1116
+ ### `project-structure` is not running
1117
+
1118
+ Make sure you are using `aiRules.configs.projects([...])` or manually setting `settings['ai-rules'].project` and enabling `ai-rules/project-structure`.
232
1119
 
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
1120
+ ### Type-aware rule error
1121
+
1122
+ `@typescript-eslint/consistent-type-imports` needs the TypeScript parser. Pair the plugin with:
241
1123
 
242
1124
  ```ts
243
- // ❌ before --fix
244
- import { local } from './local'
245
- import { a } from 'lodash'
246
- import React from 'react'
1125
+ ...tseslint.configs.recommended
1126
+ ```
1127
+
1128
+ ### Missing `dist` before lint
247
1129
 
248
- // ✅ after --fix
249
- import React from 'react'
250
- import { a } from 'lodash'
1130
+ This repository imports the local build from `dist`. Consumers should import the published package:
251
1131
 
252
- import { local } from './local'
1132
+ ```ts
1133
+ import aiRules from 'eslint-plugin-ai-rules'
253
1134
  ```
254
1135
 
255
- Supports side-effect `import 'polyfill'`, `import type`, `export * from`, `export { ... } from`.
1136
+ ### File ignored because outside of base path
256
1137
 
257
- ## TypeScript
1138
+ Run ESLint from the project root, or configure the correct `basePath`.
258
1139
 
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`.
1140
+ ### Flat config only
260
1141
 
261
- ## Troubleshooting
1142
+ This plugin exports flat config arrays. It does not support legacy `.eslintrc`.
1143
+
1144
+ ### Filename rule too strict
1145
+
1146
+ Use the `ignore` option or disable `ai-rules/filename-pascal-case` for tool-mandated files:
262
1147
 
263
- **Missing `dist` before lint (repo uses `eslint.config.mjs` → `dist`)**
1148
+ ```ts
1149
+ {
1150
+ files: ['*.config.*', 'scripts/**/*'],
1151
+ rules: {
1152
+ 'ai-rules/filename-pascal-case': 'off',
1153
+ },
1154
+ }
264
1155
  ```
265
- Error: Cannot find module './dist/index.js'
1156
+
1157
+ ## Common mistakes
1158
+
1159
+ ### Using `recommended` without `projects()`
1160
+
1161
+ You get the configuration warning and no project structure validation. Use `projects([...])` or explicitly opt out with `preset: 'other'`.
1162
+
1163
+ ### Forgetting the TypeScript parser
1164
+
1165
+ `aiRules.configs.recommended` does not configure `languageOptions.parser`. Pair it with `tseslint.configs.recommended` or a parser.
1166
+
1167
+ ### Aliases are not enforced in relations
1168
+
1169
+ Make sure the project has `tsconfig.json` with `paths`, and install `eslint-import-resolver-typescript`.
1170
+
1171
+ ### A relation layer does not match
1172
+
1173
+ Layers must point to folders, not files:
1174
+
1175
+ ```ts
1176
+ layers: {
1177
+ controller: 'src/http/controllers', // correct
1178
+ // controller: 'src/http/controllers/*.ts', // avoid
1179
+ }
266
1180
  ```
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
1181
 
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.
1182
+ ### Type-only imports are blocked
1183
+
1184
+ That is the default. Set `allowTypeImports: true` globally or per rule.
1185
+
1186
+ ### An external package or unknown file is not blocked
1187
+
1188
+ `relations` only checks known local layers. External packages, Node core modules, same-layer imports and files outside any layer are allowed.
1189
+
1190
+ ### Duplicate or overlapping roots
271
1191
 
272
- **Flat config only**
273
- This plugin exports flat config arrays. It does not support legacy `.eslintrc`. Use `eslint.config.{js,ts}`.
1192
+ Nested roots are supported, but duplicate roots are rejected. Use the most specific configuration for each project.
274
1193
 
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).
1194
+ ## Migrating from 0.2.x
1195
+
1196
+ 1. Replace `aiRules.configs.recommended` with `aiRules.configs.projects([...])`.
1197
+ 2. Choose a preset: `frontend`, `backend`, or `other`.
1198
+ 3. Set `rootFolder` to the folder containing `package.json`.
1199
+ 4. Add `relations` if you want layer boundaries.
1200
+ 5. Keep pairing with `typescript-eslint`.
1201
+
1202
+ If you intentionally do not want structure validation:
1203
+
1204
+ ```ts
1205
+ ...aiRules.configs.projects([
1206
+ { rootFolder: '.', preset: 'other' },
1207
+ ])
1208
+ ```
277
1209
 
278
1210
  ## Development
279
1211
 
@@ -282,60 +1214,107 @@ git clone <repo>
282
1214
  cd eslint-ai-guard
283
1215
  npm install
284
1216
 
285
- npm run typecheck # tsc --noEmit
286
- npm test # node:test + dist build
287
- npm run lint # eslint .
1217
+ npm run typecheck # tsc --noEmit
1218
+ npm test # node:test against dist
1219
+ npm run lint # eslint .
288
1220
  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
1221
+ npm run build # tsup -> ESM + CJS + declarations
1222
+ npm run dev # tsup --watch
1223
+ npm run verify # typecheck + test + lint + pack
1224
+ npm run test:coverage # c8 coverage with thresholds
1225
+ npm run test:consumer # install packed tarball in a temp project
1226
+ npm run test:package # publint + arethetypeswrong
1227
+ npm run test:release # consumer + package checks
1228
+ npm pack --dry-run # verify published files
293
1229
  ```
294
1230
 
295
- ### Project structure
1231
+ `test:consumer` and `test:package` are release checks and are not part of `verify`. They may use the network.
1232
+
1233
+ The consumer smoke test covers structure validation, relations with aliases and type-only imports, warning/suppression, ESM and CJS. It accepts `ESLINT_VERSION`:
296
1234
 
1235
+ ```bash
1236
+ ESLINT_VERSION=9.22.0 npm run test:consumer
1237
+ ESLINT_VERSION='^10.0.0' npm run test:consumer
297
1238
  ```
298
- eslint-ai-guard/ # package: eslint-plugin-ai-rules
1239
+
1240
+ CI runs `verify`, coverage and consumer checks on Node 20, 22 and 24.
1241
+
1242
+ ### Project structure
1243
+
1244
+ ```text
1245
+ eslint-ai-guard/
299
1246
  ├── src/
300
- │ ├── index.ts # re-exports plugin + recommended
301
- │ ├── plugin.ts # plugin.meta + rules
1247
+ │ ├── index.ts
1248
+ │ ├── plugin.ts
302
1249
  │ ├── configs/
303
- │ │ ├── recommended.ts # flat-config preset (bundled rules)
1250
+ │ │ ├── recommended.ts
1251
+ │ │ ├── projects.ts
304
1252
  │ │ └── index.ts
1253
+ │ ├── types/
1254
+ │ │ └── structure.ts
1255
+ │ ├── structures/
1256
+ │ │ ├── buildRelationsConfig.ts
1257
+ │ │ ├── hasStructureRules.ts
1258
+ │ │ ├── mergeRelations.ts
1259
+ │ │ ├── mergeStructure.ts
1260
+ │ │ ├── validateRelations.ts
1261
+ │ │ ├── validateStructure.ts
1262
+ │ │ └── presets/
1263
+ │ │ ├── frontend.ts
1264
+ │ │ ├── backend.ts
1265
+ │ │ ├── other.ts
1266
+ │ │ ├── helpers.ts
1267
+ │ │ └── index.ts
1268
+ │ ├── utils/
305
1269
  │ └── rules/
306
1270
  │ ├── FilenamePascalCase.ts
307
1271
  │ ├── NoMultilineImports.ts
308
- │ ├── SortImports.ts # also exports getSortedContent
1272
+ │ ├── ProjectSetup.ts
1273
+ │ ├── ProjectStructure.ts
1274
+ │ ├── SortImports.ts
309
1275
  │ └── 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)
1276
+ ├── test/
1277
+ ├── scripts/
1278
+ │ └── TestConsumer.mjs
1279
+ ├── .github/
1280
+ │ └── workflows/
1281
+ │ └── ci.yml
1282
+ ├── eslint/
1283
+ ├── CHANGELOG.md
1284
+ ├── eslint.config.mjs
1285
+ ├── tsconfig.json
1286
+ ├── tsup.config.ts
1287
+ ├── package.json
1288
+ └── dist/
317
1289
  ```
318
1290
 
319
1291
  ### Adding a new rule
320
1292
 
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`
1293
+ 1. Create `src/rules/MyRule.ts`.
1294
+ 2. Export it from `src/rules/index.ts`.
1295
+ 3. Register it in `src/plugin.ts`.
1296
+ 4. Add it to `src/configs/recommended.ts` if it should be enabled by default.
1297
+ 5. Add tests in `test/`.
1298
+ 6. Run `npm run verify`.
1299
+
1300
+ ## Publishing
1301
+
1302
+ 1. Change `version` in `package.json`.
1303
+ 2. Optionally run `yarn build` to check the build and generated version.
1304
+ 3. Run `npm publish`.
1305
+
1306
+ `prepack` runs `yarn build` automatically, so `npm publish` always packs a fresh `dist`. The plugin version is injected from `package.json` during the build, so you do not need to edit `src/plugin.ts`.
327
1307
 
328
- ### Publishing
1308
+ For an extra check before publishing:
329
1309
 
330
1310
  ```bash
331
- 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
1311
+ yarn verify
1312
+ npm pack --dry-run
334
1313
  ```
335
1314
 
336
1315
  ## Legacy
337
1316
 
338
- `eslint/` keeps the three original rule files before packaging (deprecated). Source of truth is now `src/rules/`.
1317
+ `eslint/` keeps the three original rule implementations before packaging. Source of truth is `src/rules/`.
339
1318
 
340
1319
  ## License
341
1320