@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
@@ -6,11 +6,11 @@
6
6
  * (`data.kind === 'oxfmt-unformatted'`); re-detect підтверджує результат. Запис permanent.
7
7
  * Тул відсутній у PATH → no-op.
8
8
  */
9
- import { spawnSync } from 'node:child_process'
10
9
  import { readFileSync } from 'node:fs'
11
10
  import { 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, якщо не читається.
@@ -30,7 +30,7 @@ export const patterns = [
30
30
  {
31
31
  id: 'text-oxfmt-write',
32
32
  test: violations => violations.some(v => v.data?.kind === 'oxfmt-unformatted' && v.file),
33
- apply: (violations, ctx) => {
33
+ apply: async (violations, ctx) => {
34
34
  const oxfmt = resolveCmd('oxfmt')
35
35
  if (!oxfmt) return { touchedFiles: [] }
36
36
  const files = [
@@ -40,7 +40,7 @@ export const patterns = [
40
40
 
41
41
  const abs = files.map(f => resolve(ctx.cwd, f))
42
42
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
43
- spawnSync(oxfmt, ['--write', ...files], { cwd: ctx.cwd, encoding: 'utf8' })
43
+ await spawnAsync(oxfmt, ['--write', ...files], { cwd: ctx.cwd })
44
44
 
45
45
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
46
46
  for (const a of touchedFiles) ctx.recordWrite?.(a)
@@ -6,23 +6,24 @@
6
6
  * `.oxfmtrc.json` (включно з `ignorePatterns`). Автофікс (`oxfmt --write`) — окремий T0
7
7
  * `fix-oxfmt.mjs`, не в detector-і. Тул відсутній у PATH → SKIP (не violation), як інші зовнішні.
8
8
  */
9
- import { spawnSync } from 'node:child_process'
10
9
  import { relative, resolve } 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
 
15
- const JSON_MAX_BUFFER = 64 * 1024 * 1024
16
15
  const FMT_EXT_RE = /\.(?:m?[jt]s|c[jt]s|jsx|tsx|vue|css|scss)$/u
17
16
  // Безконфліктна formatter-домена: oxfmt-типи, що їх не форматує інше тулінг (md/json/yaml — поза).
18
17
  const FMT_GLOB = '**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,vue,css,scss}'
19
18
 
20
19
  /**
21
20
  * Detector: повертає по одному violation на кожен неформатований файл.
21
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
22
+ * (ADR 260716-1354).
22
23
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону (cwd, files)
23
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат із violation на кожен неформатований файл
24
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із violation на кожен неформатований файл
24
25
  */
25
- export function lint(ctx) {
26
+ export async function lint(ctx) {
26
27
  const { cwd, files } = ctx
27
28
  const reporter = createViolationReporter(ctx)
28
29
  const oxfmt = resolveCmd('oxfmt')
@@ -31,7 +32,7 @@ export function lint(ctx) {
31
32
  const targets = files === undefined ? [FMT_GLOB] : files.filter(f => FMT_EXT_RE.test(f))
32
33
  if (targets.length === 0) return reporter.result()
33
34
 
34
- const r = spawnSync(oxfmt, ['--list-different', ...targets], { cwd, encoding: 'utf8', maxBuffer: JSON_MAX_BUFFER })
35
+ const r = await spawnAsync(oxfmt, ['--list-different', ...targets], { cwd })
35
36
  const unformatted = (r.stdout ?? '')
36
37
  .split('\n')
37
38
  .map(s => s.trim())
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-run-dotenv-linter.mjs
4
4
  resource: npm/rules/text/run-dotenv-linter/fix-run-dotenv-linter.mjs
5
5
  docgen:
6
- crc: b5c46c77
6
+ crc: 4fef0c3c
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 90
10
9
  issues: internal-name:runDotenvLinter,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/text/run-dotenv-linter/main.mjs
5
5
  docgen:
6
- crc: 1e0140eb
6
+ crc: 47964f53
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -48,12 +48,12 @@ export const patterns = [
48
48
  id: 'text-dotenv-fix',
49
49
  standalone: true, // §8 Phase 2: dotenv-linter fix сам ре-аналізує, test() не потрібен
50
50
  test: violations => violations.some(v => v.reason === 'dotenv-linter'),
51
- apply: (violations, ctx) => {
51
+ apply: async (violations, ctx) => {
52
52
  const files = listEnvFiles(ctx.cwd)
53
53
  if (files.length === 0) return { touchedFiles: [] }
54
54
  const abs = files.map(f => resolve(ctx.cwd, f))
55
55
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
56
- runDotenvLinter(ctx.cwd, false)
56
+ await runDotenvLinter(ctx.cwd, false)
57
57
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
58
58
  for (const a of touchedFiles) ctx.recordWrite?.(a)
59
59
  return touchedFiles.length > 0
@@ -15,11 +15,11 @@
15
15
  * фінальний `dotenv-linter check -r --quiet . --exclude …`; будь-яке залишкове порушення — ненульовий
16
16
  * код виходу.
17
17
  */
18
- import { spawnSync } from 'node:child_process'
19
18
  import { resolve } from 'node:path'
20
19
 
21
20
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
22
21
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
22
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
23
23
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
24
24
 
25
25
  /** Каталоги/файли, які виключаємо з рекурсивного сканування dotenv-linter. */
@@ -58,12 +58,14 @@ function buildTargetArgs(scopeFiles) {
58
58
 
59
59
  /**
60
60
  * Запускає dotenv-linter з авто-фіксом і фінальною перевіркою.
61
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
62
+ * (ADR 260716-1354).
61
63
  * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
62
64
  * @param {boolean} [readOnly] true → пропустити авто-фікс (`fix`), лише `check` (нуль мутацій)
63
65
  * @param {string[]} [scopeFiles] явний перелік `.env*`-файлів (delta) — якщо не задано, рекурсивний `-r .`
64
- * @returns {number} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
66
+ * @returns {Promise<number>} 0 — OK; 1 — інструмент відсутній або є залишкові порушення
65
67
  */
66
- export function runDotenvLinter(cwd, readOnly, scopeFiles) {
68
+ export async function runDotenvLinter(cwd, readOnly, scopeFiles) {
67
69
  const root = resolve(cwd ?? process.cwd())
68
70
  const bin = resolveCmd('dotenv-linter')
69
71
  if (!bin) {
@@ -73,29 +75,22 @@ export function runDotenvLinter(cwd, readOnly, scopeFiles) {
73
75
 
74
76
  const targets = buildTargetArgs(scopeFiles)
75
77
  if (!readOnly) {
76
- const fixRun = spawnSync(bin, ['fix', '--no-backup', '--quiet', ...targets], {
77
- cwd: root,
78
- encoding: 'utf8',
79
- env: process.env,
80
- stdio: ['ignore', 'pipe', 'pipe']
81
- })
82
- if (fixRun.error) {
83
- process.stderr.write(`${fixRun.error.message}\n`)
78
+ try {
79
+ await spawnAsync(bin, ['fix', '--no-backup', '--quiet', ...targets], { cwd: root, env: process.env })
80
+ } catch (error) {
81
+ process.stderr.write(`${error.message}\n`)
84
82
  return 1
85
83
  }
86
84
  }
87
85
 
88
- const checkRun = spawnSync(bin, ['check', '--quiet', ...targets], {
89
- cwd: root,
90
- encoding: 'utf8',
91
- env: process.env,
92
- stdio: ['ignore', 'pipe', 'pipe']
93
- })
94
- if (checkRun.error) {
95
- process.stderr.write(`${checkRun.error.message}\n`)
86
+ let checkRun
87
+ try {
88
+ checkRun = await spawnAsync(bin, ['check', '--quiet', ...targets], { cwd: root, env: process.env })
89
+ } catch (error) {
90
+ process.stderr.write(`${error.message}\n`)
96
91
  return 1
97
92
  }
98
- if (checkRun.status === 0) return 0
93
+ if (checkRun.exitCode === 0) return 0
99
94
  if (checkRun.stdout?.length) process.stdout.write(checkRun.stdout)
100
95
  if (checkRun.stderr?.length) process.stderr.write(checkRun.stderr)
101
96
  return 1
@@ -104,19 +99,19 @@ export function runDotenvLinter(cwd, readOnly, scopeFiles) {
104
99
  /**
105
100
  * Detector text/run-dotenv-linter: read-only dotenv-linter по `ctx.files` (delta) або по всіх `.env*` (full).
106
101
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
107
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
102
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
108
103
  */
109
- export function lint(ctx) {
104
+ export async function lint(ctx) {
110
105
  const reporter = createViolationReporter(ctx)
111
106
  const { fail } = reporter
112
107
  const scopeFiles = ctx.files === undefined ? undefined : ctx.files.filter(f => ENV_BASENAME_RE.test(f))
113
108
  if (scopeFiles !== undefined && scopeFiles.length === 0) return reporter.result()
114
109
 
115
- const code = runDotenvLinter(ctx.cwd, true, scopeFiles)
110
+ const code = await runDotenvLinter(ctx.cwd, true, scopeFiles)
116
111
  if (code !== 0) fail('dotenv-linter знайшов порушення у .env* (text.mdc)', 'dotenv-linter')
117
112
  return reporter.result()
118
113
  }
119
114
 
120
115
  if (isRunAsCli(import.meta.url)) {
121
- process.exitCode = runDotenvLinter()
116
+ process.exitCode = await runDotenvLinter()
122
117
  }
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-run-shellcheck.mjs
4
4
  resource: npm/rules/text/run-shellcheck/fix-run-shellcheck.mjs
5
5
  docgen:
6
- crc: 90367c61
6
+ crc: 190ecdc3
7
7
  model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
8
  score: 90
10
9
  issues: internal-name:runShellcheckText,judge:inaccurate:0.98
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/text/run-shellcheck/main.mjs
5
5
  docgen:
6
- crc: e365b165
6
+ crc: 809e7f3b
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -28,12 +28,12 @@ export const patterns = [
28
28
  id: 'text-shellcheck-fix',
29
29
  standalone: true, // §8 Phase 2: shellcheck diff+patch цикл сам ре-аналізує, test() не потрібен
30
30
  test: violations => violations.some(v => v.reason === 'shellcheck'),
31
- apply: (violations, ctx) => {
32
- const files = listShellScriptPaths(ctx.cwd)
31
+ apply: async (violations, ctx) => {
32
+ const files = await listShellScriptPaths(ctx.cwd)
33
33
  if (files.length === 0) return { touchedFiles: [] }
34
34
  const abs = files.map(f => resolve(ctx.cwd, f))
35
35
  const before = new Map(abs.map(a => [a, readOrNull(a)]))
36
- runShellcheckText(ctx.cwd, false)
36
+ await runShellcheckText(ctx.cwd, false)
37
37
  const touchedFiles = abs.filter(a => readOrNull(a) !== before.get(a))
38
38
  for (const a of touchedFiles) ctx.recordWrite?.(a)
39
39
  return touchedFiles.length > 0
@@ -15,12 +15,12 @@
15
15
  * Після циклу авто-виправлень виконується звичайний `shellcheck` по всіх зібраних файлах; будь-яке
16
16
  * попередження чи помилка — ненульовий код виходу.
17
17
  */
18
- import { spawnSync } from 'node:child_process'
19
18
  import { globSync } from 'node:fs'
20
19
  import { resolve } from 'node:path'
21
20
 
22
21
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
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
  /** Розширення shell-скриптів — фільтр delta-списку файлів у `lint(ctx)`. */
@@ -65,24 +65,18 @@ function printPatchInstallHints() {
65
65
 
66
66
  /**
67
67
  * Повертає відносні шляхи до shell-скриптів для перевірки.
68
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
69
+ * (ADR 260716-1354).
68
70
  * @param {string} cwd корінь проєкту
69
- * @returns {string[]} відсортований масив шляхів відносно cwd
71
+ * @returns {Promise<string[]>} відсортований масив шляхів відносно cwd
70
72
  */
71
- export function listShellScriptPaths(cwd) {
73
+ export async function listShellScriptPaths(cwd) {
72
74
  const gitPath = resolveCmd('git')
73
75
  if (gitPath) {
74
- const gitOk = spawnSync(gitPath, ['rev-parse', '--is-inside-work-tree'], {
75
- cwd,
76
- encoding: 'utf8',
77
- env: process.env
78
- })
79
- if (gitOk.status === 0 && gitOk.stdout.trim() === 'true') {
80
- const ls = spawnSync(gitPath, ['ls-files', '-z', '--', ':(glob)**/*.sh'], {
81
- cwd,
82
- encoding: 'utf8',
83
- env: process.env
84
- })
85
- if (ls.status !== 0) {
76
+ const gitOk = await spawnAsync(gitPath, ['rev-parse', '--is-inside-work-tree'], { cwd, env: process.env })
77
+ if (gitOk.exitCode === 0 && gitOk.stdout.trim() === 'true') {
78
+ const ls = await spawnAsync(gitPath, ['ls-files', '-z', '--', ':(glob)**/*.sh'], { cwd, env: process.env })
79
+ if (ls.exitCode !== 0) {
86
80
  return []
87
81
  }
88
82
  const files = ls.stdout.split('\0').filter(Boolean)
@@ -102,9 +96,9 @@ export function listShellScriptPaths(cwd) {
102
96
  * @param {string} [cwd] робочий каталог (за замовчуванням `process.cwd()`)
103
97
  * @param {boolean} [readOnly] true → пропустити авто-фікс (diff+patch), лише фінальна перевірка
104
98
  * @param {string[]} [scopeFiles] явний перелік файлів (delta) — якщо не задано, шукає всі `*.sh` через `listShellScriptPaths`
105
- * @returns {number} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
99
+ * @returns {Promise<number>} 0 — OK; 1 — помилка середовища або залишкові зауваження shellcheck
106
100
  */
107
- export function runShellcheckText(cwd, readOnly, scopeFiles) {
101
+ export async function runShellcheckText(cwd, readOnly, scopeFiles) {
108
102
  const root = resolve(cwd ?? process.cwd())
109
103
  const shellcheck = resolveCmd('shellcheck')
110
104
  if (!shellcheck) {
@@ -118,14 +112,14 @@ export function runShellcheckText(cwd, readOnly, scopeFiles) {
118
112
  return 1
119
113
  }
120
114
 
121
- const files = scopeFiles ?? listShellScriptPaths(root)
115
+ const files = scopeFiles ?? (await listShellScriptPaths(root))
122
116
  if (files.length === 0) {
123
117
  return 0
124
118
  }
125
119
 
126
120
  if (!readOnly) {
127
121
  for (const rel of files) {
128
- const fixCode = autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel)
122
+ const fixCode = await autofixOneFile(shellcheck, /** @type {string} */ (patchBin), root, rel)
129
123
  if (fixCode !== 0) return fixCode
130
124
  }
131
125
  }
@@ -136,15 +130,15 @@ export function runShellcheckText(cwd, readOnly, scopeFiles) {
136
130
  /**
137
131
  * Detector text/run-shellcheck: read-only shellcheck по `ctx.files` (delta) або по всіх `*.sh` (full).
138
132
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
139
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
133
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
140
134
  */
141
- export function lint(ctx) {
135
+ export async function lint(ctx) {
142
136
  const reporter = createViolationReporter(ctx)
143
137
  const { fail } = reporter
144
138
  const scopeFiles = ctx.files === undefined ? undefined : ctx.files.filter(f => SH_EXT_RE.test(f))
145
139
  if (scopeFiles !== undefined && scopeFiles.length === 0) return reporter.result()
146
140
 
147
- const code = runShellcheckText(ctx.cwd, true, scopeFiles)
141
+ const code = await runShellcheckText(ctx.cwd, true, scopeFiles)
148
142
  if (code !== 0) fail('shellcheck знайшов порушення у *.sh (text.mdc)', 'shellcheck')
149
143
  return reporter.result()
150
144
  }
@@ -156,22 +150,19 @@ export function lint(ctx) {
156
150
  * @param {string} patchBin абсолютний шлях до patch
157
151
  * @param {string} root абсолютний робочий каталог (cwd для spawn)
158
152
  * @param {string} rel відносний шлях файла від `root`
159
- * @returns {number} 0 — OK; 1 — помилка spawn або patch
153
+ * @returns {Promise<number>} 0 — OK; 1 — помилка spawn або patch
160
154
  */
161
- function autofixOneFile(shellcheck, patchBin, root, rel) {
155
+ async function autofixOneFile(shellcheck, patchBin, root, rel) {
162
156
  for (let round = 0; round < MAX_FIX_ROUNDS_PER_FILE; round++) {
163
- const diffResult = spawnSync(shellcheck, ['-f', 'diff', rel], {
164
- cwd: root,
165
- encoding: 'utf8',
166
- env: process.env,
167
- maxBuffer: 10 * 1024 * 1024
168
- })
169
- if (diffResult.error) {
170
- process.stderr.write(`${diffResult.error.message}\n`)
157
+ let diffResult
158
+ try {
159
+ diffResult = await spawnAsync(shellcheck, ['-f', 'diff', rel], { cwd: root, env: process.env })
160
+ } catch (error) {
161
+ process.stderr.write(`${error.message}\n`)
171
162
  return 1
172
163
  }
173
164
  if (shouldStopAutofixLoop(diffResult)) return 0
174
- const patchCode = applyShellcheckDiff(patchBin, root, rel, diffResult.stdout ?? '')
165
+ const patchCode = await applyShellcheckDiff(patchBin, root, rel, diffResult.stdout ?? '')
175
166
  if (patchCode !== 0) return patchCode
176
167
  }
177
168
  return 0
@@ -180,11 +171,11 @@ function autofixOneFile(shellcheck, patchBin, root, rel) {
180
171
  /**
181
172
  * Чи треба зупинити цикл авто-фіксів: shellcheck повернув 0, або у stderr є пометка
182
173
  * `none were auto-fixable`, або stdout порожній (нема дифу для застосування).
183
- * @param {{ status: number | null, stdout?: string | null, stderr?: string | null }} diffResult результат spawnSync
174
+ * @param {{ exitCode: number | null, stdout?: string | null, stderr?: string | null }} diffResult результат `spawnAsync`
184
175
  * @returns {boolean} true — більше нічого фіксити
185
176
  */
186
177
  function shouldStopAutofixLoop(diffResult) {
187
- const code = diffResult.status ?? 1
178
+ const code = diffResult.exitCode ?? 1
188
179
  if (code === 0) return true
189
180
  const out = (diffResult.stdout ?? '').trim()
190
181
  const err = (diffResult.stderr ?? '').trim()
@@ -198,16 +189,18 @@ function shouldStopAutofixLoop(diffResult) {
198
189
  * @param {string} root cwd для spawn
199
190
  * @param {string} rel відносний шлях для повідомлення про помилку
200
191
  * @param {string} diffStdout вміст unified-diff від shellcheck (input для patch)
201
- * @returns {number} 0 — застосовано; 1 — помилка
192
+ * @returns {Promise<number>} 0 — застосовано; 1 — помилка
202
193
  */
203
- function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
204
- const patchRun = spawnSync(patchBin, ['-p1'], {
205
- cwd: root,
206
- input: diffStdout,
207
- encoding: 'utf8',
208
- env: process.env
209
- })
210
- if (patchRun.status === 0) return 0
194
+ async function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
195
+ let patchRun
196
+ try {
197
+ patchRun = await spawnAsync(patchBin, ['-p1'], { cwd: root, input: diffStdout, env: process.env })
198
+ } catch (error) {
199
+ process.stderr.write(`${error.message}\n`)
200
+ process.stderr.write(`run-shellcheck-text: patch не застосував diff для ${rel}\n`)
201
+ return 1
202
+ }
203
+ if (patchRun.exitCode === 0) return 0
211
204
  if (patchRun.stderr?.length) process.stderr.write(patchRun.stderr)
212
205
  if (patchRun.stdout?.length) process.stderr.write(patchRun.stdout)
213
206
  process.stderr.write(`run-shellcheck-text: patch не застосував diff для ${rel}\n`)
@@ -220,26 +213,22 @@ function applyShellcheckDiff(patchBin, root, rel, diffStdout) {
220
213
  * @param {string} shellcheck абсолютний шлях до shellcheck
221
214
  * @param {string[]} files відносні шляхи файлів для перевірки
222
215
  * @param {string} root cwd для spawn
223
- * @returns {number} 0 — чисто; 1 — помилка spawn або зауваження shellcheck
216
+ * @returns {Promise<number>} 0 — чисто; 1 — помилка spawn або зауваження shellcheck
224
217
  */
225
- function runFinalShellcheck(shellcheck, files, root) {
226
- const finalRun = spawnSync(shellcheck, files, {
227
- cwd: root,
228
- encoding: 'utf8',
229
- env: process.env,
230
- maxBuffer: 10 * 1024 * 1024,
231
- stdio: ['ignore', 'pipe', 'pipe']
232
- })
233
- if (finalRun.error) {
234
- process.stderr.write(`${finalRun.error.message}\n`)
218
+ async function runFinalShellcheck(shellcheck, files, root) {
219
+ let finalRun
220
+ try {
221
+ finalRun = await spawnAsync(shellcheck, files, { cwd: root, env: process.env })
222
+ } catch (error) {
223
+ process.stderr.write(`${error.message}\n`)
235
224
  return 1
236
225
  }
237
- if (finalRun.status === 0) return 0
226
+ if (finalRun.exitCode === 0) return 0
238
227
  if (finalRun.stdout?.length) process.stdout.write(finalRun.stdout)
239
228
  if (finalRun.stderr?.length) process.stderr.write(finalRun.stderr)
240
229
  return 1
241
230
  }
242
231
 
243
232
  if (isRunAsCli(import.meta.url)) {
244
- process.exitCode = runShellcheckText()
233
+ process.exitCode = await runShellcheckText()
245
234
  }
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/text/run-v8r/main.mjs
5
5
  docgen:
6
- crc: 5c8aab7b
6
+ crc: 7abeb17f
7
7
  model: manual
8
8
  ---
9
9
 
@@ -44,7 +44,6 @@
44
44
  * незалежно від verbosity) і виводить у stdout окреме попередження на кожен такий файл із порадою
45
45
  * додати схему в `npm/schemas/v8r-catalog.json`.
46
46
  */
47
- import { spawnSync } from 'node:child_process'
48
47
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
49
48
  import { tmpdir } from 'node:os'
50
49
  import { basename, delimiter, dirname, isAbsolute, join } from 'node:path'
@@ -53,6 +52,7 @@ import { fileURLToPath } from 'node:url'
53
52
 
54
53
  import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
55
54
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
55
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
56
56
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
57
57
 
58
58
  /** Розширення, які валідує v8r — фільтр delta-списку файлів у `lint(ctx)`. */
@@ -183,31 +183,30 @@ export function stripBunNodeShimDirs(pathValue) {
183
183
  * (`lint()`) для вбудовування у violation-повідомлення, яке бачить LLM fix-worker: без нього
184
184
  * fix-ladder отримує лише "щось не пройшло" й не має шансів вгадати, що саме (спостережено —
185
185
  * усі 4 rung-и незмінно падають у timeout на v8r-порушеннях).
186
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
187
+ * (ADR 260716-1354).
186
188
  * @param {string[]} targets glob-и або конкретні шляхи файлів
187
189
  * @param {string} configPath шлях до `V8R_CONFIG_FILE`
188
190
  * @param {boolean} [verbose] друкувати повний raw stdout/stderr v8r при помилці; інакше — лише
189
191
  * рядки `✖ …` без `ℹ`-шуму (Pre-warming the cache, Processing <file>, Found schema in …)
190
- * @returns {{ exitError: true } | { exitError: false, code: number, detail: string }} помилка
191
- * spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків
192
+ * @returns {Promise<{ exitError: true } | { exitError: false, code: number, detail: string }>}
193
+ * помилка spawn або код v8r (0/98 — трактує викликач) + деталь `✖ …`-рядків
192
194
  */
193
- function runOneV8rInvocation(targets, configPath, verbose = false) {
195
+ async function runOneV8rInvocation(targets, configPath, verbose = false) {
194
196
  const bunPath = resolveCmd('bun') ?? process.execPath
195
- const result = spawnSync(bunPath, ['x', 'v8r', ...targets], {
196
- encoding: 'utf8',
197
- maxBuffer: 50 * 1024 * 1024,
198
- shell: false,
199
- stdio: ['ignore', 'pipe', 'pipe'],
200
- env: { ...env, PATH: stripBunNodeShimDirs(env.PATH), V8R_CONFIG_FILE: configPath }
201
- })
202
-
203
- if (result.error) {
204
- process.stderr.write(`${result.error.message}\n`)
197
+ let result
198
+ try {
199
+ result = await spawnAsync(bunPath, ['x', 'v8r', ...targets], {
200
+ env: { ...env, PATH: stripBunNodeShimDirs(env.PATH), V8R_CONFIG_FILE: configPath }
201
+ })
202
+ } catch (error) {
203
+ process.stderr.write(`${error.message}\n`)
205
204
  return { exitError: true }
206
205
  }
207
206
 
208
207
  warnAboutRemoteSchemaFallback(result.stderr ?? '')
209
208
 
210
- const exitCode = result.status ?? 1
209
+ const exitCode = result.exitCode ?? 1
211
210
  let detail = ''
212
211
  if (exitCode !== 0 && exitCode !== 98) {
213
212
  detail = extractFailureLines(`${result.stdout ?? ''}\n${result.stderr ?? ''}`)
@@ -225,12 +224,15 @@ function runOneV8rInvocation(targets, configPath, verbose = false) {
225
224
  * Запускає послідовні виклики v8r по glob-ам (full-режим); не змінює process.exitCode.
226
225
  * Один виклик на glob навмисно (не batch) — v8r падає з кодом 98, якщо хоч один переданий
227
226
  * glob не знаходить файлів, і тоді решта розширень не перевіряються в тому ж виклику.
227
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
228
+ * (ADR 260716-1354). Виклики по glob-ам лишаються строго послідовними (не Promise.all) — той
229
+ * самий один-виклик-на-glob підхід, лише блокуючий spawnSync замінено на await у циклі.
228
230
  * @param {string[]} [globs] патерни; за замовчуванням DEFAULT_V8R_GLOBS
229
231
  * @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
230
- * @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
232
+ * @returns {Promise<{ code: number, detail: string }>} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
231
233
  * каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
232
234
  */
233
- export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
235
+ export async function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
234
236
  if (!existsSync(V8R_CATALOG_PATH)) {
235
237
  process.stderr.write(
236
238
  `run-v8r: не знайдено каталог схем за шляхом ${V8R_CATALOG_PATH} (очікується npm/schemas/v8r-catalog.json у пакеті)\n`
@@ -241,7 +243,7 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
241
243
  const configPath = writeResolvedV8rConfig()
242
244
 
243
245
  for (const pattern of globs) {
244
- const r = runOneV8rInvocation([pattern], configPath, verbose)
246
+ const r = await runOneV8rInvocation([pattern], configPath, verbose)
245
247
  if (r.exitError) return { code: 1, detail: '' }
246
248
  if (r.code !== 0 && r.code !== 98) return { code: r.code, detail: r.detail }
247
249
  }
@@ -251,12 +253,14 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS, verbose = false) {
251
253
  /**
252
254
  * Запускає v8r по конкретному списку файлів (delta-режим) — один виклик, не по одному glob-у,
253
255
  * бо кожен переданий шлях уже існує (не glob), тож код 98 "порожній glob" тут не виникає.
256
+ * Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
257
+ * (ADR 260716-1354).
254
258
  * @param {string[]} files абсолютні або відносні до cwd v8r-процесу шляхи файлів
255
259
  * @param {boolean} [verbose] друкувати повний raw вивід v8r при помилці (див. runOneV8rInvocation)
256
- * @returns {{ code: number, detail: string }} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
260
+ * @returns {Promise<{ code: number, detail: string }>} `code`: 0 — OK, 1 — помилка spawn, 2 — немає
257
261
  * каталогу схем, інше — код v8r; `detail` — рядки `✖ …` (порожньо, якщо `code` не про валідацію)
258
262
  */
259
- export function runV8rWithFiles(files, verbose = false) {
263
+ export async function runV8rWithFiles(files, verbose = false) {
260
264
  if (files.length === 0) return { code: 0, detail: '' }
261
265
  if (!existsSync(V8R_CATALOG_PATH)) {
262
266
  process.stderr.write(
@@ -266,7 +270,7 @@ export function runV8rWithFiles(files, verbose = false) {
266
270
  }
267
271
 
268
272
  const configPath = writeResolvedV8rConfig()
269
- const r = runOneV8rInvocation(files, configPath, verbose)
273
+ const r = await runOneV8rInvocation(files, configPath, verbose)
270
274
  if (r.exitError) return { code: 1, detail: '' }
271
275
  return { code: r.code === 98 ? 0 : r.code, detail: r.detail }
272
276
  }
@@ -285,28 +289,29 @@ function v8rFailMessage(detail) {
285
289
  /**
286
290
  * Detector text/run-v8r: read-only v8r по `ctx.files` (delta) або за дефолтними glob-ами (full).
287
291
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
288
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
292
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
289
293
  */
290
- export function lint(ctx) {
294
+ export async function lint(ctx) {
291
295
  const reporter = createViolationReporter(ctx)
292
296
  const { fail } = reporter
293
297
 
294
298
  const verbose = ctx.verbose === true
295
299
 
296
300
  if (ctx.files === undefined) {
297
- const { code, detail } = runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
301
+ const { code, detail } = await runV8rWithGlobs(DEFAULT_V8R_GLOBS, verbose)
298
302
  if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
299
303
  return reporter.result()
300
304
  }
301
305
 
302
306
  const files = ctx.files.filter(f => V8R_EXT_RE.test(f))
303
307
  if (files.length === 0) return reporter.result()
304
- const { code, detail } = runV8rWithFiles(files, verbose)
308
+ const { code, detail } = await runV8rWithFiles(files, verbose)
305
309
  if (code !== 0) fail(v8rFailMessage(detail), 'v8r')
306
310
  return reporter.result()
307
311
  }
308
312
 
309
313
  if (isRunAsCli(import.meta.url)) {
310
314
  const globs = process.argv.length > 2 ? process.argv.slice(2) : DEFAULT_V8R_GLOBS
311
- process.exitCode = runV8rWithGlobs(globs).code
315
+ const { code } = await runV8rWithGlobs(globs)
316
+ process.exitCode = code
312
317
  }