@7n/rules 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +13 -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/rego/conftest_verify/docs/main.md +1 -2
  27. package/rules/rego/conftest_verify/main.mjs +12 -10
  28. package/rules/rego/lib/docs/run-external-tool.md +1 -1
  29. package/rules/rego/lib/run-external-tool.mjs +10 -7
  30. package/rules/rego/opa_check/docs/main.md +1 -1
  31. package/rules/rego/opa_check/main.mjs +3 -3
  32. package/rules/rego/regal/docs/main.md +1 -1
  33. package/rules/rego/regal/main.mjs +3 -3
  34. package/rules/security/scan/docs/main.md +1 -1
  35. package/rules/security/scan/main.mjs +9 -9
  36. package/rules/style/lint/docs/fix-lint.md +1 -1
  37. package/rules/style/lint/docs/main.md +1 -1
  38. package/rules/style/lint/fix-lint.mjs +8 -8
  39. package/rules/style/lint/main.mjs +6 -6
  40. package/rules/text/cspell-fix/docs/fix-worker.md +1 -1
  41. package/rules/text/cspell-fix/docs/main.md +1 -1
  42. package/rules/text/cspell-fix/fix-worker.mjs +1 -1
  43. package/rules/text/cspell-fix/main.mjs +14 -15
  44. package/rules/text/oxfmt/docs/fix-oxfmt.md +1 -1
  45. package/rules/text/oxfmt/docs/main.md +1 -1
  46. package/rules/text/oxfmt/fix-oxfmt.mjs +3 -3
  47. package/rules/text/oxfmt/main.mjs +6 -5
  48. package/rules/text/run-dotenv-linter/docs/fix-run-dotenv-linter.md +1 -2
  49. package/rules/text/run-dotenv-linter/docs/main.md +1 -1
  50. package/rules/text/run-dotenv-linter/fix-run-dotenv-linter.mjs +2 -2
  51. package/rules/text/run-dotenv-linter/main.mjs +19 -24
  52. package/rules/text/run-shellcheck/docs/fix-run-shellcheck.md +1 -2
  53. package/rules/text/run-shellcheck/docs/main.md +1 -1
  54. package/rules/text/run-shellcheck/fix-run-shellcheck.mjs +3 -3
  55. package/rules/text/run-shellcheck/main.mjs +46 -57
  56. package/rules/text/run-v8r/docs/main.md +1 -1
  57. package/rules/text/run-v8r/main.mjs +31 -26
  58. package/scripts/lib/docs/resolve-plugins.md +1 -1
  59. package/scripts/lib/lint-surface/blocking-inventory.mjs +12 -40
  60. package/scripts/lib/lint-surface/docs/blocking-inventory.md +1 -2
  61. package/scripts/utils/docs/spawn-async.md +1 -2
  62. package/scripts/utils/spawn-async.mjs +4 -1
  63. package/rules/python/applies/concern.json +0 -4
  64. package/rules/python/applies/docs/main.md +0 -29
  65. package/rules/python/applies/main.mjs +0 -24
  66. package/rules/python/docs/index.md +0 -11
  67. package/rules/python/lib/docs/index.md +0 -9
  68. package/rules/python/lib/docs/uv-run.md +0 -32
  69. package/rules/python/lib/uv-run.mjs +0 -48
  70. package/rules/python/main.json +0 -1
  71. package/rules/python/main.mdc +0 -8
  72. package/rules/python/mypy/concern.json +0 -7
  73. package/rules/python/mypy/docs/main.md +0 -32
  74. package/rules/python/mypy/main.mjs +0 -36
  75. package/rules/python/project/concern.json +0 -6
  76. package/rules/python/project/docs/main.md +0 -43
  77. package/rules/python/project/main.mjs +0 -115
  78. package/rules/python/pyproject_toml/concern.json +0 -9
  79. package/rules/python/pyproject_toml/pyproject_toml.mdc +0 -29
  80. package/rules/python/pyproject_toml/pyproject_toml.rego +0 -53
  81. package/rules/python/pyproject_toml/template/pyproject.toml.deny.toml +0 -2
  82. package/rules/python/pyproject_toml/template/pyproject.toml.snippet.toml +0 -12
  83. package/rules/python/ruff/concern.json +0 -7
  84. package/rules/python/ruff/docs/fix-ruff.md +0 -34
  85. package/rules/python/ruff/docs/main.md +0 -35
  86. package/rules/python/ruff/fix-ruff.mjs +0 -63
  87. package/rules/python/ruff/main.mjs +0 -52
  88. package/rules/python/tooling/concern.json +0 -15
  89. package/rules/python/tooling/docs/index.md +0 -11
  90. package/rules/python/tooling/docs/main.md +0 -35
  91. package/rules/python/tooling/main.mjs +0 -45
  92. package/rules/python/tooling/tooling.mdc +0 -22
  93. package/rules/rust/applies/concern.json +0 -4
  94. package/rules/rust/applies/docs/index.md +0 -11
  95. package/rules/rust/applies/docs/main.md +0 -30
  96. package/rules/rust/applies/main.mjs +0 -28
  97. package/rules/rust/check/check.mdc +0 -16
  98. package/rules/rust/check/concern.json +0 -7
  99. package/rules/rust/check/docs/fix-check.md +0 -30
  100. package/rules/rust/check/docs/index.md +0 -11
  101. package/rules/rust/check/docs/main.md +0 -37
  102. package/rules/rust/check/fix-check.mjs +0 -79
  103. package/rules/rust/check/main.mjs +0 -81
  104. package/rules/rust/docs/index.md +0 -11
  105. package/rules/rust/lib/docs/has-cargo-toml.md +0 -134
  106. package/rules/rust/lib/docs/index.md +0 -11
  107. package/rules/rust/lib/has-cargo-toml.mjs +0 -37
  108. package/rules/rust/main.json +0 -1
  109. package/rules/rust/main.mdc +0 -8
  110. package/rules/rust/package_json/concern.json +0 -10
  111. package/rules/rust/package_json/package_json.mdc +0 -12
  112. package/rules/rust/package_json/package_json.rego +0 -20
  113. package/rules/rust/vscode_extensions/concern.json +0 -11
  114. package/rules/rust/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
  115. package/rules/rust/vscode_extensions/docs/index.md +0 -11
  116. package/rules/rust/vscode_extensions/fix-vscode_extensions.mjs +0 -1
  117. package/rules/rust/vscode_extensions/template/extensions.json.snippet.json +0 -1
  118. package/rules/rust/vscode_extensions/vscode_extensions.mdc +0 -9
  119. package/rules/rust/vscode_extensions/vscode_extensions.rego +0 -15
@@ -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
  ## Огляд
@@ -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