@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.20.1] - 2026-07-18
4
+
5
+ ### Changed
6
+
7
+ - docs(adr): brainstorm — внутрішній паралелізм lint-оркестратора (#86)
8
+ - Оновлено внутрішній lint-оркестратор.
9
+
3
10
  ## [1.20.0] - 2026-07-18
4
11
 
5
12
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7n/rules",
3
- "version": "1.20.0",
3
+ "version": "1.20.1",
4
4
  "description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-licensee.mjs
4
4
  resource: npm/rules/bun/licensee/fix-licensee.mjs
5
5
  docgen:
6
- crc: 23a211fd
6
+ crc: 5c27ef32
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.99
11
10
  judgeModel: openai-codex/gpt-5.4-mini
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/bun/licensee/main.mjs
5
5
  docgen:
6
- crc: 3f4c4d25
6
+ crc: 787e39cc
7
7
  model: manual
8
8
  ---
9
9
 
@@ -4,23 +4,22 @@
4
4
  * вручну). Реальні `license-violation` (файл є, але залежності не проходять) — НЕ
5
5
  * T0-фікс: потребують людського рішення про ліцензійну політику, не regen.
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
7
  import { join } from 'node:path'
9
8
 
10
9
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
10
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
11
 
12
12
  /** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
13
13
  export const patterns = [
14
14
  {
15
15
  id: 'bun-licensee-config-init',
16
16
  test: violations => violations.some(v => v.reason === 'licensee-config-missing'),
17
- apply: (violations, ctx) => {
17
+ apply: async (violations, ctx) => {
18
18
  const bun = resolveCmd('bun')
19
19
  if (!bun) return { touchedFiles: [] }
20
20
 
21
- spawnSync(bun, ['x', 'licensee', '--init', '--production', '--quiet'], {
21
+ await spawnAsync(bun, ['x', 'licensee', '--init', '--production', '--quiet'], {
22
22
  cwd: ctx.cwd,
23
- encoding: 'utf8',
24
23
  shell: false
25
24
  })
26
25
 
@@ -4,17 +4,17 @@
4
4
  */
