@7n/rules 1.49.25 → 1.50.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 +16 -0
- package/package.json +1 -1
- package/rules/doc-files/check/docs/index.md +10 -0
- package/rules/doc-files/check/docs/main.md +4 -2
- package/rules/doc-files/check/main.mjs +12 -4
- package/rules/doc-files/docgen-crc/docs/index.md +9 -0
- package/rules/doc-files/docgen-crc/docs/main.md +47 -26
- package/rules/doc-files/docgen-crc/main.mjs +21 -4
- package/rules/doc-files/docgen-files-batch/docs/main.md +20 -14
- package/rules/doc-files/docgen-files-batch/main.mjs +54 -20
- package/rules/doc-files/docgen-gen/docs/main.md +35 -3
- package/rules/doc-files/docgen-gen/main.mjs +253 -19
- package/rules/doc-files/docgen-prompts/docs/main.md +18 -12
- package/rules/doc-files/docgen-prompts/main.mjs +19 -12
- package/rules/doc-files/docgen-scan/docs/main.md +44 -32
- package/rules/doc-files/docgen-scan/main.mjs +6 -3
- package/rules/doc-files/docgen-test-context/docs/index.md +9 -0
- package/rules/doc-files/docgen-test-context/docs/main.md +57 -0
- package/rules/doc-files/docgen-test-context/main.mjs +212 -0
- package/rules/doc-files/main.mdc +37 -6
- package/rules/k8s/manifests/main.mjs +94 -38
- package/scripts/lib/docs/resolve-plugins.md +26 -53
- package/scripts/lib/lint-surface/lint-lock.mjs +81 -16
- package/scripts/lib/lint-surface/progress.mjs +15 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +9 -1
- package/scripts/lib/lint-surface/types.mjs +2 -0
- package/scripts/lib/resolve-plugins.mjs +70 -6
- package/skills/doc-files/SKILL.md +21 -6
|
@@ -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), але каталогу нема — битий пакет.
|
|
@@ -13,9 +13,10 @@ version: '1.0'
|
|
|
13
13
|
у теці `docs/` **поряд із самим файлом** (`<dir>/docs/<stem>.md`). Це **обовʼязковий крок кожної
|
|
14
14
|
задачі** — як `lint`: після зміни коду його дока має бути перегенерована.
|
|
15
15
|
|
|
16
|
-
Застарілість визначається **детерміновано за CRC**: кожна дока несе у frontmatter
|
|
17
|
-
суму
|
|
18
|
-
|
|
16
|
+
Застарілість визначається **детерміновано за CRC evidence**: кожна дока несе у frontmatter
|
|
17
|
+
контрольну суму source + повʼязаних test/spec-файлів на момент генерації. Без повʼязаних
|
|
18
|
+
тестів це звичайний CRC source. Дока **застаріла**, якщо її немає або поточний evidence CRC
|
|
19
|
+
не збігається з `crc` у frontmatter.
|
|
19
20
|
|
|
20
21
|
```markdown
|
|
21
22
|
---
|
|
@@ -60,6 +61,18 @@ npx @7n/rules lint doc-files
|
|
|
60
61
|
(`stale`) → генерує локальною моделлю → пише доку зі **свіжим CRC** (і degraded-маркером,
|
|
61
62
|
якщо не дотягнула) → друкує прогрес і підсумок.
|
|
62
63
|
|
|
64
|
+
Повʼязані тести визначаються за relative reference, який резолвиться у source (`import`,
|
|
65
|
+
`require`, dynamic import, `vi.mock` тощо), плюс naming/layout evidence (`foo.test` → `foo`
|
|
66
|
+
або `module/tests/*` → module `main`/`index`). Shared test helpers, які тест лише імпортує,
|
|
67
|
+
не стають evidence. JS детерміновано рендерить один компактний рядок на test-файл: до двох
|
|
68
|
+
груп, пʼять дослівних прикладів і точний лічильник решти; test-код і сценарії не потрапляють до
|
|
69
|
+
моделі. Зміна такого тесту робить source-доку stale.
|
|
70
|
+
|
|
71
|
+
Авторські коментарі теж є першоджерелом: для JSDoc, rustdoc і Python docstring-ів JS дослівно
|
|
72
|
+
збирає «Огляд» і «Публічний API». Детальний наратив дає `comment-only` (0 LLM); короткий
|
|
73
|
+
pointer або складний flow — `comment+behavior`, де LLM дописує тільки «Поведінку», а judge
|
|
74
|
+
перевіряє лише її. За неповних коментарів лишається звичайний `fallback`.
|
|
75
|
+
|
|
63
76
|
### Крок 2: Підтвердження
|
|
64
77
|
|
|
65
78
|
Дочекайся підсумку `✓ OK: <N> ⚠ degraded: <D> ✗ Err: <E>`. Якщо є помилки — перелічи
|
|
@@ -80,7 +93,9 @@ preflight). Degraded — не помилка: дока існує, CRC свіж
|
|
|
80
93
|
## Нотатки
|
|
81
94
|
|
|
82
95
|
- Не комітити автоматично — користувач вирішує, коли комітити згенеровану доку.
|
|
83
|
-
- Scanner
|
|
84
|
-
|
|
85
|
-
|
|
96
|
+
- Scanner не створює окремих док для `*.test.*` / `*.spec.*`, але використовує повʼязані
|
|
97
|
+
тести як evidence для source-доки. Він ігнорує `node_modules`, `dist`, `.git`,
|
|
98
|
+
`__pycache__`, `coverage`, `.cursor`, `.claude`, усі теки `docs/` і `*.d.ts`.
|
|
99
|
+
Кореневий repo `docs/` — system-wide only: file-level docs туди не пишуться. Список
|
|
100
|
+
glob-ів — `docgen-ignore.mjs`.
|
|
86
101
|
- Агрегуюча документація (module-summary, доменні доки) — окремий скіл `doc-aggregate`, за запитом.
|