@7n/rules 1.18.1 → 1.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.19.0] - 2026-07-18
4
+
5
+ ### Added
6
+
7
+ - js/eslint T0-патерн js-eslint-mechanical-text-fix — текстові заміни для suggestion-only правил поза покриттям oxlint/eslint --fix (наразі unicorn/prefer-number-is-safe-integer); зменшує обсяг порушень, що йдуть у LLM-ладдер
8
+
3
9
  ## [1.18.1] - 2026-07-18
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.18.1",
3
+ "version": "1.19.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,30 +3,48 @@ type: JS Module
3
3
  title: fix-eslint.mjs
4
4
  resource: npm/rules/js/eslint/fix-eslint.mjs
5
5
  docgen:
6
- crc: e354d939
6
+ crc: 31afb96e
7
+ model: manual
7
8
  ---
8
9
 
9
10
  ## Огляд
10
11
 
11
- T0-autofix для `js/eslint`: детермінований прогін лінтерів у fix-режимі
12
- (`oxlint --fix` + `eslint --fix`) по файлах, де детектор знайшов порушення. Виправляє лише
13
- авто-fixable правила; усе інше лишається детектору на повторну перевірку, а далі — LLM-ладдеру.
14
- Запис незворотний (поза rollback).
12
+ T0-autofix для `js/eslint`, два патерни в порядку застосування. `js-eslint-autofix`:
13
+ детермінований прогін лінтерів у fix-режимі (`oxlint --fix` + `eslint --fix`) по файлах, де
14
+ детектор знайшов порушення виправляє лише авто-fixable правила. `js-eslint-mechanical-text-fix`:
15
+ для правил, які `--fix` НЕ покриває (suggestion-only у власній реалізації інструментів, перевірено
16
+ емпірично на реальних lint-прогонах), але заміна на позначеному `data.line` текстуально однозначна
17
+ й безпечна без AST-парсингу. Усе, що лишається непокритим — детектору на повторну перевірку, а
18
+ далі LLM-ладдеру. Запис незворотний (поза rollback).
15
19
 
16
20
  ## Поведінка
17
21
 
22
+ `js-eslint-autofix`:
18
23
  - Збирає унікальні файли з порушень концерну, лишає лише js-сімейство (`.mjs/.cjs/.js/.jsx/.ts/.tsx/.vue`).
19
24
  - Послідовно застосовує `oxlint --fix` (CLI) і `eslint --fix` (через API з `outputFixes`).
20
25
  - До списку змінених потрапляють лише файли, чий вміст фактично змінився (порівняння до/після).
21
26
 
27
+ `js-eslint-mechanical-text-fix`:
28
+ - `MECHANICAL_TEXT_FIXES` — реєстр `{ reasons, replace }`: `reasons` покриває обидва формати
29
+ `violation.reason` (eslint `"plugin/rule"`, oxlint `"plugin(rule)"` — той самий rule різні тули
30
+ віддають по-різному); `replace(line)` — чиста функція рядка, `null` якщо шаблон не знайдено.
31
+ - Групує порушення за файлом, для кожного цільового `data.line` (1-indexed) застосовує
32
+ відповідну `replace`; рядок без очікуваного шаблону (файл змінився з моменту detect-у) —
33
+ пропускається без гадання.
34
+ - Наразі покриває `unicorn/prefer-number-is-safe-integer` (`Number.isInteger` → `Number.isSafeInteger`).
35
+
22
36
  ## Публічний API
23
37
 
24
- - `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline.
25
- Єдиний патерн `js-eslint-autofix` спрацьовує, якщо є порушення з прив'язкою до файлу.
38
+ - `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline:
39
+ `js-eslint-autofix` (лінтери в fix-режимі), `js-eslint-mechanical-text-fix` (текстові заміни для
40
+ suggestion-only правил).
26
41
 
27
42
  ## Гарантії поведінки
28
43
 
29
- - Якщо серед порушень немає js-файлів — лінтери не запускаються, змін немає.
30
- - Повертаються лише фактично змінені файли; перед записом кожен реєструється через `recordWrite`.
44
+ - `js-eslint-autofix`: якщо серед порушень немає js-файлів — лінтери не запускаються, змін немає.
45
+ - `js-eslint-mechanical-text-fix`: пише лише файли з реальною зміною рядка (шаблон збігся); не
46
+ чіпає рядки поза `MECHANICAL_TEXT_FIXES`-реєстром.
47
+ - Обидва патерни повертають лише фактично змінені файли; перед записом кожен реєструється через
48
+ `recordWrite`.
31
49
  - Межа CI: модуль належить fix-фазі; у `--no-fix` (CI) не запускається — узгоджено із
32
50
  забороною `oxlint --fix`/`eslint --fix` у CI.
@@ -11,7 +11,7 @@
11
11
  * узгоджено із забороною `oxlint --fix`/`eslint --fix` у CI (js.mdc / lint_js_yml).
12
12
  */
13
13
  import { spawnSync } from 'node:child_process'
14
- import { readFileSync } from 'node:fs'
14
+ import { readFileSync, writeFileSync } from 'node:fs'
15
15
  import { resolve } from 'node:path'
