@7n/rules 1.7.6 → 1.8.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.8.0] - 2026-07-16
4
+
5
+ ### Added
6
+
7
+ - js/check: T0-фікс для .oxlintrc.json (копіювання/merge канону без LLM-ладдер)
8
+
3
9
  ## [1.7.6] - 2026-07-16
4
10
 
5
11
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.7.6",
3
+ "version": "1.8.0",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,29 +3,46 @@ type: JS Module
3
3
  title: fix-check.mjs
4
4
  resource: npm/rules/js/check/fix-check.mjs
5
5
  docgen:
6
- crc: e3214d8e
6
+ crc: 48182147
7
7
  model: manual
8
8
  ---
9
9
 
10
10
  ## Огляд
11
11
 
12
- T0-autofix для `js/check`: детермінований scaffold/merge `eslint.config.js` за
13
- планом `planEslintConfigFix` (детекція воркспейс-типів node/vue). Раніше ці
14
- порушення йшли у LLM-ладдер, який переписував конфіг цілком і вгадував типи
15
- (інцидент: у vue-монорепо записано `getConfig({ node: ['npm'] })` eslint перестав
16
- обробляти .vue файли).
12
+ T0-autofix для `js/check`: два детерміновані патерни, обидва без LLM.
13
+
14
+ - `js-check-eslint-config` scaffold/merge `eslint.config.js` за планом
15
+ `planEslintConfigFix` (детекція воркспейс-типів node/vue). Раніше ці
16
+ порушення йшли у LLM-ладдер, який переписував конфіг цілком і вгадував типи
17
+ (інцидент: у vue-монорепо записано `getConfig({ node: ['npm'] })` — eslint
18
+ перестав обробляти .vue файли).
19
+ - `js-check-oxlintrc` — scaffold/merge `.oxlintrc.json` за планом
20
+ `planOxlintrcFix` (`../tooling/main.mjs`). Раніше ці порушення теж йшли у
21
+ LLM-ладдер: 15 КБ канону дешева модель не відтворює byte-perfect (verify
22
+ fail), а дорожча не встигає за один rung-таймаут.
17
23
 
18
24
  ## Поведінка
19
25
 
20
- - Патерн `js-check-eslint-config` тригериться лише на reason-и
21
- `eslint-config-missing` / `eslint-config-ignores` / `eslint-config-vue-workspace`;
22
- решта порушень `js/check` (engines, workflows, oxlintrc) ідуть стандартним шляхом.
23
- - `apply` перераховує план із поточного стану диска (ідемпотентно): відсутній
24
- конфіг створюється з детектованими типами; наявний зазнає хірургічного merge,
25
- без повного перезапису. Перед записом викликається `ctx.recordWrite`.
26
- - Якщо план порожній (`null`) жодних змін (`touchedFiles: []`).
26
+ - `js-check-eslint-config` тригериться на reason-и `eslint-config-missing` /
27
+ `eslint-config-ignores` / `eslint-config-vue-workspace`. `apply` перераховує
28
+ план із поточного стану диска (ідемпотентно): відсутній конфіг
29
+ створюється з детектованими типами; наявний зазнає хірургічного merge, без
30
+ повного перезапису. Якщо план порожній (`null`)жодних змін
31
+ (`touchedFiles: []`).
32
+ - `js-check-oxlintrc` тригериться на reason-и `oxlintrc-missing` /
33
+ `oxlintrc-drift` (`OXLINTRC_MISSING` / `OXLINTRC_DRIFT` з `../tooling/main.mjs`).
34
+ `apply` читає наявний `.oxlintrc.json` (`null`, якщо відсутній або невалідний
35
+ JSON) і канон, будує злитий обʼєкт через `planOxlintrcFix` і перезаписує
36
+ файл цілком (JSON, 2-space, з кінцевим переносом рядка).
37
+ - Решта порушень `js/check` (engines, workflows) — поза цим T0, стандартний
38
+ шлях (ladder/manual).
39
+ - Обидва патерни викликають `ctx.recordWrite` перед записом (pre-image для
40
+ central rollback).
27
41
 
28
42
  ## Гарантії поведінки
29
43
 
