@7n/rules 1.36.1 → 1.38.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/k8s/hasura_configmap/docs/index.md +9 -0
- package/rules/k8s/hasura_configmap/docs/main.md +31 -0
- package/rules/k8s/hasura_configmap/main.mjs +35 -0
- package/rules/k8s/hasura_httproute/docs/index.md +9 -0
- package/rules/k8s/hasura_httproute/docs/main.md +32 -0
- package/rules/k8s/hasura_httproute/main.mjs +35 -0
- package/rules/k8s/kubeconform/kubeconform.mdc +8 -0
- package/rules/k8s/manifests/main.mjs +151 -26
- package/rules/tauri/core_test_isolation/core_test_isolation.mdc +21 -7
- package/rules/tauri/core_test_isolation/docs/main.md +1 -1
- package/rules/tauri/core_test_isolation/main.mjs +12 -27
- package/rules/tauri/gitignore_target/docs/main.md +1 -2
- package/rules/tauri/gitignore_target/gitignore_target.mdc +25 -6
- package/rules/tauri/gitignore_target/main.mjs +28 -13
- package/scripts/utils/cargo-workspace.mjs +99 -0
- package/scripts/utils/docs/cargo-workspace.md +40 -0
- package/scripts/utils/docs/index.md +1 -0
- package/skills/storybook/SKILL.md +96 -0
- package/skills/storybook/main.json +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.38.0] - 2026-07-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- tauri/gitignore_target, tauri/core_test_isolation: інваріант workspace-кореня замість хибної typo-версії
|
|
8
|
+
|
|
9
|
+
## [1.37.0] - 2026-07-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- skills/storybook: скіл `n-storybook` — тонка обгортка запуску канону Storybook (`@7n/rules-lang-js`): звичайний прогін `lint storybook` і `--adopt`-режим для rollout-у на пакетах з уже наявним ручним Storybook (ADR канон-storybook-для-vue-компонентних-бібліотек, Кластер 8)
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- k8s: hasura_configmap/hasura_httproute тепер gated власним main.mjs (без false positive на ConfigMap/HTTPRoute без сусіднього Hasura Deployment); kubescape більше не падає fatal на Job/CronJob через auto-generated per-resource виняток C-0056/C-0018
|
|
18
|
+
|
|
3
19
|
## [1.36.1] - 2026-07-20
|
|
4
20
|
|
|
5
21
|
### Changed
|
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: npm/rules/k8s/hasura_configmap/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: ec4396c4
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
score: 90
|
|
9
|
+
issues: internal-name:validateHasuraConfigMapRemoteSchemaPermissions,judge:inaccurate:0.99
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Gated detector для поверхні `k8s/hasura_configmap`, який захищає `hasura_configmap.rego` від промоції в ungated standalone detector через generic lint-surface з `hasHandWrittenMain` у `scripts/lib/lint-surface/detect.mjs`. Саме `k8s/manifests/main.mjs` через `findDeploymentDocInDir` і `isHasuraDeploymentManifest` робить cross-file JS-гейт і лише тоді викликає `validateHasuraConfigMapRemoteSchemaPermissions`, тож перевірка охоплює тільки ті `ConfigMap`, для яких поруч є Hasura Deployment. Це потрібно, щоб не проганяти Hasura-специфічний `rego` на звичайні `ConfigMap` у `k8s` і не ловити false positive на CronJob/Job `ConfigMap` поза Hasura-манифестами, зокрема в контексті issue `efes-cloud/backend`.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
1. `lint` запускає перевірку поверхні `k8s/hasura_configmap` у режимі read-only та збирає підсумок через reporter.
|
|
20
|
+
2. Вона бере корінь workspace, враховує `cursor`-ігнори й шукає YAML-файли лише в межах `k8s`.
|
|
21
|
+
3. Якщо в `k8s` немає YAML-файлів, перевірку пропускає і повертає успішний результат без порушень.
|
|
22
|
+
4. Якщо YAML-файли знайдені, перевіряє лише ті ConfigMap, для яких поруч є Hasura Deployment; звичайні ConfigMap без такого сусідства не ескалюються в цю перевірку.
|
|
23
|
+
5. У разі виявлення невідповідностей фіксує порушення, а за відсутності проблем — підтверджує успішний стан.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- lint — Detector k8s/hasura_configmap (read-only).
|
|
28
|
+
|
|
29
|
+
## Гарантії поведінки
|
|
30
|
+
|
|
31
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint-поверхня k8s/hasura_configmap: gated detector. Без власного `main.mjs` generic
|
|
3
|
+
* lint-surface (`hasHandWrittenMain` у `scripts/lib/lint-surface/detect.mjs`) промотує
|
|
4
|
+
* будь-який concern із резолвним `policy.files.walkGlob` у **ungated** standalone detector —
|
|
5
|
+
* rego `hasura_configmap.rego` прогнав би тоді напряму на всі `configmap.yaml` під k8s,
|
|
6
|
+
* без перевірки, чи є поруч Hasura Deployment (false positive на звичайних CronJob/Job
|
|
7
|
+
* ConfigMap, issue: efes-cloud/backend). Cross-file JS-гейт (`findDeploymentDocInDir` +
|
|
8
|
+
* `isHasuraDeploymentManifest`) і сам виклик rego живуть у `k8s/manifests/main.mjs`
|
|
9
|
+
* (`validateHasuraConfigMapRemoteSchemaPermissions`, export) — тут лише тонка обгортка.
|
|
10
|
+
*/
|
|
11
|
+
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
12
|
+
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
13
|
+
import { findK8sYamlFiles, validateHasuraConfigMapRemoteSchemaPermissions } from '../manifests/main.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detector k8s/hasura_configmap (read-only).
|
|
17
|
+
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
|
|
18
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
|
|
19
|
+
*/
|
|
20
|
+
export async function lint(ctx) {
|
|
21
|
+
const reporter = createViolationReporter(ctx)
|
|
22
|
+
const { pass, fail } = reporter
|
|
23
|
+
const root = ctx.cwd
|
|
24
|
+
|
|
25
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
26
|
+
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
27
|
+
if (yamlFiles.length === 0) {
|
|
28
|
+
pass('Немає *.yaml під k8s — перевірку hasura_configmap пропущено')
|
|
29
|
+
return reporter.result()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFiles, fail, pass)
|
|
33
|
+
|
|
34
|
+
return reporter.result()
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: main.mjs
|
|
4
|
+
resource: npm/rules/k8s/hasura_httproute/main.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: 2f28a731
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
score: 90
|
|
9
|
+
issues: internal-name:validateHasuraHttpRouteCanon,judge:inaccurate:0.98
|
|
10
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Огляд
|
|
14
|
+
|
|
15
|
+
Read-only `lint`-обгортка для Kubernetes YAML у `k8s`, яка не має власного `main.mjs` і тому через `hasHandWrittenMain` у `scripts/lib/lint-surface/detect.mjs` не повинна промотуватися в ungated standalone detector. Її роль — делегувати перевірку в `k8s/manifests/main.mjs` через `validateHasuraHttpRouteCanon`, де `collectHasuraDeploymentsAndHttpRoutes` зв’язує `hasura_httproute.rego` лише з HTTPRoute, що має поруч Hasura Deployment з тим самим `metadata.name`. Це запобігає запуску `hasura_httproute.rego` напряму на всі `hr.yaml` під `k8s` і прибирає false positive на HTTPRoute без Hasura.
|
|
16
|
+
|
|
17
|
+
## Поведінка
|
|
18
|
+
|
|
19
|
+
1. `lint` збирає стан перевірки для поточного робочого каталогу й працює лише на читання.
|
|
20
|
+
2. `lint` знаходить Kubernetes YAML у межах `k8s` з урахуванням ignore-шляхів з `.cursor`.
|
|
21
|
+
3. Якщо під `k8s` немає YAML-файлів, `lint` фіксує пропуск і завершує роботу без помилки.
|
|
22
|
+
4. Якщо YAML-файли є, `lint` запускає перевірку `hasura_httproute` тільки на цьому наборі файлів.
|
|
23
|
+
5. Перевірка навмисно gated: вона не повинна перетворюватися на ungated standalone detector для всіх `hr.yaml` під `k8s`, бо тоді з’являлися б false positive для HTTPRoute без відповідного Hasura Deployment з тим самим `metadata.name`.
|
|
24
|
+
6. `lint` повертає підсумок із зафіксованими порушеннями або повідомленням про пропуск, не змінюючи файлову систему чи бази даних.
|
|
25
|
+
|
|
26
|
+
## Публічний API
|
|
27
|
+
|
|
28
|
+
- lint — Detector k8s/hasura_httproute (read-only).
|
|
29
|
+
|
|
30
|
+
## Гарантії поведінки
|
|
31
|
+
|
|
32
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint-поверхня k8s/hasura_httproute: gated detector. Без власного `main.mjs` generic
|
|
3
|
+
* lint-surface (`hasHandWrittenMain` у `scripts/lib/lint-surface/detect.mjs`) промотує
|
|
4
|
+
* будь-який concern із резолвним `policy.files.walkGlob` у **ungated** standalone detector —
|
|
5
|
+
* rego `hasura_httproute.rego` прогнав би тоді напряму на всі `hr.yaml` під k8s, без перевірки,
|
|
6
|
+
* чи є поруч Hasura Deployment з тим самим `metadata.name` (false positive на HTTPRoute без
|
|
7
|
+
* Hasura, issue: efes-cloud/backend). Cross-file JS-гейт (`collectHasuraDeploymentsAndHttpRoutes`)
|
|
8
|
+
* і сам виклик rego живуть у `k8s/manifests/main.mjs` (`validateHasuraHttpRouteCanon`, export) —
|
|
9
|
+
* тут лише тонка обгортка.
|
|
10
|
+
*/
|
|
11
|
+
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
12
|
+
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
13
|
+
import { findK8sYamlFiles, validateHasuraHttpRouteCanon } from '../manifests/main.mjs'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detector k8s/hasura_httproute (read-only).
|
|
17
|
+
* @param {import('../../../scripts/lib/lint-surface/types.mjs').LintContext} ctx контекст лінту.
|
|
18
|
+
* @returns {Promise<import('../../../scripts/lib/lint-surface/types.mjs').LintResult>} результат із порушеннями
|
|
19
|
+
*/
|
|
20
|
+
export async function lint(ctx) {
|
|
21
|
+
const reporter = createViolationReporter(ctx)
|
|
22
|
+
const { pass, fail } = reporter
|
|
23
|
+
const root = ctx.cwd
|
|
24
|
+
|
|
25
|
+
const ignorePaths = await loadCursorIgnorePaths(root)
|
|
26
|
+
const yamlFiles = await findK8sYamlFiles(root, ignorePaths)
|
|
27
|
+
if (yamlFiles.length === 0) {
|
|
28
|
+
pass('Немає *.yaml під k8s — перевірку hasura_httproute пропущено')
|
|
29
|
+
return reporter.result()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await validateHasuraHttpRouteCanon(root, yamlFiles, fail)
|
|
33
|
+
|
|
34
|
+
return reporter.result()
|
|
35
|
+
}
|
|
@@ -36,3 +36,11 @@
|
|
|
36
36
|
**Увага:** готовий snippet-файл цього прикладу (`js/templates/kubescape_exceptions/.kubescape-exceptions.json.snippet.json` до `da05f89d`) під час "combine concern" **не був перенесений** у жоден поточний concern-каталог і в репозиторії відсутній — знайти оригінал не вдалося; за потреби відтвори JSON вручну за прикладом вище.
|
|
37
37
|
|
|
38
38
|
Виключай контрольно, а не глобально (не додавай винятки без `attributes.name`/`labels`).
|
|
39
|
+
|
|
40
|
+
## Auto-generated виняток C-0056/C-0018 для Job/CronJob
|
|
41
|
+
|
|
42
|
+
Controls **C-0056**/**C-0018** (liveness/readiness probe) структурно незастосовні до **`kind: Job`**/**`kind: CronJob`** — под виконується один раз і завершується, on-going readiness/liveness нема чим міряти. Без винятку kubescape (поріг `--severity-threshold high`) падає fatal на першому ж такому ресурсі й зупиняє скан усього дерева `k8s` (issue: efes-cloud/backend, 261 ресурс, жоден job не стартує Hasura server-side).
|
|
43
|
+
|
|
44
|
+
Лінт **сам** генерує по одному exception-запису на **кожен реальний** Job/CronJob-ресурс (з `attributes.kind`+`attributes.name`, за наявності — `attributes.namespace`) для scan-таргету, що його якраз сканує kubescape (`autoJobCronJobProbeExceptions` у `npm/rules/k8s/manifests/main.mjs`, виклик з `runKubescapeManifest`/`scanRawK8sDir`). Це **не** kind-only виняток — кожен запис прив'язаний до конкретного ресурсу, тому відповідає конвенції "виключай контрольно, а не глобально" вище, і не вимагає ручної підтримки: нові CronJob покриваються автоматично при наступному скані.
|
|
45
|
+
|
|
46
|
+
Auto-generated записи **мержаться** з `.kubescape-exceptions.json` користувача (якщо файл є) в один tmp-файл, переданий через `--exceptions`; committed-файл користувача лишається джерелом істини для власних, ручних винятків (напр. C-0012 вище) — цей механізм його не замінює і не редагує. Deployment/StatefulSet/DaemonSet-ресурси винятку **не** отримують — C-0056/C-0018 там і надалі fail-closed.
|
|
@@ -1589,7 +1589,7 @@ export function baseKustomizationNamespaceViolation(obj) {
|
|
|
1589
1589
|
* @param {string[]} [ignorePaths] шляхи каталогів, повністю виключених з обходу
|
|
1590
1590
|
* @returns {Promise<string[]>} відсортовані абсолютні шляхи до файлів
|
|
1591
1591
|
*/
|
|
1592
|
-
async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
1592
|
+
export async function findK8sYamlFiles(root, ignorePaths = []) {
|
|
1593
1593
|
/**
|
|
1594
1594
|
@type {string[]}
|
|
1595
1595
|
*/
|
|
@@ -3184,7 +3184,7 @@ function recordHasuraDeploymentName(rec, dir, hasuraByDir) {
|
|
|
3184
3184
|
* @param {(msg: string, opts?: string | { reason?: string, file?: string, data?: object }) => void} fail callback реєстрації помилки
|
|
3185
3185
|
* @returns {Promise<void>} результат
|
|
3186
3186
|
*/
|
|
3187
|
-
async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
3187
|
+
export async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
|
|
3188
3188
|
const { hasuraByDir, httpRoutes } = await collectHasuraDeploymentsAndHttpRoutes(yamlFiles)
|
|
3189
3189
|
if (hasuraByDir.size === 0 || httpRoutes.length === 0) return
|
|
3190
3190
|
|
|
@@ -3610,7 +3610,7 @@ async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail,
|
|
|
3610
3610
|
* @param {(msg: string) => void} fail callback при помилці
|
|
3611
3611
|
* @param {(msg: string) => void} passFn callback при успіху
|
|
3612
3612
|
*/
|
|
3613
|
-
async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs, fail, passFn) {
|
|
3613
|
+
export async function validateHasuraConfigMapRemoteSchemaPermissions(root, yamlFilesAbs, fail, passFn) {
|
|
3614
3614
|
const cmFiles = yamlFilesAbs.filter(abs => {
|
|
3615
3615
|
const rel = relative(root, abs).replaceAll('\\', '/')
|
|
3616
3616
|
return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
|
|
@@ -6553,9 +6553,15 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs, fail) {
|
|
|
6553
6553
|
/**
|
|
6554
6554
|
* Plan B (rego-authoritative): на початку `check()` батч-викликаємо path-фільтровані
|
|
6555
6555
|
* rego-пакети з `npm/policy/k8s/` через `runConftestBatch`. Пакети hasura_configmap і
|
|
6556
|
-
* hasura_httproute мають cross-file gating (паруються з Hasura-Deployment) —
|
|
6557
|
-
*
|
|
6558
|
-
*
|
|
6556
|
+
* hasura_httproute мають cross-file gating (паруються з Hasura-Deployment) — цей JS-гейт
|
|
6557
|
+
* (`validateHasuraConfigMapRemoteSchemaPermissions`, `validateHasuraHttpRouteCanon`, обидві
|
|
6558
|
+
* export) викликається з **власного** `main.mjs` кожного з цих двох концернів
|
|
6559
|
+
* (`k8s/hasura_configmap/main.mjs`, `k8s/hasura_httproute/main.mjs`) — не звідси. Це навмисно:
|
|
6560
|
+
* без власного `main.mjs` generic lint-surface (`hasHandWrittenMain` у
|
|
6561
|
+
* `scripts/lib/lint-surface/detect.mjs`) промотує concern із самостійним `policy.files.walkGlob`
|
|
6562
|
+
* у **ungated** detector, що прогонить rego напряму на всі файли-збіги glob — саме так виникали
|
|
6563
|
+
* false positive на ConfigMap/HTTPRoute без сусіднього Hasura Deployment (issue: efes-cloud/backend).
|
|
6564
|
+
* Структурна частина HPA/PDB (`k8s.hpa_pdb`) тут на всіх yaml,
|
|
6559
6565
|
* env-залежні межі min/maxReplicas і expected-name — JS-cross-file у `validateDeploymentHpaPdbAndTopology`.
|
|
6560
6566
|
* @param {string} root корінь репозиторію (cwd)
|
|
6561
6567
|
* @param {string[]} yamlFiles абсолютні шляхи знайдених *.yaml під `…/k8s/`
|
|
@@ -6716,7 +6722,8 @@ export async function lint(ctx) {
|
|
|
6716
6722
|
|
|
6717
6723
|
await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
|
|
6718
6724
|
|
|
6719
|
-
|
|
6725
|
+
// validateHasuraHttpRouteCanon — переїхав у власний main.mjs концерну k8s/hasura_httproute
|
|
6726
|
+
// (gated detector, див. коментар над runAllK8sRego вище).
|
|
6720
6727
|
|
|
6721
6728
|
await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
|
|
6722
6729
|
|
|
@@ -6728,7 +6735,8 @@ export async function lint(ctx) {
|
|
|
6728
6735
|
|
|
6729
6736
|
await validateConfigMapNameMatchesDeployment(root, yamlFiles, fail, pass)
|
|
6730
6737
|
|
|
6731
|
-
|
|
6738
|
+
// validateHasuraConfigMapRemoteSchemaPermissions — переїхав у власний main.mjs концерну
|
|
6739
|
+
// k8s/hasura_configmap (gated detector, див. коментар над runAllK8sRego вище).
|
|
6732
6740
|
|
|
6733
6741
|
await validateDeploymentHpaPdbAndTopology(root, yamlFiles, fail, pass)
|
|
6734
6742
|
|
|
@@ -6826,13 +6834,118 @@ export async function runKubeconform(dirs, verbose = false) {
|
|
|
6826
6834
|
}
|
|
6827
6835
|
|
|
6828
6836
|
/**
|
|
6829
|
-
*
|
|
6837
|
+
* `kind`, для яких kubescape-контроли C-0056/C-0018 (liveness/readiness probe) структурно
|
|
6838
|
+
* незастосовні — под Job/CronJob виконується один раз і завершується, on-going readiness/liveness
|
|
6839
|
+
* нема чим міряти (k8s.mdc).
|
|
6840
|
+
* @type {Set<string>}
|
|
6841
|
+
*/
|
|
6842
|
+
const KUBESCAPE_PROBE_EXEMPT_KINDS = new Set(['Job', 'CronJob'])
|
|
6843
|
+
|
|
6844
|
+
/**
|
|
6845
|
+
* Auto-generated kubescape `postureExceptionPolicy`-записи для C-0056/C-0018 — по одному на
|
|
6846
|
+
* кожен реальний Job/CronJob-ресурс з непорожнім `metadata.name`, знайдений у `yamlText`.
|
|
6847
|
+
* Навмисно **не** kind-only (без `name`) — house-конвенція "виключай контрольно, а не глобально"
|
|
6848
|
+
* (`k8s/kubeconform/kubeconform.mdc`): kind-only виняток замовчав би C-0056/C-0018 і на
|
|
6849
|
+
* Deployment-подібних ресурсах, для яких ці controls реально застосовні.
|
|
6850
|
+
* @param {string} yamlText вміст одного або декількох YAML-документів (kubescape scan target)
|
|
6851
|
+
* @returns {object[]} масив exception-записів (може бути порожній)
|
|
6852
|
+
*/
|
|
6853
|
+
export function autoJobCronJobProbeExceptions(yamlText) {
|
|
6854
|
+
const docs = tryParseAllYamlDocs(yamlText)
|
|
6855
|
+
if (docs === undefined) return []
|
|
6856
|
+
const out = []
|
|
6857
|
+
for (const doc of docs) {
|
|
6858
|
+
if (doc.errors.length !== 0) continue
|
|
6859
|
+
const rec = asPlainRecord(doc.toJSON())
|
|
6860
|
+
if (rec === null || typeof rec.kind !== 'string' || !KUBESCAPE_PROBE_EXEMPT_KINDS.has(rec.kind)) continue
|
|
6861
|
+
const meta = asPlainRecord(rec.metadata)
|
|
6862
|
+
const name = meta === null ? undefined : meta.name
|
|
6863
|
+
if (typeof name !== 'string' || name === '') continue
|
|
6864
|
+
const namespace = meta === null ? undefined : meta.namespace
|
|
6865
|
+
const hasNamespace = typeof namespace === 'string' && namespace !== ''
|
|
6866
|
+
const attributes = hasNamespace ? { kind: rec.kind, name, namespace } : { kind: rec.kind, name }
|
|
6867
|
+
out.push({
|
|
6868
|
+
name: `auto-${rec.kind.toLowerCase()}-${hasNamespace ? namespace : 'default'}-${name}-probes`,
|
|
6869
|
+
policyType: 'postureExceptionPolicy',
|
|
6870
|
+
actions: ['alertOnly'],
|
|
6871
|
+
resources: [{ designatorType: 'Attributes', attributes }],
|
|
6872
|
+
posturePolicies: [{ controlID: 'C-0056' }, { controlID: 'C-0018' }]
|
|
6873
|
+
})
|
|
6874
|
+
}
|
|
6875
|
+
return out
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
/**
|
|
6879
|
+
* Читає та конкатенує вміст усіх `*.yaml`/`*.yml` під каталогом — для похідних auto-exceptions
|
|
6880
|
+
* (треба геть увесь вміст `dir`, що його рекурсивно сканує `kubescape scan <dir>`, а не лише
|
|
6881
|
+
* basename-фільтровані файли k8s.mdc-обходу).
|
|
6882
|
+
* @param {string} dir абсолютний шлях каталогу
|
|
6883
|
+
* @returns {Promise<string>} конкатенований YAML-текст (документи розділені `---`)
|
|
6884
|
+
*/
|
|
6885
|
+
async function readAllYamlTextUnderDir(dir) {
|
|
6886
|
+
const files = []
|
|
6887
|
+
await walkDir(dir, p => {
|
|
6888
|
+
if (YAML_EXTENSION_RE.test(p)) files.push(p)
|
|
6889
|
+
})
|
|
6890
|
+
const parts = []
|
|
6891
|
+
for (const f of files) {
|
|
6892
|
+
const raw = await tryReadFileUtf8(f)
|
|
6893
|
+
if (raw !== undefined) parts.push(raw)
|
|
6894
|
+
}
|
|
6895
|
+
return parts.join('\n---\n')
|
|
6896
|
+
}
|
|
6897
|
+
|
|
6898
|
+
/**
|
|
6899
|
+
* Безпечно парсить user-файл `.kubescape-exceptions.json` як масив exception-записів.
|
|
6900
|
+
* @param {string} exceptionsPath абсолютний шлях до файлу
|
|
6901
|
+
* @returns {object[]} масив (порожній при помилці читання/парсингу або якщо це не масив)
|
|
6902
|
+
*/
|
|
6903
|
+
function readUserKubescapeExceptions(exceptionsPath) {
|
|
6904
|
+
try {
|
|
6905
|
+
const parsed = JSON.parse(readFileSync(exceptionsPath, 'utf8'))
|
|
6906
|
+
return Array.isArray(parsed) ? parsed : []
|
|
6907
|
+
} catch {
|
|
6908
|
+
console.error(
|
|
6909
|
+
`${exceptionsPath}: не вдалося розпарсити як JSON-масив — auto-generated винятки Job/CronJob застосовано без нього`
|
|
6910
|
+
)
|
|
6911
|
+
return []
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
|
|
6915
|
+
/**
|
|
6916
|
+
* Будує CLI-аргументи `--exceptions`. Без `autoExceptions` — сумісна поведінка: пряме
|
|
6917
|
+
* посилання на `.kubescape-exceptions.json` у корені (якщо є), інакше `[]`. З непорожнім
|
|
6918
|
+
* `autoExceptions` — мержить його з user-файлом (якщо є) у tmp-файл; виклик відповідає за
|
|
6919
|
+
* cleanup через `cleanupKubescapeExceptionsArgs`.
|
|
6830
6920
|
* @param {string} root корінь репозиторію.
|
|
6831
|
-
* @
|
|
6921
|
+
* @param {object[]} [autoExceptions] auto-generated exception-записи для мержу з user-файлом.
|
|
6922
|
+
* @returns {string[]} масив аргументів (порожній, якщо винятків немає взагалі).
|
|
6832
6923
|
*/
|
|
6833
|
-
export function buildKubescapeExceptionsArgs(root) {
|
|
6924
|
+
export function buildKubescapeExceptionsArgs(root, autoExceptions = []) {
|
|
6834
6925
|
const exceptionsPath = join(root, KUBESCAPE_EXCEPTIONS_FILE)
|
|
6835
|
-
|
|
6926
|
+
const userFileExists = existsSync(exceptionsPath)
|
|
6927
|
+
if (autoExceptions.length === 0) {
|
|
6928
|
+
return userFileExists ? ['--exceptions', exceptionsPath] : []
|
|
6929
|
+
}
|
|
6930
|
+
const userExceptions = userFileExists ? readUserKubescapeExceptions(exceptionsPath) : []
|
|
6931
|
+
const merged = [...userExceptions, ...autoExceptions]
|
|
6932
|
+
const dir = mkdtempSync(join(tmpdir(), 'nitra-cursor-k8s-exceptions-'))
|
|
6933
|
+
const tmpFile = join(dir, 'kubescape-exceptions.json')
|
|
6934
|
+
writeFileSync(tmpFile, JSON.stringify(merged))
|
|
6935
|
+
return ['--exceptions', tmpFile]
|
|
6936
|
+
}
|
|
6937
|
+
|
|
6938
|
+
/**
|
|
6939
|
+
* Прибирає tmp-файл винятків, згенерований `buildKubescapeExceptionsArgs` (якщо це не пряме
|
|
6940
|
+
* посилання на committed `.kubescape-exceptions.json` користувача — той не чіпаємо).
|
|
6941
|
+
* @param {string[]} exceptionsArgs результат `buildKubescapeExceptionsArgs`.
|
|
6942
|
+
* @param {string} root корінь репозиторію.
|
|
6943
|
+
* @returns {void} результат
|
|
6944
|
+
*/
|
|
6945
|
+
function cleanupKubescapeExceptionsArgs(exceptionsArgs, root) {
|
|
6946
|
+
const p = exceptionsArgs[1]
|
|
6947
|
+
if (p === undefined || p === join(root, KUBESCAPE_EXCEPTIONS_FILE)) return
|
|
6948
|
+
rmSync(dirname(p), { recursive: true, force: true })
|
|
6836
6949
|
}
|
|
6837
6950
|
|
|
6838
6951
|
/**
|
|
@@ -6884,16 +6997,20 @@ async function runKustomizeBuild(kubectlPath, dir, verbose = false) {
|
|
|
6884
6997
|
}
|
|
6885
6998
|
|
|
6886
6999
|
/**
|
|
6887
|
-
* Сканує один зібраний маніфест через `kubescape scan` (пише у tmp-файл, чистить його).
|
|
7000
|
+
* Сканує один зібраний маніфест через `kubescape scan` (пише у tmp-файл, чистить його). Auto-
|
|
7001
|
+
* generated виняток C-0056/C-0018 для Job/CronJob-ресурсів у `manifest` (див.
|
|
7002
|
+
* `autoJobCronJobProbeExceptions`) мержиться з user-файлом `.kubescape-exceptions.json` (якщо є).
|
|
6888
7003
|
* @param {string} kubescapePath шлях до бінарника kubescape.
|
|
6889
7004
|
* @param {string|Buffer} manifest вміст маніфеста для сканування.
|
|
6890
|
-
* @param {string
|
|
7005
|
+
* @param {string} root корінь репозиторію (для user-файла винятків).
|
|
6891
7006
|
* @param {boolean} [verbose] показати повний нативний вивід kubescape (`stdio: 'inherit'`).
|
|
6892
7007
|
* @returns {Promise<{ status: number, enoent: boolean }>} exit-код і прапорець відсутнього тула.
|
|
6893
7008
|
*/
|
|
6894
|
-
async function runKubescapeManifest(kubescapePath, manifest,
|
|
7009
|
+
async function runKubescapeManifest(kubescapePath, manifest, root, verbose = false) {
|
|
6895
7010
|
const dir = mkdtempSync(join(tmpdir(), 'nitra-cursor-k8s-'))
|
|
6896
7011
|
const file = join(dir, 'manifest.yaml')
|
|
7012
|
+
const exceptionsArgs = buildKubescapeExceptionsArgs(root, autoJobCronJobProbeExceptions(String(manifest)))
|
|
7013
|
+
if (verbose && exceptionsArgs.length > 0) console.log(`run-k8s: kubescape exceptions — ${exceptionsArgs[1]}`)
|
|
6897
7014
|
try {
|
|
6898
7015
|
writeFileSync(file, manifest)
|
|
6899
7016
|
try {
|
|
@@ -6906,20 +7023,26 @@ async function runKubescapeManifest(kubescapePath, manifest, exceptionsArgs, ver
|
|
|
6906
7023
|
return { status: 1, enoent }
|
|
6907
7024
|
}
|
|
6908
7025
|
} finally {
|
|
7026
|
+
cleanupKubescapeExceptionsArgs(exceptionsArgs, root)
|
|
6909
7027
|
rmSync(dir, { recursive: true, force: true })
|
|
6910
7028
|
}
|
|
6911
7029
|
}
|
|
6912
7030
|
|
|
6913
7031
|
/**
|
|
6914
|
-
* Сканує сирий k8s-каталог (без kustomization) через `kubescape scan <dir>`.
|
|
7032
|
+
* Сканує сирий k8s-каталог (без kustomization) через `kubescape scan <dir>`. Auto-generated
|
|
7033
|
+
* виняток C-0056/C-0018 для Job/CronJob-ресурсів під `dir` мержиться з user-файлом
|
|
7034
|
+
* `.kubescape-exceptions.json` (якщо є).
|
|
6915
7035
|
* @param {string} kubescapePath шлях до бінарника kubescape.
|
|
6916
7036
|
* @param {string} dir каталог для сканування.
|
|
6917
|
-
* @param {string
|
|
7037
|
+
* @param {string} root корінь репозиторію (для user-файла винятків).
|
|
6918
7038
|
* @param {boolean} [verbose] показати повний нативний вивід kubescape (`stdio: 'inherit'`) і хід сканування.
|
|
6919
7039
|
* @returns {Promise<number>} exit-код (127 якщо тул відсутній).
|
|
6920
7040
|
*/
|
|
6921
|
-
async function scanRawK8sDir(kubescapePath, dir,
|
|
7041
|
+
async function scanRawK8sDir(kubescapePath, dir, root, verbose = false) {
|
|
6922
7042
|
if (verbose) console.log(`run-k8s: kubescape scan ${dir} (без kustomization — сирий dir-скан)`)
|
|
7043
|
+
const yamlText = await readAllYamlTextUnderDir(dir)
|
|
7044
|
+
const exceptionsArgs = buildKubescapeExceptionsArgs(root, autoJobCronJobProbeExceptions(yamlText))
|
|
7045
|
+
if (verbose && exceptionsArgs.length > 0) console.log(`run-k8s: kubescape exceptions — ${exceptionsArgs[1]}`)
|
|
6923
7046
|
try {
|
|
6924
7047
|
const r = await spawnAsync(kubescapePath, ['scan', dir, '--severity-threshold', 'high', ...exceptionsArgs], {
|
|
6925
7048
|
stdio: verbose ? 'inherit' : 'pipe'
|
|
@@ -6931,6 +7054,8 @@ async function scanRawK8sDir(kubescapePath, dir, exceptionsArgs, verbose = false
|
|
|
6931
7054
|
return 127
|
|
6932
7055
|
}
|
|
6933
7056
|
return 1
|
|
7057
|
+
} finally {
|
|
7058
|
+
cleanupKubescapeExceptionsArgs(exceptionsArgs, root)
|
|
6934
7059
|
}
|
|
6935
7060
|
}
|
|
6936
7061
|
|
|
@@ -6939,16 +7064,16 @@ async function scanRawK8sDir(kubescapePath, dir, exceptionsArgs, verbose = false
|
|
|
6939
7064
|
* @param {string} kubectlPath шлях до бінарника kubectl.
|
|
6940
7065
|
* @param {string} kubescapePath шлях до бінарника kubescape.
|
|
6941
7066
|
* @param {string[]} kdirs каталоги з kustomization.
|
|
6942
|
-
* @param {string
|
|
7067
|
+
* @param {string} root корінь репозиторію (для user-файла винятків).
|
|
6943
7068
|
* @param {boolean} [verbose] показати повний нативний вивід kubectl/kubescape і хід сканування.
|
|
6944
7069
|
* @returns {Promise<number>} 0 якщо всі чисті, інакше перший ненульовий exit-код (127 — тул відсутній).
|
|
6945
7070
|
*/
|
|
6946
|
-
async function scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs,
|
|
7071
|
+
async function scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, root, verbose = false) {
|
|
6947
7072
|
for (const kdir of kdirs) {
|
|
6948
7073
|
if (verbose) console.log(`run-k8s: kubectl kustomize ${kdir} | kubescape scan <tmp>`)
|
|
6949
7074
|
const build = await runKustomizeBuild(kubectlPath, kdir, verbose)
|
|
6950
7075
|
if (build.status !== 0) return build.status
|
|
6951
|
-
const ks = await runKubescapeManifest(kubescapePath, build.stdout,
|
|
7076
|
+
const ks = await runKubescapeManifest(kubescapePath, build.stdout, root, verbose)
|
|
6952
7077
|
if (ks.enoent) {
|
|
6953
7078
|
console.error(KUBESCAPE_MISSING_HINT)
|
|
6954
7079
|
return 127
|
|
@@ -6960,20 +7085,20 @@ async function scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, exception
|
|
|
6960
7085
|
|
|
6961
7086
|
/**
|
|
6962
7087
|
* Оркеструє kubescape-скан по k8s-коренях: kustomize-каталоги збираються, решта — сирий скан.
|
|
7088
|
+
* Виняток C-0056/C-0018 для Job/CronJob — auto-generated з реального контенту кожного скан-таргету
|
|
7089
|
+
* (не один спільний файл на весь репо), див. `runKubescapeManifest`/`scanRawK8sDir`.
|
|
6963
7090
|
* @param {string[]} dirs абсолютні шляхи k8s-коренів.
|
|
6964
|
-
* @param {string} root корінь репозиторію (для
|
|
7091
|
+
* @param {string} root корінь репозиторію (для user-файла винятків).
|
|
6965
7092
|
* @param {boolean} [verbose] показати повний нативний вивід kubectl/kubescape (`stdio: 'inherit'`) і хід сканування.
|
|
6966
7093
|
* @returns {Promise<number>} 0 якщо все чисто, інакше перший ненульовий exit-код (127 — тул відсутній).
|
|
6967
7094
|
*/
|
|
6968
7095
|
async function runKubescape(dirs, root, verbose = false) {
|
|
6969
|
-
const exceptionsArgs = buildKubescapeExceptionsArgs(root)
|
|
6970
|
-
if (verbose && exceptionsArgs.length > 0) console.log(`run-k8s: kubescape exceptions — ${KUBESCAPE_EXCEPTIONS_FILE}`)
|
|
6971
7096
|
const kubescapePath = ensureTool('kubescape')
|
|
6972
7097
|
let kubectlPath = null
|
|
6973
7098
|
for (const d of dirs) {
|
|
6974
7099
|
const kdirs = await findKustomizationDirs(d)
|
|
6975
7100
|
if (kdirs.length === 0) {
|
|
6976
|
-
const rawStatus = await scanRawK8sDir(kubescapePath, d,
|
|
7101
|
+
const rawStatus = await scanRawK8sDir(kubescapePath, d, root, verbose)
|
|
6977
7102
|
if (rawStatus !== 0) return rawStatus
|
|
6978
7103
|
continue
|
|
6979
7104
|
}
|
|
@@ -6984,7 +7109,7 @@ async function runKubescape(dirs, root, verbose = false) {
|
|
|
6984
7109
|
return 127
|
|
6985
7110
|
}
|
|
6986
7111
|
}
|
|
6987
|
-
const buildStatus = await scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs,
|
|
7112
|
+
const buildStatus = await scanKustomizeK8sDirs(kubectlPath, kubescapePath, kdirs, root, verbose)
|
|
6988
7113
|
if (buildStatus !== 0) return buildStatus
|
|
6989
7114
|
}
|
|
6990
7115
|
return 0
|
|
@@ -15,23 +15,37 @@ rebuild (лінковка, bundling, іноді webview) замість секу
|
|
|
15
15
|
|
|
16
16
|
```
|
|
17
17
|
my-tauri-app/
|
|
18
|
-
├──
|
|
19
|
-
|
|
20
|
-
└──
|
|
21
|
-
|
|
18
|
+
├── Cargo.toml ← workspace root (virtual, без [package]): [workspace] members = ["src-tauri", "agent-core"]
|
|
19
|
+
├── src-tauri/ ← app-shell: Tauri runtime, window/AppHandle, НЕ містить LLM-залежностей
|
|
20
|
+
│ └── Cargo.toml ← [package], без [workspace] — сам по собі НЕ workspace root
|
|
21
|
+
└── agent-core/ ← agent/LLM-логіка: prompt building, tool dispatch, provider abstraction
|
|
22
|
+
├── Cargo.toml ← без залежності на tauri
|
|
22
23
|
└── tests/
|
|
23
24
|
└── fake_provider.rs ← FakeLlmProvider / MockLlmClient — без мережі
|
|
24
25
|
```
|
|
25
26
|
|
|
27
|
+
`[workspace] members = [".", "../agent-core"]` **всередині** `src-tauri/Cargo.toml` — невалідний
|
|
28
|
+
Cargo layout: `../agent-core` лежить поза деревом workspace root (`src-tauri/`), і Cargo відмовляє
|
|
29
|
+
з `workspace member '.../agent-core/Cargo.toml' is not hierarchically below the workspace root`.
|
|
30
|
+
Тому `[workspace]` живе у product-root `Cargo.toml`, що лежить НАД обома крейтами (канон
|
|
31
|
+
`rust/workspace_root.mdc` — один кореневий Cargo workspace на репозиторій).
|
|
32
|
+
|
|
33
|
+
**Наслідок:** `target/` компілюється у корінь цього workspace (`my-tauri-app/target/`), а не в
|
|
34
|
+
`src-tauri/target/` — це впливає на очікуваний запис у `.gitignore` (`tauri/gitignore_target`) і на
|
|
35
|
+
шлях у `workspaces:`-полі `Swatinem/rust-cache` GitHub Action (action бере шлях буквально як
|
|
36
|
+
`<path>/target`, тож там має стояти корінь цього workspace, не `src-tauri`).
|
|
37
|
+
|
|
26
38
|
Концерн `tauri/core_test_isolation.mjs` (read-only detector, `structural`, авто-фікс ризикований)
|
|
27
39
|
для кожного `src-tauri/Cargo.toml` перевіряє:
|
|
28
40
|
|
|
29
41
|
1. **LLM-залежність не в app-shell.** Якщо сам `src-tauri/Cargo.toml` має залежність на відомий
|
|
30
42
|
LLM SDK (напр. `async-openai`, `anthropic`, `ollama-rs`, `rig-core`, ...) — фейл: винеси
|
|
31
43
|
agent-логіку в окремий crate.
|
|
32
|
-
2. **Окремий crate не тягне tauri.**
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
2. **Окремий crate не тягне tauri.** `[workspace] members` резолвиться або з самого
|
|
45
|
+
`src-tauri/Cargo.toml` (standalone-патерн), або — коли там `[workspace]` немає — з
|
|
46
|
+
найближчого предка-workspace root (product-root `Cargo.toml` над `src-tauri/`). Якщо серед
|
|
47
|
+
members є crate з LLM-залежністю, і цей crate сам залежить від `tauri`/`tauri-*` — фейл:
|
|
48
|
+
`cargo test -p` цього крейту все одно перезбирає Tauri runtime.
|
|
35
49
|
3. **Є fake-провайдер.** Якщо такий crate є і не залежить від tauri, але ніде в його дереві
|
|
36
50
|
(`**/*.rs`) немає маркера `Fake*Llm`/`Mock*Provider`/`Stub*Client` — фейл: інтеграційні тести
|
|
37
51
|
на prompt/tool ітерації або мовчки пропускають LLM-логіку, або б'ють по реальному провайдеру.
|
|
@@ -14,6 +14,7 @@ import { parse as parseToml } from 'smol-toml'
|
|
|
14
14
|
|
|
15
15
|
import { scanGlob } from '../../../scripts/utils/glob-compat.mjs'
|
|
16
16
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
17
|
+
import { findAncestorWorkspaceRoot, resolveWorkspaceMemberDirs } from '../../../scripts/utils/cargo-workspace.mjs'
|
|
17
18
|
import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
|
|
18
19
|
|
|
19
20
|
/** Стабільні reasons для трьох типів порушення. */
|
|
@@ -55,29 +56,6 @@ async function findSrcTauriDirs(cwd) {
|
|
|
55
56
|
return result
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
* Резолвить `[workspace] members` (шляхи й прості glob-патерни) відносно `src-tauri/`
|
|
60
|
-
* у список абсолютних каталогів крейтів, що мають власний Cargo.toml.
|
|
61
|
-
* @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
|
|
62
|
-
* @param {string[]} members патерни з `workspace.members`
|
|
63
|
-
* @returns {Promise<string[]>} абсолютні шляхи членів воркспейсу (без дублікатів)
|
|
64
|
-
*/
|
|
65
|
-
async function resolveWorkspaceMemberDirs(srcTauriDir, members) {
|
|
66
|
-
const found = new Set()
|
|
67
|
-
for (const pattern of members) {
|
|
68
|
-
if (pattern.includes('*')) {
|
|
69
|
-
for await (const rel of scanGlob(pattern, srcTauriDir)) {
|
|
70
|
-
const abs = resolve(srcTauriDir, rel)
|
|
71
|
-
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
72
|
-
}
|
|
73
|
-
continue
|
|
74
|
-
}
|
|
75
|
-
const abs = resolve(srcTauriDir, pattern)
|
|
76
|
-
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
77
|
-
}
|
|
78
|
-
return [...found]
|
|
79
|
-
}
|
|
80
|
-
|
|
81
59
|
/**
|
|
82
60
|
* Рекурсивно шукає в дереві крейту рядок, що відповідає `FAKE_PROVIDER_RE`
|
|
83
61
|
* (fake/mock/stub-реалізацію LLM-провайдера, зазвичай у `tests/` чи `src/`).
|
|
@@ -117,12 +95,19 @@ async function checkOneSrcTauri(srcTauriDir, cwd, reporter) {
|
|
|
117
95
|
return
|
|
118
96
|
}
|
|
119
97
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
// `[workspace]` живе або у самому src-tauri/Cargo.toml (старий/standalone патерн), або —
|
|
99
|
+
// канонічно (rust/workspace_root.mdc) — у предку-workspace root над src-tauri/.
|
|
100
|
+
let workspaceRootDir = srcTauriDir
|
|
101
|
+
let members = Array.isArray(parsed?.workspace?.members) ? parsed.workspace.members : []
|
|
102
|
+
if (members.length === 0) {
|
|
103
|
+
const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
|
|
104
|
+
const ancestorMembers = ancestor?.parsed?.workspace?.members
|
|
105
|
+
if (!ancestor || !Array.isArray(ancestorMembers) || ancestorMembers.length === 0) return
|
|
106
|
+
workspaceRootDir = ancestor.rootDir
|
|
107
|
+
members = ancestorMembers
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
const memberDirs = await resolveWorkspaceMemberDirs(
|
|
110
|
+
const memberDirs = await resolveWorkspaceMemberDirs(workspaceRootDir, members)
|
|
126
111
|
const otherMemberDirs = memberDirs.filter(d => resolve(d) !== resolve(srcTauriDir))
|
|
127
112
|
|
|
128
113
|
for (const memberDir of otherMemberDirs) {
|
|
@@ -3,9 +3,8 @@ type: JS Module
|
|
|
3
3
|
title: main.mjs
|
|
4
4
|
resource: npm/rules/tauri/gitignore_target/main.mjs
|
|
5
5
|
docgen:
|
|
6
|
-
crc:
|
|
6
|
+
crc: c9fcfc58
|
|
7
7
|
model: openai-codex/gpt-5.5
|
|
8
|
-
tier: cloud-avg
|
|
9
8
|
score: 85
|
|
10
9
|
issues: internal-name:findSrcTauriDirs,anchor-miss:(tauri.mdc),judge:inaccurate:0.98
|
|
11
10
|
judgeModel: openai-codex/gpt-5.4-mini
|
|
@@ -1,23 +1,42 @@
|
|
|
1
|
-
## `.gitignore`: ignore build-артефактів
|
|
1
|
+
## `.gitignore`: ignore build-артефактів фактичного Cargo workspace root
|
|
2
2
|
|
|
3
|
-
Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в
|
|
3
|
+
Кожен `<ws>/src-tauri/` (Rust-крейт Tauri) компілюється в `target/` свого фактичного
|
|
4
|
+
Cargo workspace root — а не завжди в `<ws>/src-tauri/target/`. Якщо над `src-tauri/` є
|
|
5
|
+
предок-workspace (канон `rust/workspace_root.mdc` — один кореневий Cargo workspace на
|
|
6
|
+
репозиторій, наприклад `<ws>/Cargo.toml` чи кореневий `Cargo.toml` монорепо), Cargo кладе
|
|
7
|
+
`target/` саме туди. Корінний `.gitignore` монорепо повинен містити точний ignore-запис
|
|
8
|
+
для кожного такого крейту, обчислений від цього фактичного workspace root:
|
|
4
9
|
|
|
5
10
|
```gitignore
|
|
6
11
|
# Tauri — Rust build artifacts (tauri.mdc)
|
|
7
|
-
<
|
|
12
|
+
<workspace-root>/target/
|
|
8
13
|
```
|
|
9
14
|
|
|
15
|
+
де `<workspace-root>` — найближчий предок `src-tauri/` з `[workspace]`, чиї `members`
|
|
16
|
+
покривають цей крейт (якщо це корінь монорепо — запис голий `target/`); якщо предка-workspace
|
|
17
|
+
немає — крейт сам собі workspace root, і запис лишається `<ws>/src-tauri/target/`.
|
|
18
|
+
|
|
10
19
|
### Чому точний шлях, а не голий `target/`
|
|
11
20
|
|
|
12
|
-
Реальний
|
|
21
|
+
Реальний прецедент: у `nitra/task` коміт `3cb0df3` помилково "виправив" коректний запис
|
|
22
|
+
`owner/target/` назад на `owner/src-tauri/target/` — автор вважав це друкарською помилкою,
|
|
23
|
+
не врахувавши, що `owner/Cargo.toml` є Cargo workspace root для `owner/src-tauri` і тому
|
|
24
|
+
`target/` справді компілюється в `owner/target/`, а не в `owner/src-tauri/target/`. Коміт
|
|
25
|
+
`ac3451e` це скасував. Тобто `<ws>/src-tauri/target/` — не універсальний дефолт, а лише
|
|
26
|
+
окремий випадок (crate без workspace root над собою); перевірка мусить рахувати очікуваний
|
|
27
|
+
шлях від реального workspace root, а не від фіксованого суфікса.
|
|
13
28
|
|
|
14
|
-
Тому перевірка звіряє **точний рядок**
|
|
29
|
+
Тому перевірка звіряє **точний рядок** обчисленого запису для кожного знайденого `src-tauri/`,
|
|
30
|
+
а не факт наявності підрядка `target/` десь у файлі — голий `target/` чи запис не того рівня
|
|
31
|
+
не закривають violation. Сторонній/мертвий каталог `target/` на диску (за старим, вже
|
|
32
|
+
невірним шляхом) також не задовольняє перевірку — вона звіряє лише вміст `.gitignore`.
|
|
15
33
|
|
|
16
34
|
### Область дії
|
|
17
35
|
|
|
18
36
|
- Обхід усіх `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config` (`findSrcTauriDirs`, без дублювання обходу монорепо).
|
|
37
|
+
- Очікуваний шлях — `findAncestorWorkspaceRoot` (`scripts/utils/cargo-workspace.mjs`, спільно з `rust/workspace_root`).
|
|
19
38
|
- Ціль фіксу — **один** корінний `.gitignore` монорепо (не по workspace).
|
|
20
|
-
- Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи
|
|
39
|
+
- Детектор лише **додає** відсутнє: сторонні/застарілі ignore-записи не видаляються — вони можуть бути навмисними чи потребувати ручного review.
|
|
21
40
|
|
|
22
41
|
### Автофікс (T0)
|
|
23
42
|
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* @see ./docs/main.md
|
|
3
3
|
*
|
|
4
4
|
* Read-only detector: для кожного знайденого `<ws>/src-tauri/` у монорепо
|
|
5
|
-
* корінний `.gitignore` повинен містити точний ignore-запис
|
|
6
|
-
*
|
|
5
|
+
* корінний `.gitignore` повинен містити точний ignore-запис для build-артефактів
|
|
6
|
+
* — але шлях залежить від фактичного Cargo workspace root крейту, не від
|
|
7
|
+
* розташування самого `src-tauri/` (tauri.mdc).
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
9
|
+
* Крейт `src-tauri/` компілюється в `<workspace-root>/target/`, а не обов'язково
|
|
10
|
+
* в `<ws>/src-tauri/target/`: коли над `src-tauri/` є предок-workspace (канон
|
|
11
|
+
* `rust/workspace_root` — один кореневий Cargo workspace на репозиторій), Cargo
|
|
12
|
+
* кладе `target/` саме туди. Реальний прецедент: у `nitra/task` коміт `3cb0df3`
|
|
13
|
+
* помилково "виправив" коректний запис `owner/target/` назад на `owner/src-tauri/target/`
|
|
14
|
+
* (бо `owner/Cargo.toml` — workspace root для `owner/src-tauri`), що скасував наступний
|
|
15
|
+
* коміт `ac3451e`. Тому перевірка обчислює очікуваний шлях від реального workspace root,
|
|
16
|
+
* а не від фіксованого суфікса `src-tauri/target/`, і звіряє точний рядок — голий
|
|
17
|
+
* `target/` чи запис не-того рівня не рахуються присутністю потрібного запису.
|
|
14
18
|
*
|
|
15
19
|
* Обхід `<ws>/src-tauri/` — спільний з `tauri/cargo_mutants_config`
|
|
16
20
|
* (`findSrcTauriDirs`), без дублювання.
|
|
@@ -20,6 +24,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
20
24
|
import { join, relative } from 'node:path'
|
|
21
25
|
|
|
22
26
|
import { createViolationReporter } from '../../../scripts/lib/lint-surface/violation-reporter.mjs'
|
|
27
|
+
import { findAncestorWorkspaceRoot } from '../../../scripts/utils/cargo-workspace.mjs'
|
|
23
28
|
import { findSrcTauriDirs } from '../cargo_mutants_config/main.mjs'
|
|
24
29
|
|
|
25
30
|
/** Стабільний reason: у корінному `.gitignore` бракує ignore-запису(ів) для `src-tauri/target/`. */
|
|
@@ -29,13 +34,22 @@ export const MISSING_GITIGNORE_TARGET_ENTRIES = 'missing-gitignore-target-entrie
|
|
|
29
34
|
export const ROOT_GITIGNORE = '.gitignore'
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
|
-
* Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri
|
|
37
|
+
* Очікуваний ignore-запис для build-артефактів одного `<ws>/src-tauri/`: шлях до
|
|
38
|
+
* `target/` фактичного Cargo workspace root цього крейту (найближчий предок з
|
|
39
|
+
* `[workspace]`, чиї `members` покривають `src-tauri/`), а не суфікс `src-tauri/target/`
|
|
40
|
+
* за замовчуванням. Якщо предка-workspace немає — крейт сам собі workspace root
|
|
41
|
+
* (standalone `src-tauri/Cargo.toml`), і `target/` лишається під `src-tauri/`.
|
|
33
42
|
* @param {string} cwd корінь монорепо
|
|
34
43
|
* @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
|
|
35
|
-
* @returns {string} очікуваний рядок у корінному `.gitignore`
|
|
44
|
+
* @returns {Promise<string>} очікуваний рядок у корінному `.gitignore`
|
|
36
45
|
*/
|
|
37
|
-
export function expectedTargetEntry(cwd, srcTauriDir) {
|
|
38
|
-
|
|
46
|
+
export async function expectedTargetEntry(cwd, srcTauriDir) {
|
|
47
|
+
const ancestor = await findAncestorWorkspaceRoot(srcTauriDir, cwd)
|
|
48
|
+
if (!ancestor) {
|
|
49
|
+
return `${relative(cwd, srcTauriDir)}/target/`
|
|
50
|
+
}
|
|
51
|
+
const relRoot = relative(cwd, ancestor.rootDir)
|
|
52
|
+
return relRoot === '' ? 'target/' : `${relRoot}/target/`
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
/**
|
|
@@ -62,7 +76,8 @@ export async function lint(ctx) {
|
|
|
62
76
|
const srcTauriDirs = await findSrcTauriDirs(cwd)
|
|
63
77
|
if (srcTauriDirs.length === 0) return reporter.result()
|
|
64
78
|
|
|
65
|
-
const expectedEntries =
|
|
79
|
+
const expectedEntries = []
|
|
80
|
+
for (const dir of srcTauriDirs) expectedEntries.push(await expectedTargetEntry(cwd, dir))
|
|
66
81
|
const abs = join(cwd, ROOT_GITIGNORE)
|
|
67
82
|
const content = existsSync(abs) ? await readFile(abs, 'utf8') : ''
|
|
68
83
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Спільні T0 (без spawn `cargo`) утиліти для роботи з Cargo workspace-структурою:
|
|
3
|
+
* читання Cargo.toml, резолв `[workspace].members`-glob-патернів у каталоги,
|
|
4
|
+
* пошук найближчого предка-workspace root для крейту. Спільно використовується
|
|
5
|
+
* `plugins/lang-rust/rules/rust/workspace_root` і `npm/rules/tauri/gitignore_target`.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import { readFile } from 'node:fs/promises'
|
|
9
|
+
import { dirname, join, resolve } from 'node:path'
|
|
10
|
+
|
|
11
|
+
import { parse as parseToml } from 'smol-toml'
|
|
12
|
+
|
|
13
|
+
import { scanGlob } from './glob-compat.mjs'
|
|
14
|
+
|
|
15
|
+
const TRAILING_SLASH_RE = /\/$/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
|
|
19
|
+
* @param {string} absPath абсолютний шлях до Cargo.toml
|
|
20
|
+
* @returns {Promise<Record<string, unknown>|null>} розпарсений маніфест або null
|
|
21
|
+
*/
|
|
22
|
+
export async function readCargoManifest(absPath) {
|
|
23
|
+
if (!existsSync(absPath)) return null
|
|
24
|
+
try {
|
|
25
|
+
return parseToml(await readFile(absPath, 'utf8'))
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
33
|
+
* glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
34
|
+
* Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
|
|
35
|
+
* @param {string} rootDir корінь workspace (каталог з Cargo.toml, де живе `[workspace]`)
|
|
36
|
+
* @param {string[]} patterns патерни з `members`/`exclude`
|
|
37
|
+
* @returns {Promise<string[]>} абсолютні шляхи (без дублікатів)
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveWorkspaceMemberDirs(rootDir, patterns) {
|
|
40
|
+
const found = new Set()
|
|
41
|
+
for (const pattern of patterns ?? []) {
|
|
42
|
+
const norm = pattern.replace(TRAILING_SLASH_RE, '')
|
|
43
|
+
if (norm.includes('*')) {
|
|
44
|
+
// Шаблон для `Cargo.toml` напряму (не для каталогів) — деякі раннери (Bun.Glob)
|
|
45
|
+
// за замовчуванням матчать лише файли, тож патерн на голий каталог даватиме 0 збігів.
|
|
46
|
+
for await (const relManifest of scanGlob(`${norm}/Cargo.toml`, rootDir)) {
|
|
47
|
+
found.add(resolve(rootDir, dirname(relManifest)))
|
|
48
|
+
}
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const abs = resolve(rootDir, norm)
|
|
52
|
+
if (existsSync(join(abs, 'Cargo.toml'))) found.add(abs)
|
|
53
|
+
}
|
|
54
|
+
return [...found]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
|
|
59
|
+
* @param {string} rootDir корінь workspace
|
|
60
|
+
* @param {string} crateDirAbs абсолютний шлях крейту
|
|
61
|
+
* @param {string[]} members `workspace.members`
|
|
62
|
+
* @param {string[]} excludes `workspace.exclude`
|
|
63
|
+
* @returns {Promise<boolean>} true — крейт є членом workspace
|
|
64
|
+
*/
|
|
65
|
+
export async function isWorkspaceMemberDir(rootDir, crateDirAbs, members, excludes) {
|
|
66
|
+
const target = resolve(crateDirAbs)
|
|
67
|
+
const memberDirs = await resolveWorkspaceMemberDirs(rootDir, members)
|
|
68
|
+
if (memberDirs.every(d => resolve(d) !== target)) return false
|
|
69
|
+
if (!excludes || excludes.length === 0) return true
|
|
70
|
+
const excludedDirs = await resolveWorkspaceMemberDirs(rootDir, excludes)
|
|
71
|
+
return excludedDirs.every(d => resolve(d) !== target)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
|
|
76
|
+
* шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
|
|
77
|
+
* покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
|
|
78
|
+
* @param {string} crateDirAbs абсолютний шлях крейту (напр. `<repo>/owner/src-tauri`)
|
|
79
|
+
* @param {string} repoRootAbs абсолютний корінь репозиторію (межа обходу вгору)
|
|
80
|
+
* @returns {Promise<{rootDir: string, parsed: Record<string, unknown>}|null>} найближчий ancestor workspace root або null
|
|
81
|
+
*/
|
|
82
|
+
export async function findAncestorWorkspaceRoot(crateDirAbs, repoRootAbs) {
|
|
83
|
+
const stopAt = resolve(repoRootAbs)
|
|
84
|
+
let dir = dirname(resolve(crateDirAbs))
|
|
85
|
+
for (;;) {
|
|
86
|
+
const parsed = await readCargoManifest(join(dir, 'Cargo.toml'))
|
|
87
|
+
if (parsed?.workspace) {
|
|
88
|
+
const members = Array.isArray(parsed.workspace.members) ? parsed.workspace.members : []
|
|
89
|
+
const excludes = Array.isArray(parsed.workspace.exclude) ? parsed.workspace.exclude : []
|
|
90
|
+
if (await isWorkspaceMemberDir(dir, crateDirAbs, members, excludes)) {
|
|
91
|
+
return { rootDir: dir, parsed }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (dir === stopAt) return null
|
|
95
|
+
const parent = dirname(dir)
|
|
96
|
+
if (parent === dir) return null
|
|
97
|
+
dir = parent
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: cargo-workspace.mjs
|
|
4
|
+
resource: npm/scripts/utils/cargo-workspace.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: c852e1fe
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Спільні утиліти для читання `Cargo.toml`, розгортання `[workspace].members`-glob-патернів у каталоги та пошуку найближчого предка-workspace root для крейта. Публічні `readCargoManifest`, `resolveWorkspaceMemberDirs`, `isWorkspaceMemberDir`, `findAncestorWorkspaceRoot` працюють без запуску `cargo`, перехоплюють помилки й у частині випадків повертають порожнє значення замість винятку, щоб інші правила могли безпечно перевіряти належність каталогу до workspace або знаходити workspace root.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
- **readCargoManifest** — читає `Cargo.toml` і повертає розпарсений manifest, або `null`, якщо файл відсутній чи TOML невалідний.
|
|
21
|
+
- **resolveWorkspaceMemberDirs** — перетворює workspace-патерни на список абсолютних каталогів із власним `Cargo.toml`, усуваючи дублікати.
|
|
22
|
+
- **isWorkspaceMemberDir** — визначає, чи належить конкретний каталог до workspace з урахуванням `members` і `exclude`.
|
|
23
|
+
- **findAncestorWorkspaceRoot** — шукає найближчий предок `Cargo.toml` з `[workspace]`, чий workspace покриває крейт; сам каталог крейту не перевіряє.
|
|
24
|
+
|
|
25
|
+
## Публічний API
|
|
26
|
+
|
|
27
|
+
- readCargoManifest — Розпарсений Cargo.toml або null (файл відсутній чи невалідний TOML).
|
|
28
|
+
- resolveWorkspaceMemberDirs — Резолвить `[workspace].members`/`.exclude`-патерни (літеральні шляхи й прості
|
|
29
|
+
glob з `*`) відносно `rootDir` у список абсолютних каталогів, що мають власний
|
|
30
|
+
Cargo.toml. Без повної Cargo glob-семантики — лише `*`-сегменти й літерали.
|
|
31
|
+
- isWorkspaceMemberDir — Чи покриває `[workspace].members` (мінус `.exclude`) конкретний каталог-крейт.
|
|
32
|
+
- findAncestorWorkspaceRoot — Йде від `dirname(crateDirAbs)` вгору по предках до `repoRootAbs` (включно),
|
|
33
|
+
шукаючи найближчий Cargo.toml з `[workspace]`, чиї `members` (мінус `exclude`)
|
|
34
|
+
покривають `crateDirAbs`. Не перевіряє сам `crateDirAbs` (виклик для нього — окремо).
|
|
35
|
+
|
|
36
|
+
## Гарантії поведінки
|
|
37
|
+
|
|
38
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
39
|
+
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
|
|
40
|
+
- За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
|
|
@@ -9,6 +9,7 @@ resource: npm/scripts/utils/
|
|
|
9
9
|
| [apply-to-files.mjs](apply-to-files.md) | JS Module |
|
|
10
10
|
| [ast-extract.mjs](ast-extract.md) | JS Module |
|
|
11
11
|
| [ast-scan-utils.mjs](ast-scan-utils.md) | JS Module |
|
|
12
|
+
| [cargo-workspace.mjs](cargo-workspace.md) | JS Module |
|
|
12
13
|
| [ensure-gitignore-entries.mjs](ensure-gitignore-entries.md) | JS Module |
|
|
13
14
|
| [find-package-json-paths.mjs](find-package-json-paths.md) | JS Module |
|
|
14
15
|
| [glob-compat.mjs](glob-compat.md) | JS Module |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-storybook
|
|
3
|
+
description: >-
|
|
4
|
+
Канон Storybook для Vue-компонентних бібліотек — прогін lint-поверхонь правила
|
|
5
|
+
storybook (scope/scaffold/vitest-config/hygiene) по поточному репо, і окремий
|
|
6
|
+
--adopt режим для пакетів із уже наявним ручним Storybook (діагностика diff
|
|
7
|
+
проти канону без сліпого перезапису)
|
|
8
|
+
version: '1.0'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# n-storybook — канон Storybook для Vue-компонентних бібліотек
|
|
12
|
+
|
|
13
|
+
Джерело рішення: `docs/adr/канон-storybook-для-vue-компонентних-бібліотек.md`. Уся логіка канону
|
|
14
|
+
(скоуп, скафолд, vitest-конфіг, гігієна залежностей, adopt-діагностика) живе в правилі `storybook`
|
|
15
|
+
пакета `@7n/rules-lang-js` (`node_modules/@7n/rules-lang-js/rules/storybook/`) — цей скіл лише
|
|
16
|
+
тонка обгортка запуску, за зразком `doc-files`.
|
|
17
|
+
|
|
18
|
+
## Передумови
|
|
19
|
+
|
|
20
|
+
- Правило `storybook` увімкнене у `.n-rules.json` (`"storybook"` у `rules`) — інакше lint-поверхні
|
|
21
|
+
concern-ів не виконуються (правило `alwaysApply: false`, хвильовий rollout, ADR Decision Outcome).
|
|
22
|
+
- Плагін `@7n/rules-lang-js` встановлений (JS/Vue-екосистема) — сигнал автодетекту: кореневий
|
|
23
|
+
`package.json`.
|
|
24
|
+
|
|
25
|
+
## Звичайний запуск (rule увімкнене, без адопції)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @7n/rules lint storybook
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Прогонить усі lint-поверхні правила (`scope`, `scaffold`, `vitest-config`, `hygiene`) по всьому
|
|
32
|
+
репо. T0-autofix (`fixability: config` — детермінований, без LLM-ladder) відтворює канонічний
|
|
33
|
+
скафолд ЛИШЕ для секцій, яких немає ВЗАЄМНО (`.storybook/main.js`, `preview.js`,
|
|
34
|
+
`package.json#scripts.storybook`, `vitest.config`-projects, `vitest.stryker.config`) — з
|
|
35
|
+
`template/` правила. Наявний-але-розбіжний файл (маркер канону відсутній) — `fixability: config`
|
|
36
|
+
зупиняється після T0 без LLM-ladder (canon — одна правильна форма, вгадувати нема чого): звіт
|
|
37
|
+
покаже violation, ручне чи agent-виправлення за посиланням на `storybook.mdc`/`vitest-config.mdc`.
|
|
38
|
+
|
|
39
|
+
Звіт користувачу — стандартний вивід `n-rules lint`: список violations (якщо є) + підсумок
|
|
40
|
+
виправлених T0-патернів.
|
|
41
|
+
|
|
42
|
+
## `--adopt` — пакети з уже наявним ручним Storybook (ADR Кластер 8)
|
|
43
|
+
|
|
44
|
+
Для пакетів, де `.storybook/` вже існує (ручне впровадження ДО канону чи паралельно з ним) —
|
|
45
|
+
окремий діагностичний режим: diff по секціях проти канонічних `template/`
|
|
46
|
+
(`main.js`/`preview.js`/`mocks/gql-sse.js`/`package.json#scripts.storybook`/vitest
|
|
47
|
+
`test.projects`/`vitest.stryker.config`), **без сліпого перезапису** розбіжних файлів.
|
|
48
|
+
Автофікс (`--fix-missing`) генерує ЛИШЕ секції, яких немає ВЗАГАЛІ — розбіжні секції завжди
|
|
49
|
+
йдуть як інструкція для агента/людини, ніколи не переписуються автоматично.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Діагностика всіх пакетів у скоупі (без запису)
|
|
53
|
+
bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs
|
|
54
|
+
|
|
55
|
+
# + генерація повністю відсутніх секцій (main.js/preview.js/mocks/scripts/vitest-конфіги
|
|
56
|
+
# лишаються недоторканими, якщо вже існують хоч у якомусь вигляді)
|
|
57
|
+
bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs --fix-missing
|
|
58
|
+
|
|
59
|
+
# Звузити діагностику до конкретних пакетів (root dir, той самий формат що storybook.optOut)
|
|
60
|
+
bun node_modules/@7n/rules-lang-js/rules/storybook/adopt/main.mjs --fix-missing packages/ui packages/legacy-ui
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Прогін через прямий виклик скрипта плагіна (`node_modules/@7n/rules-lang-js/...`) — CLI-плюмбінг
|
|
64
|
+
окремого прапорця `--adopt` у ядро `n-rules.js` для однієї команди одного правила визнано
|
|
65
|
+
надлишковим (нема інших concern-ів з подібним diagnostic-only режимом; якщо з'явиться другий
|
|
66
|
+
кандидат — тоді вартий узагальнення на рівні ядра).
|
|
67
|
+
|
|
68
|
+
### Формат звіту
|
|
69
|
+
|
|
70
|
+
Для кожного пакета — статус (`canonical` / `missing-files` / `differs` / `broken`) і розклад
|
|
71
|
+
по секціях (`match` / `differ` / `missing`, з поясненням для `differ`). При `--fix-missing` —
|
|
72
|
+
перелік згенерованих файлів. Приклад одного пакета:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
⚠️ [packages/legacy-ui] differs
|
|
76
|
+
✗ main.js (packages/legacy-ui/.storybook/main.js): differ — бракує: viteFinal-override vite.config пакета
|
|
77
|
+
+ preview.js (packages/legacy-ui/.storybook/preview.js): missing
|
|
78
|
+
✗ package.json#scripts.storybook (packages/legacy-ui/package.json): differ — зараз 'storybook dev', канон '...'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Прочитай звіт користувачу: скільки пакетів канонічні, скільки мають лише прогалини (безпечно
|
|
82
|
+
`--fix-missing`), скільки мають розбіжності (потребують ручного рішення — мігрувати секцію на
|
|
83
|
+
канон чи свідомо лишити виняток), скільки зламані (circuit breaker нижче).
|
|
84
|
+
|
|
85
|
+
## Circuit breaker (ADR)
|
|
86
|
+
|
|
87
|
+
Якщо діагностика чи фікс одного пакета кидає виняток (пошкоджений файл, directory замість файлу
|
|
88
|
+
тощо) — цей пакет позначається `status: 'broken'` з текстом помилки, а решта пакетів прогону
|
|
89
|
+
обробляються далі. Один зламаний пакет ніколи не валить увесь `--adopt`-прогін.
|
|
90
|
+
|
|
91
|
+
## Rollout-порядок (ADR Кластер 8)
|
|
92
|
+
|
|
93
|
+
Пілот на одному репо перед глобальним увімкненням правила; серед пакетів одного репо — малі
|
|
94
|
+
спершу (менше `.vue`-файлів → менший blast radius одного скафолд-фейлу). Це організаційна
|
|
95
|
+
рекомендація для агента/людини, що вмикає правило — скіл сам не має списку "усіх репо
|
|
96
|
+
організації", тому не автоматизує послідовність.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "auto": ["storybook"], "worktree": true }
|