@nitra/cursor 1.9.1 → 1.9.3

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 CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.9.3] - 2026-05-11
8
+
9
+ ### Fixed
10
+
11
+ - **k8s — `pathHasK8sSegment` тепер відносно кореня репо; `.github/` явно поза скоупом:** функція `pathHasK8sSegment(filePath)` у `npm/scripts/check-k8s.mjs` та `npm/scripts/run-k8s.mjs` розбивала **абсолютний** шлях і шукала компонент `k8s`. У проєктах, де сам корінь репо називається `k8s/` (напр. `/Users/.../abie/k8s/`), сегмент `k8s` присутній в абсолютному шляху **усіх** файлів — і весь репозиторій, включно з `.github/workflows/*.yml`, потрапляв у `findK8sYamlFiles` як k8s-маніфести, після чого `checkK8sYamlFile` падав на «розширення .yml — перейменуй на .yaml» (територія `ga.mdc`, де канон протилежний). Виправлено: (1) сигнатура тепер `pathHasK8sSegment(filePath, root?)` — коли `root` передано, шлях спершу нормалізується через `node:path` `relative(root, filePath)`, і компоненти беруться **відносно кореня** (порожній relative — це сам root, повертає false); (2) `findK8sYamlFiles` у `check-k8s.mjs` і `check-abie.mjs`, а також `findK8sRoots` у `run-k8s.mjs` тепер передають `root` і додатково мають defense-in-depth ранній `return` для шляхів, що починаються з `.github/`; (3) `k8s.mdc` явно фіксує: правило стосується каталогів `k8s` відносно кореня; `.github/workflows/` і `.github/actions/` — поза скоупом (їх веде `ga.mdc`). Без `root` (юніт-тести з відносним шляхом) функція веде себе як раніше. Додано тести у `tests/check-k8s-schema.test.mjs` (worst-case з префіксом `/home/test/some/k8s/`) і `tests/run-k8s-roots.test.mjs` (інтеграційний — `findK8sRoots` у репі, корінь якого називається `k8s/`).
12
+
13
+ ## [1.9.2] - 2026-05-11
14
+
15
+ ### Changed
16
+
17
+ - **k8s — modeline `$schema` тепер опційний; `file:…` заборонено як плейсхолдер:** правило `k8s.mdc` уточнено — рядок `# yaml-language-server: $schema=…` обов'язковий **лише** коли для поєднання `apiVersion`/`kind` існує надійна публічна схема (kustomization / yannh / datree CRDs-catalog). Якщо публічної схеми немає, modeline **не додається зовсім** (раніше п. 5 розділу «Визначення схеми YAML» допускав `file:` за узгодженням — це створювало фальшиву видимість валідації, а автовиправлення n-fix залишало плейсхолдер `# yaml-language-server: $schema=file:.`). У `check-k8s.mjs`: (1) файли без modeline більше не падають як «перший рядок має бути коментарем», натомість `pass` із позначкою «без modeline — перевірка $schema пропущена»; (2) `$schema=file:…`тепер реєструється як помилка з підказкою прибрати modeline; (3) modeline нижче першого рядка все ще порушення; (4)`HttpBackendGroup`(Yandex ALB) як виняток без modeline залишається без змін.`lint-k8s`(kubeconform з прапорцем ignore-missing-schemas) продовжує покривати валідацію і для файлів без modeline. JSDoc на початку`check-k8s.mjs` оновлено.
18
+
7
19
  ## [1.9.1] - 2026-05-11
8
20
 
9
21
  ### Added
package/mdc/k8s.mdc CHANGED
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
3
- version: '1.27'
3
+ version: '1.29'
4
4
  globs: "**/k8s/**/*.yaml"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
8
  # Kubernetes YAML у шляхах з `k8s`
9
9
 