30
- - Записує лише один файл — `eslint.config.js`/`eslint.config.mjs` у корені репо.
31
- - Наявний конфіг ніколи не перезаписується цілком — тільки точкові вставки/заміни.
44
+ - `js-check-eslint-config` записує лише `eslint.config.js`/`eslint.config.mjs`
45
+ у корені репо; наявний конфіг ніколи не перезаписується цілком — тільки
46
+ точкові вставки/заміни.
47
+ - `js-check-oxlintrc` записує лише `.oxlintrc.json` у корені репо; результат
48
+ завжди проходить `verifyOxlintRcAgainstCanonical` без ручного втручання.
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/js/check/main.mjs
5
5
  docgen:
6
- crc: 0b9c8a32
6
+ crc: 128c6700
7
7
  model: manual
8
8
  ---
9
9
 
@@ -27,7 +27,10 @@ Detector concern-а `js/check`: read-only перевірка відповідн
27
27
  `engines.bun >= 1.3` (кореневий `package.json` валідує Rego
28
28
  `npm/policy/js_lint/package_json/`).
29
29
  - `.oxlintrc.json`: існує, валідний JSON, збігається з каноном oxlint із пакета
30
- `@7n/rules`.
30
+ `@7n/rules` (`verifyOxlintRcAgainstCanonical` з `../tooling/main.mjs`).
31
+ Відсутність — reason `oxlintrc-missing`, розходження з каноном — reason
32
+ `oxlintrc-drift` (константи `OXLINTRC_MISSING`/`OXLINTRC_DRIFT`); обидва
33
+ детерміновано виправляє T0 `fix-check.mjs` (патерн `js-check-oxlintrc`).
31
34
  - `.github/workflows/lint-js.yml`: існує; `lint.yml` (якщо є) не дублює
32
35
  oxlint/eslint/jscpd кроки.
33
36
  - `knip.json`: якщо відсутній — копіюється канонічний baseline із пакета
@@ -1,18 +1,29 @@
1
1
  /** @see ./docs/fix-check.md */
2
2
 
3
3
  /**
4
- * T0-autofix для `js/check` — детермінований scaffold/merge `eslint.config.js`.
5
- * Раніше ці порушення йшли у LLM-ладдер, який переписував конфіг цілком і
6
- * вгадував воркспейс-типи (інцидент: у vue-монорепо записано
7
- * `getConfig({ node: ['npm'] })` — eslint перестав обробляти .vue файли).
8
- * Тепер джерело правди`planEslintConfigFix` (детекція типів із workspaces
9
- * root package.json + vue-залежність/.vue-файли), а наявний конфіг оновлюється точковим merge
10
- * хірургічно, без повного перезапису.
4
+ * T0-autofix для `js/check` — детермінований scaffold/merge `eslint.config.js`
5
+ * і `.oxlintrc.json`. Раніше обидва йшли у LLM-ладдер, який переписував конфіги
6
+ * цілком: для eslint.config.js це вгадувало воркспейс-типи (інцидент: у
7
+ * vue-монорепо записано `getConfig({ node: ['npm'] })` — eslint перестав
8
+ * обробляти .vue файли), а для .oxlintrc.json 15 КБ канону, який дешевій
9
+ * моделі не відтворити byte-perfect (verify fail), а дорожчій не встигнути
10
+ * за один rung-таймаут.
11
11
  *
12
- * Решта порушень js/check (engines, workflows, oxlintrc) поза цим T0,
13
- * стандартний шлях (ladder/manual).
12
+ * Джерело правди для eslint.config.js — `planEslintConfigFix` (детекція типів
13
+ * із workspaces root package.json + vue-залежність/.vue-файли), наявний конфіг
14
+ * оновлюється точковим merge хірургічно, без повного перезапису.
15
+ *
16
+ * Джерело правди для .oxlintrc.json — `planOxlintrcFix`, що дзеркалить
17
+ * `verifyOxlintRcAgainstCanonical`: відсутній файл копіює канон, наявний —
18
+ * доповнюється до канону без втрати project-specific розширень (зайві
19
+ * `rules`-ключі й `ignorePatterns` зберігаються).
20
+ *
21
+ * Решта порушень js/check (engines, workflows) — поза цим T0, стандартний шлях
22
+ * (ladder/manual).
14
23
  */
15
- import { writeFile } from 'node:fs/promises'
24
+ import { existsSync } from 'node:fs'
25
+ import { readFile, writeFile } from 'node:fs/promises'
26
+ import { join } from 'node:path'
16
27
 
