@7n/rules 1.49.24 → 1.49.26
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/package.json +1 -1
- package/rules/doc-files/check/docs/fix-worker.md +2 -2
- package/rules/doc-files/check/docs/main.md +2 -2
- package/rules/doc-files/check/fix-worker.mjs +7 -4
- package/rules/doc-files/check/main.mjs +13 -8
- package/scripts/lib/docs/resolve-plugins.md +26 -53
- package/scripts/lib/resolve-plugins.mjs +70 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.49.26] - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- gate incompatible plugin API version and pin first-party plugin installs to a compatible semver range
|
|
8
|
+
|
|
9
|
+
## [1.49.25] - 2026-07-27
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Збережено --path scope у doc-files fixer
|
|
14
|
+
|
|
3
15
|
## [1.49.24] - 2026-07-27
|
|
4
16
|
|
|
5
17
|
### Changed
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: fix-worker.mjs
|
|
4
4
|
resource: npm/rules/doc-files/check/fix-worker.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: 444c817d
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
issues: judge:inaccurate:0.98
|
|
@@ -18,7 +18,7 @@ docgen:
|
|
|
18
18
|
## Поведінка
|
|
19
19
|
|
|
20
20
|
1. Викликається `fixWorker`.
|
|
21
|
-
2.
|
|
21
|
+
2. З порушень, знайдених detector-ом, відновлюються повні цілі застарілих документів; fixer не робить нового повного обходу checkout. Тому hook і `lint doc-files --path` генерують docs лише для власного explicit scope (рукописні доки без docgen-frontmatter — не цілі).
|
|
22
22
|
3. Якщо знайдені застарілі документи, кожна цільова дока реєструється durable через `ctx.recordDurableWrite` (fallback — `ctx.recordWrite`), а потім запускається генерація батчу через `runGenerationBatch`.
|
|
23
23
|
4. Батч отримує м'який дедлайн — 80% від `ctx.timeoutMs` рунга: генерація сама зупиняється до backstop-таймауту, повертає частковий прогрес штатно й не лишає фонового батчу поверх наступного rung-а. Дедлайн діє і всередині файлу: `generateDoc` ріже per-call LLM-таймаути під залишок бюджету, тож навіть перший файл, що не вкладається у рунг, обривається сам (transient), а не по backstop-таймеру runner-а.
|
|
24
24
|
5. Фільтруються порушення, що стосуються сирітських документів (вихідні файли видалені); pre-image кожного реєструється у `ctx.recordWrite`.
|
|
@@ -3,7 +3,7 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/doc-files/check/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: b5921123
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Огляд
|
|
@@ -12,7 +12,7 @@ Lint-детектор doc-files: знаходить застарілі файл
|
|
|
12
12
|
|
|
13
13
|
## Поведінка
|
|
14
14
|
|
|
15
|
-
`lint(ctx)` збирає застарілі доки через `collectStale` (для конкретного набору змінених файлів або, якщо файлів не передано, повним скануванням дерева) і додає для кожної окреме порушення з причиною (`missing`/`crc-mismatch`/`degraded`) та шляхом джерела.
|
|
15
|
+
`lint(ctx)` збирає застарілі доки через `collectStale` (для конкретного набору змінених файлів або, якщо файлів не передано, повним скануванням дерева) і додає для кожної окреме порушення з причиною (`missing`/`crc-mismatch`/`degraded`) та шляхом джерела. Сирітські доки (`scanOrphanedDocs`) перевіряються лише за повного repo-wide скану: explicit files від hook або `--path` не мають безпечної межі для такого обходу, тому не можуть зачіпати docs поза своїм scope.
|
|
16
16
|
|
|
17
17
|
Реверс-мапінг: якщо серед змінених файлів трапляється сама `.md`-дока (а не її джерело), `sourceForDoc` шукає відповідний вихідний файл поруч (той самий basename, легальне розширення) і саме його передає далі в перевірку застарілості — так зміна доки теж тригерить звірку CRC її джерела.
|
|
18
18
|
|
|
@@ -24,16 +24,19 @@ const DEADLINE_FRACTION = 0.8
|
|
|
24
24
|
/** @type {FixWorkerFn} */
|
|
25
25
|
export async function fixWorker(violations, ctx) {
|
|
26
26
|
const { cwd } = ctx
|
|
27
|
-
const {
|
|
27
|
+
const { describeFile } = await import('../docgen-scan/main.mjs')
|
|
28
28
|
const { runGenerationBatch, purgeOrphanedDocs } = await import('../docgen-files-batch/main.mjs')
|
|
29
29
|
|
|
30
30
|
/** @type {string[]} */
|
|
31
31
|
const touchedFiles = []
|
|
32
32
|
const recordDoc = ctx.recordDurableWrite ?? ctx.recordWrite
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
34
|
+
// Відновлюємо повні target-обʼєкти лише для порушень detector-а. Повний re-scan
|
|
35
|
+
// тут ігнорував би explicit files від hook/`lint --path` і генерував би docs
|
|
36
|
+
// для всього checkout.
|
|
37
|
+
const stale = [...new Set(violations.filter(v => v.reason !== 'orphaned-doc' && v.file).map(v => v.file))]
|
|
38
|
+
.map(sourcePath => describeFile(cwd, sourcePath))
|
|
39
|
+
.filter(f => f.stale)
|
|
37
40
|
if (stale.length > 0) {
|
|
38
41
|
for (const f of stale) {
|
|
39
42
|
const docAbs = join(cwd, f.docPath)
|
|
@@ -86,14 +86,19 @@ export function lint(ctx) {
|
|
|
86
86
|
})
|
|
87
87
|
)
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
// Явний files-набір (hook/--path) не містить межі дерева для безпечного
|
|
90
|
+
// orphan-скану. Повний скан тут порушив би scope і міг би видалити доки поза
|
|
91
|
+
// сервісом, тому orphan-cleanup лишається лише repo-wide прогоном.
|
|
92
|
+
if (files === undefined) {
|
|
93
|
+
for (const orphan of scanOrphanedDocs(cwd)) {
|
|
94
|
+
violations.push(
|
|
95
|
+
/** @type {Partial<import('../../../scripts/lib/lint-surface/types.mjs').LintViolation>} */ ({
|
|
96
|
+
reason: 'orphaned-doc',
|
|
97
|
+
message: `сирітський док (source видалено): ${orphan}`,
|
|
98
|
+
file: orphan
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
}
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
const unavailable = unavailableDocFilesPlugins(cwd)
|
|
@@ -3,71 +3,35 @@ type: JS Module
|
|
|
3
3
|
title: resolve-plugins.mjs
|
|
4
4
|
resource: npm/scripts/lib/resolve-plugins.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
7
|
-
model:
|
|
8
|
-
tier:
|
|
9
|
-
score:
|
|
10
|
-
issues:
|
|
6
|
+
crc: e7eaf408
|
|
7
|
+
model: openai-codex/gpt-5.5
|
|
8
|
+
tier: cloud-avg
|
|
9
|
+
score: 90
|
|
10
|
+
issues: surzhik,judge-refine:kept-original,judge:inaccurate:0.99
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
12
|
---
|
|
12
13
|
|
|
13
14
|
## Огляд
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
Файл визначає, які плагіни `@7n/rules` активні для проєкту, щоб core міг підключити правильні каталоги правил, capabilities, документаційні розширення та handlers. Джерело правди — поле `plugins: string[]` у `.n-rules.json`: явний `[]` вимикає плагіни й автодетект, а відсутнє поле запускає повний `detectPluginsFromRepo`.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Перевіряє наявність файлу-сигналу у корені або підтеках до `maxDepth` рівнів. Використовує BFS-обхід, який пропускає приховані директорії, `node_modules`, та `target`, для забезпечення ефективного виклику на hot-path. Повертає `true` при знаходженні сигналу. Викликає `listScannableSubdirs`.
|
|
20
|
-
|
|
21
|
-
### listScannableSubdirs
|
|
22
|
-
Отримує видимі підтеки для неглибокого скану, ігноруючи приховані та службові директорії. Повертає масив абсолютних шляхів підтеків (порожній, якщо директорія нечитабельна).
|
|
23
|
-
|
|
24
|
-
### readRepositoryUrl
|
|
25
|
-
Отримує `repository.url` з кореневого `package.json` (як рядок або об'єкт). Повертає `null`, якщо URL відсутній або файл нечитабельний.
|
|
26
|
-
|
|
27
|
-
### hasGithubWorkflows
|
|
28
|
-
Перевіряє наявність файлів у директорії `.github/workflows/` будь-якого YAML/YML файлу. Повертає `true` при наявності.
|
|
29
|
-
|
|
30
|
-
### detectCiPlugins
|
|
31
|
-
Автоматично визначає CI-плагіни за станом репозиторію. Пріоритет надається файловим сигналам. Якщо файлових сигналів немає, використовується `repository.url` з кореневого `package.json`. Якщо обидва сигнали присутні, обидва плагіни повертаються. Якщо немає жодного сигналу, повертається порожній масив. Викликає `hasGithubWorkflows` та `readRepositoryUrl`.
|
|
18
|
+
Якщо `plugins` непорожній і містить лише пакети за конвенцією `@7n/rules-<category>-<name>`, автодетект домішує тільки відсутні категорії. Будь-який сторонній пакет у списку вимикає backfill повністю, щоб змішана або користувацька конфігурація залишалась ручною й передбачуваною.
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
Автоматично визначає плагіни за станом репозиторію. Використовує файлові сигнали як пріоритет, з fallback на `repository.url` лише для CI-плагінів, коли файлових сигналів немає. Для Rust використовується перевірка до трьох рівнів підтек. Викликає `detectCiPlugins` та `hasLangSignal`.
|
|
20
|
+
Автодетект спирається на файлові сигнали `.github/workflows/*.yml` і `azure-pipelines.yml`, а за їх відсутності — на `repository.url` з ознаками `github.com` або `dev.azure.com`. Резолв працює fail-safe: недоступні, не встановлені або несумісні плагіни не зривають lint/sync, а пропускаються; для несумісного `requiresPluginApi > PLUGIN_API_VERSION` видається warning, крім режиму `quiet:true`, де пропуск тихий. Результати кешуються в межах прогону.
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
Визначає категорію плагіна на основі naming convention `@7n/rules-<category>-<name>`. Повертає `null` для пакетів, що не відповідають цій конвенції (сторонні/кастомні плагіни) — такий пакет ніколи не з'являється сам через автодетект, а якщо присутній у явному `config.plugins`, вимикає backfill категорій для всього списку.
|
|
38
|
-
|
|
39
|
-
### resolvePluginList
|
|
40
|
-
Обчислює список плагінів проєкту. Використовує явне поле `config.plugins` з `.n-rules.json` або автоматичний автодетект. Явний пустий масив означає "плагіни вимкнено". Якщо `config.plugins` містить сторонній пакет, який не є `@7n/rules-*`, то backfill для заповнення списку вимикається повністю.
|
|
41
|
-
|
|
42
|
-
### computePluginList
|
|
43
|
-
Обчислює список плагінів без кешу. Використовує сире значення `config.plugins` з `.n-rules.json`. Повертає список плагінів. Викликає `detectPluginsFromRepo` та `pluginCategory`.
|
|
44
|
-
|
|
45
|
-
### ensurePluginInstalled
|
|
46
|
-
Гарантує наявність пакета: якщо він відсутній у `node_modules`, виконує `bun add -d <pkg>` для додання його як `devDependency`. Фейл повертає `false` з попередженням.
|
|
47
|
-
|
|
48
|
-
### readPluginManifest
|
|
49
|
-
Отримує нормалізований маніфест плагіна з його `package.json` з блоку `"n-rules"`.
|
|
50
|
-
|
|
51
|
-
### resolvePlugins
|
|
52
|
-
Виконує повне визначення доступних плагінів проєкту з використанням кешу. Дозволяє встановити вже встановлені пакети через `allowInstall: false` (для hot-path хука), ігнорує `bun add`, ігнорує `quiet` для уникнення попереджень при виклику на кожному файлі. Повертає масив доступних плагінів.
|
|
22
|
+
## Поведінка
|
|
53
23
|
|
|
54
|
-
|
|
55
|
-
Отримує шляхи до каталогів для всіх поверхонь ядра. Ядро має пріоритет. Надає масив об'єктів, що містять ім'я плагіна, шлях до його директорії та шлях до його кореня пакета.
|
|
24
|
+
`resolvePluginList` бере список із `.n-rules.json`: відсутнє поле запускає повний автодетект через `detectPluginsFromRepo`, явний порожній список вимикає плагіни, а непорожній список може доповнюватися лише відсутніми категоріями. `pluginCategory` визначає ці категорії; якщо у списку є сторонній пакет, backfill не застосовується.
|
|
56
25
|
|
|
57
|
-
|
|
58
|
-
Отримує набір capability-рядків з усіх доступних плагінів, що використовуються для визначення `requires.capability` у `concern.json`.
|
|
26
|
+
`detectPluginsFromRepo` зіставляє файлові сигнали з `KNOWN_CI_PLUGINS` і `KNOWN_LANG_PLUGINS`. Для CI файлові сигнали мають пріоритет, а fallback за URL з кореневого `package.json` використовується лише коли CI-конфігів немає. Для мов працюють тільки файлові сигнали; під час неглибокого скану службові й приховані каталоги на кшталт `.github`, `.git` і `node_modules` не обходяться.
|
|
59
27
|
|
|
60
|
-
|
|
61
|
-
Агрегує розширення файлів, що використовуються для генерації мапи від розширення до типу-мітка. Повертає мапу `extension -> type-мітка`.
|
|
28
|
+
`resolvePlugins` перетворює обрані імена на доступні плагіни: за потреби делегує встановлення в `ensurePluginInstalled`, читає маніфест, відкидає несумісні плагіни й не зриває виконання через недоступний пакет. `KNOWN_PLUGIN_RANGES` обмежує версії first-party плагінів під час автоматичного встановлення, щоб core і плагіни лишалися сумісними.
|
|
62
29
|
|
|
63
|
-
|
|
64
|
-
Повертає список плагінів, які задекларовані у `config.plugins`, але відсутні у `node_modules`. Нічого не встановлює і нічого не друкує — чистий предикат для explicit CLI-діагностики.
|
|
30
|
+
Результати резолву кешуються в межах процесу, щоб повторні виклики з тих самих даних не дублювали сканування, встановлення та попередження. `clearPluginResolveCache` скидає цей спільний стан для ізольованих перевірок.
|
|
65
31
|
|
|
66
|
-
|
|
67
|
-
Отримує шляхи до модулів-обробників для певного extension-point правила ядра.
|
|
32
|
+
`resolveRulesDirs` використовує `resolvePlugins`, щоб зібрати джерела правил: core іде першим, далі активні плагіни у визначеному порядку. `getActiveCapabilities` агрегує capabilities з маніфестів; ці значення використовуються для гейтів у `concern.json`.
|
|
68
33
|
|
|
69
|
-
|
|
70
|
-
Скидає кеш, призначений для тестів.
|
|
34
|
+
`getDocFilesExtensions` і `getHandlers` читають внесок активних плагінів для документаційних розширень та extension-points. `getUnavailableDeclaredPlugins` окремо перевіряє саме явно задекларовані, але недоступні пакети, щоб CLI міг показати діагностику без автоматичного встановлення.
|
|
71
35
|
|
|
72
36
|
## Публічний API
|
|
73
37
|
|
|
@@ -98,8 +62,17 @@ per-категорійний backfill для всього списку (див.
|
|
|
98
62
|
(через `resolveRulesDirs` тощо) і прямий виклик у sync-CLI інакше дублювали б і файловий
|
|
99
63
|
скан, і warning про backfill.
|
|
100
64
|
ігнорується при cache hit — warning друкується щонайбільше раз на `(root, declared)` за процес
|
|
65
|
+
- KNOWN_PLUGIN_RANGES — Сумісний semver-range для first-party плагінів: обмежує автоматичну інсталяцію (`ensurePluginInstalled`)
|
|
66
|
+
поточною core-сумісною лінією, щоб майбутній несумісний major/minor плагіна не встановився
|
|
67
|
+
мовчки поверх старого core (Фаза 0, spec 2026-07-27-universal-plugin-slots-lang-php-extraction.md
|
|
68
|
+
§10). Для `0.x`-пакетів — caret на поточний minor (`^0.22`, а не голий `^0`, який під caret-
|
|
69
|
+
семантикою розгортається у весь діапазон `0.x`); для `>=1` — caret на поточний major (`^1`).
|
|
70
|
+
Невідомий (сторонній, не з цієї таблиці) пакет інсталюється без обмеження версії — як і раніше.
|
|
101
71
|
- ensurePluginInstalled — Гарантує, що плагін встановлений: якщо `node_modules/<pkg>` нема — `bun add -d <pkg>`
|
|
102
|
-
(дописує devDependency і ставить).
|
|
72
|
+
(дописує devDependency і ставить). Для first-party пакетів з `KNOWN_PLUGIN_RANGES` версія
|
|
73
|
+
обмежується сумісним range (`<pkg>@^<major>` або `@^<major>.<minor>` для `0.x`); сторонні
|
|
74
|
+
пакети встановлюються без обмеження, bun сам резолвить latest. Фейл — warning + false, без
|
|
75
|
+
винятку.
|
|
103
76
|
- resolvePlugins — Повний резолв плагінів проєкту (з кешем на процес).
|
|
104
77
|
лише вже встановлені пакети, без `bun add`; `quiet` — без warning-ів (hook на кожен файл)
|
|
105
78
|
- resolveRulesDirs — Rules-каталоги для всіх поверхонь ядра: ядро першим (його правила/концерни виграють
|
|
@@ -23,10 +23,17 @@
|
|
|
23
23
|
* `{ "capabilities": ["ci:github"], "contributes": { "rules": true, "handlers": { "<point>": "./mod.mjs" } } }`.
|
|
24
24
|
* `capabilities` живлять гейт концернів (`concern.json` → `requires.capability`);
|
|
25
25
|
* `handlers` — іменовані extension-points правил ядра (v1: лише API, споживачі — v2).
|
|
26
|
+
*
|
|
27
|
+
* Сумісність plugin API (Фаза 0, spec 2026-07-27-universal-plugin-slots-lang-php-extraction.md
|
|
28
|
+
* §10): маніфест може декларувати число `requiresPluginApi`. Якщо воно більше за
|
|
29
|
+
* `PLUGIN_API_VERSION` цього core — плагін несумісний і пропускається у `resolvePlugins()` із
|
|
30
|
+
* warning (окрім `quiet:true`, де пропуск тихий). Відсутнє або нечислове поле — сумісний, як і
|
|
31
|
+
* всі чинні на сьогодні маніфести (жоден з них поля ще не декларує).
|
|
26
32
|
*/
|
|
27
33
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
28
34
|
import { spawnSync } from 'node:child_process'
|
|
29
35
|
import { join, resolve } from 'node:path'
|
|
36
|
+
import { PLUGIN_API_VERSION } from './plugin-api.mjs'
|
|
30
37
|
|
|
31
38
|
/** Відомі CI-плагіни для автовизначення: сигнал у дереві репо → npm-пакет. */
|
|
32
39
|
export const KNOWN_CI_PLUGINS = Object.freeze({
|
|
@@ -246,19 +253,41 @@ function computePluginList(root, declared, options) {
|
|
|
246
253
|
return [...names, ...missing]
|
|
247
254
|
}
|
|
248
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Сумісний semver-range для first-party плагінів: обмежує автоматичну інсталяцію (`ensurePluginInstalled`)
|
|
258
|
+
* поточною core-сумісною лінією, щоб майбутній несумісний major/minor плагіна не встановився
|
|
259
|
+
* мовчки поверх старого core (Фаза 0, spec 2026-07-27-universal-plugin-slots-lang-php-extraction.md
|
|
260
|
+
* §10). Для `0.x`-пакетів — caret на поточний minor (`^0.22`, а не голий `^0`, який під caret-
|
|
261
|
+
* семантикою розгортається у весь діапазон `0.x`); для `>=1` — caret на поточний major (`^1`).
|
|
262
|
+
* Невідомий (сторонній, не з цієї таблиці) пакет інсталюється без обмеження версії — як і раніше.
|
|
263
|
+
*/
|
|
264
|
+
export const KNOWN_PLUGIN_RANGES = Object.freeze({
|
|
265
|
+
'@7n/rules-ci-github': '^1',
|
|
266
|
+
'@7n/rules-ci-azure': '^1',
|
|
267
|
+
'@7n/rules-lang-js': '^0.22',
|
|
268
|
+
'@7n/rules-lang-python': '^0.10',
|
|
269
|
+
'@7n/rules-lang-rust': '^0.13'
|
|
270
|
+
})
|
|
271
|
+
|
|
249
272
|
/**
|
|
250
273
|
* Гарантує, що плагін встановлений: якщо `node_modules/<pkg>` нема — `bun add -d <pkg>`
|
|
251
|
-
* (дописує devDependency і ставить).
|
|
274
|
+
* (дописує devDependency і ставить). Для first-party пакетів з `KNOWN_PLUGIN_RANGES` версія
|
|
275
|
+
* обмежується сумісним range (`<pkg>@^<major>` або `@^<major>.<minor>` для `0.x`); сторонні
|
|
276
|
+
* пакети встановлюються без обмеження, bun сам резолвить latest. Фейл — warning + false, без
|
|
277
|
+
* винятку.
|
|
252
278
|
* @param {string} projectRoot корінь репозиторію
|
|
253
279
|
* @param {string} packageName npm-ім'я плагіна
|
|
280
|
+
* @param {typeof import('node:child_process').spawnSync} [spawnFn] інжект для тестів (типово — реальний `spawnSync`)
|
|
254
281
|
* @returns {boolean} true — пакет доступний у node_modules після виклику
|
|
255
282
|
*/
|
|
256
|
-
export function ensurePluginInstalled(projectRoot, packageName) {
|
|
283
|
+
export function ensurePluginInstalled(projectRoot, packageName, spawnFn = spawnSync) {
|
|
257
284
|
const installed = join(projectRoot, 'node_modules', packageName, 'package.json')
|
|
258
285
|
if (existsSync(installed)) return true
|
|
259
286
|
if (!existsSync(join(projectRoot, 'package.json'))) return false
|
|
260
287
|
|
|
261
|
-
const
|
|
288
|
+
const range = KNOWN_PLUGIN_RANGES[packageName]
|
|
289
|
+
const spec = range ? `${packageName}@${range}` : packageName
|
|
290
|
+
const r = spawnFn('bun', ['add', '-d', spec], { cwd: projectRoot, encoding: 'utf8', shell: false })
|
|
262
291
|
if (r.error || r.status !== 0) {
|
|
263
292
|
const reason = r.error ? r.error.message : `bun add exit ${r.status}`
|
|
264
293
|
console.warn(`⚠️ Плагін ${packageName} не встановився (${reason}) — пропускаю\n`)
|
|
@@ -272,22 +301,30 @@ export function ensurePluginInstalled(projectRoot, packageName) {
|
|
|
272
301
|
* @property {string} name npm-ім'я пакета (`@7n/rules` для ядра)
|
|
273
302
|
* @property {string} packageRoot абсолютний корінь пакета
|
|
274
303
|
* @property {string} rulesDir абсолютний шлях до `rules/` пакета
|
|
275
|
-
* @property {{ capabilities: string[], contributes: { rules?: boolean, handlers?: Record<string, string>, docFilesExtensions?: Record<string, string> } }} manifest нормалізований блок `n-rules` з package.json плагіна
|
|
304
|
+
* @property {{ capabilities: string[], requiresPluginApi: number | null, contributes: { rules?: boolean, handlers?: Record<string, string>, docFilesExtensions?: Record<string, string> } }} manifest нормалізований блок `n-rules` з package.json плагіна
|
|
276
305
|
*/
|
|
277
306
|
|
|
278
307
|
/**
|
|
279
308
|
* Маніфест плагіна з блоку `"n-rules"` його package.json (з дефолтами).
|
|
309
|
+
* `requiresPluginApi` — необов'язкове число; нечислове/відсутнє значення нормалізується у
|
|
310
|
+
* `null` (сумісний за замовчуванням — сумісність перевіряє `resolvePlugins()`).
|
|
280
311
|
* @param {string} packageRoot корінь пакета
|
|
281
312
|
* @returns {ResolvedPlugin['manifest']} нормалізований маніфест
|
|
282
313
|
*/
|
|
283
314
|
function readPluginManifest(packageRoot) {
|
|
284
315
|
/** @type {ResolvedPlugin['manifest']} */
|
|
285
|
-
const fallback = {
|
|
316
|
+
const fallback = {
|
|
317
|
+
capabilities: [],
|
|
318
|
+
requiresPluginApi: null,
|
|
319
|
+
contributes: { rules: true, handlers: {}, docFilesExtensions: {} }
|
|
320
|
+
}
|
|
286
321
|
try {
|
|
287
322
|
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'))
|
|
288
323
|
const raw = pkg?.['n-rules']
|
|
289
324
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback
|
|
290
325
|
const capabilities = Array.isArray(raw.capabilities) ? raw.capabilities.filter(c => typeof c === 'string') : []
|
|
326
|
+
const requiresPluginApi =
|
|
327
|
+
typeof raw.requiresPluginApi === 'number' && Number.isFinite(raw.requiresPluginApi) ? raw.requiresPluginApi : null
|
|
291
328
|
const contributes = raw.contributes && typeof raw.contributes === 'object' ? raw.contributes : {}
|
|
292
329
|
const handlers =
|
|
293
330
|
contributes.handlers && typeof contributes.handlers === 'object' && !Array.isArray(contributes.handlers)
|
|
@@ -303,12 +340,38 @@ function readPluginManifest(packageRoot) {
|
|
|
303
340
|
Object.entries(rawDocFiles.extensions).filter(([k, v]) => k.startsWith('.') && typeof v === 'string')
|
|
304
341
|
)
|
|
305
342
|
: {}
|
|
306
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
capabilities,
|
|
345
|
+
requiresPluginApi,
|
|
346
|
+
contributes: { rules: contributes.rules !== false, handlers, docFilesExtensions }
|
|
347
|
+
}
|
|
307
348
|
} catch {
|
|
308
349
|
return fallback
|
|
309
350
|
}
|
|
310
351
|
}
|
|
311
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Чи декларує маніфест plugin API, несумісний із цією core-лінією (Фаза 0 передумова повної
|
|
355
|
+
* slots-міграції: `requiresPluginApi > PLUGIN_API_VERSION`). Друкує warning (окрім
|
|
356
|
+
* `quiet:true`) — виносить умову й побічний ефект з `resolvePlugins()`, щоб не роздувати її
|
|
357
|
+
* cognitive complexity.
|
|
358
|
+
* @param {string} name npm-ім'я плагіна (для повідомлення)
|
|
359
|
+
* @param {ResolvedPlugin['manifest']} manifest нормалізований маніфест плагіна
|
|
360
|
+
* @param {boolean} quiet без warning-у
|
|
361
|
+
* @returns {boolean} true — плагін несумісний, пропускаємо
|
|
362
|
+
*/
|
|
363
|
+
function isIncompatiblePluginApi(name, manifest, quiet) {
|
|
364
|
+
if (manifest.requiresPluginApi === null || manifest.requiresPluginApi <= PLUGIN_API_VERSION) return false
|
|
365
|
+
// Плагін декларує plugin API, несумісний із цією core-лінією — пропускаємо, а не
|
|
366
|
+
// завантажуємо його як rules-only з мовчазною втратою handlers/doc-files/fragments.
|
|
367
|
+
if (!quiet) {
|
|
368
|
+
console.warn(
|
|
369
|
+
`⚠️ Плагін ${name} потребує plugin API v${manifest.requiresPluginApi}, ця core-лінія підтримує v${PLUGIN_API_VERSION} — пропускаю, онови @7n/rules\n`
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
return true
|
|
373
|
+
}
|
|
374
|
+
|
|
312
375
|
/**
|
|
313
376
|
* Повний резолв плагінів проєкту (з кешем на процес).
|
|
314
377
|
* @param {string} projectRoot корінь репозиторію
|
|
@@ -338,6 +401,7 @@ export function resolvePlugins(projectRoot, config, options = {}) {
|
|
|
338
401
|
continue
|
|
339
402
|
}
|
|
340
403
|
const manifest = readPluginManifest(packageRoot)
|
|
404
|
+
if (isIncompatiblePluginApi(name, manifest, options.quiet === true)) continue
|
|
341
405
|
const rulesDir = join(packageRoot, 'rules')
|
|
342
406
|
if (manifest.contributes.rules && !existsSync(rulesDir)) {
|
|
343
407
|
// Плагін ДЕКЛАРУЄ правила (rules !== false), але каталогу нема — битий пакет.
|