@7n/rules 1.86.1 → 1.87.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 +22 -3
- package/package.json +5 -5
- package/rules/k8s/manifests/main.mjs +29 -6
- package/rules/security/tracked_symlink/main.mjs +5 -1
- package/scripts/lib/lint-surface/wasm-plugins.mjs +43 -8
- package/skills/wasm-plugin/SKILL.md +4 -2
- package/wasm-plugins/builtin-pins.json +0 -6
- package/wasm-plugins/plugin-lang-js.wasm +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.
|
|
3
|
+
## [1.87.1] - 2026-08-05
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- docs(plans): реєстр — перенесення зі зрізу 6 і відновлення втраченого (#404)
|
|
8
|
+
|
|
9
|
+
## [1.87.0] - 2026-08-05
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Ще два концерни переїхали з JS у wasm-компонент `lang-js/wasm-concerns` на
|
|
14
|
+
поверхні `exec-tool` (зріз 6 контракту v3.1): `style/lint` (обгортка
|
|
15
|
+
`stylelint`) і `js/jscpd_duplicates` (обгортка `jscpd`, перший реальний
|
|
16
|
+
споживач `scratch-out` — JSON-звіт тула хост забирає з тимчасового каталогу
|
|
17
|
+
обміну, дерево репо не мутується).
|
|
18
|
+
|
|
19
|
+
Резолв тулів у `manifest.tools` навчився третьої схеми — `npm:<name>`:
|
|
20
|
+
бінарник шукається в `node_modules/.bin` консюмер-репо, з фолбеком на PATH.
|
|
21
|
+
Це та сама послідовність, якою `style/lint` резолвив `stylelint` у JS.
|
|
22
|
+
|
|
23
|
+
T0-фікси обох концернів лишаються JS.
|
|
24
|
+
- `k8s/manifests` — другий зріз порту в `rules-core`. Портовано ще два з чотирьох шарів `lint()`: per-file цикл `checkK8sYamlFile` (modeline `# yaml-language-server: $schema=…`, звірка URL схеми з `apiVersion`/`kind`, виняток `HttpBackendGroup` Yandex ALB) і дві великі самодостатні `validate*` — `validateDeploymentHpaPdbAndTopology` (канон `topologySpreadConstraints`, заборона локальних `hpa.yaml`/`pdb.yaml` у `k8s/…/base/`, повний контракт sibling-каталогу `components/`) та `validateNetworkPoliciesForK8sWorkloads`. Концерн у `NATIVE_CONCERNS` як і раніше **не** заведено — він неподільний для диспатчу і зайде туди разом із двома шарами, що лишились (kubescape-контур і kustomize-резолюція з пʼятьма залежними `validate*`), тож поведінка лінту незмінна.
|
|
6
25
|
|
|
7
|
-
|
|
26
|
+
Полагоджено латентний дефект канону: `readDocsByKindInDir` обходив каталог у порядку `readdir`, тобто в порядку файлової системи (APFS впорядковує, ext4 віддає hash-порядок). Обхід став відсортованим. Спостережуваної зміни немає — обидва виклики передають фільтр за іменем файла, тож збігається щонайбільше один файл; сортування прибирає ту саму міну, на якій уже спіткнулись `k8s/hasura_configmap` і звірка імені ConfigMap.
|
|
8
27
|
|
|
9
28
|
## [1.86.0] - 2026-08-05
|
|
10
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7n/rules",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.87.1",
|
|
4
4
|
"description": "CLI еталонних правил і skills (префікс n-): синк у репозиторій, дельта-lint, конформність",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"rename-yaml-extensions": "bun ./bin/n-rules.js rename-yaml-extensions"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"@7n/llm-lib": "3.0.
|
|
69
|
+
"@7n/llm-lib": "3.0.1",
|
|
70
70
|
"ajv": "^8.20.0",
|
|
71
71
|
"cli-progress": "^3.12.0",
|
|
72
72
|
"github-actionlint": "^1.7.12",
|
|
@@ -83,9 +83,9 @@
|
|
|
83
83
|
"zod": "^4.4.3"
|
|
84
84
|
},
|
|
85
85
|
"optionalDependencies": {
|
|
86
|
-
"@7n/rules-darwin-arm64": "1.
|
|
87
|
-
"@7n/rules-linux-x64": "1.
|
|
88
|
-
"@7n/rules-win32-x64": "1.
|
|
86
|
+
"@7n/rules-darwin-arm64": "1.87.0",
|
|
87
|
+
"@7n/rules-linux-x64": "1.87.0",
|
|
88
|
+
"@7n/rules-win32-x64": "1.87.0",
|
|
89
89
|
"@earendil-works/pi-ai": "0.80.10",
|
|
90
90
|
"@earendil-works/pi-coding-agent": "0.80.10"
|
|
91
91
|
},
|
|
@@ -3294,13 +3294,18 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
|
|
|
3294
3294
|
|
|
3295
3295
|
/**
|
|
3296
3296
|
* Перевіряє один YAML у дереві k8s (modeline, схема).
|
|
3297
|
+
*
|
|
3298
|
+
* Експортовано заради cross-language parity-гейта
|
|
3299
|
+
* (`crates/rules-core/tests/k8s_manifests_slice2_parity.rs`): концерн ще не в
|
|
3300
|
+
* `NATIVE_CONCERNS`, тож native-бік звіряється з каноном напряму, а не через
|
|
3301
|
+
* `runConcernDetector`.
|
|
3297
3302
|
* @param {string} abs абсолютний шлях до файлу
|
|
3298
3303
|
* @param {string} root корінь репозиторію
|
|
3299
3304
|
* @param {(msg: string) => void} fail реєстрація помилки
|
|
3300
3305
|
* @param {(msg: string) => void} pass реєстрація успіху
|
|
3301
3306
|
* @returns {Promise<void>} результат
|
|
3302
3307
|
*/
|
|
3303
|
-
async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
3308
|
+
export async function checkK8sYamlFile(abs, root, fail, pass) {
|
|
3304
3309
|
const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
|
|
3305
3310
|
const base = basename(abs)
|
|
3306
3311
|
const baseLower = base.toLowerCase()
|
|
@@ -4189,18 +4194,26 @@ function matchesYamlFilter(entry, filenameFilter) {
|
|
|
4189
4194
|
}
|
|
4190
4195
|
|
|
4191
4196
|
/**
|
|
4192
|
-
* Збирає всі документи з **k8s**-yaml за заданим `kind` у
|
|
4197
|
+
* Збирає всі документи з **k8s**-yaml за заданим `kind` у каталозі,
|
|
4198
|
+
* у **відсортованому** порядку імен файлів.
|
|
4199
|
+
*
|
|
4200
|
+
* Сортування обов'язкове з тієї ж причини, що й у `collectDeploymentDocsInDir`:
|
|
4201
|
+
* без нього порядок документів (а отже і `.find(…)` за ним) залежав би від
|
|
4202
|
+
* порядку `readdir`, тобто від файлової системи — APFS впорядковує, ext4 віддає
|
|
4203
|
+
* hash-порядок. Зараз обидва виклики передають `filenameFilter`, тож збіг
|
|
4204
|
+
* щонайбільше один і різниці не видно; сортування прибирає міну на майбутнє.
|
|
4193
4205
|
* @param {string} dirPath абсолютний шлях до каталогу
|
|
4194
4206
|
* @param {string} kind очікуваний `kind` (наприклад, `HorizontalPodAutoscaler`)
|
|
4195
4207
|
* @param {string} [filenameFilter] фільтр за basename (наприклад, `hpa.yaml`); якщо заданий — лише цей файл
|
|
4196
|
-
* @returns {Promise<Record<string, unknown>[]>} список знайдених документів
|
|
4208
|
+
* @returns {Promise<Record<string, unknown>[]>} список знайдених документів у детермінованому порядку
|
|
4197
4209
|
*/
|
|
4198
4210
|
async function readDocsByKindInDir(dirPath, kind, filenameFilter) {
|
|
4199
4211
|
/**
|
|
4200
4212
|
@type {Record<string, unknown>[]}
|
|
4201
4213
|
*/
|
|
4202
4214
|
const out = []
|
|
4203
|
-
const
|
|
4215
|
+
const dirEntries = await tryReaddir(dirPath)
|
|
4216
|
+
const entries = dirEntries.toSorted()
|
|
4204
4217
|
for (const entry of entries) {
|
|
4205
4218
|
if (!matchesYamlFilter(entry, filenameFilter)) {
|
|
4206
4219
|
continue
|
|
@@ -5414,12 +5427,17 @@ async function collectNetworkPolicyWorkloadsByDir(yamlFilesAbs) {
|
|
|
5414
5427
|
* наявність і канон sibling каталогу **`components/`** (Kustomize Component) з `hpa.yaml` і `pdb.yaml` через
|
|
5415
5428
|
* `validateComponentsForBaseDeployment`. У не-base шарах — звична схема (`hpa.yaml` / `pdb.yaml` поруч).
|
|
5416
5429
|
* Env-залежні межі — за сегментом після `/k8s/`: dev-like vs прод.
|
|
5430
|
+
*
|
|
5431
|
+
* Експортовано заради cross-language parity-гейта
|
|
5432
|
+
* (`crates/rules-core/tests/k8s_manifests_slice2_parity.rs`): концерн ще не в
|
|
5433
|
+
* `NATIVE_CONCERNS`, тож native-бік звіряється з каноном напряму, а не через
|
|
5434
|
+
* `runConcernDetector`.
|
|
5417
5435
|
* @param {string} root корінь репозиторію
|
|
5418
5436
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
5419
5437
|
* @param {(msg: string) => void} fail callback при помилці
|
|
5420
5438
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
5421
5439
|
*/
|
|
5422
|
-
async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, passFn) {
|
|
5440
|
+
export async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, passFn) {
|
|
5423
5441
|
const rootNorm = resolve(root)
|
|
5424
5442
|
/**
|
|
5425
5443
|
@type {Map<string, Record<string, unknown>[]>}
|
|
@@ -5446,13 +5464,18 @@ async function validateDeploymentHpaPdbAndTopology(root, yamlFilesAbs, fail, pas
|
|
|
5446
5464
|
/**
|
|
5447
5465
|
* Перевіряє NetworkPolicy для **Deployment**, **StatefulSet**, **DaemonSet**, **Job**, **CronJob**
|
|
5448
5466
|
* під `k8s` — у `networkpolicy.yaml` поруч з workload-маніфестом (у base, у не-base — як overlay-specific override).
|
|
5467
|
+
*
|
|
5468
|
+
* Експортовано заради cross-language parity-гейта
|
|
5469
|
+
* (`crates/rules-core/tests/k8s_manifests_slice2_parity.rs`): концерн ще не в
|
|
5470
|
+
* `NATIVE_CONCERNS`, тож native-бік звіряється з каноном напряму, а не через
|
|
5471
|
+
* `runConcernDetector`.
|
|
5449
5472
|
* @param {string} root корінь репозиторію
|
|
5450
5473
|
* @param {string[]} yamlFilesAbs yaml під k8s
|
|
5451
5474
|
* @param {(msg: string) => void} fail callback при помилці
|
|
5452
5475
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
5453
5476
|
* @returns {Promise<void>} результат
|
|
5454
5477
|
*/
|
|
5455
|
-
async function validateNetworkPoliciesForK8sWorkloads(root, yamlFilesAbs, fail, passFn) {
|
|
5478
|
+
export async function validateNetworkPoliciesForK8sWorkloads(root, yamlFilesAbs, fail, passFn) {
|
|
5456
5479
|
const rootNorm = resolve(root)
|
|
5457
5480
|
const workloadsByDir = await collectNetworkPolicyWorkloadsByDir(yamlFilesAbs)
|
|
5458
5481
|
for (const [dir, workloads] of workloadsByDir) {
|
|
@@ -108,7 +108,11 @@ export async function lint(ctx) {
|
|
|
108
108
|
// послідовний cat-file на запис дешевший за розбір бінарного `cat-file --batch`.
|
|
109
109
|
const blob = await spawnAsync(git, ['cat-file', 'blob', entry.sha], { cwd, signal: ctx.signal })
|
|
110
110
|
if (blob.exitCode !== 0) continue
|
|
111
|
-
|
|
111
|
+
// Хвостові переводи рядка зрізаємо циклом, а не регексом: `/\n+$/`
|
|
112
|
+
// позначений як super-linear (backtracking), а `trimEnd()` зʼїв би ще й
|
|
113
|
+
// пробіли — а вони в цілі симлінка значущі.
|
|
114
|
+
let target = blob.stdout
|
|
115
|
+
while (target.endsWith('\n')) target = target.slice(0, -1)
|
|
112
116
|
if (target.length === 0) continue
|
|
113
117
|
|
|
114
118
|
const absolute = isAbsoluteTarget(target)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* **Run-tool контур (задача N1, рішення Д спеки)**: `manifest.tools` —
|
|
10
10
|
* задекларовані зовнішні tool-залежності плагіна (напр. `"shellcheck@^0.9"`,
|
|
11
|
-
* `"path:bun"` — схеми резолву, рішення В спеки
|
|
11
|
+
* `"path:bun"`, `"npm:stylelint"` — схеми резолву, рішення В спеки
|
|
12
12
|
* `docs/specs/2026-08-01-plugin-contract-v31-surfaces.md`, [`parseToolRef`]).
|
|
13
13
|
* Для кожного запису резолвер кличе ensure-tool контур (`ensureToolAsync`,
|
|
14
14
|
* `../ensure-tool.mjs`, injectable через `opts.ensureToolFn`) — будує мапу
|
|
@@ -444,7 +444,16 @@ const TOOL_SCHEMES = {
|
|
|
444
444
|
/** Github-реліз із закріпленою версією (`TOOLS` + `tool-pins.json`) — дефолт за відсутності схеми. */
|
|
445
445
|
PINNED: 'pinned',
|
|
446
446
|
/** Резолв по `PATH` (`bun`, `bunx` — ensure-tool не вміє і не має їх завантажувати). */
|
|
447
|
-
PATH: 'path'
|
|
447
|
+
PATH: 'path',
|
|
448
|
+
/**
|
|
449
|
+
* `<cwd>/node_modules/.bin/<name>`, фолбек `PATH` (зріз 6 контракту v3.1):
|
|
450
|
+
* `stylelint` — задекларована залежність npm-пакета плагіна
|
|
451
|
+
* (`@7n/rules-lang-js`), тобто вже лежить у дереві консюмера після
|
|
452
|
+
* `npm install`; тягнути його github-релізом було б і зайво, і неможливо.
|
|
453
|
+
* Порядок резолву дослівно повторює `resolveStylelint` JS-канону
|
|
454
|
+
* `style/lint` — саме тому схема окрема, а не «`path:` з фолбеком».
|
|
455
|
+
*/
|
|
456
|
+
NPM: 'npm'
|
|
448
457
|
}
|
|
449
458
|
|
|
450
459
|
/**
|
|
@@ -465,7 +474,7 @@ function parseToolRef(declared) {
|
|
|
465
474
|
if (separator === -1) return { scheme: TOOL_SCHEMES.PINNED, name: declared.split('@', 1)[0] }
|
|
466
475
|
const prefix = declared.slice(0, separator)
|
|
467
476
|
const rest = declared.slice(separator + 1)
|
|
468
|
-
if (
|
|
477
|
+
if (!Object.values(TOOL_SCHEMES).includes(prefix)) return { scheme: null, name: declared }
|
|
469
478
|
return { scheme: prefix, name: rest.split('@', 1)[0] }
|
|
470
479
|
}
|
|
471
480
|
|
|
@@ -482,17 +491,22 @@ function parseToolRef(declared) {
|
|
|
482
491
|
* @param {string} pluginName ім'я плагіна (лише для diagnostics-повідомлень)
|
|
483
492
|
* @param {string[]} declaredTools `manifest.tools` — рядки виду `"shellcheck@^0.9"`
|
|
484
493
|
* @param {(toolId: string) => Promise<string>} ensureToolFn ін'єкція `ensureToolAsync` (тести підміняють)
|
|
485
|
-
* @param {(cmd: string) => string | null} resolveCmdFn ін'єкція `resolveCmd` для
|
|
494
|
+
* @param {(cmd: string) => string | null} resolveCmdFn ін'єкція `resolveCmd` для схем `path:`/`npm:` (тести підміняють — інакше результат залежав би від PATH машини)
|
|
495
|
+
* @param {string} cwd абсолютний корінь consumer-репо — база `node_modules/.bin` для схеми `npm:`
|
|
486
496
|
* @returns {Promise<Record<string, string>>} ім'я тула (без semver-суфікса) → абсолютний шлях
|
|
487
497
|
*/
|
|
488
|
-
async function ensureDeclaredTools(pluginName, declaredTools, ensureToolFn, resolveCmdFn) {
|
|
498
|
+
async function ensureDeclaredTools(pluginName, declaredTools, ensureToolFn, resolveCmdFn, cwd) {
|
|
489
499
|
/** @type {Record<string, string>} */
|
|
490
500
|
const toolPaths = {}
|
|
491
501
|
for (const declared of declaredTools) {
|
|
492
502
|
const { scheme, name } = parseToolRef(declared)
|
|
493
503
|
try {
|
|
494
504
|
if (scheme === null)
|
|
495
|
-
throw new Error(
|
|
505
|
+
throw new Error(
|
|
506
|
+
`невідома схема резолву (відомі ${Object.values(TOOL_SCHEMES)
|
|
507
|
+
.map(s => `"${s}:"`)
|
|
508
|
+
.join(', ')})`
|
|
509
|
+
)
|
|
496
510
|
if (scheme === TOOL_SCHEMES.PATH) {
|
|
497
511
|
// `path:` НЕ йде в ensure-tool контур узагалі: той уміє лише
|
|
498
512
|
// github-релізи, а `bun`/`bunx` встановлює користувач. Відсутність —
|
|
@@ -502,6 +516,21 @@ async function ensureDeclaredTools(pluginName, declaredTools, ensureToolFn, reso
|
|
|
502
516
|
toolPaths[name] = fromPath
|
|
503
517
|
continue
|
|
504
518
|
}
|
|
519
|
+
if (scheme === TOOL_SCHEMES.NPM) {
|
|
520
|
+
// `npm:` — локальний `.bin` консюмера, потім PATH: дослівно
|
|
521
|
+
// `resolveStylelint` JS-канону `style/lint`. Так само повз
|
|
522
|
+
// ensure-tool контур (npm-залежність ставить `npm install`, не
|
|
523
|
+
// github-реліз).
|
|
524
|
+
const local = join(cwd, 'node_modules', '.bin', name)
|
|
525
|
+
if (existsSync(local)) {
|
|
526
|
+
toolPaths[name] = local
|
|
527
|
+
continue
|
|
528
|
+
}
|
|
529
|
+
const fromPath = resolveCmdFn(name)
|
|
530
|
+
if (!fromPath) throw new Error('немає ні в node_modules/.bin, ні в PATH')
|
|
531
|
+
toolPaths[name] = fromPath
|
|
532
|
+
continue
|
|
533
|
+
}
|
|
505
534
|
toolPaths[name] = await ensureToolFn(name)
|
|
506
535
|
} catch (error) {
|
|
507
536
|
console.warn(
|
|
@@ -538,7 +567,13 @@ async function buildWasmConcernMap(cwd, ctx) {
|
|
|
538
567
|
console.warn(`⚠️ wasm-плагін "${entry.name}" пропущено: не вдалось завантажити (${error.message})`)
|
|
539
568
|
continue
|
|
540
569
|
}
|
|
541
|
-
const toolPaths = await ensureDeclaredTools(
|
|
570
|
+
const toolPaths = await ensureDeclaredTools(
|
|
571
|
+
entry.name,
|
|
572
|
+
manifest.tools ?? [],
|
|
573
|
+
ctx.ensureToolFn,
|
|
574
|
+
ctx.resolveCmdFn,
|
|
575
|
+
cwd
|
|
576
|
+
)
|
|
542
577
|
// `manifest.concerns` — масив структурованих контрибуцій `{ key, scope, glob }`
|
|
543
578
|
// (задача N2, передумова full-scope мосту, доккомент `wit/world.wit`
|
|
544
579
|
// `record concern-contribution`), не голі рядки — мапа концернів індексується
|
|
@@ -562,7 +597,7 @@ async function buildWasmConcernMap(cwd, ctx) {
|
|
|
562
597
|
* @param {{fetchFn?: typeof fetch, cacheDir?: string, env?: Record<string, string | undefined>, ensureToolFn?: (toolId: string) => Promise<string>, resolveCmdFn?: (cmd: string) => string | null, nativeFn?: typeof loadNative, builtinPinsDir?: string}} [opts] ін'єкції для тестів:
|
|
563
598
|
* `fetchFn` (дефолт — глобальний `fetch`), `cacheDir` (дефолт — `resolvePluginCacheDir`), `env` (дефолт — `process.env`),
|
|
564
599
|
* `ensureToolFn` (дефолт — `ensureToolAsync`), `resolveCmdFn` (дефолт — `resolveCmd`; тести підміняють, бо інакше
|
|
565
|
-
* `path
|
|
600
|
+
* схеми `path:`/`npm:` давали б різний `toolPaths` залежно від PATH машини), `nativeFn` (дефолт — `loadNative`, wiring-тести підміняють фейковим addon-ом),
|
|
566
601
|
* `builtinPinsDir` (дефолт — [`WASM_PLUGINS_DIR`], реальна `npm/wasm-plugins/`; тести ізолюють неіснуючим каталогом,
|
|
567
602
|
* щоб локальна wasm-збірка в робочому дереві не підмішувала builtin-контрибуції в контрольовані сценарії)
|
|
568
603
|
* @returns {Promise<Map<string, WasmConcernMapEntry>>} ключ концерну (`ruleId/concernId`) → `{ wasmPath, toolPaths }`
|
|
@@ -465,8 +465,10 @@ per-file: пиши `detect_whole_batch(files: &[SourceFile]) -> Vec<Diagnostic>`
|
|
|
465
465
|
нічого не добуває — провізіонінг живе в окремій команді `tools ensure`.
|
|
466
466
|
|
|
467
467
|
**Схеми `manifest.tools`** (рішення В): `pinned:` (дефолт за відсутності
|
|
468
|
-
схеми — github-реліз із закріпленою версією)
|
|
469
|
-
`path:bun`, `path:bunx` — те, що ensure-tool завантажити не може й не має)
|
|
468
|
+
схеми — github-реліз із закріпленою версією), `path:` (резолв по `PATH`:
|
|
469
|
+
`path:bun`, `path:bunx` — те, що ensure-tool завантажити не може й не має)
|
|
470
|
+
і `npm:` (`<cwd>/node_modules/.bin/<name>`, фолбек `PATH` —
|
|
471
|
+
`npm:stylelint`, тобто задекларована залежність npm-пакета плагіна).
|
|
470
472
|
|
|
471
473
|
> **Trust boundary — прочитай ДО того, як напишеш перший `exec-tool`.**
|
|
472
474
|
> Спавнений тул виконується **з правами хоста, ПОЗА wasm-пісочницею**, і
|
|
Binary file
|