16
16
 
17
17
  import { ESLint } from 'eslint'
@@ -58,6 +58,59 @@ async function runLinterFix(jsFiles, cwd) {
58
58
  return touched
59
59
  }
60
60
 
61
+ /**
62
+ * Реєстр "механічних" правил `js/eslint`: `oxlint --fix`/`eslint --fix` (T0
63
+ * `js-eslint-autofix` вище) їх НЕ покриває (suggestion-only у власній реалізації
64
+ * інструментів — перевірено емпірично: лишаються порушеними після `--fix` на
65
+ * реальних прогонах), але текстуальна заміна на позначеному рядку однозначна й
66
+ * безпечна без повного AST-парсингу — проста заміна ідентифікатора/API-виклику.
67
+ * `reasons` — обидва формати `reason` з `main.mjs` (`f.rule`): eslint (`ruleId`,
68
+ * `"plugin/rule"`) і oxlint (`d.code`, `"plugin(rule)"`) — те саме правило різні
69
+ * тули віддають по-різному.
70
+ * @type {Array<{ reasons: string[], replace: (line: string) => string|null }>}
71
+ */
72
+ const MECHANICAL_TEXT_FIXES = [
73
+ {
74
+ // Number.isInteger(x) не ловить x поза Number.MIN/MAX_SAFE_INTEGER — сама заміна
75
+ // імені методу семантично точна для рядка, що ЛИШЕ так і викликає isInteger.
76
+ reasons: ['unicorn/prefer-number-is-safe-integer', 'unicorn(prefer-number-is-safe-integer)'],
77
+ replace: line =>
78
+ line.includes('Number.isInteger') ? line.replaceAll('Number.isInteger', 'Number.isSafeInteger') : null
79
+ }
80
+ ]
81
+
82
+ /**
83
+ * Знаходить механічний фікс для reason-у порушення (обидва формати tool-у).
84
+ * @param {string} reason `violation.reason`
85
+ * @returns {((line: string) => string|null)|null} replace-функція або null
86
+ */
87
+ function mechanicalFixFor(reason) {
88
+ return MECHANICAL_TEXT_FIXES.find(f => f.reasons.includes(reason))?.replace ?? null
89
+ }
90
+
91
+ /**
92
+ * Застосовує механічні текстові заміни по конкретних рядках (`data.line`, 1-indexed)
93
+ * файлу. Рядок без збігу очікуваного шаблону (файл змінився з моменту detect-у) —
94
+ * пропускається, не гадаємо.
95
+ * @param {string} content вміст файлу
96
+ * @param {Array<{ line: number, replace: (line: string) => string|null }>} targets рядки й replace-функції
97
+ * @returns {string|null} новий вміст або null, якщо жоден рядок не змінився
98
+ */
99
+ function applyMechanicalLineFixes(content, targets) {
100
+ const lines = content.split('\n')
101
+ let changed = false
102
+ for (const { line, replace } of targets) {
103
+ const idx = line - 1
104
+ if (idx < 0 || idx >= lines.length) continue
105
+ const next = replace(lines[idx])
106
+ if (next !== null && next !== lines[idx]) {
107
+ lines[idx] = next
108
+ changed = true
109
+ }
110
+ }
111
+ return changed ? lines.join('\n') : null
112
+ }
113
+
61
114
  /** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
62
115
  export const patterns = [
63
116
  {
@@ -73,5 +126,35 @@ export const patterns = [
73
126
  ? { touchedFiles, message: `oxlint/eslint --fix: ${touchedFiles.length} файл(ів)` }
74
127
  : { touchedFiles: [] }
75
128
  }
129
+ },
130
+ {
131
+ id: 'js-eslint-mechanical-text-fix',
132
+ test: violations => violations.some(v => v.file && v.data?.line && mechanicalFixFor(v.reason)),
133
+ apply: (violations, ctx) => {
134
+ /** @type {Map<string, Array<{ line: number, replace: (line: string) => string|null }>>} */
135
+ const byFile = new Map()
136
+ for (const v of violations) {
137
+ const replace = v.file && v.data?.line ? mechanicalFixFor(v.reason) : null
138
+ if (!replace) continue
139
+ const arr = byFile.get(v.file)
140
+ const target = { line: v.data.line, replace }
141
+ if (arr) arr.push(target)
142
+ else byFile.set(v.file, [target])
143
+ }
144
+ const touchedFiles = []
145
+ for (const [rel, targets] of byFile) {
146
+ const abs = resolve(ctx.cwd, rel)
147
+ const content = readOrNull(abs)
148
+ if (content === null) continue
149
+ const next = applyMechanicalLineFixes(content, targets)
150
+ if (next === null) continue
151
+ ctx.recordWrite?.(abs)
152
+ writeFileSync(abs, next)
153
+ touchedFiles.push(abs)
154
+ }
155
+ return touchedFiles.length > 0
156
+ ? { touchedFiles, message: `механічні текстові заміни: ${touchedFiles.length} файл(ів)` }
157
+ : { touchedFiles: [] }
158
+ }
76
159
  }
77
160
  ]