@7n/rules 1.7.5 → 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 +12 -0
- package/bin/n-rules.js +1 -1
- package/package.json +1 -1
- package/rules/js/check/docs/fix-check.md +32 -15
- package/rules/js/check/docs/main.md +5 -2
- package/rules/js/check/fix-check.mjs +55 -12
- package/rules/js/check/main.mjs +4 -2
- package/rules/js/eslint/docs/main.md +1 -1
- package/rules/js/tooling/docs/main.md +31 -9
- package/rules/js/tooling/main.mjs +34 -0
- package/rules/tauri/tool_surface/docs/main.md +1 -1
- package/rules/tauri/tool_surface/main.mjs +1 -27
- package/rules/tauri/updater/docs/main.md +1 -1
- package/rules/tauri/updater/main.mjs +3 -2
- package/rules/test/lib/collect-test-file-offenders.mjs +49 -0
- package/rules/test/lib/docs/collect-test-file-offenders.md +30 -0
- package/rules/test/lib/docs/index.md +9 -0
- package/rules/test/no-console-store-restore/docs/main.md +1 -1
- package/rules/test/no-console-store-restore/main.mjs +2 -35
- package/rules/test/vitest-api-conventions/docs/main.md +1 -1
- package/rules/test/vitest-api-conventions/main.mjs +2 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
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
|
+
|
|
9
|
+
## [1.7.6] - 2026-07-16
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Прибрано борг self-lint: `CLAUDE.md`-генератор вставляв список без порожнього рядка перед ним (MD032); дедуплікація `collectCapabilityPermissionIds` (tauri/tool_surface ↔ tauri/updater) та скелета "обхід тестових файлів" (test/no-console-store-restore ↔ test/vitest-api-conventions, винесено у test/lib/collect-test-file-offenders.mjs)
|
|
14
|
+
|
|
3
15
|
## [1.7.5] - 2026-07-16
|
|
4
16
|
|
|
5
17
|
### Fixed
|
package/bin/n-rules.js
CHANGED
|
@@ -819,7 +819,7 @@ async function syncClaudeMd(ignore) {
|
|
|
819
819
|
const lines = [`<!-- Цей файл генерується автоматично через \`npx ${PACKAGE_NAME}\`. Не редагуй вручну. -->`, '']
|
|
820
820
|
|
|
821
821
|
if (Array.isArray(ignore) && ignore.length > 0) {
|
|
822
|
-
lines.push('## Захищені директорії', '', 'Ніколи не змінюй, не видаляй і не створюй файли у цих директоріях:')
|
|
822
|
+
lines.push('## Захищені директорії', '', 'Ніколи не змінюй, не видаляй і не створюй файли у цих директоріях:', '')
|
|
823
823
|
for (const dir of ignore) {
|
|
824
824
|
let d = dir
|
|
825
825
|
while (d.endsWith('/')) d = d.slice(0, -1)
|
package/package.json
CHANGED
|
@@ -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:
|
|
6
|
+
crc: 48182147
|
|
7
7
|
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## Огляд
|
|
11
11
|
|
|
12
|
-
T0-autofix для `js/check`:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
(
|
|
16
|
-
|
|
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
|
-
-
|
|
21
|
-
`eslint-config-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
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
|
-
-
|
|
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:
|
|
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
|
-
* Раніше
|
|
6
|
-
*
|
|
7
|
-
* `getConfig({ node: ['npm'] })` — eslint перестав
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
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
|
|
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 =>
|
|
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
|
]
|
package/rules/js/check/main.mjs
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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 —
|
|
20
|
-
|
|
21
|
-
|
|
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 —
|
|
26
|
-
- KNIP_CANONICAL_JSON_PATH —
|
|
27
|
-
-
|
|
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
|
+
}
|
|
@@ -6,7 +6,7 @@ import { join } from 'node:path'
|
|
|
6
6
|
import { globby } from 'globby'
|
|
7
7
|
|
|
8
8
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
9
|
-
import { findTauriAppWorkspaces, groupCargoDepsBySection } from '../updater/main.mjs'
|
|
9
|
+
import { collectCapabilityPermissionIds, findTauriAppWorkspaces, groupCargoDepsBySection } from '../updater/main.mjs'
|
|
10
10
|
|
|
11
11
|
/** JS торкається плагіна, але crate відсутній у `src-tauri/Cargo.toml`. */
|
|
12
12
|
export const PLUGIN_DEP_MISSING = 'tool-surface-plugin-dep-missing'
|
|
@@ -43,32 +43,6 @@ async function collectPluginUsages(srcDir) {
|
|
|
43
43
|
return slugs
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Збирає всі permission-ідентифікатори з `capabilities/*.json` workspace-каталогу.
|
|
48
|
-
* @param {string} capDir абсолютний шлях до каталогу capabilities
|
|
49
|
-
* @returns {Promise<Set<string>>} множина permission-ідентифікаторів
|
|
50
|
-
*/
|
|
51
|
-
async function collectCapabilityPermissionIds(capDir) {
|
|
52
|
-
const ids = new Set()
|
|
53
|
-
if (!existsSync(capDir)) return ids
|
|
54
|
-
|
|
55
|
-
const files = await globby('*.json', { cwd: capDir, onlyFiles: true, gitignore: false })
|
|
56
|
-
for (const file of files) {
|
|
57
|
-
let cap
|
|
58
|
-
try {
|
|
59
|
-
cap = JSON.parse(await readFile(join(capDir, file), 'utf8'))
|
|
60
|
-
} catch {
|
|
61
|
-
continue
|
|
62
|
-
}
|
|
63
|
-
const perms = Array.isArray(cap.permissions) ? cap.permissions : []
|
|
64
|
-
for (const p of perms) {
|
|
65
|
-
const id = typeof p === 'string' ? p : p?.identifier
|
|
66
|
-
if (id) ids.add(id)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return ids
|
|
70
|
-
}
|
|
71
|
-
|
|
72
46
|
/**
|
|
73
47
|
* Перевіряє один workspace: кожен Tauri-плагін, якого JS торкається через
|
|
74
48
|
* wrapper-пакет `@tauri-apps/plugin-*` чи прямий `invoke('plugin:...')`, має бути
|
|
@@ -256,9 +256,10 @@ async function checkLibRs(ws, cwd, reporter) {
|
|
|
256
256
|
* @param {string} capDir абсолютний шлях до каталогу capabilities
|
|
257
257
|
* @returns {Promise<Set<string>>} множина permission-ідентифікаторів
|
|
258
258
|
*/
|
|
259
|
-
async function collectCapabilityPermissionIds(capDir) {
|
|
260
|
-
const files = await globby('*.json', { cwd: capDir, onlyFiles: true, gitignore: false })
|
|
259
|
+
export async function collectCapabilityPermissionIds(capDir) {
|
|
261
260
|
const ids = new Set()
|
|
261
|
+
if (!existsSync(capDir)) return ids
|
|
262
|
+
const files = await globby('*.json', { cwd: capDir, onlyFiles: true, gitignore: false })
|
|
262
263
|
for (const file of files) {
|
|
263
264
|
let cap
|
|
264
265
|
try {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** @see ../docs/collect-test-file-offenders.md */
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, relative } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
6
|
+
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Чи файл — JS-тест (`*.test.mjs` / `*.test.js`).
|
|
10
|
+
* @param {string} absPath абсолютний шлях
|
|
11
|
+
* @returns {boolean} `true` для `.test.{mjs,js}` файлів
|
|
12
|
+
*/
|
|
13
|
+
export function isTestFile(absPath) {
|
|
14
|
+
const name = basename(absPath)
|
|
15
|
+
return name.endsWith('.test.mjs') || name.endsWith('.test.js')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Обходить репо (поважаючи `.n-rules.json:ignore`), збирає `*.test.{mjs,js}` файли
|
|
20
|
+
* і прогонить кожен через `findOffenders(body)` — спільний скелет для per-file
|
|
21
|
+
* regex/parse-based test-конвенцій (напр. `no-console-store-restore`, `vitest-api-conventions`).
|
|
22
|
+
* @param {string} cwd корінь репозиторію
|
|
23
|
+
* @param {(body: string) => Array<{line: number}>} findOffenders детектор порушень у тілі файлу
|
|
24
|
+
* @returns {Promise<{ testFiles: string[], offenders: Array<{file: string, line: number}> }>} усі тестові файли й знайдені порушення (з relative-шляхом)
|
|
25
|
+
*/
|
|
26
|
+
export async function collectTestFileOffenders(cwd, findOffenders) {
|
|
27
|
+
const ignorePaths = await loadCursorIgnorePaths(cwd)
|
|
28
|
+
|
|
29
|
+
/** @type {string[]} */
|
|
30
|
+
const testFiles = []
|
|
31
|
+
await walkDir(
|
|
32
|
+
cwd,
|
|
33
|
+
absPath => {
|
|
34
|
+
if (isTestFile(absPath)) testFiles.push(absPath)
|
|
35
|
+
},
|
|
36
|
+
ignorePaths
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
/** @type {Array<{file: string, line: number}>} */
|
|
40
|
+
const offenders = []
|
|
41
|
+
for (const absPath of testFiles) {
|
|
42
|
+
const body = await readFile(absPath, 'utf8')
|
|
43
|
+
for (const o of findOffenders(body)) {
|
|
44
|
+
offenders.push({ file: relative(cwd, absPath), ...o })
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { testFiles, offenders }
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: collect-test-file-offenders.mjs
|
|
4
|
+
resource: npm/rules/test/lib/collect-test-file-offenders.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: d1c3db48
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Файл знаходить JS-тести `*.test.mjs` і `*.test.js` у репозиторії з урахуванням `.n-rules.json` та повертає для кожного з них відносні шляхи до offenders. Це дає змогу окремо перевіряти, які саме тестові файли виходять за межі правил проєкту, без додаткового парсингу чи нормалізації результату.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- `isTestFile` — визначає, чи шлях належить до JS-тесту `*.test.mjs` або `*.test.js`.
|
|
21
|
+
- `collectTestFileOffenders` — знаходить усі тестові файли в репозиторії з урахуванням `.n-rules.json` та збирає для них знайдені порушення з відносними шляхами.
|
|
22
|
+
|
|
23
|
+
## Публічний API
|
|
24
|
+
|
|
25
|
+
- isTestFile — Розпізнає JS-файли з тестовою назвою `*.test.mjs` або `*.test.js`.
|
|
26
|
+
- collectTestFileOffenders — Обходить репозиторій з урахуванням `.n-rules.json:ignore`, знаходить `*.test.{mjs,js}` і проганяє кожен файл через `findOffenders` для спільних test-конвенцій на кшталт `no-console-store-restore` і `vitest-api-conventions`.
|
|
27
|
+
|
|
28
|
+
## Гарантії поведінки
|
|
29
|
+
|
|
30
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Directory Index
|
|
3
|
+
title: npm/rules/test/lib
|
|
4
|
+
resource: npm/rules/test/lib/
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
| Файл | Тип |
|
|
8
|
+
| ----------------------------------------------------------------- | --------- |
|
|
9
|
+
| [collect-test-file-offenders.mjs](collect-test-file-offenders.md) | JS Module |
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/** @see ./docs/no-console-store-restore.md */
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, relative } from 'node:path'
|
|
4
|
-
|
|
5
2
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
6
|
-
import {
|
|
7
|
-
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
3
|
+
import { collectTestFileOffenders } from '../lib/collect-test-file-offenders.mjs'
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* Ловить пряме присвоєння `console.<method> = …` у `*.test.{js,mjs}`.
|
|
@@ -14,16 +10,6 @@ import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
|
14
10
|
const CONSOLE_ASSIGN_RE =
|
|
15
11
|
/\bconsole\.(?:log|error|warn|info|debug|dir|table|trace|group|groupEnd|time|timeEnd)\s*=(?!=)/u
|
|
16
12
|
|
|
17
|
-
/**
|
|
18
|
-
* Чи файл — JS-тест (`*.test.mjs` / `*.test.js`).
|
|
19
|
-
* @param {string} absPath абсолютний шлях
|
|
20
|
-
* @returns {boolean} `true` для `.test.{mjs,js}` файлів
|
|
21
|
-
*/
|
|
22
|
-
function isTestFile(absPath) {
|
|
23
|
-
const name = basename(absPath)
|
|
24
|
-
return name.endsWith('.test.mjs') || name.endsWith('.test.js')
|
|
25
|
-
}
|
|
26
|
-
|
|
27
13
|
/**
|
|
28
14
|
* Знаходить рядки з прямим присвоєнням `console.<method> = …`.
|
|
29
15
|
* @param {string} body вміст файлу
|
|
@@ -51,26 +37,7 @@ export async function lint(ctx) {
|
|
|
51
37
|
const { pass, fail } = reporter
|
|
52
38
|
|
|
53
39
|
const cwd = ctx.cwd
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
/** @type {string[]} */
|
|
57
|
-
const testFiles = []
|
|
58
|
-
await walkDir(
|
|
59
|
-
cwd,
|
|
60
|
-
absPath => {
|
|
61
|
-
if (isTestFile(absPath)) testFiles.push(absPath)
|
|
62
|
-
},
|
|
63
|
-
ignorePaths
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
/** @type {Array<{file: string, line: number}>} */
|
|
67
|
-
const offenders = []
|
|
68
|
-
for (const absPath of testFiles) {
|
|
69
|
-
const body = await readFile(absPath, 'utf8')
|
|
70
|
-
for (const o of findOffenders(body)) {
|
|
71
|
-
offenders.push({ file: relative(cwd, absPath), ...o })
|
|
72
|
-
}
|
|
73
|
-
}
|
|
40
|
+
const { testFiles, offenders } = await collectTestFileOffenders(cwd, findOffenders)
|
|
74
41
|
|
|
75
42
|
if (offenders.length === 0) {
|
|
76
43
|
pass(`Жоден з ${testFiles.length} тестових файлів не присвоює console.<method> = … (test.mdc)`)
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/** @see ./docs/main.md */
|
|
2
|
-
import { readFile } from 'node:fs/promises'
|
|
3
|
-
import { basename, relative } from 'node:path'
|
|
4
|
-
|
|
5
2
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
6
|
-
import {
|
|
7
|
-
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
3
|
+
import { collectTestFileOffenders } from '../lib/collect-test-file-offenders.mjs'
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* `expect(x).toBe({…})` / `expect(x).toBe([…])` — `toBe` — це `Object.is`
|
|
@@ -23,16 +19,6 @@ const QUOTE_CHARS = new Set(['"', "'", '`'])
|
|
|
23
19
|
/** Мапа закриваючої дужки на парну дужку, яка її відкриває — для перевірки балансу стеку. */
|
|
24
20
|
const CLOSE_TO_OPEN = { '}': '{', ']': '[' }
|
|
25
21
|
|
|
26
|
-
/**
|
|
27
|
-
* Чи файл — JS-тест (`*.test.mjs` / `*.test.js`).
|
|
28
|
-
* @param {string} absPath абсолютний шлях
|
|
29
|
-
* @returns {boolean} `true` для `.test.{mjs,js}` файлів
|
|
30
|
-
*/
|
|
31
|
-
function isTestFile(absPath) {
|
|
32
|
-
const name = basename(absPath)
|
|
33
|
-
return name.endsWith('.test.mjs') || name.endsWith('.test.js')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
22
|
/**
|
|
37
23
|
* Пропускає пробіли/переноси рядків, повертає індекс першого не-whitespace символу.
|
|
38
24
|
* @param {string} body вміст файлу
|
|
@@ -148,26 +134,7 @@ export async function lint(ctx) {
|
|
|
148
134
|
const { pass, fail } = reporter
|
|
149
135
|
|
|
150
136
|
const cwd = ctx.cwd
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
/** @type {string[]} */
|
|
154
|
-
const testFiles = []
|
|
155
|
-
await walkDir(
|
|
156
|
-
cwd,
|
|
157
|
-
absPath => {
|
|
158
|
-
if (isTestFile(absPath)) testFiles.push(absPath)
|
|
159
|
-
},
|
|
160
|
-
ignorePaths
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
/** @type {Array<{file: string, line: number}>} */
|
|
164
|
-
const offenders = []
|
|
165
|
-
for (const absPath of testFiles) {
|
|
166
|
-
const body = await readFile(absPath, 'utf8')
|
|
167
|
-
for (const o of findOffenders(body)) {
|
|
168
|
-
offenders.push({ file: relative(cwd, absPath), ...o })
|
|
169
|
-
}
|
|
170
|
-
}
|
|
137
|
+
const { testFiles, offenders } = await collectTestFileOffenders(cwd, findOffenders)
|
|
171
138
|
|
|
172
139
|
if (offenders.length === 0) {
|
|
173
140
|
pass(`Жоден з ${testFiles.length} тестових файлів не викликає toBe(...) з об'єктним/масивним літералом (test.mdc)`)
|