5
5
  import { existsSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
- import { spawnSync } from 'node:child_process'
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
13
  * Detector bun/licensee: ліцензії npm-залежностей через `licensee` (read-only).
14
14
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
15
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
15
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
16
16
  */
17
- export function lint(ctx) {
17
+ export async function lint(ctx) {
18
18
  const reporter = createViolationReporter(ctx)
19
19
  const { fail } = reporter
20
20
  const cwd = ctx.cwd
@@ -40,8 +40,8 @@ export function lint(ctx) {
40
40
  // of undefined (reading 'localeCompare')" — спостережено з @npmcli/arborist на
41
41
  // bun-based node_modules) завжди йде у stderr через die(); легітимний звіт про
42
42
  // порушення — лише у stdout. Канал розрізняє crash від реального порушення.
43
- const r = spawnSync(bun, ['x', 'licensee', '--production', '--errors-only'], { cwd, encoding: 'utf8', shell: false })
44
- if (r.status !== 0) {
43
+ const r = await spawnAsync(bun, ['x', 'licensee', '--production', '--errors-only'], { cwd, shell: false })
44
+ if (r.exitCode !== 0) {
45
45
  const stderr = (r.stderr ?? '').trim().slice(0, 2000)
46
46
  if (stderr) {
47
47
  // Crash самого тула — НЕ підтверджене ліцензійне порушення: fail-open
@@ -54,7 +54,7 @@ export function lint(ctx) {
54
54
  level: 'warn',
55
55
  message:
56
56
  `lint-bun: licensee — інструмент завершився з помилкою, це НЕ підтверджене ліцензійне порушення ` +
57
- `(код ${r.status}, bun.mdc). Ймовірна причина — несумісність @npmcli/arborist з деревом bun install. ` +
57
+ `(код ${r.exitCode}, bun.mdc). Ймовірна причина — несумісність @npmcli/arborist з деревом bun install. ` +
58
58
  `Перевір вручну: \`bunx licensee --production\`.\n${stderr}`
59
59
  }
60
60
  ]
@@ -62,7 +62,7 @@ export function lint(ctx) {
62
62
  }
63
63
  const stdout = (r.stdout ?? '').trim().slice(0, 2000)
64
64
  const detail = stdout ? `\n${stdout}` : ''
65
- fail(`lint-bun: licensee — порушення ліцензій (код ${r.status}, bun.mdc)${detail}`, 'license-violation')
65
+ fail(`lint-bun: licensee — порушення ліцензій (код ${r.exitCode}, bun.mdc)${detail}`, 'license-violation')
66
66
  }
67
67
  return reporter.result()
68
68
  }
@@ -6,10 +6,10 @@
6
6
  * прибрано — hadolint ставиться як **нативний бінарник**, без `docker run`.
7
7
  * Використовується `../js/lint.mjs` (правило `n-rules lint docker` / check-docker).
8
8
  */
9
- import { spawnSync } from 'node:child_process'
10
9
  import { relative, sep } from 'node:path'
11
10
 
12
11
  import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
12
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
13
13
 
14
14
  /**
15
15
  * Відносний шлях від root з прямими слешами (стабільний вивід незалежно від OS).
@@ -27,9 +27,9 @@ export function posixRel(root, absPath) {
27
27
  * чи не вдався — повертаємо `ok: false` з підказкою (без `docker run`).
28
28
  * @param {string} root корінь репозиторію
29
29
  * @param {string} absPath абсолютний шлях до Dockerfile
30
- * @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint
30
+ * @returns {Promise<{ ok: boolean, stdout: string, stderr: string, via: string }>} результат перевірки hadolint
31
31
  */
32
- export function lintDockerfileWithHadolint(root, absPath) {
32
+ export async function lintDockerfileWithHadolint(root, absPath) {
33
33
  const rel = posixRel(root, absPath)
34
34
  let hadolintPath
35
35
  try {
@@ -46,13 +46,11 @@ export function lintDockerfileWithHadolint(root, absPath) {
46
46
  }
47
47
  }
48
48
 
49
- const local = spawnSync(hadolintPath, [rel], {
50
- cwd: root,
51
- encoding: 'utf8',
52
- maxBuffer: 10 * 1024 * 1024
49
+ const local = await spawnAsync(hadolintPath, [rel], {
50
+ cwd: root
53
51
  })
54
52
  return {
55
- ok: local.status === 0,
53
+ ok: local.exitCode === 0,
56
54
  stdout: local.stdout ?? '',
57
55
  stderr: local.stderr ?? '',
58
56
  via: 'hadolint'
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docker-hadolint.mjs
4
4
  resource: npm/rules/docker/lib/docker-hadolint.mjs
5
5
  docgen:
6
- crc: 0351d8ee
6
+ crc: 3a9df18f
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 90
9
9
  ---
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/docker/lint/main.mjs
5
5
  docgen:
6
- crc: 282bafc9
6
+ crc: 26fdea23
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -351,7 +351,7 @@ async function checkDockerfile(reporter, root, abs) {
351
351
  const nginxUserHint = getNginxUnprivilegedUserHint(content)
352
352
  if (nginxUserHint) fail(`${rel} (nginx non-root): ${nginxUserHint}`)
353
353
 
354
- const { ok, stdout, stderr, via } = lintDockerfileWithHadolint(root, abs)
354
+ const { ok, stdout, stderr, via } = await lintDockerfileWithHadolint(root, abs)
355
355
  const tail = (stdout + stderr).trim()
356
356
  if (!ok) {
357
357
  const detail = tail ? `:\n${tail}` : ''
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: fix-check.mjs
4
4
  resource: npm/rules/image-compress/check/fix-check.mjs
5
5
  docgen:
6
- crc: e5f5a01c
6
+ crc: 0c9ec731
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: 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/image-compress/check/main.mjs
5
5
  docgen:
6
- crc: 7b0d7f7d
6
+ crc: 0cb94f6b
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -4,10 +4,10 @@
4
4
  * Detector лише ЗВІТУЄ `needsCompression` (read-only contract); мутація — тут, до LLM-ladder:
5
5
  * стиснення — бінарна трансформація, не текстовий diff, тож LLM-фікс тут апріорі недоречний.
6
6
  */
7
- import { spawnSync } from 'node:child_process'
8
7
  import { env } from 'node:process'
9
8
 
10
9
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
10
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
11
 
12
12
  const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
13
13
  const JSON_MAX_BUFFER = 20 * 1024 * 1024
@@ -17,37 +17,42 @@ const JSON_MAX_BUFFER = 20 * 1024 * 1024
17
17
  * Best-effort: відсутній npx / помилка / ненульовий код — лог-варн без падіння (re-detect
18
18
  * наступного кроку покаже справжній стан).
19
19
  * @param {string} cwd корінь репозиторію
20
- * @returns {string[]} абсолютні шляхи стиснутих файлів (порожньо при помилці/no-op)
20
+ * @returns {Promise<string[]>} абсолютні шляхи стиснутих файлів (порожньо при помилці/no-op)
21
21
  */
22
- function runCompression(cwd) {
22
+ async function runCompression(cwd) {
23
23
  const npxPath = resolveCmd('npx')
24
24
  if (!npxPath) {
25
25
  console.log(` ⚠️ 'npx' не знайдено в PATH — пропускаємо стиснення зображень`)
26
26
  return []
27
27
  }
28
28
 
29
- const writeResult = spawnSync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--write'], {
30
- cwd,
31
- encoding: 'utf8',
32
- env,
33
- maxBuffer: JSON_MAX_BUFFER
34
- })
35
- if (writeResult.error) {
36
- console.log(` ⚠️ не вдалося запустити \`npx ${MINIFY_PACKAGE_NAME} --write\`: ${writeResult.error.message}`)
29
+ let writeResult
30
+ try {
31
+ writeResult = await spawnAsync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--write'], {
32
+ cwd,
33
+ env,
34
+ maxBuffer: JSON_MAX_BUFFER
35
+ })
36
+ } catch (error) {
37
+ console.log(` ⚠️ не вдалося запустити \`npx ${MINIFY_PACKAGE_NAME} --write\`: ${error.message}`)
37
38
  return []
38
39
  }
39
- if (typeof writeResult.status === 'number' && writeResult.status !== 0) {
40
- console.log(` ⚠️ \`npx ${MINIFY_PACKAGE_NAME} --write\` завершився з кодом ${writeResult.status}`)
40
+ if (typeof writeResult.exitCode === 'number' && writeResult.exitCode !== 0) {
41
+ console.log(` ⚠️ \`npx ${MINIFY_PACKAGE_NAME} --write\` завершився з кодом ${writeResult.exitCode}`)
41
42
  return []
42
43
  }
43
44
 
44
- const jsonResult = spawnSync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--json'], {
45
- cwd,
46
- encoding: 'utf8',
47
- env,
48
- maxBuffer: JSON_MAX_BUFFER
49
- })
50
- if (jsonResult.error || jsonResult.status !== 0) return []
45
+ let jsonResult
46
+ try {
47
+ jsonResult = await spawnAsync(npxPath, [MINIFY_PACKAGE_NAME, '--src=.', '--json'], {
48
+ cwd,
49
+ env,
50
+ maxBuffer: JSON_MAX_BUFFER
51
+ })
52
+ } catch {
53
+ return []
54
+ }
55
+ if (jsonResult.exitCode !== 0) return []
51
56
 
52
57
  try {
53
58
  const report = JSON.parse(jsonResult.stdout)
@@ -63,8 +68,8 @@ export const patterns = [
63
68
  {
64
69
  id: 'image-compress-write',
65
70
  test: violations => violations.some(v => v.reason === 'needs-compression'),
66
- apply: (violations, ctx) => {
67
- const compressed = runCompression(ctx.cwd)
71
+ apply: async (violations, ctx) => {
72
+ const compressed = await runCompression(ctx.cwd)
68
73
  if (compressed.length === 0) return { touchedFiles: [] }
69
74
 
70
75
  for (const absPath of compressed) ctx.recordWrite?.(absPath)
@@ -3,9 +3,8 @@
3
3
  * `.n-minify-image.tsv` (`@nitra/minify-image --json`). Стиснення (`--write`) — окремий
4
4
  * fix, не в detector-і.
5
5
  */
6
- import { spawnSync } from 'node:child_process'
7
-
8
6
  import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
7
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
9
8
 
10
9
  const JSON_MAX_BUFFER = 20 * 1024 * 1024
11
10
 
@@ -20,27 +19,28 @@ function parseMinifyJson(stdout) {
20
19
  /**
21
20
  * Detector image-compress/check: \@nitra/minify-image --json (read-only).
22
21
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст lint-прогону
23
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} результат detector-а
22
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат detector-а
24
23
  */
25
- export function lint(ctx) {
24
+ export async function lint(ctx) {
26
25
  const reporter = createViolationReporter(ctx)
27
26
  const { fail } = reporter
28
27
  const cwd = ctx.cwd
29
28
 
30
- const r = spawnSync('npx', ['@nitra/minify-image', '--src=.', '--json'], {
31
- cwd,
32
- encoding: 'utf8',
33
- env: process.env,
34
- maxBuffer: JSON_MAX_BUFFER
35
- })
36
- if (r.error) {
37
- fail(`image-compress: не вдалося запустити npx @nitra/minify-image --json: ${r.error.message}`, 'tool-error')
29
+ let r
30
+ try {
31
+ r = await spawnAsync('npx', ['@nitra/minify-image', '--src=.', '--json'], {
32
+ cwd,
33
+ env: process.env,
34
+ maxBuffer: JSON_MAX_BUFFER
35
+ })
36
+ } catch (error) {
37
+ fail(`image-compress: не вдалося запустити npx @nitra/minify-image --json: ${error.message}`, 'tool-error')
38
38
  return reporter.result()
39
39
  }
40
- if (r.status !== 0) {
40
+ if (r.exitCode !== 0) {
41
41
  const detail = [r.stdout, r.stderr].filter(Boolean).join('\n').trim()
42
42
  const detailSuffix = detail ? `:\n${detail}` : ''
43
- fail(`image-compress: @nitra/minify-image --json завершився з кодом ${r.status}${detailSuffix}`, 'tool-error')
43
+ fail(`image-compress: @nitra/minify-image --json завершився з кодом ${r.exitCode}${detailSuffix}`, 'tool-error')
44
44
  return reporter.result()
45
45
  }
46
46
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/js/jscpd_duplicates/main.mjs
5
5
  docgen:
6
- crc: 8480cba8
6
+ crc: 8839c5e9
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.99
@@ -3,11 +3,12 @@
3
3
  * Кожен клон → одне порушення (anchored на `firstFile`), із посиланням на `secondFile`
4
4
  * у `data`. JSON-звіт пишеться у системний tmp (поза репо) — дерево не мутується.
5
5
  */
6
- import { spawnSync } from 'node:child_process'
7
6
  import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
8
7
  import { tmpdir } from 'node:os'
9
8
  import { join } from 'node:path'
10
9
 
10
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
11
+
11
12
  /**
12
13
  * @typedef {{ name: string, start: number, end: number }} JscpdFileRef
13
14
  * @typedef {{ firstFile: JscpdFileRef, secondFile: JscpdFileRef, lines: number, format: string }} JscpdClone
@@ -38,18 +39,16 @@ function cloneToViolation(clone) {
38
39
  }
39
40
 
40
41
  /**
41
- * Detector js/jscpd_duplicates: дублікати коду через `jscpd` (read-only).
42
+ * Detector js/jscpd_duplicates: дублікати коду через `jscpd` (read-only). Async (не блокує
43
+ * event loop) — детектор може виконуватись у parallel lane `detectAll()` (ADR 260716-1354).
42
44
  * @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту
43
- * @returns {import('../../../scripts/lib/lint-surface/types.mjs').LintResult} перелік порушень
45
+ * @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} перелік порушень
44
46
  */
45
- export function lint(ctx) {
47
+ export async function lint(ctx) {
46
48
  const cwd = ctx.cwd
47
49
  const outDir = mkdtempSync(join(tmpdir(), 'jscpd-'))
48
50
  try {
49
- const r = spawnSync('bunx', ['jscpd', '.', '--reporters', 'json', '--output', outDir, '--silent'], {
50
- cwd,
51
- encoding: 'utf8'
52
- })
51
+ const r = await spawnAsync('bunx', ['jscpd', '.', '--reporters', 'json', '--output', outDir, '--silent'], { cwd })
53
52
  let report
54
53
  try {
55
54
  report = JSON.parse(readFileSync(join(outDir, 'jscpd-report.json'), 'utf8'))
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/k8s/kubeconform/main.mjs
5
5
  docgen:
6
- crc: 76d8d01b
6
+ crc: 398bb541
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  issues: judge:inaccurate:0.98
@@ -32,7 +32,7 @@ export async function lint(ctx) {
32
32
  }
33
33
  if (targets.length === 0) return reporter.result()
34
34
 
35
- const code = runKubeconform(targets, ctx.verbose === true)
35
+ const code = await runKubeconform(targets, ctx.verbose === true)
36
36
  if (code !== 0 && code !== 127) fail('kubeconform знайшов невалідні маніфести (k8s.mdc)', 'kubeconform')
37
37
 
38
38
  return reporter.result()
@@ -3,7 +3,6 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'no
3
3
  import { readFile, readdir, stat, writeFile } from 'node:fs/promises'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { basename, dirname, join, relative, resolve } from 'node:path'
6
- import { spawnSync } from 'node:child_process'
7
6
  import { fileURLToPath } from 'node:url'
8
7
 
9
8
  import { isSeq, parse, parseAllDocuments, parseDocument, stringify } from 'yaml'
@@ -13,6 +12,7 @@ import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.m
13
12
  import { runConftestBatch } from '../../../scripts/lib/run-conftest-batch.mjs'
14
13
  import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
15
14
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
15
+ import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
16
16
  import { walkDir } from '../../../scripts/utils/walkDir.mjs'
17
17
 
18
18
  /** Версія набору схем yannh — узгоджено з k8s.mdc */
@@ -6798,9 +6798,9 @@ const DATREE_CRD_SCHEMA_LOCATION =
6798
6798
  * Прогоняє `kubeconform` по k8s-каталогах (read-only валідація схем).
6799
6799
  * @param {string[]} dirs абсолютні шляхи k8s-коренів.
6800
6800
  * @param {boolean} [verbose] показати повний нативний вивід kubeconform (`stdio: 'inherit'`).
6801
- * @returns {number} exit-код процесу (127 якщо тул відсутній).
6801
+ * @returns {Promise<number>} exit-код процесу (127 якщо тул відсутній).
6802
6802
  */
6803
- export function runKubeconform(dirs, verbose = false) {
6803
+ export async function runKubeconform(dirs, verbose = false) {
6804
6804
  const args = [
6805
6805
  '-summary',
6806
6806
  '-kubernetes-version',
@@ -6813,12 +6813,16 @@ export function runKubeconform(dirs, verbose = false) {
6813
6813
  ...dirs
6814
6814
  ]
6815
6815
  const kubeconformPath = ensureTool('kubeconform')
6816
- const r = spawnSync(kubeconformPath, args, { stdio: verbose ? 'inherit' : 'pipe', shell: false })
6817
- if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
6818
- console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
6819
- return 127
6816
+ try {
6817
+ const r = await spawnAsync(kubeconformPath, args, { stdio: verbose ? 'inherit' : 'pipe' })
6818
+ return r.exitCode ?? 1
6819
+ } catch (error) {
6820
+ if (error && error.code === 'ENOENT') {
6821
+ console.error('kubeconform не знайдено в PATH. Встанови з https://github.com/yannh/kubeconform#readme')
6822
+ return 127
6823
+ }
6824
+ return 1
6820
6825
  }
6821
- return r.status ?? 1
6822
6826
  }
6823
6827
 
6824
6828
  /**
@@ -6866,14 +6870,17 @@ export async function findKustomizationDirs(dir) {
6866
6870
  * @param {string} kubectlPath шлях до бінарника kubectl.
6867
6871
  * @param {string} dir каталог з kustomization.
6868
6872
  * @param {boolean} [verbose] показати stderr тула напряму (`stdio: 'inherit'`).
6869
- * @returns {{ status: number, stdout: Buffer }} exit-код і зібраний YAML.
6873
+ * @returns {Promise<{ status: number, stdout: string }>} exit-код і зібраний YAML.
6870
6874
  */
6871
- function runKustomizeBuild(kubectlPath, dir, verbose = false) {
6872
- const r = spawnSync(kubectlPath, ['kustomize', dir], {
6873
- stdio: ['ignore', 'pipe', verbose ? 'inherit' : 'pipe'],
6874
- shell: false
6875
- })
6876
- return { status: r.status ?? 1, stdout: r.stdout ?? Buffer.alloc(0) }
6875
+ async function runKustomizeBuild(kubectlPath, dir, verbose = false) {
6876
+ try {
6877
+ const r = await spawnAsync(kubectlPath, ['kustomize', dir], {
6878
+ stdio: ['ignore', 'pipe', verbose ? 'inherit' : 'pipe']
6879
+ })
6880
+ return { status: r.exitCode ?? 1, stdout: r.stdout ?? '' }
6881
+ } catch {
6882
+ return { status: 1, stdout: '' }
6883
+ }
6877
6884
  }
6878
6885
 
6879
6886
  /**
@@ -6882,19 +6889,22 @@ function runKustomizeBuild(kubectlPath, dir, verbose = false) {
6882
6889
  * @param {string|Buffer} manifest вміст маніфеста для сканування.
6883
6890
  * @param {string[]} exceptionsArgs додаткові CLI-аргументи (`--exceptions ...`).
6884
6891
  * @param {boolean} [verbose] показати повний нативний вивід kubescape (`stdio: 'inherit'`).
6885
- * @returns {{ status: number, enoent: boolean }} exit-код і прапорець відсутнього тула.
6892
+ * @returns {Promise<{ status: number, enoent: boolean }>} exit-код і прапорець відсутнього тула.
6886
6893
  */
6887
- function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs, verbose = false) {
6894
+ async function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs, verbose = false) {
6888
6895
  const dir = mkdtempSync(join(tmpdir(), 'nitra-cursor-k8s-'))
6889
6896
  const file = join(dir, 'manifest.yaml')
6890
6897
  try {
6891
6898
  writeFileSync(file, manifest)
6892
- const r = spawnSync(kubescapePath, ['scan', file, '--severity-threshold', 'high', ...exceptionsArgs], {
6893
- stdio: verbose ? 'inherit' : 'pipe',
6894
- shell: false
6895
- })
6896
- const enoent = Boolean(r.error && 'code' in r.error && r.error.code === 'ENOENT')
6897
- return { status: r.status ?? 1, enoent }
6899
+ try {
6900
+ const r = await spawnAsync(kubescapePath, ['scan', file, '--severity-threshold', 'high', ...exceptionsArgs], {
6901
+ stdio: verbose ? 'inherit' : 'pipe'
6902
+ })
6903
+ return { status: r.exitCode ?? 1, enoent: false }
6904
+ } catch (error) {
6905
+ const enoent = Boolean(error && error.code === 'ENOENT')
6906
+ return { status: 1, enoent }
6907
+ }
6898
6908
  } finally {
6899
6909
  rmSync(dir, { recursive: true, force: true })
6900
6910
  }
@@ -6906,19 +6916,22 @@ function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs, verbose =
6906
6916
  * @param {string} dir каталог для сканування.
6907
6917
  * @param {string[]} exceptionsArgs додаткові CLI-аргументи (`--exceptions ...`).
6908
6918
  * @param {boolean} [verbose] показати повний нативний вивід kubescape (`stdio: 'inherit'`) і хід сканування.
6909
- * @returns {number} exit-код (127 якщо тул відсутній).
6919
+ * @returns {Promise<number>} exit-код (127 якщо тул відсутній).
6910
6920
  */
6911
- function scanRawK8sDir(kubescapePath, dir, exceptionsArgs, verbose = false) {
6921
+ async function scanRawK8sDir(kubescapePath, dir, exceptionsArgs, verbose = false) {
6912
6922
  if (verbose) console.log(`run-k8s: kubescape scan ${dir} (без kustomization — сирий dir-скан)`)
6913
- const r = spawnSync(kubescapePath, ['scan', dir, '--severity-threshold', 'high', ...exceptionsArgs], {
6914
- stdio: verbose ? 'inherit' : 'pipe',
6915
- shell: false
6916
- })
6917
- if (r.error && 'code' in r.error && r.error.code === 'ENOENT') {
6918
- console.error(KUBESCAPE_MISSING_HINT)
6919
- return 127
6923
+ try {
6924
+ const r = await spawnAsync(kubescapePath, ['scan', dir, '--severity-threshold', 'high', ...exceptionsArgs], {
6925
+ stdio: verbose ? 'inherit' : 'pipe'
6926
+ })
6927
+ return r.exitCode ?? 1
6928
+ } catch (error) {
6929
+ if (error && error.code === 'ENOENT') {
6930
+ console.error(KUBESCAPE_MISSING_HINT)
6931
+ return 127
6932
+ }
6933
+ return 1
6920
6934
  }
6921
- return r.status ?? 1
6922
6935
  }
6923
6936
 
6924
6937
  /**
@@ -6928,14 +6941,14 @@ function scanRawK8sDir(kubescapePath, dir, exceptionsArgs, verbose = false) {
6928
6941
  * @param {string[]} kdirs каталоги з kustomization.
6929
6942
  * @param {string[]} exceptionsArgs додаткові CLI-аргументи (`--exceptions ...`).
6930
6943
  * @param {boolean} [verbose] показати повний нативний вивід kubectl/kubescape і хід сканування.
6931
- * @returns {number} 0 якщо всі чисті, інакше перший ненульовий exit-код (127 — тул відсутній).
6944
+ * @returns {Promise<number>} 0 якщо всі чисті, інакше перший ненульовий exit-код (127 — тул відсутній).
6932
6945
  */
6933
- function scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs, verbose = false) {
6946
+ async function scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs, verbose = false) {
6934
6947
  for (const kdir of kdirs) {
6935
6948
  if (verbose) console.log(`run-k8s: kubectl kustomize ${kdir} | kubescape scan <tmp>`)
6936
- const build = runKustomizeBuild(kubectlPath, kdir, verbose)
6949
+ const build = await runKustomizeBuild(kubectlPath, kdir, verbose)
6937
6950
  if (build.status !== 0) return build.status
6938
- const ks = runKubescapeManifest(kubescapePath, build.stdout, exceptionsArgs, verbose)
6951
+ const ks = await runKubescapeManifest(kubescapePath, build.stdout, exceptionsArgs, verbose)
6939
6952
  if (ks.enoent) {
6940
6953
  console.error(KUBESCAPE_MISSING_HINT)
6941
6954
  return 127
@@ -6960,7 +6973,7 @@ async function runKubescape(dirs, root, verbose = false) {
6960
6973
  for (const d of dirs) {
6961
6974
  const kdirs = await findKustomizationDirs(d)
6962
6975
  if (kdirs.length === 0) {
6963
- const rawStatus = scanRawK8sDir(kubescapePath, d, exceptionsArgs, verbose)
6976
+ const rawStatus = await scanRawK8sDir(kubescapePath, d, exceptionsArgs, verbose)
6964
6977
  if (rawStatus !== 0) return rawStatus
6965
6978
  continue
6966
6979
  }
@@ -6971,7 +6984,7 @@ async function runKubescape(dirs, root, verbose = false) {
6971
6984
  return 127
6972
6985
  }
6973
6986
  }
6974
- const buildStatus = scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs, verbose)
6987
+ const buildStatus = await scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exceptionsArgs, verbose)
6975
6988
  if (buildStatus !== 0) return buildStatus
6976
6989
  }
6977
6990
  return 0
@@ -3,9 +3,8 @@ type: JS Module
3
3
  title: main.mjs
4
4
  resource: npm/rules/php/cs_fixer/main.mjs
5
5
  docgen:
6
- crc: d2eae379
6
+ crc: de8de231
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- tier: local-min
9
8
  score: 100
10
9
  issues: judge:inaccurate:0.98
11
10
  judgeModel: openai-codex/gpt-5.4-mini