@7n/rules 1.13.1 → 1.14.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.
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/rules/graphql/tooling/docs/main.md +1 -1
- package/rules/graphql/tooling/main.mjs +4 -4
- package/rules/js/eslint/docs/fix-worker.md +6 -5
- package/rules/js/eslint/docs/main.md +1 -1
- package/rules/js/eslint/fix-worker.mjs +70 -30
- package/rules/js/eslint/main.mjs +8 -7
- package/rules/js-run/runtime/docs/main.md +1 -1
- package/rules/js-run/runtime/main.mjs +3 -3
- package/rules/k8s/manifests/main.mjs +6 -6
- package/rules/nginx-default-tpl/template/docs/main.md +1 -1
- package/rules/nginx-default-tpl/template/main.mjs +5 -5
- package/rules/tauri/tooling/docs/main.md +1 -1
- package/rules/tauri/tooling/main.mjs +1 -1
- package/scripts/lib/docs/ensure-tool.md +24 -22
- package/scripts/lib/docs/run-conftest-batch.md +5 -3
- package/scripts/lib/ensure-tool.mjs +125 -31
- package/scripts/lib/lint-surface/blocking-inventory.mjs +59 -0
- package/scripts/lib/lint-surface/docs/blocking-inventory.md +30 -0
- package/scripts/lib/lint-surface/docs/index.md +2 -0
- package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +2 -3
- package/scripts/lib/lint-surface/docs/run-detectors.md +3 -1
- package/scripts/lib/lint-surface/docs/scheduler.md +39 -0
- package/scripts/lib/lint-surface/docs/types.md +2 -2
- package/scripts/lib/lint-surface/policy-lint-adapter.mjs +4 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +142 -31
- package/scripts/lib/lint-surface/scheduler.mjs +96 -0
- package/scripts/lib/lint-surface/types.mjs +3 -0
- package/scripts/lib/run-conftest-batch.mjs +16 -11
- package/scripts/utils/docs/index.md +1 -0
- package/scripts/utils/docs/spawn-async.md +33 -0
- package/scripts/utils/spawn-async.mjs +112 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.14.1] - 2026-07-18
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- docs(adr): brainstorm — внутрішній паралелізм lint-оркестратора (#86)
|
|
8
|
+
- Оновлено внутрішній lint-оркестратор.
|
|
9
|
+
|
|
10
|
+
## [1.14.0] - 2026-07-18
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- js/eslint: fix-worker.mjs — обмежений паралельний пул (MAX_PARALLEL_FILES=4) замість послідовного циклу по файлах; профайлінг спростував гіпотезу про bootstrap-накладні витрати сесії
|
|
15
|
+
|
|
3
16
|
## [1.13.1] - 2026-07-18
|
|
4
17
|
|
|
5
18
|
### Changed
|
package/package.json
CHANGED
|
@@ -67,10 +67,10 @@ async function collectGqlHits(root, candidates) {
|
|
|
67
67
|
* (умовне правило — без gql цей крок не запускається).
|
|
68
68
|
* @param {(msg: string) => void} pass success-репортер
|
|
69
69
|
* @param {(msg: string) => void} fail fail-репортер
|
|
70
|
-
* @returns {void}
|
|
70
|
+
* @returns {Promise<void>} результат
|
|
71
71
|
* @param {string} cwd корінь репозиторію
|
|
72
72
|
*/
|
|
73
|
-
function checkExtensionsRecommendation(pass, fail, cwd) {
|
|
73
|
+
async function checkExtensionsRecommendation(pass, fail, cwd) {
|
|
74
74
|
const pathRel = '.vscode/extensions.json'
|
|
75
75
|
const pathAbs = join(cwd, pathRel)
|
|
76
76
|
if (!existsSync(pathAbs)) {
|
|
@@ -79,7 +79,7 @@ function checkExtensionsRecommendation(pass, fail, cwd) {
|
|
|
79
79
|
)
|
|
80
80
|
return
|
|
81
81
|
}
|
|
82
|
-
const violations = runConftestBatch({
|
|
82
|
+
const violations = await runConftestBatch({
|
|
83
83
|
policyDirRel: 'graphql/vscode_extensions',
|
|
84
84
|
namespace: 'graphql.vscode_extensions',
|
|
85
85
|
files: [pathAbs]
|
|
@@ -123,7 +123,7 @@ export async function lint(ctx) {
|
|
|
123
123
|
)
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
checkExtensionsRecommendation(pass, fail, root)
|
|
126
|
+
await checkExtensionsRecommendation(pass, fail, root)
|
|
127
127
|
|
|
128
128
|
return reporter.result()
|
|
129
129
|
}
|
|
@@ -3,21 +3,21 @@ type: JS Module
|
|
|
3
3
|
title: fix-worker.mjs
|
|
4
4
|
resource: npm/rules/js/eslint/fix-worker.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 802ef1f3
|
|
7
7
|
model: manual
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## Огляд
|
|
11
11
|
|
|
12
|
-
Custom fix-worker `js/eslint` (перекриває дефолтний `default-worker.mjs`): замість одного `runAgentFix`-виклику з усіма порушеннями з усіх файлів concern-а — окрема агентна сесія на кожен файл,
|
|
12
|
+
Custom fix-worker `js/eslint` (перекриває дефолтний `default-worker.mjs`): замість одного `runAgentFix`-виклику з усіма порушеннями з усіх файлів concern-а — окрема агентна сесія на кожен файл, ПАРАЛЕЛЬНО (обмежений пул, не більше `MAX_PARALLEL_FILES`). Мотивація й еволюція дизайну — `docs/adr/260718-0754-js-eslint-fix-worker-per-session-overhead.md`: послідовна версія стабільно впиралась у timeout на всіх 4 rung-ах драбини (local-min→cloud-avg); профайлінг реального прогону показав, що домінує кількість/довжина раундів моделі на файл, а не фіксовані bootstrap-витрати сесії — тож паралелізм (кілька файлів одночасно замість черги) дає пряме пришвидшення незалежно від причини.
|
|
13
13
|
|
|
14
14
|
## Поведінка
|
|
15
15
|
|
|
16
16
|
1. Групує вхідні `violations` за `file`; порушення без `file` ігноруються.
|
|
17
|
-
2. Дедлайн — `DEADLINE_FRACTION` (0.8) від `ctx.timeoutMs`;
|
|
18
|
-
3.
|
|
17
|
+
2. Дедлайн — `DEADLINE_FRACTION` (0.8) від `ctx.timeoutMs`; гейтить лише СТАРТ нового файлу з черги (не скасовує вже запущені).
|
|
18
|
+
3. `runPooled` — обмежений пул воркерів (`MAX_PARALLEL_FILES=4`): при `files.length ≤ MAX_PARALLEL_FILES` усі файли стартують майже одночасно й отримують практично весь бюджет незалежно один від одного; при більшій кількості черга природно звужує бюджет пізніших хвиль (`callTimeoutMs` рахується в момент старту файлу з черги, не наперед).
|
|
19
19
|
4. На кожен файл — окремий `runAgentFix` із `targetFiles: [file]` і власним `verify` (`verifyFile`): item-scoped canonical re-detect лише цього файлу, не всього concern-а — інакше evidence-гейт хибно вважав би файл незакритим через порушення в ІНШИХ файлах.
|
|
20
|
-
5. Один файл, що завершився з `error
|
|
20
|
+
5. Один файл, що завершився з `error` АБО кинув виняток (try/catch у тілі воркера), не обриває пул — пропускається, решта файлів обробляються незалежно.
|
|
21
21
|
6. Повертає лише `touchedFiles` з успішних (`!error`) викликів; success rung-а все одно визначає whole-concern canonical re-detect runner-а (`runRung`), не цей worker.
|
|
22
22
|
|
|
23
23
|
## Публічний API
|
|
@@ -28,3 +28,4 @@ Custom fix-worker `js/eslint` (перекриває дефолтний `default-
|
|
|
28
28
|
|
|
29
29
|
- Пише лише у файли з переданих `violations` (`targetFiles: [file]` на кожен виклик) — той самий semantic-collateral guard, що й у дефолтного worker-а.
|
|
30
30
|
- `recordWrite` (не durable) на кожен файл: rollback-контракт незмінний — якщо після worker-а в concern-і лишилось хоч одне порушення будь-де, `runRung` відкочує ВСІ правки цього rung-а, включно з уже полагодженими файлами.
|
|
31
|
+
- Конкурентні виклики `recordWrite` безпечні: central snapshot — синхронні Map-операції без `await` усередині, event loop не перериває їх посеред виконання.
|
|
@@ -4,22 +4,33 @@
|
|
|
4
4
|
* з УСІХ файлів в один `runAgentFix`-виклик — на реальних lint-прогонах (tauri-components)
|
|
5
5
|
* це давало 100% timeout на всіх 4 rung-ах драбини (local-min→cloud-avg), навіть коли
|
|
6
6
|
* порушень було лише 20-90 у 1-3 файлах: одна сесія жонглює кількома файлами одразу і не
|
|
7
|
-
* встигає в rung-таймаут.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* встигає в rung-таймаут.
|
|
8
|
+
*
|
|
9
|
+
* Файли обробляються ПАРАЛЕЛЬНО (обмежений пул, `MAX_PARALLEL_FILES`), не послідовно —
|
|
10
|
+
* ADR docs/adr/260718-0754-js-eslint-fix-worker-per-session-overhead.md. Профайлінг
|
|
11
|
+
* реального прогону (2026-07-18, trace `runAgentFix`) спростував гіпотезу про фіксовані
|
|
12
|
+
* bootstrap-витрати сесії: `dispatch.test.js` (2 порушення) — 9 раундів моделі, 29.4с;
|
|
13
|
+
* тривіальний фікс так само вимагав багато tool-раундів, а не одноразової плати за bootstrap.
|
|
14
|
+
* Оскільки домінує саме кількість/довжина turn-раундів, а не старт сесії, паралелізм
|
|
15
|
+
* (кілька файлів одночасно замість черги) дає пряме пришвидшення незалежно від причини —
|
|
16
|
+
* секвенційна версія витрачала весь бюджет rung-а на 1 файл, лишаючи іншим 0 шансів
|
|
17
|
+
* навіть стартувати.
|
|
10
18
|
*
|
|
11
19
|
* Rollback-контракт незмінний: `recordWrite` (не durable) на кожен файл — правки наявного
|
|
12
20
|
* стороннього коду. Якщо після worker-а `runRung`-ів canonical re-detect (whole-concern)
|
|
13
21
|
* знайде хоч одне порушення будь-де, увесь rung однаково відкотиться (S1) — той самий
|
|
14
|
-
* контракт, що й у дефолтного worker-а;
|
|
15
|
-
* до 0 порушень, а не замінює rollback-семантику.
|
|
22
|
+
* контракт, що й у дефолтного worker-а; паралелізм підвищує ймовірність, що ЦЕЙ rung
|
|
23
|
+
* дійде до 0 порушень, а не замінює rollback-семантику. `snapshot.record`/`recordWrite` —
|
|
24
|
+
* синхронні Map-операції без `await` усередині, тож конкурентні виклики безпечні (Node
|
|
25
|
+
* event loop не перериває синхронний блок).
|
|
16
26
|
*
|
|
17
27
|
* Дедлайн (`DEADLINE_FRACTION` від `ctx.timeoutMs`, той самий підхід, що й
|
|
18
|
-
* `doc-files/check/fix-worker.mjs`)
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
28
|
+
* `doc-files/check/fix-worker.mjs`) гейтить лише СТАРТ нового файлу з черги — не
|
|
29
|
+
* скасовує вже запущені. При files.length ≤ MAX_PARALLEL_FILES усі стартують майже
|
|
30
|
+
* одночасно й отримують практично весь бюджет незалежно один від одного (справжній
|
|
31
|
+
* паралелізм); при більшій кількості — черга природно звужує бюджет пізніших хвиль
|
|
32
|
+
* (`callTimeoutMs` рахується в момент старту, не наперед), не даючи перевищити
|
|
33
|
+
* backstop ×1.25 runner-а.
|
|
23
34
|
* @typedef {import('../../../scripts/lib/lint-surface/types.mjs').FixWorkerFn} FixWorkerFn
|
|
24
35
|
*/
|
|
25
36
|
import { resolve } from 'node:path'
|
|
@@ -28,9 +39,12 @@ import { anchoredEnabled } from '../../../scripts/lib/lint-surface/default-worke
|
|
|
28
39
|
import { renderViolations } from '../../../scripts/lib/lint-surface/render.mjs'
|
|
29
40
|
import { lint } from './main.mjs'
|
|
30
41
|
|
|
31
|
-
/** Частка ctx.timeoutMs, після якої
|
|
42
|
+
/** Частка ctx.timeoutMs, після якої черга не стартує новий файл (запас до backstop ×1.25). */
|
|
32
43
|
const DEADLINE_FRACTION = 0.8
|
|
33
44
|
|
|
45
|
+
/** Максимум файлів, що обробляються одночасно (без необмеженого burst на великих concern-ах). */
|
|
46
|
+
const MAX_PARALLEL_FILES = 4
|
|
47
|
+
|
|
34
48
|
/**
|
|
35
49
|
* Item-scoped (один файл) canonical re-detect для evidence-гейта `runAgentFix` —
|
|
36
50
|
* НЕ те саме, що whole-concern `ctx.verify` з `runRung` (той перевірив би ВСІ файли
|
|
@@ -47,6 +61,27 @@ async function verifyFile(cwd, ruleId, concernId, file) {
|
|
|
47
61
|
return { ok: stamped.length === 0, output: renderViolations(stamped) }
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Обробляє `items` обмеженим пулом воркерів (не більше `MAX_PARALLEL_FILES` одночасно) —
|
|
66
|
+
* власна черга замість `Promise.all(items.map(...))`, щоб великий concern не відкрив
|
|
67
|
+
* необмежену кількість конкурентних агентних сесій одразу.
|
|
68
|
+
* @param {string[]} items елементи черги (шляхи файлів)
|
|
69
|
+
* @param {(item: string) => Promise<void>} worker обробник одного елемента; винятки ловить сам
|
|
70
|
+
* @returns {Promise<void>}
|
|
71
|
+
*/
|
|
72
|
+
async function runPooled(items, worker) {
|
|
73
|
+
const queue = [...items]
|
|
74
|
+
const runnerCount = Math.min(MAX_PARALLEL_FILES, items.length)
|
|
75
|
+
await Promise.all(
|
|
76
|
+
Array.from({ length: runnerCount }, async () => {
|
|
77
|
+
let item
|
|
78
|
+
while ((item = queue.shift())) {
|
|
79
|
+
await worker(item)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
50
85
|
/** @type {FixWorkerFn} */
|
|
51
86
|
export async function fixWorker(violations, ctx) {
|
|
52
87
|
// lazy import — тримає detect-шлях вільним від pi/oxc (read-only --no-fix не вантажить їх).
|
|
@@ -71,29 +106,34 @@ export async function fixWorker(violations, ctx) {
|
|
|
71
106
|
const anchoredEdits = anchoredEnabled(ctx.model, isLocalModel)
|
|
72
107
|
|
|
73
108
|
const touchedFiles = []
|
|
74
|
-
|
|
75
|
-
if (deadlineAt && Date.now() >= deadlineAt)
|
|
109
|
+
await runPooled(files, async file => {
|
|
110
|
+
if (deadlineAt && Date.now() >= deadlineAt) return
|
|
76
111
|
const callTimeoutMs = deadlineAt ? Math.max(1000, deadlineAt - Date.now()) : ctx.timeoutMs
|
|
77
112
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
113
|
+
let res
|
|
114
|
+
try {
|
|
115
|
+
res = await runAgentFix(ctx.ruleId, renderViolations(byFile.get(file)), ctx.cwd, {
|
|
116
|
+
model: ctx.model,
|
|
117
|
+
tier: ctx.tier,
|
|
118
|
+
timeoutMs: callTimeoutMs,
|
|
119
|
+
feedback: ctx.feedback ?? null,
|
|
120
|
+
caller: `fix:${ctx.ruleId}/${ctx.concernId}:${ctx.tier}:${file}`,
|
|
121
|
+
recordWrite: ctx.recordWrite,
|
|
122
|
+
chain: ctx.chain ?? null,
|
|
123
|
+
targetFiles: [file],
|
|
124
|
+
verify: () => verifyFile(ctx.cwd, ctx.ruleId, ctx.concernId, file),
|
|
125
|
+
verifyMax: ctx.verifyMax,
|
|
126
|
+
anchoredEdits,
|
|
127
|
+
deps: { astContext: p => extractContext(resolve(ctx.cwd, p)) }
|
|
128
|
+
})
|
|
129
|
+
} catch {
|
|
130
|
+
// Один файл впав винятком — не валимо решту паралельних воркерів у пулі; whole-concern
|
|
131
|
+
// canonical re-detect runner-а (не цей worker) визначить, чи rung закрито.
|
|
132
|
+
return
|
|
133
|
+
}
|
|
92
134
|
|
|
93
|
-
// Один файл не впорався — не кидаємо, пробуємо решту в межах дедлайну; whole-concern
|
|
94
|
-
// canonical re-detect runner-а (не цей worker) визначить, чи rung закрито.
|
|
95
135
|
if (!res.error) touchedFiles.push(...(res.touchedFiles ?? []))
|
|
96
|
-
}
|
|
136
|
+
})
|
|
97
137
|
|
|
98
138
|
return { touchedFiles }
|
|
99
139
|
}
|
package/rules/js/eslint/main.mjs
CHANGED
|
@@ -3,16 +3,15 @@
|
|
|
3
3
|
* eslint --fix) — окремий T0 `fix-eslint.mjs` (детермінований), не в detector-і.
|
|
4
4
|
*/
|
|
5
5
|
import { resolve, relative } from 'node:path'
|
|
6
|
-
import { spawnSync } from 'node:child_process'
|
|
7
6
|
|
|
8
7
|
import { ESLint } from 'eslint'
|
|
9
8
|
|
|
10
9
|
import { addedLinesByFile } from '../../../scripts/lib/diff-added-lines.mjs'
|
|
10
|
+
import { spawnAsync } from '../../../scripts/utils/spawn-async.mjs'
|
|
11
11
|
import { WORKTREE_CHECKOUT_GLOBS } from '../../../scripts/utils/walkDir.mjs'
|
|
12
12
|
import { classifyFindings, eslintResultsToFindings, parseOxlint } from '../lint-findings/main.mjs'
|
|
13
13
|
|
|
14
14
|
const JS_EXT_RE = /\.(?:mjs|cjs|js|jsx|ts|tsx|vue)$/u
|
|
15
|
-
const JSON_MAX_BUFFER = 64 * 1024 * 1024
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* @param {string[]} files список шляхів
|
|
@@ -23,13 +22,15 @@ export function filterJsFiles(files) {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
/**
|
|
25
|
+
* Async (не блокує event loop) — детектор може виконуватись у parallel lane `detectAll()`
|
|
26
|
+
* (ADR 260716-1354).
|
|
26
27
|
* @param {string[]} args аргументи запуску oxlint
|
|
27
28
|
* @param {string} cwd робочий каталог
|
|
28
|
-
* @returns {{ status: number, stdout: string, stderr: string }} код завершення, stdout і stderr процесу
|
|
29
|
+
* @returns {Promise<{ status: number, stdout: string, stderr: string }>} код завершення, stdout і stderr процесу
|
|
29
30
|
*/
|
|
30
|
-
function runOxlintJson(args, cwd) {
|
|
31
|
-
const r =
|
|
32
|
-
return { status: typeof r.
|
|
31
|
+
async function runOxlintJson(args, cwd) {
|
|
32
|
+
const r = await spawnAsync('bunx', args, { cwd })
|
|
33
|
+
return { status: typeof r.exitCode === 'number' ? r.exitCode : 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' }
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
@@ -68,7 +69,7 @@ async function collectFindings(js, cwd) {
|
|
|
68
69
|
js === null
|
|
69
70
|
? ['oxlint', '--format=json', ...worktreeIgnoreArgs]
|
|
70
71
|
: ['oxlint', '--format=json', ...worktreeIgnoreArgs, ...js]
|
|
71
|
-
const oxRes = runOxlintJson(oxArgs, cwd)
|
|
72
|
+
const oxRes = await runOxlintJson(oxArgs, cwd)
|
|
72
73
|
const ox = parseOxlint(oxRes.stdout)
|
|
73
74
|
if (ox === null && oxRes.status !== 0) {
|
|
74
75
|
// Хвости stdout/stderr — інакше на CI причина крашу (OOM, конфіг, версія) невидима.
|
|
@@ -45,10 +45,10 @@ function backendPackageHasSrcDir(absPackageRoot) {
|
|
|
45
45
|
* @param {string} label префікс `[pkg] `
|
|
46
46
|
* @param {(msg: string) => void} fail callback для повідомлень про порушення
|
|
47
47
|
* @param {(msg: string) => void} passFn callback для повідомлень про успішну перевірку
|
|
48
|
-
* @returns {void}
|
|
48
|
+
* @returns {Promise<void>} результат
|
|
49
49
|
* @param {string} cwd корінь репозиторію
|
|
50
50
|
*/
|
|
51
|
-
function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn, cwd) {
|
|
51
|
+
async function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn, cwd) {
|
|
52
52
|
if (!backendPackageHasSrcDir(absPackageRoot)) return
|
|
53
53
|
|
|
54
54
|
const jcPath = join(cwd, rootDir, 'jsconfig.json')
|
|
@@ -59,7 +59,7 @@ function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail
|
|
|
59
59
|
)
|
|
60
60
|
return
|
|
61
61
|
}
|
|
62
|
-
const violations = runConftestBatch({
|
|
62
|
+
const violations = await runConftestBatch({
|
|
63
63
|
policyDirRel: 'js-run/jsconfig',
|
|
64
64
|
namespace: 'js_run.jsconfig',
|
|
65
65
|
files: [jcPath]
|
|
@@ -3201,7 +3201,7 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
|
3201
3201
|
}
|
|
3202
3202
|
}
|
|
3203
3203
|
if (pairedFiles.size === 0) return
|
|
3204
|
-
const violations = runConftestBatch({
|
|
3204
|
+
const violations = await runConftestBatch({
|
|
3205
3205
|
policyDirRel: 'k8s/hasura_httproute',
|
|
3206
3206
|
namespace: 'k8s.hasura_httproute',
|
|
3207
3207
|
files: [...pairedFiles]
|
|
@@ -3626,7 +3626,7 @@ async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs
|
|
|
3626
3626
|
}
|
|
3627
3627
|
}
|
|
3628
3628
|
if (paired.length === 0) return
|
|
3629
|
-
const violations = runConftestBatch({
|
|
3629
|
+
const violations = await runConftestBatch({
|
|
3630
3630
|
policyDirRel: 'k8s/hasura_configmap',
|
|
3631
3631
|
namespace: 'k8s.hasura_configmap',
|
|
3632
3632
|
files: paired
|
|
@@ -6604,9 +6604,9 @@ function k8sRegoFixHint(ns, file, message) {
|
|
|
6604
6604
|
* @param {string} root корінь репозиторію.
|
|
6605
6605
|
* @param {string[]} yamlFiles абсолютні шляхи *.yaml під `…/k8s/`.
|
|
6606
6606
|
* @param {(msg: string, hint?: unknown) => void} fail callback реєстрації порушення.
|
|
6607
|
-
* @returns {void}
|
|
6607
|
+
* @returns {Promise<void>} результат
|
|
6608
6608
|
*/
|
|
6609
|
-
function runAllK8sRego(root, yamlFiles, fail) {
|
|
6609
|
+
async function runAllK8sRego(root, yamlFiles, fail) {
|
|
6610
6610
|
const relOf = abs => relative(root, abs).replaceAll('\\', '/') || abs
|
|
6611
6611
|
|
|
6612
6612
|
const allYaml = yamlFiles
|
|
@@ -6645,7 +6645,7 @@ function runAllK8sRego(root, yamlFiles, fail) {
|
|
|
6645
6645
|
|
|
6646
6646
|
for (const t of targets) {
|
|
6647
6647
|
if (t.files.length === 0) continue
|
|
6648
|
-
const violations = runConftestBatch({
|
|
6648
|
+
const violations = await runConftestBatch({
|
|
6649
6649
|
policyDirRel: t.dir,
|
|
6650
6650
|
namespace: t.ns,
|
|
6651
6651
|
files: t.files,
|
|
@@ -6707,7 +6707,7 @@ export async function lint(ctx) {
|
|
|
6707
6707
|
// Plan B: пер-документні структурні правила — у rego-полісі `npm/policy/k8s/*`,
|
|
6708
6708
|
// викликаємо одним батчем на namespace через runConftestBatch. JS нижче робить
|
|
6709
6709
|
// лише cross-file orchestration, modeline та FS-existence перевірки.
|
|
6710
|
-
runAllK8sRego(root, yamlFiles, fail)
|
|
6710
|
+
await runAllK8sRego(root, yamlFiles, fail)
|
|
6711
6711
|
pass(`Rego-полісі (npm/policy/k8s/*) виконано на ${yamlFiles.length} файл(ах)`)
|
|
6712
6712
|
|
|
6713
6713
|
for (const abs of yamlFiles) {
|
|
@@ -378,12 +378,12 @@ async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
|
|
|
378
378
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
379
379
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
380
380
|
* @param {string} cwd корінь репозиторію
|
|
381
|
-
* @returns {void}
|
|
381
|
+
* @returns {Promise<void>} результат
|
|
382
382
|
*/
|
|
383
|
-
function checkVscodeNginx(passFn, failFn, cwd) {
|
|
383
|
+
async function checkVscodeNginx(passFn, failFn, cwd) {
|
|
384
384
|
const extPath = join(cwd, '.vscode/extensions.json')
|
|
385
385
|
if (existsSync(extPath)) {
|
|
386
|
-
const violations = runConftestBatch({
|
|
386
|
+
const violations = await runConftestBatch({
|
|
387
387
|
policyDirRel: 'nginx-default-tpl/vscode_extensions',
|
|
388
388
|
namespace: 'nginx_default_tpl.vscode_extensions',
|
|
389
389
|
files: [extPath]
|
|
@@ -402,7 +402,7 @@ function checkVscodeNginx(passFn, failFn, cwd) {
|
|
|
402
402
|
failFn('Очікується .vscode/settings.json з форматером nginx і formatOnSave (див. nginx-default-tpl.mdc)')
|
|
403
403
|
return
|
|
404
404
|
}
|
|
405
|
-
const violations = runConftestBatch({
|
|
405
|
+
const violations = await runConftestBatch({
|
|
406
406
|
policyDirRel: 'nginx-default-tpl/vscode_settings',
|
|
407
407
|
namespace: 'nginx_default_tpl.vscode_settings',
|
|
408
408
|
files: [setPath]
|
|
@@ -499,7 +499,7 @@ export async function lint(ctx) {
|
|
|
499
499
|
}
|
|
500
500
|
|
|
501
501
|
await checkDockerfiles(root, ignorePaths, pass, fail)
|
|
502
|
-
checkVscodeNginx(pass, fail, root)
|
|
502
|
+
await checkVscodeNginx(pass, fail, root)
|
|
503
503
|
|
|
504
504
|
return reporter.result()
|
|
505
505
|
}
|
|
@@ -79,7 +79,7 @@ export async function lint(ctx) {
|
|
|
79
79
|
fail(`${extPath} не існує — створи з recommendations "tauri-apps.tauri-vscode" (tauri.mdc)`)
|
|
80
80
|
return reporter.result()
|
|
81
81
|
}
|
|
82
|
-
const violations = runConftestBatch({
|
|
82
|
+
const violations = await runConftestBatch({
|
|
83
83
|
policyDirRel: 'tauri/vscode_extensions',
|
|
84
84
|
namespace: 'tauri.vscode_extensions',
|
|
85
85
|
files: [extPath]
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: ensure-tool.mjs
|
|
4
4
|
resource: npm/scripts/lib/ensure-tool.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: b1b054d0
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Модуль `ensure-tool.mjs` — єдина точка резолву зовнішніх CLI-залежностей пакета `@7n/rules`. Він гарантує, що потрібний бінарник (`hk`, `conftest`, `shellcheck`, `actionlint`, `dotenv-linter`, `opa`, `regal`, `hadolint`, `kubeconform`, `kubescape`) доступний у системі, виконуючи послідовний пошук:
|
|
@@ -15,14 +15,17 @@ docgen:
|
|
|
15
15
|
|
|
16
16
|
Така архітектура усуває дублювання install-логіки в кожному `lint.mjs` / `fix.mjs`: щоб додати нову зовнішню утиліту, достатньо одного запису в реєстрі `TOOLS`. Додатково модуль експортує `ensureHkInstall`, який реєструє git pre-commit hook через `hk install` (пропускається в CI).
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Поруч із синхронною `ensureTool` (публічний API пакета, сигнатура не змінюється) модуль експортує async-варіант `ensureToolAsync(toolId)` для parallel lane `detectAll()` (ADR 260716-1354-внутрішній-паралелізм-lint-оркестратора): конкурентні виклики того самого `toolId` в одному Node-процесі колапсують в один install (in-process single-flight), а auto-install крок додатково серіалізується між процесами через `withLock` (ключ `ensure-tool/<toolId>`) — паралельні Node-процеси (різні CI-shard-и, кілька агентів) не тягнуть той самий бінарник конкурентно. Завантажений архів завжди пишеться в унікальний per-call temp-каталог і публікується атомарним `renameSync` під фіксованим flat-іменем `<toolId>` — цей hardened install-крок спільний для sync і async шляхів.
|
|
19
|
+
|
|
20
|
+
Файл написаний для Node.js (ESM), використовує лише стандартну бібліотеку, локальний хелпер `resolveCmd` і `withLock` (`../utils/with-lock.mjs`) для міжпроцесної серіалізації async-install-кроку.
|
|
19
21
|
|
|
20
22
|
## Експорти / API
|
|
21
23
|
|
|
22
|
-
| Експорт
|
|
23
|
-
|
|
|
24
|
-
| `ensureTool(toolId)`
|
|
25
|
-
| `
|
|
24
|
+
| Експорт | Тип | Призначення |
|
|
25
|
+
| --------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
26
|
+
| `ensureTool(toolId)` | `function` | Резолвить і за потреби встановлює зовнішній CLI (sync). Повертає абсолютний шлях до бінарника або кидає `Error`. |
|
|
27
|
+
| `ensureToolAsync(toolId)` | `function` | Async-варіант для parallel lane `detectAll()`: single-flight (in-process) + `withLock` (cross-process) навколо auto-install кроку. Повертає `Promise<string>`. |
|
|
28
|
+
| `ensureHkInstall(hkBin)` | `function` | Виконує `hk install` для реєстрації git pre-commit hook. Жодного return value; на помилку лише `console.warn`. |
|
|
26
29
|
|
|
27
30
|
Внутрішні (не експортуються, але формують контракт модуля):
|
|
28
31
|
|
|
@@ -81,19 +84,18 @@ docgen:
|
|
|
81
84
|
1. Резолвить `curl` та `tar` у PATH; за відсутності — кидає `Error`.
|
|
82
85
|
2. Через `fetchLatestVersion` отримує актуальну версію.
|
|
83
86
|
3. Формує назву asset через `entry.asset(ver)` і URL `https://github.com/<github>/releases/download/v<ver>/<asset>`.
|
|
84
|
-
4. Створює `cacheDir` (`mkdirSync` з `recursive: true`).
|
|
85
|
-
5. Завантажує asset через `curl -sSL -o <
|
|
86
|
-
6. Якщо `entry.archive === false` —
|
|
87
|
-
7. Інакше викликає `tar` із прапорцем `-xJf` (для `.tar.xz`) або `-xzf` (для `.tar.gz`) для розпакування в `cacheDir
|
|
88
|
-
8.
|
|
89
|
-
9. Опціонально видаляє завантажений архів через `rm` (м’яко: якщо `rm` не знайдено, продовжує).
|
|
87
|
+
4. Створює `cacheDir` (`mkdirSync` з `recursive: true`) і унікальний per-call temp-каталог усередині нього (`mkdtempSync(join(cacheDir, '.tmp-<toolId>-'))`) — той самий filesystem гарантує, що фінальний `renameSync` не впаде з `EXDEV`.
|
|
88
|
+
5. Завантажує asset у temp-каталог через `curl -sSL -o <tmpDir>/<asset> <downloadUrl>`.
|
|
89
|
+
6. Якщо `entry.archive === false` — `chmodSync` завантаженого файлу (`0o755`) і атомарний `renameSync` у `<cacheDir>/<toolId>`.
|
|
90
|
+
7. Інакше викликає `tar` із прапорцем `-xJf` (для `.tar.xz`) або `-xzf` (для `.tar.gz`) для розпакування в temp-каталог, знаходить реальний шлях бінарника через `entry.binFinder(ver)` або просто `toolId`, перевіряє його існування — і так само атомарним `renameSync` публікує його у `<cacheDir>/<toolId>` (flat-ім'я, незалежно від вкладеної структури архіву).
|
|
91
|
+
8. У `finally` прибирає весь temp-каталог (`rmSync(tmpDir, { recursive: true, force: true })`) — і архів, і проміжні файли розпакування зникають одним викликом.
|
|
90
92
|
- **Помилки:**
|
|
91
93
|
- `curl не знайдено в PATH — потрібен для завантаження <toolId>`.
|
|
92
94
|
- `tar не знайдено в PATH — потрібен для встановлення <toolId>`.
|
|
93
95
|
- `Завантаження <toolId> не вдалось: ...` / `curl exit <status> при завантаженні <toolId>: ...`.
|
|
94
96
|
- `tar failed for <toolId>: ...` / `tar exit <status> для <toolId>: ...`.
|
|
95
|
-
- `Бінарник <toolId> не знайдено після розпакування: <
|
|
96
|
-
- **Side effects:** мережа, файлова система (
|
|
97
|
+
- `Бінарник <toolId> не знайдено після розпакування: <extractedBin>`.
|
|
98
|
+
- **Side effects:** мережа, файлова система (унікальний temp-каталог, атомарна публікація, chmod, очищення temp).
|
|
97
99
|
|
|
98
100
|
### `installViaBrew(toolId, entry)`
|
|
99
101
|
|
|
@@ -149,7 +151,7 @@ docgen:
|
|
|
149
151
|
- **Послідовність резолву:**
|
|
150
152
|
1. **Валідація** — якщо `TOOLS[toolId]` відсутній, кидає `ensureTool: невідомий тул '<toolId>'`.
|
|
151
153
|
2. **PATH** — `resolveCmd(toolId)`; якщо знайдено — повертає одразу.
|
|
152
|
-
3. **Кеш** — `join(getCacheDir(), toolId)`; якщо файл існує — повертає його шлях.
|
|
154
|
+
3. **Кеш** — `join(getCacheDir(), toolId)`; якщо файл існує — повертає його шлях. Install завжди публікує бінарник під цим самим flat-іменем (атомарний `renameSync`, незалежно від вкладеної структури архіву — напр. `shellcheck` розпаковується у `shellcheck-v<ver>/shellcheck`), тож ця перевірка коректно бачить кеш і після `entry.binFinder`-архівів.
|
|
153
155
|
4. **Авто-install** — якщо змінна середовища `N_CURSOR_NO_AUTO_INSTALL` не виставлена, викликає `autoInstall(toolId, entry, cacheDir)`.
|
|
154
156
|
5. **Hard-fail** — кидає `Error(buildHint(toolId, entry))`.
|
|
155
157
|
- **Помилки:** будь-яка з помилок `autoInstall` / `installFrom*` піднімається вгору; додатково — `невідомий тул` та `❌ ... не знайдено в PATH`.
|
|
@@ -209,13 +211,13 @@ docgen:
|
|
|
209
211
|
- `fetchLatestVersion('koalaman/shellcheck', curl)` → наприклад `0.10.0`.
|
|
210
212
|
- asset name = `shellcheck-v0.10.0.linux.x86_64.tar.xz`.
|
|
211
213
|
- URL = `https://github.com/koalaman/shellcheck/releases/download/v0.10.0/<asset>`.
|
|
212
|
-
- `mkdirSync(cacheDir, { recursive: true })`.
|
|
213
|
-
- `curl -sSL -o <
|
|
214
|
-
- `tar -xJf <asset> -C <
|
|
215
|
-
- `binFinder('0.10.0')` →
|
|
216
|
-
-
|
|
217
|
-
- `
|
|
218
|
-
- Повертає `<cacheDir>/shellcheck
|
|
214
|
+
- `mkdirSync(cacheDir, { recursive: true })` і унікальний `tmpDir = mkdtempSync(join(cacheDir, '.tmp-shellcheck-'))`.
|
|
215
|
+
- `curl -sSL -o <tmpDir>/<asset> <url>`.
|
|
216
|
+
- `tar -xJf <asset> -C <tmpDir>` (бо `.tar.xz`).
|
|
217
|
+
- `binFinder('0.10.0')` → `<tmpDir>/shellcheck-v0.10.0/shellcheck`; перевірка `existsSync`.
|
|
218
|
+
- Атомарний `renameSync(<tmpDir>/shellcheck-v0.10.0/shellcheck, <cacheDir>/shellcheck)` — публікація під flat-іменем.
|
|
219
|
+
- `rmSync(tmpDir, { recursive: true, force: true })` у `finally`.
|
|
220
|
+
- Повертає `<cacheDir>/shellcheck`.
|
|
219
221
|
7. Викликач отримує абсолютний шлях і запускає `spawnSync(bin, [...args])`.
|
|
220
222
|
|
|
221
223
|
### Сценарій блокування авто-installу
|
|
@@ -3,20 +3,22 @@ type: JS Module
|
|
|
3
3
|
title: run-conftest-batch.mjs
|
|
4
4
|
resource: npm/scripts/lib/run-conftest-batch.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 39e76cec
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Файл запускає `conftest test` на заданому списку файлів, виявляючи порушення правил, визначених у Rego-полісіях. Він використовується для автоматизованої перевірки конфігураційних файлів на відповідність заданим вимогам. Результати перевірки повертаються у структурованому вигляді, що дозволяє інтегрувати результати в інші процеси валідації.
|
|
10
10
|
|
|
11
|
+
`runConftestBatch` — **async** (ADR 260716-1354-внутрішній-паралелізм-lint-оркестратора): `conftest` запускається через non-blocking `spawnAsync` (не `spawnSync`), а бінарник резолвиться через `ensureToolAsync('conftest')` — це дозволяє функції брати участь у parallel lane `detectAll()` замість того, щоб блокувати event loop цілком. Приймає опційні `signal` (`AbortSignal`) і `timeoutMs`, обидва прокидаються у `spawnAsync`.
|
|
12
|
+
|
|
11
13
|
## Поведінка
|
|
12
14
|
|
|
13
15
|
buildConftestArgs: Будує аргументи командного рядка для запуску `conftest test`, враховуючи список файлів, namespace та додаткові аргументи.
|
|
14
|
-
runConftestBatch:
|
|
16
|
+
runConftestBatch: Асинхронно запускає `conftest test` для заданого списку файлів, повертає `Promise` з масивом порушень у форматі JSON, якщо `conftest` успішно завершився, і кидає виняток (реджектить), якщо `conftest` не знайдено або завершився з помилкою (включно з `null` exit-кодом — процес вбито через `timeoutMs`/`signal`). Створює тимчасову директорію для збереження даних шаблону, якщо передано `templateData`.
|
|
15
17
|
|
|
16
18
|
## Публічний API
|
|
17
19
|
|
|
18
20
|
- buildConftestArgs: Створює аргументи для тесту conftest. Витягнуто для зручності тестування. Зберігає поточну структуру аргументів (файли перед `-p`, `--output json` та `--no-color` для читабельного виводу) і вставляє `--data` після `--namespace`, якщо він заданий.
|
|
19
|
-
- runConftestBatch:
|
|
21
|
+
- runConftestBatch: Асинхронно запускає `conftest test` для всіх файлів з одного процесу та повертає `Promise` з масивом помилок. Якщо `files` порожній, резолвиться порожнім масивом без спавну. Якщо `conftest` не знайдено в системному шляху та автоматична установка не вдалася, `Promise` реджектиться.
|
|
20
22
|
|
|
21
23
|
## Гарантії поведінки
|
|
22
24
|
|