@7n/rules 1.7.4 → 1.7.6

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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.7.6] - 2026-07-16
4
+
5
+ ### Fixed
6
+
7
+ - Прибрано борг 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)
8
+
9
+ ## [1.7.5] - 2026-07-16
10
+
11
+ ### Fixed
12
+
13
+ - npm: підняти нижню межу @7n/llm-lib до ^2.7.0 (skills-cli.mjs імпортує ./acp, доданий лише у 2.7.0 — старіші 2.x без цього subpath ламали npx @7n/rules з ERR_PACKAGE_PATH_NOT_EXPORTED)
14
+
3
15
  ## [1.7.4] - 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.7.4",
3
+ "version": "1.7.6",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -63,7 +63,7 @@
63
63
  "rename-yaml-extensions": "bun ./bin/n-rules.js rename-yaml-extensions"
64
64
  },
65
65
  "dependencies": {
66
- "@7n/llm-lib": "^2.0.0",
66
+ "@7n/llm-lib": "^2.7.0",
67
67
  "@7n/mt": "^0.5.1",
68
68
  "@zed-industries/agent-client-protocol": "^0.4.5",
69
69
  "ajv": "^8.20.0",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/js/eslint/main.mjs
5
5
  docgen:
6
- crc: 0d2a5e02
6
+ crc: fa1753b7
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/tauri/tool_surface/main.mjs
5
5
  docgen:
6
- crc: 87b28b8d
6
+ crc: 35ae08a5
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -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:...')`, має бути
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/tauri/updater/main.mjs
5
5
  docgen:
6
- crc: f85675d3
6
+ crc: d08660a5
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -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 |
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/test/no-console-store-restore/main.mjs
5
5
  docgen:
6
- crc: 7b2c0909
6
+ crc: 5a1ab9dd
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -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 { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
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 ignorePaths = await loadCursorIgnorePaths(cwd)
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)`)
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/test/vitest-api-conventions/main.mjs
5
5
  docgen:
6
- crc: 4ef8e575
6
+ crc: 806e74e3
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -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 { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
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 ignorePaths = await loadCursorIgnorePaths(cwd)
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)`)