@7n/rules 1.20.0 → 1.20.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +1 -1
  3. package/rules/bun/licensee/docs/fix-licensee.md +1 -2
  4. package/rules/bun/licensee/docs/main.md +1 -1
  5. package/rules/bun/licensee/fix-licensee.mjs +3 -4
  6. package/rules/bun/licensee/main.mjs +7 -7
  7. package/rules/docker/lib/docker-hadolint.mjs +6 -8
  8. package/rules/docker/lib/docs/docker-hadolint.md +1 -1
  9. package/rules/docker/lint/docs/main.md +1 -1
  10. package/rules/docker/lint/main.mjs +1 -1
  11. package/rules/image-compress/check/docs/fix-check.md +1 -2
  12. package/rules/image-compress/check/docs/main.md +1 -1
  13. package/rules/image-compress/check/fix-check.mjs +27 -22
  14. package/rules/image-compress/check/main.mjs +14 -14
  15. package/rules/js/jscpd_duplicates/docs/main.md +1 -1
  16. package/rules/js/jscpd_duplicates/main.mjs +7 -8
  17. package/rules/k8s/kubeconform/docs/main.md +1 -1
  18. package/rules/k8s/kubeconform/main.mjs +1 -1
  19. package/rules/k8s/manifests/main.mjs +52 -39
  20. package/rules/php/cs_fixer/docs/main.md +1 -2
  21. package/rules/php/cs_fixer/main.mjs +8 -7
  22. package/rules/php/phpcs/docs/main.md +1 -2
  23. package/rules/php/phpcs/main.mjs +12 -11
  24. package/rules/php/project/docs/main.md +1 -2
  25. package/rules/php/project/main.mjs +19 -16
  26. package/rules/python/lib/docs/uv-run.md +1 -2
  27. package/rules/python/lib/uv-run.mjs +12 -8
  28. package/rules/python/mypy/docs/main.md +1 -1
  29. package/rules/python/mypy/main.mjs +9 -8
  30. package/rules/python/project/docs/main.md +1 -2
  31. package/rules/python/project/main.mjs +33 -23
  32. package/rules/python/ruff/docs/fix-ruff.md +1 -2
  33. package/rules/python/ruff/docs/main.md +1 -1
  34. package/rules/python/ruff/fix-ruff.mjs +12 -10
  35. package/rules/python/ruff/main.mjs +13 -12
  36. package/rules/rego/conftest_verify/docs/main.md +1 -2
  37. package/rules/rego/conftest_verify/main.mjs +12 -10
  38. package/rules/rego/lib/docs/run-external-tool.md +1 -1
  39. package/rules/rego/lib/run-external-tool.mjs +10 -7
  40. package/rules/rego/opa_check/docs/main.md +1 -1
  41. package/rules/rego/opa_check/main.mjs +3 -3
  42. package/rules/rego/regal/docs/main.md +1 -1
  43. package/rules/rego/regal/main.mjs +3 -3
  44. package/rules/rust/check/docs/fix-check.md +1 -1
  45. package/rules/rust/check/docs/main.md +1 -1
  46. package/rules/rust/check/fix-check.mjs +13 -12
  47. package/rules/rust/check/main.mjs +19 -13
  48. package/rules/security/scan/docs/main.md +1 -1
  49. package/rules/security/scan/main.mjs +9 -9
  50. package/rules/style/lint/docs/fix-lint.md +1 -1
  51. package/rules/style/lint/docs/main.md +1 -1
  52. package/rules/style/lint/fix-lint.mjs +8 -8
  53. package/rules/style/lint/main.mjs +6 -6
  54. package/rules/text/cspell-fix/docs/fix-worker.md +1 -1
  55. package/rules/text/cspell-fix/docs/main.md +1 -1
  56. package/rules/text/cspell-fix/fix-worker.mjs +1 -1
  57. package/rules/text/cspell-fix/main.mjs +14 -15
  58. package/rules/text/oxfmt/docs/fix-oxfmt.md +1 -1
  59. package/rules/text/oxfmt/docs/main.md +1 -1
  60. package/rules/text/oxfmt/fix-oxfmt.mjs +3 -3
  61. package/rules/text/oxfmt/main.mjs +6 -5
  62. package/rules/text/run-dotenv-linter/docs/fix-run-dotenv-linter.md +1 -2
  63. package/rules/text/run-dotenv-linter/docs/main.md +1 -1
  64. package/rules/text/run-dotenv-linter/fix-run-dotenv-linter.mjs +2 -2
  65. package/rules/text/run-dotenv-linter/main.mjs +19 -24
  66. package/rules/text/run-shellcheck/docs/fix-run-shellcheck.md +1 -2
  67. package/rules/text/run-shellcheck/docs/main.md +1 -1
  68. package/rules/text/run-shellcheck/fix-run-shellcheck.mjs +3 -3
  69. package/rules/text/run-shellcheck/main.mjs +46 -57
  70. package/rules/text/run-v8r/docs/main.md +1 -1
  71. package/rules/text/run-v8r/main.mjs +31 -26
  72. package/scripts/lib/lint-surface/blocking-inventory.mjs +12 -40
  73. package/scripts/lib/lint-surface/docs/blocking-inventory.md +1 -2
  74. package/scripts/utils/docs/spawn-async.md +1 -2
  75. package/scripts/utils/spawn-async.mjs +4 -1
