@7n/rules 1.4.1 → 1.5.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.
- package/CHANGELOG.md +12 -0
- package/bin/n-rules.js +65 -17
- package/package.json +11 -1
- package/rules/abie/clean_merged_ignore_branches/concern.json +3 -0
- package/rules/docker/lint_docker_yml/concern.json +3 -0
- package/rules/js/lint_js_yml/concern.json +3 -0
- package/rules/k8s/lint_k8s_yml/concern.json +3 -0
- package/rules/npm-module/npm_publish_yml/concern.json +3 -0
- package/rules/php/lint_php_yml/concern.json +3 -0
- package/rules/python/lint_python_yml/concern.json +3 -0
- package/rules/rust/lint_rust_yml/concern.json +3 -0
- package/rules/rust/toolchain_cache/concern.json +10 -1
- package/rules/security/lint_security_yml/concern.json +3 -0
- package/rules/style/lint_style_yml/concern.json +3 -0
- package/rules/tauri/main.mdc +1 -1
- package/rules/tauri/release/release.mdc +15 -0
- package/rules/text/lint_text/concern.json +3 -0
- package/schemas/concern.json +86 -16
- package/schemas/n-rules.json +11 -1
- package/schemas/v8r-catalog.json +123 -27
- package/scripts/auto-rules.mjs +47 -6
- package/scripts/docs/auto-rules.md +3 -1
- package/scripts/docs/skills-cli.md +3 -2
- package/scripts/docs/upgrade-n-rules-and-install.md +3 -1
- package/scripts/lib/acp-runner.mjs +174 -0
- package/scripts/lib/concern-meta.mjs +8 -1
- package/scripts/lib/docs/acp-runner.md +58 -0
- package/scripts/lib/docs/concern-meta.md +1 -1
- package/scripts/lib/docs/read-n-rules-config-lite.md +3 -1
- package/scripts/lib/docs/resolve-plugins.md +17 -0
- package/scripts/lib/docs/run-conftest-batch.md +3 -1
- package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +3 -1
- package/scripts/lib/lint-surface/docs/run-detectors.md +5 -1
- package/scripts/lib/lint-surface/policy-lint-adapter.mjs +2 -0
- package/scripts/lib/lint-surface/run-detectors.mjs +72 -5
- package/scripts/lib/read-n-rules-config-lite.mjs +5 -3
- package/scripts/lib/resolve-plugins.mjs +248 -0
- package/scripts/lib/run-conftest-batch.mjs +3 -1
- package/scripts/skills-cli.mjs +30 -52
- package/scripts/upgrade-n-rules-and-install.mjs +5 -4
- package/rules/ga/clean_ga_workflows/clean_ga_workflows.mdc +0 -20
- package/rules/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -112
- package/rules/ga/clean_ga_workflows/concern.json +0 -10
- package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +0 -33
- package/rules/ga/clean_ga_workflows/docs/index.md +0 -9
- package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +0 -8
- package/rules/ga/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +0 -26
- package/rules/ga/clean_merged_branch/clean_merged_branch.mdc +0 -24
- package/rules/ga/clean_merged_branch/clean_merged_branch.rego +0 -163
- package/rules/ga/clean_merged_branch/concern.json +0 -10
- package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +0 -27
- package/rules/ga/clean_merged_branch/docs/index.md +0 -9
- package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +0 -8
- package/rules/ga/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +0 -37
- package/rules/ga/docs/index.md +0 -11
- package/rules/ga/git_ai/concern.json +0 -11
- package/rules/ga/git_ai/docs/fix-git_ai.md +0 -30
- package/rules/ga/git_ai/docs/index.md +0 -9
- package/rules/ga/git_ai/fix-git_ai.mjs +0 -5
- package/rules/ga/git_ai/git_ai.mdc +0 -19
- package/rules/ga/git_ai/git_ai.rego +0 -76
- package/rules/ga/git_ai/template/git-ai.yml.snippet.yml +0 -30
- package/rules/ga/lint_ga/concern.json +0 -10
- package/rules/ga/lint_ga/docs/fix-lint_ga.md +0 -32
- package/rules/ga/lint_ga/docs/index.md +0 -9
- package/rules/ga/lint_ga/fix-lint_ga.mjs +0 -5
- package/rules/ga/lint_ga/lint_ga.mdc +0 -21
- package/rules/ga/lint_ga/lint_ga.rego +0 -132
- package/rules/ga/lint_ga/template/lint-ga.yml.snippet.yml +0 -41
- package/rules/ga/main.json +0 -1
- package/rules/ga/main.mdc +0 -17
- package/rules/ga/vscode_extensions/concern.json +0 -11
- package/rules/ga/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
- package/rules/ga/vscode_extensions/docs/index.md +0 -11
- package/rules/ga/vscode_extensions/fix-vscode_extensions.mjs +0 -1
- package/rules/ga/vscode_extensions/template/extensions.json.snippet.json +0 -1
- package/rules/ga/vscode_extensions/vscode_extensions.mdc +0 -9
- package/rules/ga/vscode_extensions/vscode_extensions.rego +0 -18
- package/rules/ga/vscode_settings/concern.json +0 -11
- package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +0 -26
- package/rules/ga/vscode_settings/docs/index.md +0 -9
- package/rules/ga/vscode_settings/fix-vscode_settings.mjs +0 -5
- package/rules/ga/vscode_settings/template/settings.json.snippet.json +0 -1
- package/rules/ga/vscode_settings/vscode_settings.mdc +0 -9
- package/rules/ga/vscode_settings/vscode_settings.rego +0 -22
- package/rules/ga/workflow_common/concern.json +0 -9
- package/rules/ga/workflow_common/template/uses-min-versions.snippet.json +0 -4
- package/rules/ga/workflow_common/workflow_common.mdc +0 -18
- package/rules/ga/workflow_common/workflow_common.rego +0 -288
- package/rules/ga/workflows/concern.json +0 -8
- package/rules/ga/workflows/docs/fix-workflows.md +0 -37
- package/rules/ga/workflows/docs/main.md +0 -38
- package/rules/ga/workflows/fix-workflows.mjs +0 -212
- package/rules/ga/workflows/main.mjs +0 -433
- package/rules/ga/workflows/workflows.mdc +0 -33
- package/rules/ga/zizmor_yml/concern.json +0 -11
- package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +0 -28
- package/rules/ga/zizmor_yml/docs/index.md +0 -9
- package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +0 -3
- package/rules/ga/zizmor_yml/template/zizmor.yml.snippet.yml +0 -5
- package/rules/ga/zizmor_yml/zizmor_yml.mdc +0 -9
- package/rules/ga/zizmor_yml/zizmor_yml.rego +0 -27
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: acp-runner.mjs
|
|
4
|
+
resource: npm/scripts/lib/acp-runner.mjs
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Огляд
|
|
8
|
+
|
|
9
|
+
Модуль виконує скіл через зовнішнього ACP-агента (Agent Client Protocol,
|
|
10
|
+
agentclientprotocol.com) — JSON-RPC поверх stdio замість сирого piping
|
|
11
|
+
`stdin`/`stdout` у CLI. Замінює колишній `runLlmCli` (`spawnSync` + `-p`/`exec -`)
|
|
12
|
+
для раннерів `cursor`/`codex`/`claude` у `skills-cli.mjs`.
|
|
13
|
+
|
|
14
|
+
`cursor` запускається через нативний ACP-режим `cursor-agent acp` (зовнішній
|
|
15
|
+
бінарник у PATH). `codex`/`claude` — через офіційні бандловані адаптери
|
|
16
|
+
(`@agentclientprotocol/codex-acp`, `@agentclientprotocol/claude-agent-acp`), що
|
|
17
|
+
самі керують своїм рушієм — зовнішній бінарник `codex`/`claude` у PATH більше
|
|
18
|
+
не потрібен.
|
|
19
|
+
|
|
20
|
+
## Поведінка
|
|
21
|
+
|
|
22
|
+
- `ACP_AGENT_COMMANDS` — команда запуску агента на провайдер: `cursor` — бінарник
|
|
23
|
+
у PATH; `codex`/`claude` — резолвляться з `bin`-запису бандлованого пакета-адаптера.
|
|
24
|
+
- `stopReasonToExitCode` — мапить ACP `StopReason` прогону на exit code: `end_turn` → `0`,
|
|
25
|
+
усе інше (`max_tokens`, `max_turn_requests`, `refusal`, `cancelled`) → `1`.
|
|
26
|
+
- `resolveAdapterBin` — резолвить абсолютний шлях до bin-файлу адаптера з його `package.json`.
|
|
27
|
+
- `pickAutoPermissionOptionId` — обирає `PermissionOption` без участі людини:
|
|
28
|
+
`allow_always` > `allow_once` > перша опція. Паритет із non-interactive `-p`/`exec -`
|
|
29
|
+
режимом — скіл є явною user-invocation, тож дозволи на tool-calls не питаються
|
|
30
|
+
інтерактивно.
|
|
31
|
+
- `AcpSkillClient` (internal) — ACP `Client` для скіл-раннера: `requestPermission`
|
|
32
|
+
автоапрувляє через `pickAutoPermissionOptionId`, `sessionUpdate` стрімить текстові
|
|
33
|
+
дельти (`agent_message_chunk`) у переданий `out`, `readTextFile`/`writeTextFile`
|
|
34
|
+
реалізовані напряму через `node:fs` (full-trust режим, без write-guard).
|
|
35
|
+
- `runAcpRunner` — підʼєднується до агента (`initialize` → `newSession` → `prompt`),
|
|
36
|
+
повертає exit code за `stopReasonToExitCode`; при stopReason ≠ `end_turn` додатково
|
|
37
|
+
логує причину через `logError`.
|
|
38
|
+
|
|
39
|
+
## Публічний API
|
|
40
|
+
|
|
41
|
+
- `ACP_AGENT_COMMANDS` — таблиця команд запуску на провайдер.
|
|
42
|
+
- `stopReasonToExitCode(stopReason)` — `StopReason` → `0 | 1`.
|
|
43
|
+
- `resolveAdapterBin(adapterPackage)` — шлях до bin-файлу адаптера.
|
|
44
|
+
- `pickAutoPermissionOptionId(options)` — `optionId` автообраного варіанту дозволу.
|
|
45
|
+
- `runAcpRunner(kind, prompt, projectDir, logError, deps?)` — виконує скіл через
|
|
46
|
+
ACP-агента `kind` (`cursor`|`codex`|`claude`); `deps` — інжекти для тестів
|
|
47
|
+
(`acp`, `spawnFn`, `out`, `resolveAdapterBin`, `isBinaryInPath`).
|
|
48
|
+
|
|
49
|
+
## Гарантії поведінки
|
|
50
|
+
|
|
51
|
+
- Permission-реквести від агента автоапрувляться без інтерактивного проміту —
|
|
52
|
+
парність із колишнім non-interactive `-p`/`exec -` режимом (full user-trust,
|
|
53
|
+
без write-guard, як і в `@7n/llm-lib/agent-skill`).
|
|
54
|
+
- `cursor` кидає, якщо `cursor-agent` відсутній у PATH (перевіряється до спавну
|
|
55
|
+
дочірнього процесу). `codex`/`claude` перевірки PATH не потребують — резолвляться
|
|
56
|
+
з установлених npm-залежностей.
|
|
57
|
+
- Дочірній процес агента завжди завершується (`child.kill()`) у `finally`, незалежно
|
|
58
|
+
від результату прогону.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: read-n-rules-config-lite.mjs
|
|
4
4
|
resource: npm/scripts/lib/read-n-rules-config-lite.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: b942af8d
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
---
|
|
@@ -24,3 +24,5 @@ isRuleEnabled — визначає, чи діє певне правило на
|
|
|
24
24
|
## Гарантії поведінки
|
|
25
25
|
|
|
26
26
|
- Read-only: не виконує операцій запису (ФС/БД).
|
|
27
|
+
|
|
28
|
+
**plugins:** LiteConfig містить `plugins` (масив npm-імен або `undefined`, якщо поля немає — тоді resolve-plugins робить автодетект).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: resolve-plugins.mjs
|
|
4
|
+
resource: npm/scripts/lib/resolve-plugins.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 4fbb42be
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Резолв плагінів @7n/rules: визначає, які пакети-плагіни активні у проєкті, де їхні `rules/`-каталоги, які capabilities вони надають і які handlers надають.
|
|
10
|
+
|
|
11
|
+
Джерело правди — поле `plugins: string[]` у `.n-rules.json`; воно завжди перекриває автодетект, а явний порожній масив означає «плагіни вимкнено». Якщо поля немає, `detectPluginsFromRepo` шукає файлові сигнали: наявність yml у `.github/workflows/` дає `@7n/rules-ci-github`, файл `azure-pipelines.yml` у корені — `@7n/rules-ci-azure` (реєстр `KNOWN_CI_PLUGINS`). Лише коли файлових сигналів немає, вмикається fallback за `repository.url` кореневого package.json (`github.com` → github, `dev.azure.com`/`visualstudio.com` → azure). Обидва сигнали дають обидва плагіни, жодного — порожній список.
|
|
12
|
+
|
|
13
|
+
`ensurePluginInstalled` ставить відсутній плагін через `bun add -d` (пакет стає devDependency — зміна видима у diff). Будь-який фейл установки (offline, пакет не опублікований) — warning і graceful skip, ніколи не hard-fail: лінт і синк мають працювати без мережі.
|
|
14
|
+
|
|
15
|
+
`resolvePlugins(projectRoot, config, options)` — головна функція: повертає масив `{name, packageRoot, rulesDir, manifest}` доступних плагінів з кешем на процес. `options.allowInstall: false` — hot-path режим (hook, lint): лише вже встановлені пакети, без `bun add`; `options.quiet: true` глушить warning-и (hook викликається на кожен файл). Плагін без каталогу `rules/` пропускається.
|
|
16
|
+
|
|
17
|
+
Маніфест — блок `"n-rules"` у package.json плагіна: `capabilities` (масив рядків на кшталт `ci:github`, живлять гейт концернів `requires.capability`) і `contributes.handlers` (мапа extension-point → відносний шлях модуля). `getActiveCapabilities` агрегує capabilities усіх плагінів у Set; `getHandlers(point)` повертає абсолютні шляхи модулів-обробників (v1 — лише API, споживачі handlers з'являться у v2). `resolveRulesDirs` віддає впорядковані джерела правил: ядро завжди перше (його правила й концерни виграють колізії), далі плагіни у порядку списку. `clearPluginResolveCache` скидає кеш (для тестів).
|
|
@@ -3,7 +3,7 @@ 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: 1ae29233
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
Файл запускає `conftest test` на заданому списку файлів, виявляючи порушення правил, визначених у Rego-полісіях. Він використовується для автоматизованої перевірки конфігураційних файлів на відповідність заданим вимогам. Результати перевірки повертаються у структурованому вигляді, що дозволяє інтегрувати результати в інші процеси валідації.
|
|
@@ -28,3 +28,5 @@ runConftestBatch: Запускає `conftest test` для заданого сп
|
|
|
28
28
|
- Не використовує кешування.
|
|
29
29
|
- Не перехоплює помилки, а завершує роботу з помилкою.
|
|
30
30
|
- Не має взаємодії з мережею.
|
|
31
|
+
|
|
32
|
+
**policyDirAbs:** опційний абсолютний шлях до policy-теки (правила з плагінів поза вбудованим rules/); за наявності має пріоритет над `policyDirRel`.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: policy-lint-adapter.mjs
|
|
4
4
|
resource: npm/scripts/lib/lint-surface/policy-lint-adapter.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: facc5e07
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
8
|
tier: cloud-avg
|
|
9
9
|
score: 80
|
|
@@ -42,3 +42,5 @@ docgen:
|
|
|
42
42
|
## Гарантії поведінки
|
|
43
43
|
|
|
44
44
|
- Read-only: не виконує операцій запису (ФС/БД).
|
|
45
|
+
|
|
46
|
+
**Плагіни:** у `runConftestBatch` передається `policyDirAbs: cfg.policyDir` — policy-тека concern-а може жити поза вбудованим rules/ ядра.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: run-detectors.mjs
|
|
4
4
|
resource: npm/scripts/lib/lint-surface/run-detectors.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: e8a0ec47
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -30,3 +30,7 @@ detectAll виконує прохід лінтера у режим детекц
|
|
|
30
30
|
## Гарантії поведінки
|
|
31
31
|
|
|
32
32
|
- Read-only: не виконує операцій запису (ФС/БД).
|
|
33
|
+
|
|
34
|
+
**Multi-dir (плагіни):** `effectiveRulesDirs` додає rules-каталоги плагінів з `.n-rules.json` (hot-path: без install, quiet); `readLintConcernsByRuleMulti` зливає концерни за іменем (перший власник виграє) — плагін може додавати концерни до правила ядра (mixin).
|
|
35
|
+
|
|
36
|
+
**Capability-гейт:** `filterByCapabilities` відкидає концерни з незадоволеним `requires.capability` (capabilities надають встановлені плагіни через маніфест `n-rules.capabilities`; явний `opts.capabilities` у тестах перекриває резолв).
|
|
@@ -92,6 +92,8 @@ export async function evaluatePolicyConcern(ctx, cfg) {
|
|
|
92
92
|
const templateData = await resolveConcernTemplateData(cfg.policyDir, { files: cfg.files })
|
|
93
93
|
const denies = runConftestBatch({
|
|
94
94
|
policyDirRel: `${ruleId}/${concernId}`,
|
|
95
|
+
// Абсолютний шлях теки concern-а — правило може жити поза вбудованим rules/ ядра (плагін).
|
|
96
|
+
policyDirAbs: cfg.policyDir,
|
|
95
97
|
namespace,
|
|
96
98
|
files,
|
|
97
99
|
templateData
|
|
@@ -17,6 +17,7 @@ import picomatch from 'picomatch'
|
|
|
17
17
|
import { listConcerns } from '../concern-meta.mjs'
|
|
18
18
|
import { collectChangedFilesSince, resolveChangedBase } from '../changed-files.mjs'
|
|
19
19
|
import { readNRulesConfigLite, isRuleEnabled } from '../read-n-rules-config-lite.mjs'
|
|
20
|
+
import { getActiveCapabilities, resolvePlugins } from '../resolve-plugins.mjs'
|
|
20
21
|
import { runConcernDetector, DetectorError } from './detect.mjs'
|
|
21
22
|
import { renderViolations, renderDiagnostics } from './render.mjs'
|
|
22
23
|
import { createProgressReporter } from './progress.mjs'
|
|
@@ -83,6 +84,71 @@ async function readLintConcernsByRule(rulesDir) {
|
|
|
83
84
|
return out
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Rules-каталоги прогону: явний `opts.rulesDirs`, або базовий (`opts.rulesDir` ?? вбудований)
|
|
89
|
+
* плюс каталоги плагінів з `.n-rules.json` (hot-path: без install і без warning-шуму).
|
|
90
|
+
* @param {{ rulesDirs?: string[], rulesDir?: string, cwd: string }} opts опції прогону.
|
|
91
|
+
* @returns {Promise<string[]>} упорядковані rules-каталоги (ядро перше).
|
|
92
|
+
*/
|
|
93
|
+
async function effectiveRulesDirs(opts) {
|
|
94
|
+
if (Array.isArray(opts.rulesDirs) && opts.rulesDirs.length > 0) return opts.rulesDirs
|
|
95
|
+
const base = opts.rulesDir ?? DEFAULT_RULES_DIR
|
|
96
|
+
const config = await readNRulesConfigLite(opts.cwd)
|
|
97
|
+
const plugins = resolvePlugins(opts.cwd, { plugins: config.plugins }, { allowInstall: false, quiet: true })
|
|
98
|
+
return [base, ...plugins.map(p => p.rulesDir)]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Відкидає концерни з незадоволеним `requires.capability`: capability надають
|
|
103
|
+
* встановлені плагіни (маніфест `n-rules.capabilities`). Явний `opts.capabilities`
|
|
104
|
+
* (тести) перекриває резолв.
|
|
105
|
+
* @param {Record<string, ConcernMeta[]>} byRule concerns за rule-id.
|
|
106
|
+
* @param {{ capabilities?: Iterable<string>, cwd: string }} opts опції прогону.
|
|
107
|
+
* @returns {Promise<Record<string, ConcernMeta[]>>} відфільтровані concerns.
|
|
108
|
+
*/
|
|
109
|
+
async function filterByCapabilities(byRule, opts) {
|
|
110
|
+
let caps
|
|
111
|
+
if (opts.capabilities) {
|
|
112
|
+
caps = new Set(opts.capabilities)
|
|
113
|
+
} else {
|
|
114
|
+
const config = await readNRulesConfigLite(opts.cwd)
|
|
115
|
+
caps = getActiveCapabilities(opts.cwd, { plugins: config.plugins }, { allowInstall: false, quiet: true })
|
|
116
|
+
}
|
|
117
|
+
/** @type {Record<string, ConcernMeta[]>} */
|
|
118
|
+
const out = {}
|
|
119
|
+
for (const [ruleId, concerns] of Object.entries(byRule)) {
|
|
120
|
+
const kept = concerns.filter(c => c.requiresCapability === undefined || caps.has(c.requiresCapability))
|
|
121
|
+
if (kept.length > 0) out[ruleId] = kept
|
|
122
|
+
}
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Мердж concerns кількох rules-каталогів: правила зливаються за id, концерни — за іменем
|
|
128
|
+
* (перший власник виграє: ядро → плагіни у порядку списку). Плагін може ДОДАВАТИ концерни
|
|
129
|
+
* до правила ядра (mixin), але не перекривати наявні.
|
|
130
|
+
* @param {string[]} rulesDirs упорядковані rules-каталоги.
|
|
131
|
+
* @returns {Promise<Record<string, ConcernMeta[]>>} об'єднані concerns за rule-id.
|
|
132
|
+
*/
|
|
133
|
+
async function readLintConcernsByRuleMulti(rulesDirs) {
|
|
134
|
+
/** @type {Record<string, ConcernMeta[]>} */
|
|
135
|
+
const merged = {}
|
|
136
|
+
for (const dir of rulesDirs) {
|
|
137
|
+
const byRule = await readLintConcernsByRule(dir)
|
|
138
|
+
for (const [ruleId, concerns] of Object.entries(byRule)) {
|
|
139
|
+
if (!(ruleId in merged)) {
|
|
140
|
+
merged[ruleId] = [...concerns]
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
const seen = new Set(merged[ruleId].map(c => c.name))
|
|
144
|
+
for (const c of concerns) {
|
|
145
|
+
if (!seen.has(c.name)) merged[ruleId].push(c)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return merged
|
|
150
|
+
}
|
|
151
|
+
|
|
86
152
|
/**
|
|
87
153
|
* Активні rule-id з `.n-rules.json` (для delta/full режимів).
|
|
88
154
|
* @param {Record<string, ConcernMeta[]>} byRule concerns згруповані за rule-id.
|
|
@@ -111,7 +177,8 @@ function sortEntries(entries) {
|
|
|
111
177
|
* Будує план прогону для заданих опцій (discovery + scope-table).
|
|
112
178
|
* Спільне джерело для detect-only і fix-pipeline.
|
|
113
179
|
* @param {object} opts опції прогону.
|
|
114
|
-
* @param {string} opts.rulesDir корінь із
|
|
180
|
+
* @param {string} [opts.rulesDir] базовий корінь із правилами (дефолт — вбудований).
|
|
181
|
+
* @param {string[]} [opts.rulesDirs] явні rules-каталоги (ядро + плагіни); без них — базовий + плагіни з конфігу.
|
|
115
182
|
* @param {string} opts.cwd робоча директорія прогону.
|
|
116
183
|
* @param {boolean} [opts.full] whole-repo режим (усі enabled-concerns).
|
|
117
184
|
* @param {string[]} [opts.rules] scoped rule-id (порожній → delta/full).
|
|
@@ -119,7 +186,7 @@ function sortEntries(entries) {
|
|
|
119
186
|
* @returns {Promise<PlanItem[]>} впорядкований план прогону.
|
|
120
187
|
*/
|
|
121
188
|
export async function buildDetectPlan(opts) {
|
|
122
|
-
const byRule = await
|
|
189
|
+
const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(await effectiveRulesDirs(opts)), opts)
|
|
123
190
|
return buildPlan({
|
|
124
191
|
byRule,
|
|
125
192
|
full: opts.full === true,
|
|
@@ -231,7 +298,8 @@ async function buildPlan({ byRule, full, rules, explicitFiles, cwd }) {
|
|
|
231
298
|
/**
|
|
232
299
|
* Запускає detect-only прохід. Повертає всі violations і похідний exitCode.
|
|
233
300
|
* @param {object} opts опції прогону.
|
|
234
|
-
* @param {string} opts.rulesDir корінь із
|
|
301
|
+
* @param {string} [opts.rulesDir] базовий корінь із правилами (дефолт — вбудований).
|
|
302
|
+
* @param {string[]} [opts.rulesDirs] явні rules-каталоги (ядро + плагіни); без них — базовий + плагіни з конфігу.
|
|
235
303
|
* @param {string} opts.cwd робоча директорія прогону.
|
|
236
304
|
* @param {boolean} [opts.full] whole-repo режим.
|
|
237
305
|
* @param {string[]} [opts.rules] scoped rule-id (порожній → delta/full).
|
|
@@ -243,7 +311,6 @@ async function buildPlan({ byRule, full, rules, explicitFiles, cwd }) {
|
|
|
243
311
|
* @returns {Promise<{ violations: LintViolation[], exitCode: 0|1|2, ran: LintEntry[] }>} violations, exitCode і виконані entries.
|
|
244
312
|
*/
|
|
245
313
|
export async function detectAll(opts) {
|
|
246
|
-
const rulesDir = opts.rulesDir ?? DEFAULT_RULES_DIR
|
|
247
314
|
const { cwd } = opts
|
|
248
315
|
const full = opts.full === true
|
|
249
316
|
const rules = Array.isArray(opts.rules) ? opts.rules : []
|
|
@@ -251,7 +318,7 @@ export async function detectAll(opts) {
|
|
|
251
318
|
const verbose = opts.verbose === true
|
|
252
319
|
const baseLog = opts.log ?? (s => process.stdout.write(s))
|
|
253
320
|
|
|
254
|
-
const byRule = await
|
|
321
|
+
const byRule = await filterByCapabilities(await readLintConcernsByRuleMulti(await effectiveRulesDirs(opts)), opts)
|
|
255
322
|
const plan = await buildPlan({ byRule, full, rules, explicitFiles, cwd })
|
|
256
323
|
|
|
257
324
|
// Detect-only бар — ЛИШЕ в TTY (без тикера «виправлено»). У не-TTY (hooks, CI-gate,
|
|
@@ -22,6 +22,7 @@ const LEGACY_CONFIG_FILE = '.n-cursor.json'
|
|
|
22
22
|
* @property {boolean} exists чи існує .n-rules.json (або legacy .n-rules.json) у поточному каталозі
|
|
23
23
|
* @property {string[]} rules id правил з whitelist (порожній якщо файл відсутній)
|
|
24
24
|
* @property {string[]} disableRules id правил, явно вимкнених у `disable-rules`
|
|
25
|
+
* @property {string[] | undefined} plugins npm-пакети-плагіни з конфігу; undefined — поля немає (→ автодетект у resolve-plugins)
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -34,16 +35,17 @@ export async function readNRulesConfigLite(cwd = process.cwd()) {
|
|
|
34
35
|
configPath = join(cwd, LEGACY_CONFIG_FILE)
|
|
35
36
|
}
|
|
36
37
|
if (!existsSync(configPath)) {
|
|
37
|
-
return { exists: false, rules: [], disableRules: [] }
|
|
38
|
+
return { exists: false, rules: [], disableRules: [], plugins: undefined }
|
|
38
39
|
}
|
|
39
40
|
const raw = await readFile(configPath, 'utf8')
|
|
40
|
-
/** @type {{ rules?: unknown, ['disable-rules']?: unknown }} */
|
|
41
|
+
/** @type {{ rules?: unknown, ['disable-rules']?: unknown, plugins?: unknown }} */
|
|
41
42
|
const parsed = JSON.parse(raw)
|
|
42
43
|
const rules = Array.isArray(parsed.rules) ? parsed.rules.filter(r => typeof r === 'string') : []
|
|
43
44
|
const disableRules = Array.isArray(parsed['disable-rules'])
|
|
44
45
|
? parsed['disable-rules'].filter(r => typeof r === 'string')
|
|
45
46
|
: []
|
|
46
|
-
|
|
47
|
+
const plugins = Array.isArray(parsed.plugins) ? parsed.plugins.filter(p => typeof p === 'string') : undefined
|
|
48
|
+
return { exists: true, rules, disableRules, plugins }
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
/**
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Резолв плагінів `@7n/rules`: які пакети-плагіни активні у проєкті, де їхні `rules/`,
|
|
3
|
+
* які capabilities вони дають і які handlers надають.
|
|
4
|
+
*
|
|
5
|
+
* Джерело правди — поле `plugins: string[]` у `.n-rules.json` (завжди перекриває автодетект;
|
|
6
|
+
* явний `[]` = «плагіни вимкнено»). Якщо поля немає — `detectPluginsFromRepo`: файлові сигнали
|
|
7
|
+
* (`.github/workflows/*.yml` → `@7n/rules-ci-github`; `azure-pipelines.yml` → `@7n/rules-ci-azure`),
|
|
8
|
+
* а без них — fallback за `repository.url` кореневого package.json (`github.com` / `dev.azure.com`).
|
|
9
|
+
*
|
|
10
|
+
* Установка: `ensurePluginInstalled` — плагін стає devDependency через `bun add -d` (bun сам
|
|
11
|
+
* резолвить актуальну версію; зміна видима у diff package.json). Фейл установки (offline,
|
|
12
|
+
* пакет ще не опублікований) — warning + graceful skip, ніколи не hard-fail: лінт/синк
|
|
13
|
+
* мають працювати без мережі. Hot-path (hook) НЕ встановлює — лише резолвить уже встановлені
|
|
14
|
+
* (`allowInstall: false`).
|
|
15
|
+
*
|
|
16
|
+
* Маніфест плагіна — блок `"n-rules"` у його package.json:
|
|
17
|
+
* `{ "capabilities": ["ci:github"], "contributes": { "rules": true, "handlers": { "<point>": "./mod.mjs" } } }`.
|
|
18
|
+
* `capabilities` живлять гейт концернів (`concern.json` → `requires.capability`);
|
|
19
|
+
* `handlers` — іменовані extension-points правил ядра (v1: лише API, споживачі — v2).
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
import { join, resolve } from 'node:path'
|
|
24
|
+
|
|
25
|
+
/** Відомі плагіни для автовизначення: сигнал у дереві репо → npm-пакет. */
|
|
26
|
+
export const KNOWN_CI_PLUGINS = Object.freeze({
|
|
27
|
+
github: '@7n/rules-ci-github',
|
|
28
|
+
azure: '@7n/rules-ci-azure'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const WORKFLOW_YML_RE = /\.ya?ml$/u
|
|
32
|
+
const GITHUB_URL_RE = /github\.com/iu
|
|
33
|
+
const AZURE_URL_RE = /dev\.azure\.com|visualstudio\.com/iu
|
|
34
|
+
|
|
35
|
+
/** Кеш резолву на процес: projectRoot → результат `resolvePlugins`. */
|
|
36
|
+
const RESOLVE_CACHE = new Map()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `repository.url` з кореневого package.json (string або {url}); null, якщо нема/нечитабельний.
|
|
40
|
+
* @param {string} projectRoot корінь репозиторію
|
|
41
|
+
* @returns {string | null} URL або null
|
|
42
|
+
*/
|
|
43
|
+
function readRepositoryUrl(projectRoot) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'))
|
|
46
|
+
const repo = pkg?.repository
|
|
47
|
+
if (typeof repo === 'string') return repo
|
|
48
|
+
if (repo && typeof repo === 'object' && typeof repo.url === 'string') return repo.url
|
|
49
|
+
} catch {
|
|
50
|
+
/* нема package.json або битий JSON — сигналу нема */
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Чи є у `.github/workflows/` хоч один yml/yaml.
|
|
57
|
+
* @param {string} projectRoot корінь репозиторію
|
|
58
|
+
* @returns {boolean} true — GitHub Actions присутні
|
|
59
|
+
*/
|
|
60
|
+
function hasGithubWorkflows(projectRoot) {
|
|
61
|
+
const dir = join(projectRoot, '.github', 'workflows')
|
|
62
|
+
if (!existsSync(dir)) return false
|
|
63
|
+
try {
|
|
64
|
+
return readdirSync(dir).some(name => WORKFLOW_YML_RE.test(name))
|
|
65
|
+
} catch {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Автодетект CI-плагінів за станом репозиторію.
|
|
72
|
+
* Файлові сигнали мають пріоритет; `repository.url` — лише коли файлових сигналів нема
|
|
73
|
+
* (свіже репо без CI-конфігів). Обидва сигнали → обидва плагіни; жодного → [].
|
|
74
|
+
* @param {string} projectRoot корінь репозиторію
|
|
75
|
+
* @returns {string[]} npm-імена плагінів
|
|
76
|
+
*/
|
|
77
|
+
export function detectPluginsFromRepo(projectRoot) {
|
|
78
|
+
const out = []
|
|
79
|
+
if (hasGithubWorkflows(projectRoot)) out.push(KNOWN_CI_PLUGINS.github)
|
|
80
|
+
if (existsSync(join(projectRoot, 'azure-pipelines.yml'))) out.push(KNOWN_CI_PLUGINS.azure)
|
|
81
|
+
if (out.length > 0) return out
|
|
82
|
+
|
|
83
|
+
const url = readRepositoryUrl(projectRoot)
|
|
84
|
+
if (typeof url === 'string') {
|
|
85
|
+
if (GITHUB_URL_RE.test(url)) out.push(KNOWN_CI_PLUGINS.github)
|
|
86
|
+
if (AZURE_URL_RE.test(url)) out.push(KNOWN_CI_PLUGINS.azure)
|
|
87
|
+
}
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Список плагінів проєкту: явний `config.plugins` (включно з порожнім = вимкнено) або автодетект.
|
|
93
|
+
* @param {string} projectRoot корінь репозиторію
|
|
94
|
+
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json` (може бути відсутній)
|
|
95
|
+
* @returns {string[]} npm-імена плагінів
|
|
96
|
+
*/
|
|
97
|
+
export function resolvePluginList(projectRoot, config) {
|
|
98
|
+
const declared = config?.plugins
|
|
99
|
+
if (Array.isArray(declared)) {
|
|
100
|
+
return declared.filter(p => typeof p === 'string' && p.trim() !== '')
|
|
101
|
+
}
|
|
102
|
+
return detectPluginsFromRepo(projectRoot)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Гарантує, що плагін встановлений: якщо `node_modules/<pkg>` нема — `bun add -d <pkg>`
|
|
107
|
+
* (дописує devDependency і ставить). Фейл — warning + false, без винятку.
|
|
108
|
+
* @param {string} projectRoot корінь репозиторію
|
|
109
|
+
* @param {string} packageName npm-ім'я плагіна
|
|
110
|
+
* @returns {boolean} true — пакет доступний у node_modules після виклику
|
|
111
|
+
*/
|
|
112
|
+
export function ensurePluginInstalled(projectRoot, packageName) {
|
|
113
|
+
const installed = join(projectRoot, 'node_modules', packageName, 'package.json')
|
|
114
|
+
if (existsSync(installed)) return true
|
|
115
|
+
if (!existsSync(join(projectRoot, 'package.json'))) return false
|
|
116
|
+
|
|
117
|
+
const r = spawnSync('bun', ['add', '-d', packageName], { cwd: projectRoot, encoding: 'utf8', shell: false })
|
|
118
|
+
if (r.error || r.status !== 0) {
|
|
119
|
+
const reason = r.error ? r.error.message : `bun add exit ${r.status}`
|
|
120
|
+
console.warn(`⚠️ Плагін ${packageName} не встановився (${reason}) — пропускаю\n`)
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
return existsSync(installed)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @typedef {object} ResolvedPlugin
|
|
128
|
+
* @property {string} name npm-ім'я пакета (`@7n/rules` для ядра)
|
|
129
|
+
* @property {string} packageRoot абсолютний корінь пакета
|
|
130
|
+
* @property {string} rulesDir абсолютний шлях до `rules/` пакета
|
|
131
|
+
* @property {{ capabilities: string[], contributes: { rules?: boolean, handlers?: Record<string, string> } }} manifest нормалізований блок `n-rules` з package.json плагіна
|
|
132
|
+
*/
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Маніфест плагіна з блоку `"n-rules"` його package.json (з дефолтами).
|
|
136
|
+
* @param {string} packageRoot корінь пакета
|
|
137
|
+
* @returns {ResolvedPlugin['manifest']} нормалізований маніфест
|
|
138
|
+
*/
|
|
139
|
+
function readPluginManifest(packageRoot) {
|
|
140
|
+
/** @type {ResolvedPlugin['manifest']} */
|
|
141
|
+
const fallback = { capabilities: [], contributes: { rules: true, handlers: {} } }
|
|
142
|
+
try {
|
|
143
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
144
|
+
const raw = pkg?.['n-rules']
|
|
145
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback
|
|
146
|
+
const capabilities = Array.isArray(raw.capabilities) ? raw.capabilities.filter(c => typeof c === 'string') : []
|
|
147
|
+
const contributes = raw.contributes && typeof raw.contributes === 'object' ? raw.contributes : {}
|
|
148
|
+
const handlers =
|
|
149
|
+
contributes.handlers && typeof contributes.handlers === 'object' && !Array.isArray(contributes.handlers)
|
|
150
|
+
? Object.fromEntries(Object.entries(contributes.handlers).filter(([, v]) => typeof v === 'string'))
|
|
151
|
+
: {}
|
|
152
|
+
return { capabilities, contributes: { rules: contributes.rules !== false, handlers } }
|
|
153
|
+
} catch {
|
|
154
|
+
return fallback
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Повний резолв плагінів проєкту (з кешем на процес).
|
|
160
|
+
* @param {string} projectRoot корінь репозиторію
|
|
161
|
+
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json`
|
|
162
|
+
* @param {{ allowInstall?: boolean, quiet?: boolean }} [options] `allowInstall:false` (hot-path hook/lint) —
|
|
163
|
+
* лише вже встановлені пакети, без `bun add`; `quiet` — без warning-ів (hook на кожен файл)
|
|
164
|
+
* @returns {ResolvedPlugin[]} доступні плагіни (без ядра)
|
|
165
|
+
*/
|
|
166
|
+
export function resolvePlugins(projectRoot, config, options = {}) {
|
|
167
|
+
const root = resolve(projectRoot)
|
|
168
|
+
const names = resolvePluginList(root, config)
|
|
169
|
+
const cacheKey = `${root}${names.join(',')}${options.allowInstall !== false}`
|
|
170
|
+
const cached = RESOLVE_CACHE.get(cacheKey)
|
|
171
|
+
if (cached) return cached
|
|
172
|
+
|
|
173
|
+
/** @type {ResolvedPlugin[]} */
|
|
174
|
+
const out = []
|
|
175
|
+
for (const name of names) {
|
|
176
|
+
const packageRoot = join(root, 'node_modules', name)
|
|
177
|
+
const available =
|
|
178
|
+
existsSync(join(packageRoot, 'package.json')) ||
|
|
179
|
+
(options.allowInstall !== false && ensurePluginInstalled(root, name))
|
|
180
|
+
if (!available) {
|
|
181
|
+
if (options.allowInstall === false && options.quiet !== true) {
|
|
182
|
+
console.warn(`⚠️ Плагін ${name} не встановлений — пропускаю (запусти npx @7n/rules)\n`)
|
|
183
|
+
}
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
const rulesDir = join(packageRoot, 'rules')
|
|
187
|
+
if (!existsSync(rulesDir)) {
|
|
188
|
+
if (options.quiet !== true) console.warn(`⚠️ Плагін ${name} без каталогу rules/ — пропускаю\n`)
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
out.push({ name, packageRoot, rulesDir, manifest: readPluginManifest(packageRoot) })
|
|
192
|
+
}
|
|
193
|
+
RESOLVE_CACHE.set(cacheKey, out)
|
|
194
|
+
return out
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Rules-каталоги для всіх поверхонь ядра: ядро першим (його правила/концерни виграють
|
|
199
|
+
* колізії), далі плагіни у порядку списку.
|
|
200
|
+
* @param {string} projectRoot корінь репозиторію
|
|
201
|
+
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json`
|
|
202
|
+
* @param {string} bundledRulesDir `rules/` встановленого/вбудованого ядра
|
|
203
|
+
* @param {{ allowInstall?: boolean }} [options] прокидається у `resolvePlugins`
|
|
204
|
+
* @returns {Array<{ name: string, rulesDir: string, packageRoot: string | null }>} джерела правил
|
|
205
|
+
*/
|
|
206
|
+
export function resolveRulesDirs(projectRoot, config, bundledRulesDir, options = {}) {
|
|
207
|
+
const plugins = resolvePlugins(projectRoot, config, options)
|
|
208
|
+
return [
|
|
209
|
+
{ name: '@7n/rules', rulesDir: bundledRulesDir, packageRoot: null },
|
|
210
|
+
...plugins.map(p => ({ name: p.name, rulesDir: p.rulesDir, packageRoot: p.packageRoot }))
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Активні capabilities від усіх доступних плагінів (для гейта `requires.capability` у concern.json).
|
|
216
|
+
* @param {string} projectRoot корінь репозиторію
|
|
217
|
+
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json`
|
|
218
|
+
* @param {{ allowInstall?: boolean }} [options] прокидається у `resolvePlugins`
|
|
219
|
+
* @returns {Set<string>} набір capability-рядків (напр. `ci:github`)
|
|
220
|
+
*/
|
|
221
|
+
export function getActiveCapabilities(projectRoot, config, options = {}) {
|
|
222
|
+
const caps = new Set()
|
|
223
|
+
for (const p of resolvePlugins(projectRoot, config, options)) {
|
|
224
|
+
for (const c of p.manifest.capabilities) caps.add(c)
|
|
225
|
+
}
|
|
226
|
+
return caps
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Handlers для extension-point правила ядра (v1 — лише API; перший споживач — v2).
|
|
231
|
+
* @param {string} projectRoot корінь репозиторію
|
|
232
|
+
* @param {{ plugins?: unknown } | null | undefined} config розпарсений `.n-rules.json`
|
|
233
|
+
* @param {string} point ім'я extension-point (напр. `doc-files`)
|
|
234
|
+
* @returns {Array<{ pluginName: string, modulePath: string }>} абсолютні шляхи модулів-обробників
|
|
235
|
+
*/
|
|
236
|
+
export function getHandlers(projectRoot, config, point) {
|
|
237
|
+
const out = []
|
|
238
|
+
for (const p of resolvePlugins(projectRoot, config, { allowInstall: false })) {
|
|
239
|
+
const rel = p.manifest.contributes.handlers[point]
|
|
240
|
+
if (typeof rel === 'string') out.push({ pluginName: p.name, modulePath: join(p.packageRoot, rel) })
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Скидає кеш резолву (для тестів). */
|
|
246
|
+
export function clearPluginResolveCache() {
|
|
247
|
+
RESOLVE_CACHE.clear()
|
|
248
|
+
}
|
|
@@ -38,6 +38,8 @@ const RULES_ROOT = join(PACKAGE_ROOT, 'rules')
|
|
|
38
38
|
/**
|
|
39
39
|
* @typedef {object} ConftestBatchOptions
|
|
40
40
|
* @property {string} policyDirRel шлях до підкаталогу `npm/policy/...` (наприклад `abie/base_deployment_preem`)
|
|
41
|
+
* @property {string} [policyDirAbs] абсолютний шлях до теки policy-concern-а — для правил ПОЗА вбудованим
|
|
42
|
+
* `rules/` ядра (плагіни); за наявності має пріоритет над `policyDirRel`
|
|
41
43
|
* @property {string} namespace повне імʼя rego-пакета (наприклад `abie.base_deployment_preem`)
|
|
42
44
|
* @property {string[]} files список абсолютних шляхів файлів для перевірки (порожній — повертаємо порожньо)
|
|
43
45
|
* @property {string[]} [extraArgs] додаткові аргументи для conftest (наприклад `--combine` для крос-документних правил)
|
|
@@ -73,7 +75,7 @@ export function runConftestBatch(opts) {
|
|
|
73
75
|
const slash = opts.policyDirRel.indexOf('/')
|
|
74
76
|
const ruleId = slash === -1 ? opts.policyDirRel : opts.policyDirRel.slice(0, slash)
|
|
75
77
|
const sub = slash === -1 ? '' : opts.policyDirRel.slice(slash + 1)
|
|
76
|
-
const policyAbs = sub ? join(RULES_ROOT, ruleId, sub) : join(RULES_ROOT, ruleId)
|
|
78
|
+
const policyAbs = opts.policyDirAbs ?? (sub ? join(RULES_ROOT, ruleId, sub) : join(RULES_ROOT, ruleId))
|
|
77
79
|
if (!existsSync(policyAbs)) {
|
|
78
80
|
throw new Error(`runConftestBatch: rego-каталог не знайдено: ${policyAbs}`)
|
|
79
81
|
}
|