10
- Для кожного файлу `*.yaml`, у шляху якого є сегмент директорії **`k8s`** (наприклад `site/k8s/base/deployment.yaml`), **перший рядок** — коментар-директива для [YAML Language Server](https://github.com/redhat-developer/yaml-language-server):
10
+ Для кожного файлу `*.yaml`, у шляху якого є сегмент директорії **`k8s`** (наприклад `site/k8s/base/deployment.yaml`), якщо існує **публічна** схема (kustomization / yannh / datree CRDs-catalog — див. «Визначення схеми YAML»), **перший рядок** — коментар-директива для [YAML Language Server](https://github.com/redhat-developer/yaml-language-server) з URL за `https://`:
11
11
 
12
12
  ```yaml
13
13
  # yaml-language-server: $schema=https://...
@@ -15,10 +15,14 @@ alwaysApply: false
15
15
 
16
16
  Далі — вміст маніфесту. Зайвий порожній рядок між коментарем і YAML не додавай, якщо в проєкті не прийнято інше.
17
17
 
18
- **Винятокбез modeline:** `apiVersion: alb.yc.io/v1alpha1`, `kind: HttpBackendGroup` (Yandex ALB) рядка **`# yaml-language-server: $schema=…`** у файлі **не** має бути (ні в першому рядку, ні далі). Перший рядокодразу YAML (`apiVersion:` тощо). Перевірка **`check-k8s.mjs`**.
18
+ **Modelineопційний:** якщо для конкретного поєднання `apiVersion`/`kind` **немає** надійної публічної схеми (yannh/datree/schemastore не покривають), залиш файл **без** рядка `# yaml-language-server: $schema=…`. **Заборонено** ставити `$schema=file:…` як заглушку це створює видимість валідації без неї. Без modeline `check-k8s` не валідує URL, але **`lint-k8s`** (kubeconform/kubescape) продовжить роботу. Якщо modeline присутній він обов'язково перший рядок і **тільки** `https://` URL, який відповідає очікуваному за `apiVersion`/`kind`.
19
+
20
+ **Виняток — modeline заборонено:** `apiVersion: alb.yc.io/v1alpha1`, `kind: HttpBackendGroup` (Yandex ALB) — рядка **`# yaml-language-server: $schema=…`** у файлі **не** має бути (ні в першому рядку, ні далі). Перший рядок — одразу YAML (`apiVersion:` тощо). Перевірка — **`check-k8s.mjs`**.
19
21
 
20
22
  **Розширення:** усі маніфести під **`k8s`**, включно з **`kustomization.yaml`**, — лише **`.yaml`** (розширення **`.yml`** не використовуй).
21
23
 
24
+ **Скоп — поза `.github/`:** правило стосується YAML-маніфестів у каталогах **`k8s`** (визначаються відносно кореня репо, не за абсолютним шляхом). Файли під **`.github/workflows/`** та **`.github/actions/`** ця перевірка **не** зачіпає — їхній скоп визначає **`ga.mdc`** (там канон — **`.yml`**). Це робить два правила несуперечливими навіть у проєктах, де сам корінь репо випадково має ім'я `k8s/` (тоді сегмент `k8s` присутній у абсолютному шляху всіх файлів, але **відносно кореня** його там немає).
25
+
22
26
  **Dockerfile / hadolint** — окреме правило **`docker.mdc`** і **`npx @nitra/cursor check docker`**.
23
27
 
24
28
  ## lint-k8s: kubeconform і kubescape
@@ -695,7 +699,7 @@ patch: |-
695
699
  # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
696
700
  ```
697
701
 
698
- 5. **Немає надійного публічного URL** — не вигадуй: залиш коректний `$schema` або `file:` за узгодженням.
702
+ 5. **Немає надійного публічного URL** — не вигадуй URL і **не** використовуй `$schema=file:…` як заглушку (фальшива валідація). Залиш файл **без** рядка `# yaml-language-server: $schema=…` зовсім — `check-k8s` пропустить перевірку URL для таких файлів, а `lint-k8s` (kubeconform з `-ignore-missing-schemas`) усе ще покриє валідацію.
699
703
 
700
704
  ## Багатодокументні YAML
701
705
 
package/mdc/tauri.mdc CHANGED
@@ -14,7 +14,6 @@ version: '1.0'
14
14
  }
15
15
  ```
16
16
 
17
-
18
17
  ## Перевірка
19
18
 
20
- `npx @nitra/cursor check tauri`
19
+ `npx @nitra/cursor check tauri`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -323,7 +323,12 @@ async function findK8sYamlFiles(root, ignorePaths = []) {
323
323
  await walkDir(
324
324
  root,
325
325
  p => {
326
- if (!pathHasK8sSegment(p)) {
326
+ const rel = relative(root, p).replaceAll('\\', '/')
327
+ // `.github/` належить `ga.mdc`; check-abie не зачіпає workflow-файли.
328
+ if (rel.startsWith('.github/')) {
329
+ return
330
+ }
331
+ if (!pathHasK8sSegment(p, root)) {
327
332
  return
328
333
  }
329
334
  if (!YAML_EXTENSION_RE.test(p)) {
@@ -760,9 +765,9 @@ async function collectDeploymentDirs(root, yamlAbs, fail) {
760
765
  * @param {string[]} yamlFilesAbs yaml під k8s
761
766
  * @param {(msg: string) => void} fail callback
762
767
  * @param {(msg: string) => void} passFn успішне повідомлення
763
- * @returns {Promise<void>}
768
+ * @returns {void}
764
769
  */
765
- async function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
770
+ function ensureAbieBaseDeploymentPreemNodeSelector(root, yamlFilesAbs, fail, passFn) {
766
771
  const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
767
772
  if (baseFiles.length === 0) {
768
773
  passFn('Немає файлів у шляхах …/base/… — перевірку preem у base пропущено')
@@ -1425,9 +1430,9 @@ async function checkHttpRouteKustomization(abs, rel, mode, root, yamlFilesAbs, c
1425
1430
  * @param {string[]} yamlFilesAbs yaml під k8s
1426
1431
  * @param {(msg: string) => void} fail callback при помилці
1427
1432
  * @param {(msg: string) => void} passFn callback при успішній перевірці
1428
- * @returns {Promise<void>}
1433
+ * @returns {void}
1429
1434
  */
1430
- async function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
1435
+ function ensureAbieBaseHttpRouteHostnames(root, yamlFilesAbs, fail, passFn) {
1431
1436
  const baseFiles = yamlFilesAbs.filter(abs => isAbieK8sBaseYamlPath(relative(root, abs).replaceAll('\\', '/') || abs))
1432
1437
  if (baseFiles.length === 0) {
1433
1438
  passFn('Немає файлів у шляхах …/k8s/base/… — перевірку HTTPRoute hostnames пропущено')
@@ -1531,8 +1536,9 @@ async function ensureNoFirebaseHostingArtifacts(root, passFn, failFn) {
1531
1536
  * @param {string} root корінь репозиторію
1532
1537
  * @param {(msg: string) => void} pass callback при успішній перевірці
1533
1538
  * @param {(msg: string) => void} fail callback при помилці
1539
+ * @returns {void}
1534
1540
  */
1535
- async function checkCleanMergedBranch(root, pass, fail) {
1541
+ function checkCleanMergedBranch(root, pass, fail) {
1536
1542
  const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
1537
1543
  if (!existsSync(cleanMergedPath)) {
1538
1544
  fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
@@ -365,9 +365,9 @@ const GA_PER_WORKFLOW_REGO_TARGETS = [
365
365
  * @param {string[]} ymlWorkflows відносні (від `wfDir`) імена файлів `*.yml`
366
366
  * @param {(msg: string) => void} pass callback при успішній перевірці
367
367
  * @param {(msg: string) => void} fail callback при помилці
368
- * @returns {Promise<void>}
368
+ * @returns {void}
369
369
  */
370
- async function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
370
+ function runAllGaRego(wfDir, ymlWorkflows, pass, fail) {
371
371
  for (const target of GA_PER_WORKFLOW_REGO_TARGETS) {
372
372
  if (!existsSync(target.workflow)) continue
373
373
  const violations = runConftestBatch({
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Перевіряє Kubernetes YAML у шляхах з сегментом `k8s` (див. k8s.mdc).
3
3
  *
4
- * Перший рядок `# yaml-language-server: $schema=…`, без дублікатів, розширення `.yaml`
4
+ * Перший рядок `# yaml-language-server: $schema=…` (URL за `https://`), без дублікатів, розширення `.yaml`
5
5
  * (окрім `kustomization.yaml`); URL схеми за першим документом — kustomization / yannh / datree
6
6
  * (**виняток:** `apiVersion: alb.yc.io/v1alpha1`, `kind: HttpBackendGroup` — рядка `# yaml-language-server:` у файлі бути не має).
7
7
  * (datree за замовчуванням: GitHub Pages `https://datreeio.github.io/CRDs-catalog/…`).
8
8
  *
9
+ * Modeline **опційний**: якщо публічної схеми немає (yannh/datree/schemastore не покривають це поєднання
10
+ * apiVersion/kind), залиш файл **без** рядка `# yaml-language-server: $schema=…` — `check-k8s` пропустить
11
+ * перевірку URL. **Заборонено** ставити `$schema=file:…` як заглушку (це фальшива валідація). Якщо modeline
12
+ * присутній, він має бути **першим рядком** і містити `https://` URL, що відповідає очікуваному за apiVersion/kind.
13
+ *
9
14
  * Додатково: у кожному YAML-документі з **`kind: Deployment`** у кожного контейнера
10
15
  * **`spec.template.spec.containers[]`** має бути **`resources.requests.cpu`** і **`resources.requests.memory`**
11
16
  * (непорожні скаляри). У шарі **`…/k8s/…/base/…`** значення жорстко **`cpu: '0.02'`**, **`memory: '128Mi'`**
@@ -344,11 +349,23 @@ const BATCH_V1BETA1_API_VERSION_LINE_RE = /^(\s*apiVersion:\s*)["']?batch\/v1bet
344
349
 
345
350
  /**
346
351
  * Чи містить шлях сегмент директорії `k8s` (рівно ця назва компонента).
347
- * @param {string} filePath шлях до файлу
348
- * @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
349
- */
350
- export function pathHasK8sSegment(filePath) {
351
- const parts = filePath.split(PATH_SPLIT_RE)
352
+ *
353
+ * Якщо передано `root`, перевірка ведеться **відносно** кореня репо інакше випадає
354
+ * false-positive, коли сам корінь репо вже містить компонент `k8s` (напр.
355
+ * `/Users/.../abie/k8s/`): без relativize функція б повертала true для **усіх** файлів
356
+ * у проєкті, включно з `.github/workflows/*.yml`, які належать іншому правилу (`ga.mdc`).
357
+ *
358
+ * Без `root` (як у юніт-тестах або коли шлях уже відносний) спрацьовує старий шлях:
359
+ * розбиття за `/`/`\` і пошук компонента `k8s`.
360
+ * @param {string} filePath абсолютний або відносний шлях до файлу
361
+ * @param {string} [root] корінь репо для relativize (типово — без relativize)
362
+ * @returns {boolean} true, якщо серед компонентів шляху **відносно root** є каталог `k8s`
363
+ */
364
+ export function pathHasK8sSegment(filePath, root) {
365
+ const target = root ? relative(root, filePath).replaceAll('\\', '/') : filePath
366
+ // Порожній relative означає сам root — у ньому компонента `k8s` відносно себе немає.
367
+ if (target === '') return false
368
+ const parts = target.split(PATH_SPLIT_RE)
352
369
  return parts.includes('k8s')
353
370
  }
354
371
 
@@ -1718,7 +1735,11 @@ async function findK8sYamlFiles(root, ignorePaths = []) {
1718
1735
  await walkDir(
1719
1736
  root,
1720
1737
  p => {
1721
- if (!pathHasK8sSegment(p)) return
1738
+ const rel = relative(root, p).replaceAll('\\', '/')
1739
+ // `.github/` належить правилу `ga.mdc` (там канон — `.yml`); не зачіпай тут навіть
1740
+ // якщо `pathHasK8sSegment` колись зіб'ється на крайовому кейсі.
1741
+ if (rel.startsWith('.github/')) return
1742
+ if (!pathHasK8sSegment(p, root)) return
1722
1743
  if (!YAML_EXTENSION_RE.test(p)) return
1723
1744
  out.push(p)
1724
1745
  },
@@ -2885,18 +2906,11 @@ export function collectGatewayApiRouteBackendRefsWithRedundantNamespace(spec, ro
2885
2906
  return out
2886
2907
  }
2887
2908
 
2888
- /**
2889
- * Один документ: маршрут Gateway API має посилатися на **Service** з суфіксом **`-hl`**;
2890
- * у **`backendRef`** не має дублюватися **`namespace`**, що збігається з **`metadata.namespace`** маршруту.
2891
- * @param {string} rel відносний шлях до файлу
2892
- * @param {number} docIndex 1-based індекс документа
2893
- * @param {Record<string, unknown>} rec корінь маніфесту
2894
- * @param {(msg: string) => void} fail callback помилки
2895
- * @returns {void}
2896
- */
2897
2909
  // Plan B: Gateway API маршрут backendRef з суфіксом `-hl` і redundant namespace —
2898
2910
  // у rego-пакеті `k8s.gateway`, виклик через `runAllK8sRego`. JS-функції
2899
- // failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody видалено.
2911
+ // failIfGatewayRouteUsesNonHeadlessService, scanGatewayApiRouteBackendRefsInYamlBody видалено;
2912
+ // JSDoc-блок для них прибрано (eslint-plugin-jsdoc trip-ив на «orphan» JSDoc, який
2913
+ // прилипав до asPlainRecord як другий @returns).
2900
2914
 
2901
2915
  /**
2902
2916
  * Звузити `unknown` до `Record<string, unknown>` (`null`, масиви, примітиви → null).
@@ -3547,13 +3561,12 @@ function countSchemaModelines(lines) {
3547
3561
  return lines.filter(l => OXLINT_SCHEMA_MODELINE_RE.test(l.trim())).length
3548
3562
  }
3549
3563
 
3550
-
3551
3564
  /**
3552
3565
  * Файл з першим документом **HttpBackendGroup** (ALB Yandex): без modeline **$schema**.
3553
3566
  * @param {string} rel відносний шлях
3554
- * @param {string} baseLower basename
3555
- * @param {string[]} lines рядки файлу
3556
- * @param {(msg: string) => void} fail реєстрація помилки
3567
+ * @param {string} _baseLower basename (лишений для уніфікованої сигнатури `checkK8sYamlFile*`)
3568
+ * @param {string[]} _lines рядки файлу (лишені з тієї ж причини)
3569
+ * @param {(msg: string) => void} _fail реєстрація помилки (rego гейтує per-document)
3557
3570
  * @param {(msg: string) => void} pass реєстрація успіху
3558
3571
  * @returns {void}
3559
3572
  */
@@ -3593,8 +3606,13 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
3593
3606
  // topologySpread, HCP, svc/svc-hl) — делегована rego, виконано у `runAllK8sRego` вище.
3594
3607
 
3595
3608
  if (schemaUrl.startsWith('file:')) {
3596
- pass(`${rel}: локальна схема (file:) — перевірка URL за apiVersion/kind пропущена`)
3597
- } else if (HTTPS_SCHEMA_RE.test(schemaUrl)) {
3609
+ fail(
3610
+ `${rel}: $schema=file:… заборонено (фальшива валідація без публічної схеми). ` +
3611
+ `Якщо публічної схеми для цього apiVersion/kind немає — прибери modeline зовсім (k8s.mdc)`
3612
+ )
3613
+ return
3614
+ }
3615
+ if (HTTPS_SCHEMA_RE.test(schemaUrl)) {
3598
3616
  const doc = firstYamlDocument(body)
3599
3617
  const { expected, reason } = expectedSchemaUrl(abs, doc)
3600
3618
 
@@ -3610,7 +3628,9 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
3610
3628
 
3611
3629
  pass(`${rel}: $schema узгоджено (${reason})`)
3612
3630
  } else {
3613
- fail(`${rel}: $schema має бути https URL або file: (див. k8s.mdc)`)
3631
+ fail(
3632
+ `${rel}: $schema має бути https URL (file: і інші схеми заборонені — якщо публічної схеми немає, прибери modeline; k8s.mdc)`
3633
+ )
3614
3634
  }
3615
3635
  }
3616
3636
 
@@ -3641,12 +3661,7 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
3641
3661
  }
3642
3662
 
3643
3663
  const lines = toLines(raw)
3644
- if (lines.length === 0 || lines[0].trim() === '') {
3645
- fail(`${rel}: перший рядок порожній — потрібен # yaml-language-server: $schema=…`)
3646
- return
3647
- }
3648
-
3649
- const firstLineIsModeline = MODELINE_RE.test(lines[0])
3664
+ const firstLineIsModeline = lines.length > 0 && MODELINE_RE.test(lines[0])
3650
3665
  const bodyForFirstDoc = k8sYamlBodyForDocumentParse(lines)
3651
3666
  const isAlbHttpBackendGroup = k8sYamlFirstDocIsAlbYcHttpBackendGroup(bodyForFirstDoc)
3652
3667
 
@@ -3668,7 +3683,14 @@ async function checkK8sYamlFile(abs, root, fail, pass) {
3668
3683
  }
3669
3684
 
3670
3685
  if (!firstLineIsModeline) {
3671
- fail(`${rel}: перший рядок має бути коментарем # yaml-language-server: $schema=<url> (без префіксів перед #)`)
3686
+ // Modeline опційний: дозволено, якщо публічної схеми для apiVersion/kind немає (k8s.mdc).
3687
+ // Але `# yaml-language-server: $schema=…` дозволено **лише** у першому рядку — якщо він
3688
+ // зустрічається нижче, це порушення (yaml-language-server чекає на нього у заголовку файлу).
3689
+ if (countSchemaModelines(lines) > 0) {
3690
+ fail(`${rel}: рядок # yaml-language-server: $schema=… має бути першим у файлі (без префіксів перед #; k8s.mdc)`)
3691
+ return
3692
+ }
3693
+ pass(`${rel}: без modeline — перевірка $schema пропущена (немає публічної схеми; k8s.mdc)`)
3672
3694
  return
3673
3695
  }
3674
3696
 
@@ -5949,6 +5971,10 @@ function runAllK8sRego(root, yamlFiles, fail) {
5949
5971
  }
5950
5972
  }
5951
5973
 
5974
+ /**
5975
+ * Точка входу `check k8s`: повний набір перевірок маніфестів і структури `…/k8s` (див. JSDoc на початку файлу).
5976
+ * @returns {Promise<number>} `process.exitCode`: 0 при успіху, 1 при будь-якому `fail(...)`
5977
+ */
5952
5978
  export async function check() {
5953
5979
  const reporter = createCheckReporter()
5954
5980
  const { pass, fail } = reporter
@@ -13,7 +13,7 @@
13
13
  * Kubescape не має аналога цього прапорця; орієнтир цільового кластера — та сама лінія релізу (див. k8s.mdc).
14
14
  */
15
15
  import { spawnSync } from 'node:child_process'
16
- import { basename, dirname } from 'node:path'
16
+ import { basename, dirname, relative } from 'node:path'
17
17
 
18
18
  import { isRunAsCli } from './cli-entry.mjs'
19
19
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
@@ -31,12 +31,19 @@ const DATREE_CRD_SCHEMA_LOCATION =
31
31
  'https://datreeio.github.io/CRDs-catalog/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
32
32
 
33
33
  /**
34
- * Чи містить шлях сегмент директорії `k8s`.
35
- * @param {string} filePath шлях до файлу
36
- * @returns {boolean} true, якщо серед компонентів шляху є каталог `k8s`
34
+ * Чи містить шлях сегмент директорії `k8s` (відносно `root`, якщо передано).
35
+ *
36
+ * Якщо корінь репо сам має компонент `k8s` (напр. `/Users/.../abie/k8s/`), без relativize
37
+ * функція повертала б true для **усіх** файлів проєкту — включно з `.github/workflows/*.yml`,
38
+ * які належать `ga.mdc`. Передавай `root` у викликах з walkDir щоб уникнути false-positive.
39
+ * @param {string} filePath абсолютний або відносний шлях до файлу
40
+ * @param {string} [root] корінь репо для relativize (типово — без relativize)
41
+ * @returns {boolean} true, якщо серед компонентів шляху **відносно root** є каталог `k8s`
37
42
  */
38
- export function pathHasK8sSegment(filePath) {
39
- const parts = filePath.split(PATH_SEPARATOR_RE)
43
+ export function pathHasK8sSegment(filePath, root) {
44
+ const target = root ? relative(root, filePath).replaceAll('\\', '/') : filePath
45
+ if (target === '') return false
46
+ const parts = target.split(PATH_SEPARATOR_RE)
40
47
  return parts.includes('k8s')
41
48
  }
42
49
 
@@ -68,7 +75,10 @@ export async function findK8sRoots(root, ignorePaths = []) {
68
75
  await walkDir(
69
76
  root,
70
77
  p => {
71
- if (!pathHasK8sSegment(p)) return
78
+ const rel = relative(root, p).replaceAll('\\', '/')
79
+ // `.github/` належить `ga.mdc`; kubeconform/kubescape там не запускаємо.
80
+ if (rel.startsWith('.github/')) return
81
+ if (!pathHasK8sSegment(p, root)) return
72
82
  if (!YAML_EXT_RE.test(p)) return
73
83
  const k8sRoot = k8sRootFromFile(p)
74
84
  if (k8sRoot) roots.add(k8sRoot)
@@ -30,7 +30,7 @@ const POLICY_ROOT = join(PACKAGE_ROOT, 'policy')
30
30
  /**
31
31
  * Друкує install-hint для conftest і кидає виняток, щоб викликана `check-*`
32
32
  * команда ясно завершилась з кодом 1.
33
- * @returns {never}
33
+ * @returns {never} завжди кидає; для точки виклику — non-returning
34
34
  */
35
35
  function failConftestMissing() {
36
36
  throw new Error(
@@ -94,9 +94,7 @@ export function runConftestBatch(opts) {
94
94
  }
95
95
  // conftest exit 1 = є failures (це валідно для нас); >1 = справжня помилка.
96
96
  if (result.status !== 0 && result.status !== 1) {
97
- throw new Error(
98
- `conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`
99
- )
97
+ throw new Error(`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
100
98
  }
101
99
  /** @type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>} */
102
100
  let parsed