@@ -4,11 +4,11 @@
4
4
  * bundled `php/check` (spec docs/specs/2026-07-02-text-check-per-file-split-design.md "Рішення
5
5
  * python/php/rego") — php-cs-fixer приймає список конкретних файлів аргументом.
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
7
  import { existsSync } from 'node:fs'
9
8
  import { join, resolve } from 'node:path'
10
9
 
11
10
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
11
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
12
12
 
13
13
  /** Розширення `.php` — фільтр delta-списку файлів у `lint(ctx)`. */
14
14
  const PHP_EXT_RE = /\.php$/u
@@ -23,11 +23,12 @@ function vendorBin(root) {
23
23
  }
24
24
 
25
25
  /**
26
- * Detector php/cs_fixer (read-only).
26
+ * Detector php/cs_fixer (read-only). Async (не блокує event loop) — детектор може виконуватись
27
+ * у parallel lane `detectAll()` (ADR 260716-1354).
27
28
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
28
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
29
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
29
30
  */
30
- export function lint(ctx) {
31
+ export async function lint(ctx) {
31
32
  const reporter = createViolationReporter(ctx)
32
33
  const { fail } = reporter
33
34
  const root = ctx.cwd
@@ -40,9 +41,9 @@ export function lint(ctx) {
40
41
  const abs = vendorBin(root)
41
42
  if (!abs) return reporter.result() // php-cs-fixer відсутній у vendor/bin → пропущено
42
43
 
43
- const r = spawnSync(abs, ['fix', '--dry-run', '--diff', ...targets], { cwd: root, encoding: 'utf8', shell: false })
44
- if (r.status !== 0) {
45
- const code = typeof r.status === 'number' ? r.status : 1
44
+ const r = await spawnAsync(abs, ['fix', '--dry-run', '--diff', ...targets], { cwd: root })
45
+ if (r.exitCode !== 0) {
46
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
46
47
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
47
48
  const outSuffix = out ? `\n${out}` : ''
48
49
  fail(`lint-php: PHP-CS-Fixer (dry-run) — помилка (код ${code}, php.mdc)${outSuffix}`, 'php-cs-fixer-violation')
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/php/phpcs/main.mjs
5
5
  docgen:
6
- crc: 3da9ada2
6
+ crc: 389cb568
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.99
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -4,11 +4,11 @@
4
4
  * Виділено з колишнього bundled `php/check` (spec
5
5
  * docs/specs/2026-07-02-text-check-per-file-split-design.md "Рішення python/php/rego").
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
7
  import { existsSync, statSync } from 'node:fs'
9
8
  import { join, resolve } from 'node:path'
10
9
 
11
10
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
11
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
12
12
 
13
13
  /** Розширення `.php` — фільтр delta-списку файлів у `lint(ctx)`. */
14
14
  const PHP_EXT_RE = /\.php$/u
@@ -38,11 +38,12 @@ function vendorBin(root) {
38
38
  }
39
39
 
40
40
  /**
41
- * Detector php/phpcs (read-only).
41
+ * Detector php/phpcs (read-only). Async (не блокує event loop) — детектор може виконуватись
42
+ * у parallel lane `detectAll()` (ADR 260716-1354).
42
43
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
43
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
44
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
44
45
  */
45
- export function lint(ctx) {
46
+ export async function lint(ctx) {
46
47
  const reporter = createViolationReporter(ctx)
47
48
  const { fail } = reporter
48
49
  const root = ctx.cwd
@@ -55,13 +56,13 @@ export function lint(ctx) {
55
56
  const abs = vendorBin(root)
56
57
  if (!abs) return reporter.result() // phpcs відсутній у vendor/bin → пропущено
57
58
 
58
- const r = spawnSync(abs, ['--standard=Security', '--ignore=*/vendor/*,*/node_modules/*,*/.git/*', ...targets], {
59
- cwd: root,
60
- encoding: 'utf8',
61
- shell: false
62
- })
63
- if (r.status !== 0) {
64
- const code = typeof r.status === 'number' ? r.status : 1
59
+ const r = await spawnAsync(
60
+ abs,
61
+ ['--standard=Security', '--ignore=*/vendor/*,*/node_modules/*,*/.git/*', ...targets],
62
+ { cwd: root }
63
+ )
64
+ if (r.exitCode !== 0) {
65
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
65
66
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
66
67
  const outSuffix = out ? `\n${out}` : ''
67
68
  fail(`lint-php: phpcs (Security) — помилка (код ${code}, php.mdc)${outSuffix}`, 'phpcs-violation')
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/php/project/main.mjs
5
5
  docgen:
6
- crc: 0a3716eb
6
+ crc: b2bd6168
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.97
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -7,12 +7,12 @@
7
7
  * audit. Не входять у delta-план (§5): спрацьовують лише через `n-rules lint --full` або
8
8
  * scoped `n-rules lint php`.
9
9
  */
10
- import { spawnSync } from 'node:child_process'
11
10
  import { existsSync } from 'node:fs'
12
11
  import { join, resolve } from 'node:path'
13
12
 
14
13
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
15
14
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
15
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
16
16
 
17
17
  /**
18
18
  * @param {string} root корінь
@@ -25,19 +25,20 @@ function vendorBin(root, name) {
25
25
  }
26
26
 
27
27
  /**
28
- * Запускає тул і, на ненульовий код, реєструє порушення.
28
+ * Запускає тул і, на ненульовий код, реєструє порушення. Async (не блокує event loop) —
29
+ * детектор може виконуватись у parallel lane `detectAll()` (ADR 260716-1354).
29
30
  * @param {string} label назва кроку
30
31
  * @param {string} abs абсолютний шлях
31
32
  * @param {string[]} args аргументи команди.
32
33
  * @param {string} cwd робочий каталог.
33
34
  * @param {(msg: string, reason: string) => void} fail колбек реєстрації порушення.
34
35
  * @param {string} reason машиночитна причина порушення.
35
- * @returns {boolean} true якщо OK, false якщо порушення
36
+ * @returns {Promise<boolean>} true якщо OK, false якщо порушення
36
37
  */
37
- function runTool(label, abs, args, cwd, fail, reason) {
38
- const r = spawnSync(abs, args, { cwd, encoding: 'utf8', shell: false })
39
- if (r.status === 0) return true
40
- const code = typeof r.status === 'number' ? r.status : 1
38
+ async function runTool(label, abs, args, cwd, fail, reason) {
39
+ const r = await spawnAsync(abs, args, { cwd })
40
+ if (r.exitCode === 0) return true
41
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
41
42
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
42
43
  const outSuffix = out ? `\n${out}` : ''
43
44
  fail(`lint-php: ${label} — помилка (код ${code}, php.mdc)${outSuffix}`, reason)
@@ -45,11 +46,11 @@ function runTool(label, abs, args, cwd, fail, reason) {
45
46
  }
46
47
 
47
48
  /**
48
- * Detector php/project (read-only).
49
+ * Detector php/project (read-only). Async — `runTool` викликає `spawnAsync` (ADR 260716-1354).
49
50
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
50
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
51
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
51
52
  */
52
- export function lint(ctx) {
53
+ export async function lint(ctx) {
53
54
  const reporter = createViolationReporter(ctx)
54
55
  const { fail } = reporter
55
56
  const root = ctx.cwd
@@ -62,7 +63,9 @@ export function lint(ctx) {
62
63
  return reporter.result()
63
64
  }
64
65
 
65
- if (!runTool('composer audit', composer, ['audit', '--no-interaction'], root, fail, 'composer-audit-violation')) {
66
+ if (
67
+ !(await runTool('composer audit', composer, ['audit', '--no-interaction'], root, fail, 'composer-audit-violation'))
68
+ ) {
66
69
  return reporter.result()
67
70
  }
68
71
 
@@ -71,18 +74,18 @@ export function lint(ctx) {
71
74
  * @param {string} label назва кроку.
72
75
  * @param {string[]} args аргументи інструменту.
73
76
  * @param {string} reason машиночитна причина порушення.
74
- * @returns {boolean} true якщо OK / пропущено
77
+ * @returns {Promise<boolean>} true якщо OK / пропущено
75
78
  */
76
- function runOptionalVendorTool(binName, label, args, reason) {
79
+ async function runOptionalVendorTool(binName, label, args, reason) {
77
80
  const abs = vendorBin(root, binName)
78
81
  if (!abs) return true // тул відсутній у vendor/bin → крок пропущено
79
- return runTool(label, abs, args, root, fail, reason)
82
+ return await runTool(label, abs, args, root, fail, reason)
80
83
  }
81
84
 
82
- if (!runOptionalVendorTool('phpstan', 'PHPStan', ['analyse', '--no-progress'], 'phpstan-violation')) {
85
+ if (!(await runOptionalVendorTool('phpstan', 'PHPStan', ['analyse', '--no-progress'], 'phpstan-violation'))) {
83
86
  return reporter.result()
84
87
  }
85
- runOptionalVendorTool('psalm', 'Psalm', ['--no-cache'], 'psalm-violation')
88
+ await runOptionalVendorTool('psalm', 'Psalm', ['--no-cache'], 'psalm-violation')
86
89
 
87
90
  return reporter.result()
88
91
  }
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: uv-run.mjs
4
4
  resource: npm/rules/python/lib/uv-run.mjs
5
5
  docgen:
6
- crc: 6aba3ab1
6
+ crc: 1d697d71
7
7
  model: openai-codex/gpt-5.4-mini
8
- tier: cloud-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.97
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -3,35 +3,39 @@
3
3
  * `ctx.files`, інакше `.` (весь проєкт), обидва йдуть через `uv run --frozen`. Виділено зі
4
4
  * спільного дубльованого коду (jscpd) обох `main.mjs`.
5
5
  */
6
- import { spawnSync } from 'node:child_process'
7
6
  import { existsSync } from 'node:fs'
8
7
  import { join } from 'node:path'
9
8
 
10
9
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
10
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
11
 
12
12
  /** Розширення `.py` — фільтр delta-списку файлів у `lint(ctx)`. */
13
13
  export const PY_EXT_RE = /\.py$/u
14
14
 
15
15
  /**
16
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
17
+ * (ADR 260716-1354).
16
18
  * @param {string} uv шлях до бінарника uv.
17
19
  * @param {string} tool ім'я інструменту в uv-середовищі.
18
- * @returns {boolean} true якщо інструмент доступний
20
+ * @returns {Promise<boolean>} true якщо інструмент доступний
19
21
  */
20
- export function uvToolAvailable(uv, tool) {
21
- const r = spawnSync(uv, ['run', '--frozen', tool, '--version'], { stdio: 'ignore', shell: false })
22
- return r.status === 0
22
+ export async function uvToolAvailable(uv, tool) {
23
+ const r = await spawnAsync(uv, ['run', '--frozen', tool, '--version'])
24
+ return r.exitCode === 0
23
25
  }
24
26
 
25
27
  /**
26
28
  * Спільний preflight: pyproject.toml, delta/full-цілі, резолв `uv`, доступність `tool` у
27
29
  * uv-середовищі. Викликач рано виходить (`reporter.result()`), коли повертається `null` —
28
30
  * `fail()` для випадку відсутнього `uv` уже викликаний усередині.
31
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
32
+ * (ADR 260716-1354).
29
33
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
30
34
  * @param {(msg: string, opts: object) => void} fail колбек реєстрації порушення.
31
35
  * @param {string} tool ім'я інструменту (`mypy`|`ruff`) для перевірки доступності.
32
- * @returns {{uv: string, targets: string[]}|null} preflight-результат, або null (рано вийти).
36
+ * @returns {Promise<{uv: string, targets: string[]}|null>} preflight-результат, або null (рано вийти).
33
37
  */
34
- export function preparePythonRun(ctx, fail, tool) {
38
+ export async function preparePythonRun(ctx, fail, tool) {
35
39
  if (!existsSync(join(ctx.cwd, 'pyproject.toml'))) return null
36
40
 
37
41
  const targets = ctx.files === undefined ? ['.'] : ctx.files.filter(f => PY_EXT_RE.test(f))
@@ -42,7 +46,7 @@ export function preparePythonRun(ctx, fail, tool) {
42
46
  fail('lint-python: `uv` не знайдено в PATH (потрібен при наявному pyproject.toml, python.mdc)', 'uv-missing')
43
47
  return null
44
48
  }
45
- if (!uvToolAvailable(uv, tool)) return null // tool недоступний у uv-середовищі → пропущено
49
+ if (!(await uvToolAvailable(uv, tool))) return null // tool недоступний у uv-середовищі → пропущено
46
50
 
47
51
  return { uv, targets }
48
52
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/python/mypy/main.mjs
5
5
  docgen:
6
- crc: 7cc910e3
6
+ crc: 189c5b15
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -5,28 +5,29 @@
5
5
  * mypy сам транзитивно підвантажує імпортовані модулі (follow-imports), тож дає коректні
6
6
  * діагнози й на підмножині переданих файлів. Немає fix capability (mypy — detect-only).
7
7
  */
8
- import { spawnSync } from 'node:child_process'
9
-
10
8
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
9
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
10
  import { preparePythonRun } from '../lib/uv-run.mjs'
12
11
 
13
12
  /**
14
13
  * Detector python/mypy (read-only).
14
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
15
+ * (ADR 260716-1354).
15
16
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
16
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
17
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
17
18
  */
18
- export function lint(ctx) {
19
+ export async function lint(ctx) {
19
20
  const reporter = createViolationReporter(ctx)
20
21
  const { fail } = reporter
21
22
  const cwd = ctx.cwd
22
23
 
23
- const prepared = preparePythonRun(ctx, fail, 'mypy')
24
+ const prepared = await preparePythonRun(ctx, fail, 'mypy')
24
25
  if (!prepared) return reporter.result()
25
26
  const { uv, targets } = prepared
26
27
 
27
- const r = spawnSync(uv, ['run', '--frozen', 'mypy', ...targets], { cwd, encoding: 'utf8', shell: false })
28
- if (r.status !== 0) {
29
- const code = typeof r.status === 'number' ? r.status : 1
28
+ const r = await spawnAsync(uv, ['run', '--frozen', 'mypy', ...targets], { cwd })
29
+ if (r.exitCode !== 0) {
30
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
30
31
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
31
32
  const outSuffix = out ? `\n${out}` : ''
32
33
  fail(`lint-python: mypy — помилка (код ${code}, python.mdc)${outSuffix}`, 'mypy-violation')
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/python/project/main.mjs
5
5
  docgen:
6
- crc: b47f2997
6
+ crc: 2426a5fa
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  judgeModel: openai-codex/gpt-5.4-mini
11
10
  ---
@@ -5,27 +5,29 @@
5
5
  * lockfile-аудит і ліцензійна перевірка project-wide за природою, не входять у delta-план
6
6
  * (§5): спрацьовують лише через `n-rules lint --full` або scoped `n-rules lint python`.
7
7
  */
8
- import { spawnSync } from 'node:child_process'
9
8
  import { existsSync } from 'node:fs'
10
9
  import { join } from 'node:path'
11
10
 
12
11
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
13
12
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
13
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
14
14
  import { getBronzeAndAbove, isSpdxAllowed } from '../../../scripts/lib/blue-oak.mjs'
15
15
 
16
16
  /**
17
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
18
+ * (ADR 260716-1354).
17
19
  * @param {string} label назва кроку.
18
20
  * @param {string} cmd команда для запуску.
19
21
  * @param {string[]} args аргументи команди.
20
22
  * @param {string} cwd робочий каталог.
21
23
  * @param {(msg: string, reason: string) => void} fail колбек реєстрації порушення.
22
24
  * @param {string} reason машиночитна причина порушення.
23
- * @returns {boolean} true якщо OK
25
+ * @returns {Promise<boolean>} true якщо OK
24
26
  */
25
- function runTool(label, cmd, args, cwd, fail, reason) {
26
- const r = spawnSync(cmd, args, { cwd, encoding: 'utf8', shell: false })
27
- if (r.status === 0) return true
28
- const code = typeof r.status === 'number' ? r.status : 1
27
+ async function runTool(label, cmd, args, cwd, fail, reason) {
28
+ const r = await spawnAsync(cmd, args, { cwd })
29
+ if (r.exitCode === 0) return true
30
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
29
31
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
30
32
  const outSuffix = out ? `\n${out}` : ''
31
33
  fail(`lint-python: ${label} — помилка (код ${code}, python.mdc)${outSuffix}`, reason)
@@ -33,30 +35,32 @@ function runTool(label, cmd, args, cwd, fail, reason) {
33
35
  }
34
36
 
35
37
  /**
38
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
39
+ * (ADR 260716-1354).
36
40
  * @param {string} uv шлях до бінарника uv.
37
41
  * @param {string} tool ім'я інструменту в uv-середовищі.
38
- * @returns {boolean} true якщо інструмент доступний
42
+ * @returns {Promise<boolean>} true якщо інструмент доступний
39
43
  */
40
- function uvToolAvailable(uv, tool) {
41
- const r = spawnSync(uv, ['run', '--frozen', tool, '--version'], { stdio: 'ignore', shell: false })
42
- return r.status === 0
44
+ async function uvToolAvailable(uv, tool) {
45
+ const r = await spawnAsync(uv, ['run', '--frozen', tool, '--version'])
46
+ return r.exitCode === 0
43
47
  }
44
48
 
45
49
  /**
46
50
  * Перевірка ліцензій залежностей через pip-licenses (read-only).
51
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
52
+ * (ADR 260716-1354).
47
53
  * @param {string} uv шлях до бінарника uv.
48
54
  * @param {string} cwd робочий каталог.
49
55
  * @param {(msg: string, reason: string) => void} fail колбек реєстрації порушення.
50
- * @returns {boolean} true якщо OK / пропущено
56
+ * @returns {Promise<boolean>} true якщо OK / пропущено
51
57
  */
52
- function checkPipLicenses(uv, cwd, fail) {
53
- if (!uvToolAvailable(uv, 'pip-licenses')) return true // недоступний → пропущено
54
- const r = spawnSync(uv, ['run', '--frozen', 'pip-licenses', '--from=mixed', '--format=spdx-json'], {
55
- cwd,
56
- encoding: 'utf8',
57
- shell: false
58
+ async function checkPipLicenses(uv, cwd, fail) {
59
+ if (!(await uvToolAvailable(uv, 'pip-licenses'))) return true // недоступний → пропущено
60
+ const r = await spawnAsync(uv, ['run', '--frozen', 'pip-licenses', '--from=mixed', '--format=spdx-json'], {
61
+ cwd
58
62
  })
59
- if (r.status !== 0) {
63
+ if (r.exitCode !== 0) {
60
64
  fail('lint-python: pip-licenses — помилка виконання', 'pip-licenses-error')
61
65
  return false
62
66
  }
@@ -90,10 +94,12 @@ function checkPipLicenses(uv, cwd, fail) {
90
94
 
91
95
  /**
92
96
  * Detector python/project (read-only).
97
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
98
+ * (ADR 260716-1354).
93
99
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
94
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
100
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
95
101
  */
96
- export function lint(ctx) {
102
+ export async function lint(ctx) {
97
103
  const reporter = createViolationReporter(ctx)
98
104
  const { fail } = reporter
99
105
  const cwd = ctx.cwd
@@ -106,10 +112,14 @@ export function lint(ctx) {
106
112
  return reporter.result()
107
113
  }
108
114
 
109
- if (!runTool('uv lock --check', uv, ['lock', '--check'], cwd, fail, 'uv-lock-violation')) return reporter.result()
110
- if (!runTool('uv sync --frozen', uv, ['sync', '--frozen'], cwd, fail, 'uv-sync-violation')) return reporter.result()
115
+ if (!(await runTool('uv lock --check', uv, ['lock', '--check'], cwd, fail, 'uv-lock-violation'))) {
116
+ return reporter.result()
117
+ }
118
+ if (!(await runTool('uv sync --frozen', uv, ['sync', '--frozen'], cwd, fail, 'uv-sync-violation'))) {
119
+ return reporter.result()
120
+ }
111
121
 
112
- checkPipLicenses(uv, cwd, fail)
122
+ await checkPipLicenses(uv, cwd, fail)
113
123
 
114
124
  return reporter.result()
115
125
  }
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-ruff.mjs
4
4
  resource: npm/rules/python/ruff/fix-ruff.mjs
5
5
  docgen:
6
- crc: c2dc5eb6
6
+ crc: e0a79f8d
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.99
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/python/ruff/main.mjs
5
5
  docgen:
6
- crc: 00e5708a
6
+ crc: aa3519c3
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -4,11 +4,11 @@
4
4
  * (spec docs/specs/2026-07-02-text-check-per-file-split-design.md "Рішення python/php/rego").
5
5
  * Відсутній `uv` → no-op.
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
7
  import { readFileSync } from 'node:fs'
9
8
  import { resolve } from 'node:path'
10
9
 
11
10
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
11
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
12
12
 
13
13
  /**
14
14
  * Вміст файлу або null, якщо не читається.
@@ -25,12 +25,14 @@ function readOrNull(abs) {
25
25
 
26
26
  /**
27
27
  * Tracked *.py файли проєкту (через git).
28
+ * Async (не блокує event loop) — може виконуватись у parallel lane `detectAll()`
29
+ * (ADR 260716-1354).
28
30
  * @param {string} cwd корінь
29
- * @returns {string[]} posix-relative шляхи
31
+ * @returns {Promise<string[]>} posix-relative шляхи
30
32
  */
31
- function listPyFiles(cwd) {
32
- const r = spawnSync('git', ['ls-files', '-z', '--', '*.py'], { cwd, encoding: 'utf8' })
33
- if (r.status !== 0) return []
33
+ async function listPyFiles(cwd) {
34
+ const r = await spawnAsync('git', ['ls-files', '-z', '--', '*.py'], { cwd })
35
+ if (r.exitCode !== 0) return []
34
36
  return (r.stdout ?? '').split('\0').filter(Boolean)
35
37
  }
36
38
 
@@ -41,17 +43,17 @@ export const patterns = [
41
43
  standalone: true, // §8 Phase 2: ruff check --fix + format самі ре-аналізують, test() не потрібен
42
44
  test: violations =>
43
45
  violations.some(v => v.reason === 'ruff-check-violation' || v.reason === 'ruff-format-violation'),
44
- apply: (violations, ctx) => {
46
+ apply: async (violations, ctx) => {
45
47
  const uv = resolveCmd('uv')
46
48
  if (!uv) return { touchedFiles: [] }
47
- const files = listPyFiles(ctx.cwd)
49
+ const files = await listPyFiles(ctx.cwd)
48
50
  if (files.length === 0) return { touchedFiles: [] }
49
51
 
50
52
  const abs = files.map(f => resolve(ctx.cwd, f))
51
53
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
52
- const opts = { cwd: ctx.cwd, encoding: 'utf8', shell: false }
53
- spawnSync(uv, ['run', '--frozen', 'ruff', 'check', '--fix', '.'], opts)
54
- spawnSync(uv, ['run', '--frozen', 'ruff', 'format', '.'], opts)
54
+ const opts = { cwd: ctx.cwd }
55
+ await spawnAsync(uv, ['run', '--frozen', 'ruff', 'check', '--fix', '.'], opts)
56
+ await spawnAsync(uv, ['run', '--frozen', 'ruff', 'format', '.'], opts)
55
57
 
56
58
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
57
59
  for (const a of touchedFiles) ctx.recordWrite?.(a)
@@ -5,24 +5,25 @@
5
5
  * "Рішення python/php/rego") — ruff сам транзитивно підвантажує імпортовані модулі
6
6
  * (follow-imports), тож коректний і на підмножині переданих файлів.
7
7
  */
8
- import { spawnSync } from 'node:child_process'
9
-
10
8
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
9
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
10
  import { preparePythonRun } from '../lib/uv-run.mjs'
12
11
 
13
12
  /**
13
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
14
+ * (ADR 260716-1354).
14
15
  * @param {string} label назва кроку.
15
16
  * @param {string} uv шлях до бінарника uv.
16
17
  * @param {string[]} args аргументи ruff (без `uv run --frozen ruff`-префіксу).
17
18
  * @param {string} cwd робочий каталог.
18
19
  * @param {(msg: string, opts: object) => void} fail колбек реєстрації порушення.
19
20
  * @param {string} reason машиночитна причина порушення.
20
- * @returns {boolean} true якщо OK
21
+ * @returns {Promise<boolean>} true якщо OK
21
22
  */
22
- function runRuffStep(label, uv, args, cwd, fail, reason) {
23
- const r = spawnSync(uv, ['run', '--frozen', 'ruff', ...args], { cwd, encoding: 'utf8', shell: false })
24
- if (r.status === 0) return true
25
- const code = typeof r.status === 'number' ? r.status : 1
23
+ async function runRuffStep(label, uv, args, cwd, fail, reason) {
24
+ const r = await spawnAsync(uv, ['run', '--frozen', 'ruff', ...args], { cwd })
25
+ if (r.exitCode === 0) return true
26
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
26
27
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
27
28
  const outSuffix = out ? `\n${out}` : ''
28
29
  fail(`lint-python: ${label} — помилка (код ${code}, python.mdc)${outSuffix}`, reason)
@@ -32,21 +33,21 @@ function runRuffStep(label, uv, args, cwd, fail, reason) {
32
33
  /**
33
34
  * Detector python/ruff (read-only).
34
35
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
35
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
36
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
36
37
  */
37
- export function lint(ctx) {
38
+ export async function lint(ctx) {
38
39
  const reporter = createViolationReporter(ctx)
39
40
  const { fail } = reporter
40
41
  const cwd = ctx.cwd
41
42
 
42
- const prepared = preparePythonRun(ctx, fail, 'ruff')
43
+ const prepared = await preparePythonRun(ctx, fail, 'ruff')
43
44
  if (!prepared) return reporter.result()
44
45
  const { uv, targets } = prepared
45
46
 
46
- if (!runRuffStep('ruff check', uv, ['check', ...targets], cwd, fail, 'ruff-check-violation')) {
47
+ if (!(await runRuffStep('ruff check', uv, ['check', ...targets], cwd, fail, 'ruff-check-violation'))) {
47
48
  return reporter.result()
48
49
  }
49
- runRuffStep('ruff format --check', uv, ['format', '--check', ...targets], cwd, fail, 'ruff-format-violation')
50
+ await runRuffStep('ruff format --check', uv, ['format', '--check', ...targets], cwd, fail, 'ruff-format-violation')
50
51
 
51
52
  return reporter.result()
52
53
  }
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/rego/conftest_verify/main.mjs
5
5
  docgen:
6
- crc: 2ef485ea
6
+ crc: e95d6632
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  judgeModel: openai-codex/gpt-5.4-mini
11
10
  ---