17
28
  import {
18
29
  ESLINT_CONFIG_IGNORES,
@@ -20,14 +31,31 @@ import {
20
31
  ESLINT_CONFIG_VUE_WORKSPACE,
21
32
  planEslintConfigFix
22
33
  } from './eslint-config.mjs'
34
+ import { OXLINT_CANONICAL_JSON_PATH, OXLINTRC_DRIFT, OXLINTRC_MISSING, planOxlintrcFix } from '../tooling/main.mjs'
23
35
 
24
- const TRIGGER_REASONS = new Set([ESLINT_CONFIG_MISSING, ESLINT_CONFIG_IGNORES, ESLINT_CONFIG_VUE_WORKSPACE])
36
+ const ESLINT_CONFIG_REASONS = new Set([ESLINT_CONFIG_MISSING, ESLINT_CONFIG_IGNORES, ESLINT_CONFIG_VUE_WORKSPACE])
37
+ const OXLINTRC_REASONS = new Set([OXLINTRC_MISSING, OXLINTRC_DRIFT])
38
+
39
+ /**
40
+ * Читає `.oxlintrc.json`, якщо є й валідний JSON; інакше `null` (трактується
41
+ * як відсутній файл — `planOxlintrcFix` будує з канону).
42
+ * @param {string} oxPath абсолютний шлях до `.oxlintrc.json`
43
+ * @returns {Promise<unknown>} розпарсений вміст або `null`
44
+ */
45
+ async function readOxlintrcOrNull(oxPath) {
46
+ if (!existsSync(oxPath)) return null
47
+ try {
48
+ return JSON.parse(await readFile(oxPath, 'utf8'))
49
+ } catch {
50
+ return null
51
+ }
52
+ }
25
53
 
26
54
  /** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
27
55
  export const patterns = [
28
56
  {
29
57
  id: 'js-check-eslint-config',
30
- test: violations => violations.some(v => TRIGGER_REASONS.has(v.reason)),
58
+ test: violations => violations.some(v => ESLINT_CONFIG_REASONS.has(v.reason)),
31
59
  apply: async (violations, ctx) => {
32
60
  const plan = await planEslintConfigFix(ctx.cwd)
33
61
  if (plan === null) return { touchedFiles: [] }
@@ -35,5 +63,20 @@ export const patterns = [
35
63
  await writeFile(plan.path, plan.content, 'utf8')
36
64
  return { touchedFiles: [plan.path], message: plan.message }
37
65
  }
66
+ },
67
+ {
68
+ id: 'js-check-oxlintrc',
69
+ test: violations => violations.some(v => OXLINTRC_REASONS.has(v.reason)),
70
+ apply: async (violations, ctx) => {
71
+ const oxPath = join(ctx.cwd, '.oxlintrc.json')
72
+ const [actual, canonicalRaw] = await Promise.all([
73
+ readOxlintrcOrNull(oxPath),
74
+ readFile(OXLINT_CANONICAL_JSON_PATH, 'utf8')
75
+ ])
76
+ const merged = planOxlintrcFix(actual, JSON.parse(canonicalRaw))
77
+ ctx.recordWrite?.(oxPath)
78
+ await writeFile(oxPath, `${JSON.stringify(merged, null, 2)}\n`, 'utf8')
79
+ return { touchedFiles: [oxPath], message: '.oxlintrc.json: T0 merge до канону oxlint (@7n/rules)' }
80
+ }
38
81
  }
39
82
  ]
@@ -8,6 +8,8 @@ import { createViolationReporter } from '../../../scripts/lib/lint-surface/viola
8
8
  import {
9
9
  KNIP_CANONICAL_JSON_PATH,
10
10
  OXLINT_CANONICAL_JSON_PATH,
11
+ OXLINTRC_DRIFT,
12
+ OXLINTRC_MISSING,
11
13
  verifyOxlintRcAgainstCanonical
12
14
  } from '../tooling/main.mjs'
13
15
 
@@ -206,7 +208,7 @@ async function checkPackageJsonJsLint(passFn, failFn, cwd) {
206
208
  async function checkOxlintRc(passFn, failFn, cwd) {
207
209
  const oxPath = join(cwd, '.oxlintrc.json')
208
210
  if (!existsSync(oxPath)) {
209
- failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)')
211
+ failFn('.oxlintrc.json не існує — додай конфіг oxlint (js.mdc)', OXLINTRC_MISSING)
210
212
  return
211
213
  }
212
214
  let oxCfg
@@ -229,7 +231,7 @@ async function checkOxlintRc(passFn, failFn, cwd) {
229
231
  passFn('.oxlintrc.json збігається з каноном oxlint (@7n/rules)')
230
232
  } else {
231
233
  for (const msg of oxV.failures) {
232
- failFn(msg)
234
+ failFn(msg, OXLINTRC_DRIFT)
233
235
  }
234
236
  }
235
237
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/js/tooling/main.mjs
5
5
  docgen:
6
- crc: da55182a
6
+ crc: 76437b89
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -12,20 +12,42 @@ docgen:
12
12
 
13
13
  ## Огляд
14
14
 
15
- Властивості, що визначають шляхи до канонічних JSON-конфігурацій для `oxlint` та `knip` доступні через `OXLINT_CANONICAL_JSON_PATH` та `KNIP_CANONICAL_JSON_PATH`. Модуль надає можливість перевірити, чи відповідають конфігурації, визначені у `.oxlintrc.json`, офіційним стандартам, описаним у `oxlint-canonical.json`, за допомогою функції `verifyOxlintRcAgainstCanonical`. Також може бути здійснена валідація згідно з конфігурацією `knip-canonical.json`.
15
+ Шляхи до канонічних JSON-конфігурацій для `oxlint` та `knip` доступні через
16
+ `OXLINT_CANONICAL_JSON_PATH` та `KNIP_CANONICAL_JSON_PATH`. Модуль перевіряє,
17
+ чи відповідає `.oxlintrc.json` канону (`verifyOxlintRcAgainstCanonical`), і
18
+ будує детермінований merge до канону (`planOxlintrcFix`) — джерело правди для
19
+ T0-автофіксу `js/check` (`fix-check.mjs`), без LLM.
16
20
 
17
21
  ## Поведінка
18
22
 
19
- OXLINT_CANONICAL_JSON_PATH — надає шлях до канонічного конфігураційного файлу oxlint.
20
- KNIP_CANONICAL_JSON_PATH надає шлях до канонічного конфігураційного файлу knip.
21
- verifyOxlintRcAgainstCanonical перевіряє, чи відповідає конфігураційний файл .oxlintrc.json канонічним стандартам, порівнюючи його з конфігураціями oxlint-canonical.json.
23
+ - `OXLINT_CANONICAL_JSON_PATH` / `KNIP_CANONICAL_JSON_PATH` шляхи до
24
+ канонічних JSON-конфігів `oxlint`/`knip` у цьому пакеті.
25
+ - `OXLINTRC_MISSING` / `OXLINTRC_DRIFT` стабільні reason-коди для
26
+ порушень `.oxlintrc.json` (відсутній файл / розходження з каноном),
27
+ які `js/check` проставляє через `createViolationReporter`, а T0-патерн
28
+ `js-check-oxlintrc` розпізнає для автофіксу.
29
+ - `verifyOxlintRcAgainstCanonical(cfg, canonical)` — звіряє `.oxlintrc.json`
30
+ проти канону: усі `rules`-ключі канону мають точний збіг значення (зайві
31
+ локальні ключі дозволені), `ignorePatterns` канону мають бути присутні
32
+ (локальні розширення дозволені), решта полів — точний глибокий збіг.
33
+ - `planOxlintrcFix(actual, canonical)` — чиста функція, що дзеркалить
34
+ правила `verifyOxlintRcAgainstCanonical` у зворотний бік: будує обʼєкт
35
+ `.oxlintrc.json`, який гарантовано проходить повторну перевірку.
36
+ Відсутній `actual` (`null`) трактується як порожній файл — результат
37
+ дорівнює канону. Наявний `actual` доповнюється до канону без втрати
38
+ project-specific розширень: зайві `rules`-ключі й `ignorePatterns`
39
+ зберігаються, а канонічні `rules`-значення й поля верхнього рівня
40
+ перезаписуються канонічними (єдине валідне значення для перевірки).
22
41
 
23
42
  ## Публічний API
24
43
 
25
- - OXLINT_CANONICAL_JSON_PATH — Вказує на основний конфіг `oxlint` для підтвердження коректності в цьому пакеті.
26
- - KNIP_CANONICAL_JSON_PATH — Вказує на основний конфіг `knip`, який автоматично додається до кореня проєкту-споживача, якщо його там немає.
27
- - verifyOxlintRcAgainstCanonicalПорівнює конфігураційний файл `.oxlintrc.json` із канонічною версією, переконавшись, що всі правила з канону присутні, а інші поля відповідають.
44
+ - `OXLINT_CANONICAL_JSON_PATH`шлях до канонічного конфігу `oxlint` для перевірки/T0-фіксу.
45
+ - `KNIP_CANONICAL_JSON_PATH`шлях до канонічного конфігу `knip`, що копіюється у корінь проєкту-споживача, якщо відсутній.
46
+ - `OXLINTRC_MISSING` / `OXLINTRC_DRIFT` reason-коди порушень `.oxlintrc.json`.
47
+ - `verifyOxlintRcAgainstCanonical(cfg, canonical)` → `{ ok, failures }` — перевірка `.oxlintrc.json` проти канону.
48
+ - `planOxlintrcFix(actual, canonical)` → злитий обʼєкт `.oxlintrc.json` — детермінований T0-фікс без LLM.
28
49
 
29
50
  ## Гарантії поведінки
30
51
 
31
- - Read-only: не виконує операцій запису (ФС/БД).
52
+ - Read-only: не виконує операцій запису (ФС/БД); запис у `.oxlintrc.json` — відповідальність виклику (T0 `fix-check.mjs`).
53
+ - `planOxlintrcFix` — чиста функція без side effects; той самий вхід завжди дає той самий вихід.
@@ -18,6 +18,11 @@ export const KNIP_CANONICAL_JSON_PATH = join(
18
18
  'knip-canonical.json'
19
19
  )
20
20
 
21
+ /** `.oxlintrc.json` відсутній — T0 копіює канон (`fix-check.mjs`). */
22
+ export const OXLINTRC_MISSING = 'oxlintrc-missing'
23
+ /** `.oxlintrc.json` існує, але розходиться з каноном — T0 доводить до відповідності детермінованим merge. */
24
+ export const OXLINTRC_DRIFT = 'oxlintrc-drift'
25
+
21
26
  // Канонічний рядок `lint-js`-скрипта і мінімальна версія `@nitra/eslint-config` —
22
27
  // у rego (`npm/policy/js_lint/package_json/`). JS-копії (`CANONICAL_LINT_JS`,
23
28
  // `isCanonicalLintJs`, `nitraEslintConfigMeetsMinVersion`) видалено, щоб не
@@ -147,3 +152,32 @@ export function verifyOxlintRcAgainstCanonical(cfg, canonical) {
147
152
 
148
153
  return { ok: failures.length === 0, failures }
149
154
  }
155
+
156
+ /**
157
+ * Детермінований merge `.oxlintrc.json` до відповідності канону (T0 `fix-check.mjs`) —
158
+ * дзеркалить правила `verifyOxlintRcAgainstCanonical`, тож результат завжди проходить
159
+ * повторну перевірку без LLM. Project-specific розширення зберігаються: зайві ключі в
160
+ * `rules` і зайві `ignorePatterns` не видаляються, лише доповнюються канонічними.
161
+ * @param {unknown} actual поточний вміст `.oxlintrc.json` (`null`/будь-що не-object — трактується як відсутній файл)
162
+ * @param {Record<string, unknown>} canonical розпарсений `oxlint-canonical.json`
163
+ * @returns {Record<string, unknown>} злитий обʼєкт, готовий до запису у `.oxlintrc.json`
164
+ */
165
+ export function planOxlintrcFix(actual, canonical) {
166
+ const merged = { ...asRecordOrEmpty(actual) }
167
+ for (const [key, expected] of Object.entries(canonical)) {
168
+ if (key === 'rules') {
169
+ merged.rules = { ...asRecordOrEmpty(merged.rules), ...asRecordOrEmpty(expected) }
170
+ continue
171
+ }
172
+ if (key === 'ignorePatterns') {
173
+ const existing = Array.isArray(merged.ignorePatterns) ? merged.ignorePatterns : []
174
+ const canonPatterns = Array.isArray(expected) ? expected : []
175
+ merged.ignorePatterns = [...existing, ...canonPatterns.filter(p => !existing.includes(p))]
176
+ continue
177
+ }
178
+ // Інші поля вимагають точного збігу з каноном (verifyOxlintRcAgainstCanonical) —
179
+ // єдине валідне значення й так канонічне, тож перезапис безпечний.
180
+ merged[key] = expected
181
+ }
182
+ return merged
183
+ }