@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
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import { resolve } from 'node:path'
9
9
  import { existsSync } from 'node:fs'
10
- import { spawnSync } from 'node:child_process'
11
10
 
12
11
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
12
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
13
13
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
14
14
 
15
15
  const LINT_TARGETS = ['npm/rules']
@@ -19,23 +19,25 @@ const LINT_TARGETS = ['npm/rules']
19
19
  * @param {string} bin абсолютний шлях до бінарника
20
20
  * @param {string[]} args аргументи
21
21
  * @param {string} cwd робоча директорія
22
- * @returns {{ status: number, output: string }} код завершення й обрізаний stdout+stderr
22
+ * @returns {Promise<{ status: number, output: string }>} код завершення й обрізаний stdout+stderr
23
23
  */
24
- function runStep(bin, args, cwd) {
25
- const result = spawnSync(bin, args, { cwd, encoding: 'utf8', env: process.env, shell: false })
26
- if (result.error) {
27
- return { status: 1, output: `Не вдалося запустити ${bin}: ${result.error.message}` }
24
+ async function runStep(bin, args, cwd) {
25
+ let result
26
+ try {
27
+ result = await spawnAsync(bin, args, { cwd, env: process.env })
28
+ } catch (error) {
29
+ return { status: 1, output: `Не вдалося запустити ${bin}: ${error.message}` }
28
30
  }
29
31
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().slice(0, 2000)
30
- return { status: result.status ?? 1, output }
32
+ return { status: result.exitCode ?? 1, output }
31
33
  }
32
34
 
33
35
  /**
34
36
  * Detector rego/conftest_verify (read-only).
35
37
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону.
36
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат зі зібраними violations
38
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
37
39
  */
38
- export function lint(ctx) {
40
+ export async function lint(ctx) {
39
41
  const reporter = createViolationReporter(ctx)
40
42
  const { fail } = reporter
41
43
  const root = resolve(ctx.cwd)
@@ -46,7 +48,7 @@ export function lint(ctx) {
46
48
  const conftest = resolveCmd('conftest')
47
49
  if (!conftest) return reporter.result() // conftest відсутній → пропускаємо verify (старий код повертав 0)
48
50
 
49
- const verifyRes = runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
51
+ const verifyRes = await runStep(conftest, ['verify', ...targets.flatMap(t => ['-p', t])], root)
50
52
  if (verifyRes.status !== 0) {
51
53
  const verifySuffix = verifyRes.output ? `\n${verifyRes.output}` : ''
52
54
  fail(`lint-rego: conftest verify — помилка (код ${verifyRes.status})${verifySuffix}`, 'conftest-verify-violation')
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: run-external-tool.mjs
4
4
  resource: npm/rules/rego/lib/run-external-tool.mjs
5
5
  docgen:
6
- crc: 0f1da640
6
+ crc: ca820179
7
7
  model: openai-codex/gpt-5.4-mini
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import { existsSync } from 'node:fs'
7
7
  import { resolve } from 'node:path'
8
- import { spawnSync } from 'node:child_process'
8
+
9
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
9
10
 
10
11
  /** Full-режим (ctx.files undefined): корінь policy-дерева, якщо існує. */
11
12
  export const FULL_TARGET = 'npm/rules'
@@ -44,13 +45,15 @@ export function resolveTargets(files, root) {
44
45
  * @param {string} bin абсолютний шлях до бінарника
45
46
  * @param {string[]} args аргументи
46
47
  * @param {string} cwd робоча директорія
47
- * @returns {{ status: number, output: string }} код завершення й обрізаний stdout+stderr
48
+ * @returns {Promise<{ status: number, output: string }>} код завершення й обрізаний stdout+stderr
48
49
  */
49
- export function runStep(bin, args, cwd) {
50
- const result = spawnSync(bin, args, { cwd, encoding: 'utf8', env: process.env, shell: false })
51
- if (result.error) {
52
- return { status: 1, output: `Не вдалося запустити ${bin}: ${result.error.message}` }
50
+ export async function runStep(bin, args, cwd) {
51
+ let result
52
+ try {
53
+ result = await spawnAsync(bin, args, { cwd, env: process.env })
54
+ } catch (error) {
55
+ return { status: 1, output: `Не вдалося запустити ${bin}: ${error.message}` }
53
56
  }
54
57
  const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim().slice(0, 2000)
55
- return { status: result.status ?? 1, output }
58
+ return { status: result.exitCode ?? 1, output }
56
59
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/rego/opa_check/main.mjs
5
5
  docgen:
6
- crc: e2f54bd8
6
+ crc: d5d66b52
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.96
@@ -14,9 +14,9 @@ import { resolveTargets, runStep } from '../lib/run-external-tool.mjs'
14
14
  /**
15
15
  * Detector rego/opa_check (read-only).
16
16
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону.
17
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат зі зібраними violations
17
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
18
18
  */
19
- export function lint(ctx) {
19
+ export async function lint(ctx) {
20
20
  const reporter = createViolationReporter(ctx)
21
21
  const { fail } = reporter
22
22
  const root = resolve(ctx.cwd)
@@ -25,7 +25,7 @@ export function lint(ctx) {
25
25
  if (targets.length === 0) return reporter.result()
26
26
 
27
27
  const opa = ensureTool('opa')
28
- const opaRes = runStep(opa, ['check', '--strict', ...targets], root)
28
+ const opaRes = await runStep(opa, ['check', '--strict', ...targets], root)
29
29
  if (opaRes.status !== 0) {
30
30
  const opaSuffix = opaRes.output ? `\n${opaRes.output}` : ''
31
31
  fail(`lint-rego: opa check --strict — помилка (код ${opaRes.status})${opaSuffix}`, 'opa-check-violation')
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/rego/regal/main.mjs
5
5
  docgen:
6
- crc: c628819a
6
+ crc: a101cc38
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.96
@@ -14,9 +14,9 @@ import { resolveTargets, runStep } from '../lib/run-external-tool.mjs'
14
14
  /**
15
15
  * Detector rego/regal (read-only).
16
16
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону.
17
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат зі зібраними violations
17
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
18
18
  */
19
- export function lint(ctx) {
19
+ export async function lint(ctx) {
20
20
  const reporter = createViolationReporter(ctx)
21
21
  const { fail } = reporter
22
22
  const root = resolve(ctx.cwd)
@@ -25,7 +25,7 @@ export function lint(ctx) {
25
25
  if (targets.length === 0) return reporter.result()
26
26
 
27
27
  const regal = ensureTool('regal')
28
- const regalRes = runStep(regal, ['lint', ...targets], root)
28
+ const regalRes = await runStep(regal, ['lint', ...targets], root)
29
29
  if (regalRes.status !== 0) {
30
30
  const regalSuffix = regalRes.output ? `\n${regalRes.output}` : ''
31
31
  fail(`lint-rego: regal lint — помилка (код ${regalRes.status})${regalSuffix}`, 'regal-lint-violation')
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-check.mjs
4
4
  resource: npm/rules/rust/check/fix-check.mjs
5
5
  docgen:
6
- crc: 02793a78
6
+ crc: 3bd2ccd0
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/rust/check/main.mjs
5
5
  docgen:
6
- crc: bdf1c005
6
+ crc: 71821b75
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  judgeModel: openai-codex/gpt-5.4-mini
@@ -6,11 +6,11 @@
6
6
  * (`deny-config-missing`). clippy не автофіксимо (його `--fix` потенційно небезпечний) — ці
7
7
  * порушення й далі йдуть у LLM-ladder. Запис permanent. Відсутній `cargo`/`cargo-deny` → no-op.
8
8
  */
9
- import { spawnSync } from 'node:child_process'
10
9
  import { existsSync, readFileSync } from 'node:fs'
11
10
  import { join, resolve } from 'node:path'
12
11
 
13
12
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
13
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
14
14
 
15
15
  /**
16
16
  * Вміст файлу або null, якщо не читається.
@@ -26,13 +26,13 @@ function readOrNull(abs) {
26
26
  }
27
27
 
28
28
  /**
29
- * Tracked *.rs файли проєкту (через git).
29
+ * Tracked *.rs файли проєкту (через git). Async (не блокує event loop, ADR 260716-1354).
30
30
  * @param {string} cwd корінь
31
- * @returns {string[]} posix-relative шляхи
31
+ * @returns {Promise<string[]>} posix-relative шляхи
32
32
  */
33
- function listRsFiles(cwd) {
34
- const r = spawnSync('git', ['ls-files', '-z', '--', '*.rs'], { cwd, encoding: 'utf8' })
35
- if (r.status !== 0) return []
33
+ async function listRsFiles(cwd) {
34
+ const r = await spawnAsync('git', ['ls-files', '-z', '--', '*.rs'], { cwd })
35
+ if (r.exitCode !== 0) return []
36
36
  return (r.stdout ?? '').split('\0').filter(Boolean)
37
37
  }
38
38
 
@@ -42,15 +42,15 @@ export const patterns = [
42
42
  id: 'rust-cargo-fmt',
43
43
  standalone: true, // §8 Phase 2: apply самостійно перелічує *.rs (git ls-files), cargo fmt сам ре-аналізує
44
44
  test: violations => violations.some(v => v.reason === 'cargo-fmt-violation'),
45
- apply: (violations, ctx) => {
45
+ apply: async (violations, ctx) => {
46
46
  const cargo = resolveCmd('cargo')
47
47
  if (!cargo) return { touchedFiles: [] }
48
- const files = listRsFiles(ctx.cwd)
48
+ const files = await listRsFiles(ctx.cwd)
49
49
  if (files.length === 0) return { touchedFiles: [] }
50
50
 
51
51
  const abs = files.map(f => resolve(ctx.cwd, f))
52
52
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
53
- spawnSync(cargo, ['fmt', '--all'], { cwd: ctx.cwd, encoding: 'utf8', shell: false })
53
+ await spawnAsync(cargo, ['fmt', '--all'], { cwd: ctx.cwd })
54
54
 
55
55
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
56
56
  for (const a of touchedFiles) ctx.recordWrite?.(a)
@@ -62,14 +62,15 @@ export const patterns = [
62
62
  {
63
63
  id: 'rust-cargo-deny-init',
64
64
  test: violations => violations.some(v => v.reason === 'deny-config-missing'),
65
- apply: (violations, ctx) => {
65
+ apply: async (violations, ctx) => {
66
66
  const cargo = resolveCmd('cargo')
67
67
  if (!cargo) return { touchedFiles: [] }
68
- const hasDeny = spawnSync(cargo, ['deny', '--version'], { stdio: 'ignore', shell: false }).status === 0
68
+ const denyVersionResult = await spawnAsync(cargo, ['deny', '--version'])
69
+ const hasDeny = denyVersionResult.exitCode === 0
69
70
  if (!hasDeny) return { touchedFiles: [] }
70
71
 
71
72
  const denyConfigPath = join(ctx.cwd, 'deny.toml')
72
- spawnSync(cargo, ['deny', 'init'], { cwd: ctx.cwd, encoding: 'utf8', shell: false })
73
+ await spawnAsync(cargo, ['deny', 'init'], { cwd: ctx.cwd })
73
74
  if (!existsSync(denyConfigPath)) return { touchedFiles: [] }
74
75
 
75
76
  ctx.recordWrite?.(denyConfigPath)
@@ -2,26 +2,28 @@
2
2
  * lint-поверхня rust: read-only detector (cargo fmt --check + clippy -D warnings +
3
3
  * cargo deny check licenses). Генерація deny.toml — окремий T0-fix, не в detector-і.
4
4
  */
5
- import { spawnSync } from 'node:child_process'
6
5
  import { existsSync } from 'node:fs'
7
6
  import { join } from 'node:path'
8
7
 
9
8
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
10
9
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
10
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
11
 
12
12
  /**
13
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
14
+ * (ADR 260716-1354).
13
15
  * @param {string} label назва кроку.
14
16
  * @param {string} cargo шлях до бінарника cargo.
15
17
  * @param {string[]} args аргументи cargo.
16
18
  * @param {string} cwd робочий каталог.
17
19
  * @param {(msg: string, reason: string) => void} fail колбек реєстрації порушення.
18
20
  * @param {string} reason машиночитна причина порушення.
19
- * @returns {boolean} true якщо OK
21
+ * @returns {Promise<boolean>} true якщо OK
20
22
  */
21
- function runCargo(label, cargo, args, cwd, fail, reason) {
22
- const r = spawnSync(cargo, args, { cwd, encoding: 'utf8', shell: false })
23
- if (r.status === 0) return true
24
- const code = typeof r.status === 'number' ? r.status : 1
23
+ async function runCargo(label, cargo, args, cwd, fail, reason) {
24
+ const r = await spawnAsync(cargo, args, { cwd })
25
+ if (r.exitCode === 0) return true
26
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
25
27
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
26
28
  const outTail = out ? `\n${out}` : ''
27
29
  fail(`lint-rust: ${label} — помилка (код ${code}, rust.mdc)${outTail}`, reason)
@@ -29,11 +31,12 @@ function runCargo(label, cargo, args, cwd, fail, reason) {
29
31
  }
30
32
 
31
33
  /**
32
- * Detector rust/check (read-only).
34
+ * Detector rust/check (read-only). Async (не блокує event loop) — детектор може виконуватись
35
+ * у parallel lane `detectAll()` (ADR 260716-1354).
33
36
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
34
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із порушеннями
37
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
35
38
  */
36
- export function lint(ctx) {
39
+ export async function lint(ctx) {
37
40
  const reporter = createViolationReporter(ctx)
38
41
  const { fail } = reporter
39
42
  const cwd = ctx.cwd
@@ -49,11 +52,13 @@ export function lint(ctx) {
49
52
  return reporter.result()
50
53
  }
51
54
 
52
- if (!runCargo('cargo fmt --check', cargo, ['fmt', '--all', '--', '--check'], cwd, fail, 'cargo-fmt-violation')) {
55
+ if (
56
+ !(await runCargo('cargo fmt --check', cargo, ['fmt', '--all', '--', '--check'], cwd, fail, 'cargo-fmt-violation'))
57
+ ) {
53
58
  return reporter.result()
54
59
  }
55
60
 
56
- runCargo(
61
+ await runCargo(
57
62
  'cargo clippy -D warnings',
58
63
  cargo,
59
64
  ['clippy', '--all-targets', '--all-features', '--', '-D', 'warnings'],
@@ -71,9 +76,10 @@ export function lint(ctx) {
71
76
  return reporter.result()
72
77
  }
73
78
 
74
- const hasDeny = spawnSync(cargo, ['deny', '--version'], { stdio: 'ignore', shell: false }).status === 0
79
+ const denyVersionResult = await spawnAsync(cargo, ['deny', '--version'])
80
+ const hasDeny = denyVersionResult.exitCode === 0
75
81
  if (hasDeny) {
76
- runCargo('cargo deny check licenses', cargo, ['deny', 'check', 'licenses'], cwd, fail, 'cargo-deny-violation')
82
+ await runCargo('cargo deny check licenses', cargo, ['deny', 'check', 'licenses'], cwd, fail, 'cargo-deny-violation')
77
83
  }
78
84
  // cargo-deny не встановлено → перевірку ліцензій пропущено (старий код — pass)
79
85
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/security/scan/main.mjs
5
5
  docgen:
6
- crc: 254fe129
6
+ crc: a57f9d26
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -2,18 +2,18 @@
2
2
  * lint-поверхня security/scan: read-only detector секретів (`trufflehog` filesystem скан
3
3
  * усього репо). Фіксу немає — детектор лише сигналить про знайдені секрети.
4
4
  */
5
- import { spawnSync } from 'node:child_process'
6
-
7
5
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
8
6
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
7
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
9
8
 
10
9
  /**
11
10
  * Detector security/scan: trufflehog filesystem скан (read-only). Якщо бінарника немає —
12
- * скан пропускається (як і раніше при відсутності інструмента).
11
+ * скан пропускається (як і раніше при відсутності інструмента). Async (не блокує event loop) —
12
+ * детектор може виконуватись у parallel lane `detectAll()` (ADR 260716-1354).
13
13
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
14
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
14
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
15
15
  */
16
- export function lint(ctx) {
16
+ export async function lint(ctx) {
17
17
  const reporter = createViolationReporter(ctx)
18
18
  const { fail } = reporter
19
19
  const cwd = ctx.cwd
@@ -26,7 +26,7 @@ export function lint(ctx) {
26
26
  }
27
27
  }
28
28
 
29
- const r = spawnSync(
29
+ const r = await spawnAsync(
30
30
  trufflehog,
31
31
  [
32
32
  'filesystem',
@@ -37,12 +37,12 @@ export function lint(ctx) {
37
37
  '--results=verified,unknown',
38
38
  '--fail'
39
39
  ],
40
- { cwd, encoding: 'utf8', shell: false }
40
+ { cwd }
41
41
  )
42
- if (r.status !== 0) {
42
+ if (r.exitCode !== 0) {
43
43
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 4000)
44
44
  const outSuffix = out ? `\n${out}` : ''
45
- fail(`security/scan: trufflehog знайшов секрети (код ${r.status})${outSuffix}`, 'secret-found')
45
+ fail(`security/scan: trufflehog знайшов секрети (код ${r.exitCode})${outSuffix}`, 'secret-found')
46
46
  }
47
47
  return reporter.result()
48
48
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-lint.mjs
4
4
  resource: npm/rules/style/lint/fix-lint.mjs
5
5
  docgen:
6
- crc: ef3a0112
6
+ crc: 140458e8
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/style/lint/main.mjs
5
5
  docgen:
6
- crc: fe57567b
6
+ crc: cd4833fb
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -7,11 +7,11 @@
7
7
  * режимі), форматує їх і повертає лише фактично змінені. Запис permanent. Відсутній
8
8
  * stylelint → no-op.
9
9
  */
10
- import { spawnSync } from 'node:child_process'
11
10
  import { readFileSync } from 'node:fs'
12
11
  import { resolve } from 'node:path'
13
12
 
14
13
  import { filterStyleFiles, resolveStylelint } from './main.mjs'
14
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
15
15
 
16
16
  /**
17
17
  * Вміст файлу або null, якщо не читається.
@@ -31,12 +31,12 @@ function readOrNull(abs) {
31
31
  * css/scss/vue через `git ls-files`.
32
32
  * @param {string} cwd корінь
33
33
  * @param {string[]|undefined} ctxFiles файли з контексту (дельта) або undefined (повний режим)
34
- * @returns {string[]} posix-relative шляхи
34
+ * @returns {Promise<string[]>} posix-relative шляхи
35
35
  */
36
- function listStyleFiles(cwd, ctxFiles) {
36
+ async function listStyleFiles(cwd, ctxFiles) {
37
37
  if (ctxFiles !== undefined) return filterStyleFiles(ctxFiles)
38
- const r = spawnSync('git', ['ls-files', '-z', '--', '*.css', '*.scss', '*.vue'], { cwd, encoding: 'utf8' })
39
- if (r.status !== 0) return []
38
+ const r = await spawnAsync('git', ['ls-files', '-z', '--', '*.css', '*.scss', '*.vue'], { cwd })
39
+ if (r.exitCode !== 0) return []
40
40
  return (r.stdout ?? '').split('\0').filter(Boolean)
41
41
  }
42
42
 
@@ -46,15 +46,15 @@ export const patterns = [
46
46
  id: 'style-stylelint-fix',
47
47
  standalone: true, // §8 Phase 2: apply бере ctx.files (не violations), stylelint --fix сам ре-аналізує
48
48
  test: violations => violations.some(v => v.reason === 'stylelint-violation'),
49
- apply: (violations, ctx) => {
49
+ apply: async (violations, ctx) => {
50
50
  const stylelint = resolveStylelint(ctx.cwd)
51
51
  if (!stylelint) return { touchedFiles: [] }
52
- const files = listStyleFiles(ctx.cwd, ctx.files)
52
+ const files = await listStyleFiles(ctx.cwd, ctx.files)
53
53
  if (files.length === 0) return { touchedFiles: [] }
54
54
 
55
55
  const abs = files.map(f => resolve(ctx.cwd, f))
56
56
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
57
- spawnSync(stylelint, ['--fix', ...files], { cwd: ctx.cwd, encoding: 'utf8' })
57
+ await spawnAsync(stylelint, ['--fix', ...files], { cwd: ctx.cwd })
58
58
 
59
59
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
60
60
  for (const a of touchedFiles) ctx.recordWrite?.(a)
@@ -3,12 +3,12 @@
3
3
  * Per-file (ctx.files) або весь проєкт (ctx.files === undefined). Якщо stylelint
4
4
  * недоступний — крок пропускається (без npx-автовстановлення).
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 { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
11
10
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
11
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
12
12
 
13
13
  const STYLE_EXT_RE = /\.(?:css|scss|vue)$/u
14
14
 
@@ -34,9 +34,9 @@ export function resolveStylelint(cwd) {
34
34
  /**
35
35
  * Detector style/lint (read-only).
36
36
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd, files)
37
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат зі зібраними violations
37
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат зі зібраними violations
38
38
  */
39
- export function lint(ctx) {
39
+ export async function lint(ctx) {
40
40
  const reporter = createViolationReporter(ctx)
41
41
  const { fail } = reporter
42
42
  const cwd = ctx.cwd
@@ -58,11 +58,11 @@ export function lint(ctx) {
58
58
  return reporter.result()
59
59
  }
60
60
 
61
- const r = spawnSync(stylelint, targets, { cwd, encoding: 'utf8', shell: false })
62
- if (r.status !== 0) {
61
+ const r = await spawnAsync(stylelint, targets, { cwd, shell: false })
62
+ if (r.exitCode !== 0) {
63
63
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`.trim().slice(0, 2000)
64
64
  const outSuffix = out ? `\n${out}` : ''
65
- fail(`lint-style: stylelint — порушення (код ${r.status ?? 1}, style.mdc)${outSuffix}`, 'stylelint-violation')
65
+ fail(`lint-style: stylelint — порушення (код ${r.exitCode ?? 1}, style.mdc)${outSuffix}`, 'stylelint-violation')
66
66
  }
67
67
  return reporter.result()
68
68
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-worker.mjs
4
4
  resource: npm/rules/text/cspell-fix/fix-worker.mjs
5
5
  docgen:
6
- crc: 59f849d4
6
+ crc: f7dc29a5
7
7
  model: openai-codex/gpt-5.5
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/text/cspell-fix/main.mjs
5
5
  docgen:
6
- crc: 22853b73
6
+ crc: 8a2b75ff
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  ---
9
9
 
@@ -40,7 +40,7 @@ export async function fixWorker(violations, ctx) {
40
40
  const bin = resolveCmd('npx')
41
41
  if (!bin) return { touchedFiles: [] }
42
42
 
43
- const first = detectCspell(ctx.cwd, bin, ctx.files)
43
+ const first = await detectCspell(ctx.cwd, bin, ctx.files)
44
44
  if (first.code === 0) return { touchedFiles: [] }
45
45
 
46
46
  const words = unknownWords(first.out)
@@ -16,11 +16,11 @@
16
16
  * typo лишаються → cspell повертає !=0 → exit 1 (людина доправляє одруки вручну).
17
17
  */
18
18
  import { env } from 'node:process'
19
- import { spawnSync } from 'node:child_process'
20
19
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
21
20
  import { join } from 'node:path'
22
21
 
23
22
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
23
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
24
24
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
25
25
 
26
26
  /** Слово у рядку cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */
@@ -44,21 +44,18 @@ export const fixModel = () => env.N_LOCAL_MIN_MODEL || ''
44
44
  * рядок (`--no-summary` НЕ передається) лишається — з нього парситься `FILES_CHECKED_RE`.
45
45
  * @param {string} cwd корінь
46
46
  * @param {string} bin шлях до cspell (npx/локальний)
47
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
48
+ * (ADR 260716-1354).
47
49
  * @param {string[]} [files] явний перелік файлів (delta); без нього — `cspell .`
48
50
  * @param {boolean} [verbose] показати повний нативний вивід cspell (включно з прогресом)
49
- * @returns {{ code:number, out:string }} код + обʼєднаний stdout/stderr
51
+ * @returns {Promise<{ code:number, out:string }>} код + обʼєднаний stdout/stderr
50
52
  */
51
- export function detectCspell(cwd, bin, files, verbose = false) {
53
+ export async function detectCspell(cwd, bin, files, verbose = false) {
52
54
  const targets = files === undefined ? ['.'] : files
53
55
  const quietArgs = verbose ? [] : ['--no-progress']
54
- const r = spawnSync(bin, ['cspell', ...quietArgs, ...targets], {
55
- cwd,
56
- encoding: 'utf8',
57
- maxBuffer: 32 * 1024 * 1024,
58
- env: process.env
59
- })
56
+ const r = await spawnAsync(bin, ['cspell', ...quietArgs, ...targets], { cwd, env: process.env })
60
57
  const out = `${r.stdout ?? ''}${r.stderr ?? ''}`
61
- const code = typeof r.status === 'number' ? r.status : 1
58
+ const code = typeof r.exitCode === 'number' ? r.exitCode : 1
62
59
  // cspell повертає !=0 і коли жоден переданий файл не пройшов ignorePaths
63
60
  // (`.cspell.json`) — «Files checked: 0» означає «нічого перевіряти», не порушення.
64
61
  const checked = FILES_CHECKED_RE.exec(out)
@@ -138,19 +135,21 @@ export function appendWordsToDict(cwd, words) {
138
135
  /**
139
136
  * cspell-крок lint-text: read-only детект (нуль мутацій). LLM-класифікація знахідок
140
137
  * і поповнення `.cspell.json#words` живуть у `fix-worker.mjs` (Central Runner Pipeline).
138
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
139
+ * (ADR 260716-1354).
141
140
  * @param {string[]} [files] явний перелік файлів (delta); без нього — `cspell .`
142
141
  * @param {string} [cwd] корінь
143
142
  * @param {boolean} [verbose] показати повний нативний вивід cspell (включно з прогресом)
144
- * @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
143
+ * @returns {Promise<number>} 0 — чисто; 1 — лишились знахідки / помилка середовища
145
144
  */
146
- export function runCspellText(files, cwd = process.cwd(), verbose = false) {
145
+ export async function runCspellText(files, cwd = process.cwd(), verbose = false) {
147
146
  const bin = resolveCmd('npx')
148
147
  if (!bin) {
149
148
  process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
150
149
  return 1
151
150
  }
152
151
 
153
- const first = detectCspell(cwd, bin, files, verbose)
152
+ const first = await detectCspell(cwd, bin, files, verbose)
154
153
  if (first.code === 0) {
155
154
  // «Files checked: 0» при явно переданих файлах — легітимно (усі під ignorePaths),
156
155
  // але так само виглядає зламане середовище (спостережено 2026-07-12: cspell-gitignore
@@ -174,12 +173,12 @@ export function runCspellText(files, cwd = process.cwd(), verbose = false) {
174
173
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
175
174
  * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат детектора
176
175
  */
177
- export function lint(ctx) {
176
+ export async function lint(ctx) {
178
177
  const reporter = createViolationReporter(ctx)
179
178
  const { fail } = reporter
180
179
  if (ctx.files !== undefined && ctx.files.length === 0) return reporter.result()
181
180
 
182
- const code = runCspellText(ctx.files, ctx.cwd, ctx.verbose === true)
181
+ const code = await runCspellText(ctx.files, ctx.cwd, ctx.verbose === true)
183
182
  if (code !== 0) fail('cspell знайшов порушення правопису (text.mdc)', 'cspell')
184
183
  return reporter.result()
185
184
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: fix-oxfmt.mjs
4
4
  resource: npm/rules/text/oxfmt/fix-oxfmt.mjs
5
5
  docgen:
6
- crc: 3c8ca96c
6
+ crc: 41767068
7
7
  ---
8
8
 
9
9
  ## Огляд
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/text/oxfmt/main.mjs
5
5
  docgen:
6
- crc: 40c037c0
6
+ crc: df5242e9
7
7
  ---
8
8
 
9
9
  ## Огляд