@nitra/cursor 1.8.228 → 1.9.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 CHANGED
@@ -4,6 +4,28 @@
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.1] - 2026-05-11
8
+
9
+ ### Added
10
+
11
+ - **rego `k8s.base_kustomization` — defense-in-depth deny на HPA/PDB у `base/kustomization.yaml::resources:`:** додано пер-документне правило, що відмовляє, якщо `resources:` локально містить запис із basename `hpa.yaml`/`pdb.yaml`/`hpa.yml`/`pdb.yml` (у будь-якому підкаталозі). Канон k8s.mdc — HPA/PDB у sibling `components/` (Kustomize Component) і підключаються з overlay. Рекурсивний обхід дерева `resources:`/`components:`/`bases:` (із зануренням у вкладені kustomization.yaml) лишається у JS-оркестраторі `verifyK8sBaseKustomizeHasNoHpaPdb` (потребує fs-доступу). Rego-deny ловить найпоширеніший локальний випадок навіть якщо JS-крок упаде з винятку раніше. 5 нових rego-тестів (`hpa.yaml`/`pdb.yaml`/`hpa.yml` у `resources:`, чистий `resources:`, lookalike basename `myhpa.yaml`); `opa test` зелений (10/10).
12
+
13
+ ### Fixed
14
+
15
+ - **`check-k8s.mjs`:** додано константу `GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'`. Її відсутність кидала `ReferenceError` у `indexOneK8sYamlForHasuraCanon` (на лінії з `av.startsWith(GATEWAY_API_GROUP_PREFIX)`), яку ловив outer try/catch у `bin/n-cursor.js` і **тихо пропускав** усі наступні JS-валідатори в `check-k8s.mjs::check()` — серед них `validateKustomizeHpaPdbOnlyWithBaseDeployment`, `validateConfigMapNameMatchesDeployment`, `validateDeploymentHpaPdbAndTopology`, `validateProdKustomizationOverrides`. Наслідок у репах споживачів: правило «HPA/PDB заборонені у `k8s/base/`» не спрацьовувало (хоча `verifyK8sBaseKustomizeHasNoHpaPdb` логіку містив правильну), бо exception вилітав раніше за чергу JS-кроків. Rego-крок (`runAllK8sRego`) ішов **до** crash-точки й тому продовжував працювати — пер-документні перевірки залишалися активними, а cross-file JS — ні.
16
+
17
+ ## [1.9.0] - 2026-05-11
18
+
19
+ ### Changed
20
+
21
+ - **mdc frontmatter — `alwaysApply: false` + `globs` для файлово-чітких правил:** `ga` (`.github/workflows/*.yml`), `vue` (`**/*.vue`), `php` (`**/*.php`), `style-lint` (`**/*.{css,scss,vue}`), `nginx-default-tpl` (`**/default.{conf.template,tpl.conf}`), `image-avif` (`**/*.{png,jpg,jpeg,gif,avif,vue,html}`), `image-compress` (`**/*.{png,jpg,jpeg,gif,svg}`), `changelog` (`**/{CHANGELOG.md,package.json}`), `hasura` (`**/hasura/**,**/*.env`), `graphql` (`**/*.{vue,js,mjs,cjs,ts,tsx,jsx}`). Раніше тільки `docker`, `k8s`, `rego` тримали file-scoped формат; решта вантажилася в контекст Cursor завжди (`alwaysApply: true`). Тепер правило підтягується лише коли в контексті є файл за патерном — менше «шуму» у промптах для несуміжних задач. Версії bump-нуто на патч-крок у кожному `*.mdc`. Проєктно-широкі правила (`bun`, `npm-module`, `ci4`, `text`, `js-lint`) і opt-in (`abie`, `adr`) лишилися `alwaysApply: true` без globs.
22
+
23
+ ## [1.8.229] - 2026-05-11
24
+
25
+ ### Removed
26
+
27
+ - **k8s / `k8s.kustomize_managed`:** правило «`metadata.namespace` заборонено у YAML, досяжних через граф Kustomize» зняте — воно конфліктувало з `k8s.base_manifest`, який натомість **вимагає** `metadata.namespace` у `…/k8s/base/…` для namespaced kind. Перетин предикатів був порожній, що давало ~50 хибних помилок у канонічних деревах `base + overlays` (adminer, run/nginx, reference-grant, otel, dremio, gateway тощо). Видалено: правило з `mdc/k8s.mdc` (бульйт «Де не дублювати `metadata.namespace`»), rego-полісь `npm/policy/k8s/kustomize_managed/`, JS-helpers `metadataNamespaceForbiddenViolation` і `collectKustomizeManagedRelPaths` разом з відповідними тестами та плумінгом `kustomizeManagedRel` через `runAllK8sRego` / `checkK8sYamlFile`. Логіка `base_manifest` (`metadata.namespace` обов'язковий у `k8s/base/`) лишається; у overlays Kustomize це значення буде перезаписано полем `namespace:` з `kustomization.yaml`.
28
+
7
29
  ## [1.8.228] - 2026-05-10
8
30
 
9
31
  ### Changed
package/mdc/changelog.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння
3
- alwaysApply: true
4
- version: '2.0'
3
+ version: '2.1'
4
+ globs: "**/{CHANGELOG.md,package.json}"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  Bun monorepo: у кожному workspace із кореневого `package.json.workspaces` (плюс кореневий пакет, плюс `npm/`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій. Правило `npm-module` відповідає лише за публікацію типів і workflow, а CHANGELOG — за цим правилом.
package/mdc/ci4.mdc CHANGED
@@ -15,6 +15,54 @@ C4-діаграми проєкту живуть у Markdown поряд із ко
15
15
  тримати знання разом із кодом — версійно, в одному PR і доступно для агентів. Якщо щось
16
16
  важливе про систему існує лише у Confluence/Notion/месенджері — для проєкту цього **немає**.
17
17
 
18
+ ## Специфікація як джерело істини (Spec-as-Source)
19
+
20
+ У AI-native розробці первинним артефактом, який підтримують інженери, є **специфікація**, а не
21
+ кодова база. Код — похідний артефакт, «будівельне риштування», яке агент генерує, верифікує або
22
+ повністю відновлює зі специфікації. Якщо поведінка системи має змінитися — **спочатку оновлюємо
23
+ специфікацію (C4/ADR/опис компонента), і лише потім** агент генерує відповідний код. Зворотний
24
+ порядок («код вже написаний, доку напишемо потім») перетворює специфікацію на декорацію.
25
+
26
+ ## Тест на повне відтворення (Rebuild Test)
27
+
28
+ Документація вважається повною лише тоді, коли проходить бінарний тест: якщо видалити теку
29
+ `src/`, відкрити нову LLM-сесію з чистим контекстом і дати агенту доступ лише до `.md`-файлів —
30
+ агент має **відтворити робочу кодову базу**. Архітектурні збої (агент не знає структуру тек),
31
+ інтеграційні (неописані міжсервісні контракти), падіння юніт-тестів (неописана бізнес-логіка) —
32
+ це не «складність задачі», а **прогалини документації**, які треба заповнити у тому ж PR.
33
+
34
+ ## Ефективність токенів і чистий Markdown
35
+
36
+ Вікно контексту LLM обмежене, а якість міркування деградує в міру його заповнення (ефект
37
+ «Lost in the Middle»). Тому документація **очищається від HTML-розмітки, CSS-класів,
38
+ навігаційних обгорток** і всього, що не несе семантичного навантаження. Чистий Markdown замість
39
+ HTML економить 80–90 % токенів (≈16 000 → 1 600 на сторінку), що прямо впливає на точність
40
+ згенерованого коду і знижує вартість API. Жодних `<div>`/`<span>`/класів у тілі `.md`/`.mdc`.
41
+
42
+ ## Контекстна незалежність розділів
43
+
44
+ RAG витягує **фрагменти**, не цілі документи. Тому кожен розділ має сенс **без сусіднього
45
+ тексту**. Заборонено: «як було згадано вище», «ця змінна», «попередній метод», «той самий
46
+ сервіс». Замість цього — щоразу явно повторюємо назву сутності: «автентифікація OAuth 2.0»,
47
+ «функція `calculateTotal()`», «контейнер `api-gateway`». Коли агент отримає лише цей фрагмент,
48
+ у нього має бути повний словник для коректної генерації.
49
+
50
+ ## Docs-as-Code
51
+
52
+ Документація живе у Git поруч із кодом, проходить **той самий Code Review**, версіонується і
53
+ автоматично перевіряється у CI (лінтери Markdown, валідатори посилань, `npx @nitra/cursor
54
+ check`). Биті посилання й документація, що «не компілюється», — **блокуючий баг**, не
55
+ косметика. Це продовження принципу «оновлення синхронно зі змінами» нижче: один PR — код +
56
+ схема + ADR + тести.
57
+
58
+ ## Трасування як документація недетермінованої поведінки
59
+
60
+ Для компонентів, де рішення приймає нейромережа або складна евристика, класичної форми
61
+ «вхід X → вихід Y» **недостатньо** — поведінка недетермінована. Джерелом істини стає
62
+ **пайплайн логування й трасування**: які інструменти використав агент, який контекст мав,
63
+ чому ухвалив це рішення. У C4-компоненті такого типу — посилання на дашборд/storage трасувань
64
+ обов'язкове нарівні з посиланнями на тести.
65
+
18
66
  ## Розташування
19
67
 
20
68
  C4-діаграми проєкту живуть у теці `docs/ci4/` — це **канонічне місце** для всіх рівнів
package/mdc/ga.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Правила форматів для .github/workflows
3
- alwaysApply: true
4
- version: '1.7'
3
+ version: '1.8'
4
+ globs: ".github/workflows/*.yml"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**, **`git-ai.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
package/mdc/graphql.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: GraphQL у коді (tagged template `gql`) — GraphQL Config і розширення VS Code
3
- alwaysApply: true
4
- version: '1.0'
3
+ version: '1.1'
4
+ globs: "**/*.{vue,js,mjs,cjs,ts,tsx,jsx}"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\`` для GraphQL-запиту), у **корені репозиторію** мають бути:
package/mdc/hasura.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Правила для директорії з hasura graphql-engine
3
- alwaysApply: true
4
- version: '1.0'
3
+ version: '1.1'
4
+ globs: "**/hasura/**,**/*.env"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  ## Підключення для оновлення метаданих у CI (Nitra та Abinbevefes)
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: AVIF-двійники для raster-зображень з ув'язуванням у .vue/.html
3
- alwaysApply: true
4
- version: '1.1'
3
+ version: '1.2'
4
+ globs: "**/*.{png,jpg,jpeg,gif,avif,vue,html}"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor check image-avif` — у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить три кроки в порядку:
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Оптимізація raster/SVG через @nitra/minify-image у локальному lint
3
- alwaysApply: true
4
- version: '1.1'
3
+ version: '1.2'
4
+ globs: "**/*.{png,jpg,jpeg,gif,svg}"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.3.1**) запускається через `npx` (як `markdownlint-cli2` у text.mdc) і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` — авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує окреме правило `image-avif` (`npx @nitra/cursor check image-avif`), яке заодно прибирає AVIF-сироти. Split-cache робить повторні прогони дешевими — і локально, і після `git clone`.
package/mdc/k8s.mdc CHANGED
@@ -313,9 +313,7 @@ data:
313
313
 
314
314
  - **`base/kustomization.yaml`:** поле **`namespace:`** має бути **непорожнім** (перевіряє **check k8s**, якщо файл є).
315
315
 
316
- - **Де не дублювати `metadata.namespace`:** у YAML, досяжних через **граф Kustomize** (шляхи з **`kustomization.yaml`**, як у логіці **`collectKustomizeManagedRelPaths`** / **check k8s**). **Namespace** задає **`namespace:`** у kustomization.
317
-
318
- - **Коли `metadata.namespace` обов’язковий у файлі:** YAML під **`k8s`**, який **не** в графі жодного kustomization — непорожній **`metadata.namespace`** для namespaced **kind** (винятки — кластерні **kind**, перелік **`CLUSTER_SCOPED_KINDS`** у **`check-k8s.mjs`**). Якщо namespace у маніфесті не потрібен — підключи файл через **`resources`** / **`patches`** тощо.
316
+ - **Коли `metadata.namespace` обов’язковий у файлі:** YAML під **`k8s`** непорожній **`metadata.namespace`** для namespaced **kind** (винятки кластерні **kind**, перелік **`CLUSTER_SCOPED_KINDS`** у **`check-k8s.mjs`**). У overlays Kustomize значення в маніфесті буде перезаписано полем **`namespace:`** з відповідного **`kustomization.yaml`**, тому в `base` пиши канонічний dev-namespace.
319
317
 
320
318
  - **Не додавай** окремі **patches** Kustomize, які лише змінюють **namespace**: **namespace** визначає Kustomize; у overlays додаткові зміни — без дублювання логіки **namespace**.
321
319
 
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  description: Правила nginx для статичних файлів
3
- version: '1.2'
3
+ version: '1.3'
4
+ globs: "**/default.{conf.template,tpl.conf}"
5
+ alwaysApply: false
4
6
  ---
5
7
 
6
8
  > **Автоматична міграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
package/mdc/php.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: PHP
3
- alwaysApply: true
4
- version: '1.0'
3
+ version: '1.1'
4
+ globs: "**/*.php"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  Весь код повинен відповідати PHP 8.5, перевіряти за допомогою PHPCompatibility, конвертувати за допомогою Rector.
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Правила стилів CSS та SCSS
3
- alwaysApply: true
4
- version: '1.2'
3
+ version: '1.3'
4
+ globs: "**/*.{css,scss,vue}"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  ## Генерація та редагування стилів (Cursor і інші агенти)
package/mdc/vue.mdc CHANGED
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  description: Vue
3
- alwaysApply: true
4
- version: '1.7'
3
+ version: '1.8'
4
+ globs: "**/*.vue"
5
+ alwaysApply: false
5
6
  ---
6
7
 
7
8
  # Vue 3 Composition API — правила для .cursorrules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.228",
3
+ "version": "1.9.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -34,7 +34,37 @@ deny contains base_namespace_required_msg if {
34
34
  trim_space(ns) == ""
35
35
  }
36
36
 
37
+ # HPA/PDB у base заборонені — канон k8s.mdc: тримати у sibling каталозі `components/`
38
+ # і підключати з overlay (`components: [- ../components]`). Цей deny — швидкий gate
39
+ # на *локальний* `resources:` base/kustomization.yaml (точне ім'я `hpa.yaml`/`pdb.yaml`,
40
+ # у будь-якому підкаталозі). Рекурсивний обхід `resources:`/`components:`/`bases:`
41
+ # (із зануренням у вкладені kustomization.yaml) — JS-оркестратор
42
+ # `verifyK8sBaseKustomizeHasNoHpaPdb` у `check-k8s.mjs` (потребує fs-доступу). Цей
43
+ # rego-deny — defense-in-depth: спрацює навіть якщо JS-крок упаде з винятку раніше.
44
+ deny contains hpa_pdb_in_base_resources_msg(r) if {
45
+ is_kustomization
46
+ some r in object.get(input, "resources", [])
47
+ is_string(r)
48
+ is_hpa_or_pdb_filename(r)
49
+ }
50
+
51
+ hpa_pdb_in_base_resources_msg(file) := sprintf(
52
+ concat("", [
53
+ "у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB заборонені у base, ",
54
+ "перенесіть у sibling каталог components/ і підключайте з overlay (k8s.mdc)",
55
+ ]),
56
+ [file],
57
+ )
58
+
37
59
  is_kustomization if {
38
60
  input.kind == "Kustomization"
39
61
  startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
40
62
  }
63
+
64
+ is_hpa_or_pdb_filename(p) if {
65
+ basename(p) in {"hpa.yaml", "pdb.yaml", "hpa.yml", "pdb.yml"}
66
+ }
67
+
68
+ basename(p) := parts[count(parts) - 1] if {
69
+ parts := split(p, "/")
70
+ }
@@ -34,3 +34,40 @@ test_allow_non_kustomization if {
34
34
  "metadata": {"name": "cm"},
35
35
  }
36
36
  }
37
+
38
+ base_kust_ok := object.union(base_kust, {"namespace": "dev"})
39
+
40
+ test_deny_hpa_yaml_in_resources if {
41
+ count(base_kustomization.deny) > 0 with input as object.union(
42
+ base_kust_ok,
43
+ {"resources": ["deployment.yaml", "hpa.yaml"]},
44
+ )
45
+ }
46
+
47
+ test_deny_pdb_yaml_in_resources if {
48
+ count(base_kustomization.deny) > 0 with input as object.union(
49
+ base_kust_ok,
50
+ {"resources": ["pdb.yaml"]},
51
+ )
52
+ }
53
+
54
+ test_deny_hpa_yml_in_subdir if {
55
+ count(base_kustomization.deny) > 0 with input as object.union(
56
+ base_kust_ok,
57
+ {"resources": ["nested/dir/hpa.yml"]},
58
+ )
59
+ }
60
+
61
+ test_allow_resources_without_hpa_pdb if {
62
+ count(base_kustomization.deny) == 0 with input as object.union(
63
+ base_kust_ok,
64
+ {"resources": ["deployment.yaml", "service.yaml", "configmap.yaml"]},
65
+ )
66
+ }
67
+
68
+ test_allow_lookalike_basename if {
69
+ count(base_kustomization.deny) == 0 with input as object.union(
70
+ base_kust_ok,
71
+ {"resources": ["myhpa.yaml", "pdb-extra.yaml"]},
72
+ )
73
+ }
@@ -117,11 +117,11 @@ is_kustomization if {
117
117
  }
118
118
 
119
119
  resources_present if {
120
- _ := input.resources
120
+ "resources" in object.keys(input)
121
121
  }
122
122
 
123
123
  patches_present if {
124
- _ := input.patches
124
+ "patches" in object.keys(input)
125
125
  }
126
126
 
127
127
  # Список непорожніх рядкових шляхів resources у порядку файлу (для повідомлення).
@@ -215,7 +215,8 @@ has_non_empty_cpu_request(container) if {
215
215
 
216
216
  # Чи у контейнера в реальності присутнє поле resources.requests.cpu (хай і порожнє).
217
217
  has_cpu_field(container) if {
218
- _ := container.resources.requests.cpu
218
+ requests := object.get(object.get(container, "resources", {}), "requests", {})
219
+ "cpu" in object.keys(requests)
219
220
  }
220
221
 
221
222
  # Чи у контейнера є непорожнє resources.requests.memory (рядок або число > 0).
@@ -233,7 +234,8 @@ has_non_empty_memory_request(container) if {
233
234
 
234
235
  # Чи у контейнера в реальності присутнє поле resources.requests.memory.
235
236
  has_memory_field(container) if {
236
- _ := container.resources.requests.memory
237
+ requests := object.get(object.get(container, "resources", {}), "requests", {})
238
+ "memory" in object.keys(requests)
237
239
  }
238
240
 
239
241
  # Чи рядок `image` посилається на репозиторій `hasura/graphql-engine` (з тегом
@@ -309,6 +309,8 @@ const YANNH_GROUPS = new Set([
309
309
  'storagemigration.k8s.io'
310
310
  ])
311
311
 
312
+ const GATEWAY_API_GROUP_PREFIX = 'gateway.networking.k8s.io/'
313
+
312
314
  const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
313
315
  const PATH_SPLIT_RE = /[/\\]/u
314
316
  const YAML_EXTENSION_RE = /\.ya?ml$/iu
@@ -360,18 +362,6 @@ export function isForbiddenK8sDevPath(rel) {
360
362
  return n.includes('/k8s/dev/')
361
363
  }
362
364
 
363
- /**
364
- * Відносний шлях від кореня репозиторію у вигляді з `/` (для множини kustomize).
365
- * @param {string} root корінь cwd
366
- * @param {string} abs абсолютний шлях
367
- * @returns {string | null} posix-відносний шлях або null, якщо поза root
368
- */
369
- function posixRelFromAbs(root, abs) {
370
- const r = (relative(root, abs) || abs).replaceAll('\\', '/')
371
- if (r.startsWith('..')) return null
372
- return r
373
- }
374
-
375
365
  /**
376
366
  * Вбудовані та поширені **кластерні** `kind`, для яких **`metadata.namespace`** не застосовується.
377
367
  * CRD з невідомим kind лишаються з вимогою namespace, якщо файл не в kustomization — за потреби додай path у `resources`.
@@ -887,104 +877,6 @@ async function validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
887
877
  }
888
878
  }
889
879
 
890
- /**
891
- * Збирає відносні шляхи (posix) до YAML, підключених до Kustomize з будь-якого **`kustomization.yaml`** під `k8s`.
892
- * Обходить **`resources`**, **`bases`**, **`components`**, **`crds`**, **`patches[].path`**, **`patchesStrategicMerge`**;
893
- * для каталогу з **`kustomization.yaml`** виконує рекурсивний обхід.
894
- * @param {string} root корінь репозиторію
895
- * @param {string[]} yamlFilesAbs відсортовані абсолютні шляхи до `*.yaml` / `*.yml` під k8s (для `.yml` check-k8s вимагає перейменувати на `.yaml`)
896
- * @returns {Promise<Set<string>>} множина відносних шляхів до керованих файлів
897
- */
898
- export async function collectKustomizeManagedRelPaths(root, yamlFilesAbs) {
899
- /** @type {Set<string>} */
900
- const managed = new Set()
901
- const kustomizationAbsList = yamlFilesAbs.filter(abs => {
902
- const b = basename(abs).toLowerCase()
903
- return b === 'kustomization.yaml'
904
- })
905
-
906
- /** @type {Set<string>} */
907
- const visitedKustomization = new Set()
908
-
909
- /**
910
- * @param {string} kustAbs абсолютний шлях до kustomization.yaml
911
- * @returns {Promise<void>}
912
- */
913
- async function walkKustomization(kustAbs) {
914
- const normKust = resolve(kustAbs)
915
- if (visitedKustomization.has(normKust)) return
916
- visitedKustomization.add(normKust)
917
-
918
- let raw
919
- try {
920
- raw = await readFile(normKust, 'utf8')
921
- } catch {
922
- return
923
- }
924
- const lines = toLines(raw)
925
- const body = yamlBodyAfterModeline(lines)
926
-
927
- /** @type {import('yaml').Document[] | undefined} */
928
- let docs
929
- try {
930
- docs = parseAllDocuments(body)
931
- } catch {
932
- return
933
- }
934
- const first = docs[0]?.toJSON()
935
- if (first === null || first === undefined || typeof first !== 'object' || Array.isArray(first)) return
936
-
937
- const kustDir = dirname(normKust)
938
- const pathRefs = pathsFromKustomizationObject(first)
939
-
940
- /**
941
- * @param {string} ref шлях з kustomization
942
- * @returns {Promise<void>}
943
- */
944
- async function handleKustomizeManagedPathRef(ref) {
945
- if (ref.includes('://')) {
946
- return
947
- }
948
- const resolved = resolve(kustDir, ref)
949
- let st
950
- try {
951
- st = await stat(resolved)
952
- } catch {
953
- st = undefined
954
- }
955
- if (!st) {
956
- return
957
- }
958
- if (st.isFile()) {
959
- if (YAML_EXTENSION_RE.test(resolved)) {
960
- const pr = posixRelFromAbs(root, resolved)
961
- if (pr !== null) {
962
- managed.add(pr)
963
- }
964
- }
965
- return
966
- }
967
- if (!st.isDirectory()) {
968
- return
969
- }
970
- const childK = existsSync(join(resolved, 'kustomization.yaml')) ? join(resolved, 'kustomization.yaml') : null
971
- if (childK !== null) {
972
- await walkKustomization(childK)
973
- }
974
- }
975
-
976
- for (const ref of pathRefs) {
977
- await handleKustomizeManagedPathRef(ref)
978
- }
979
- }
980
-
981
- for (const k of kustomizationAbsList) {
982
- await walkKustomization(k)
983
- }
984
-
985
- return managed
986
- }
987
-
988
880
  /**
989
881
  * Шляхи лише з полів ресурсів Kustomization (**без** patch-файлів).
990
882
  * @param {unknown} obj корінь першого документа Kustomization
@@ -1331,7 +1223,7 @@ async function kustomizationTreeHasDeploymentUnderK8sBase(kustAbs, rootNorm) {
1331
1223
 
1332
1224
  /**
1333
1225
  * Збирає дескриптори ресурсів з **`resources` / `bases` / `components` / `crds`** для одного дерева kustomization.
1334
- * Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок (як у **`collectKustomizeManagedRelPaths`**).
1226
+ * Повторний вхід у той самий **`kustomization.yaml`** дає порожній внесок.
1335
1227
  * @param {string} kustAbs абсолютний шлях до **kustomization.yaml**
1336
1228
  * @param {string} rootNorm нормалізований абсолютний корінь репозиторію
1337
1229
  * @param {Set<string>} visitedKustomization нормалізовані абсолютні шляхи відвіданих **kustomization.yaml**
@@ -3505,22 +3397,6 @@ async function validateHasuraHttpRouteCanon(root, yamlFiles, fail) {
3505
3397
  }
3506
3398
  }
3507
3399
 
3508
- /**
3509
- * Для маніфестів, **підключених** до Kustomize (шлях у `resources` / `patches` / …), **metadata.namespace** не додають.
3510
- * @param {unknown} manifest корінь YAML-документа
3511
- * @returns {string | null} текст порушення або null, якщо поля немає
3512
- */
3513
- export function metadataNamespaceForbiddenViolation(manifest) {
3514
- if (manifest === null || manifest === undefined || typeof manifest !== 'object' || Array.isArray(manifest))
3515
- return null
3516
- const rec = /** @type {Record<string, unknown>} */ (manifest)
3517
- const meta = rec.metadata
3518
- if (meta !== null && typeof meta === 'object' && !Array.isArray(meta) && 'namespace' in meta) {
3519
- return 'metadata.namespace заборонено — namespace задає kustomization.yaml (поле namespace); файл підключено через resources / patches / … (див. k8s.mdc)'
3520
- }
3521
- return null
3522
- }
3523
-
3524
3400
  /**
3525
3401
  * Вимагає непорожній **metadata.namespace** для namespaced-документів (крім кластерних kind).
3526
3402
  * @param {unknown} manifest корінь YAML-документа
@@ -3574,7 +3450,7 @@ export function isK8sBaseManifestYamlPath(rel, baseLower) {
3574
3450
 
3575
3451
  // Plan B: per-document валідаційне ядро для k8s YAML повністю в rego —
3576
3452
  // `k8s.manifest`, `k8s.gateway`, `k8s.svc_yaml`, `k8s.svc_hl_yaml`,
3577
- // `k8s.kustomize_managed`, `k8s.base_manifest`. Виклик через `runAllK8sRego`.
3453
+ // `k8s.base_manifest`. Виклик через `runAllK8sRego`.
3578
3454
  // JS-функції failIfK8sPolicyNamespaceRulesViolated, failIfK8sPolicyResourceRulesViolated,
3579
3455
  // validateK8sYamlPolicyDocuments видалено.
3580
3456
 
@@ -3679,13 +3555,12 @@ function countSchemaModelines(lines) {
3679
3555
  * @param {string[]} lines рядки файлу
3680
3556
  * @param {(msg: string) => void} fail реєстрація помилки
3681
3557
  * @param {(msg: string) => void} pass реєстрація успіху
3682
- * @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
3683
3558
  * @returns {void}
3684
3559
  */
3685
- function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass, _kustomizeManagedRel) {
3560
+ function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass) {
3686
3561
  // Per-document валідація (Ingress/autoscaling/v1 заборонено, Gateway API backendRef,
3687
- // metadata.namespace правила) — у rego (`k8s.manifest`, `k8s.gateway`, `k8s.kustomize_managed`,
3688
- // `k8s.base_manifest`), батч-виклик з `runAllK8sRego` на початку `check()`.
3562
+ // metadata.namespace правила) — у rego (`k8s.manifest`, `k8s.gateway`, `k8s.base_manifest`),
3563
+ // батч-виклик з `runAllK8sRego` на початку `check()`.
3689
3564
  pass(`${rel}: HttpBackendGroup (alb.yc.io/v1alpha1) — modeline $schema не застосовується (k8s.mdc)`)
3690
3565
  }
3691
3566
 
@@ -3697,10 +3572,9 @@ function checkK8sYamlHttpBackendGroupFile(rel, _baseLower, _lines, _fail, pass,
3697
3572
  * @param {string[]} lines рядки файлу
3698
3573
  * @param {(msg: string) => void} fail реєстрація помилки
3699
3574
  * @param {(msg: string) => void} pass реєстрація успіху
3700
- * @param {Set<string>} kustomizeManagedRel kustomize-managed шляхи
3701
3575
  * @returns {void}
3702
3576
  */
3703
- function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel) {
3577
+ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass) {
3704
3578
  const match = lines[0].match(MODELINE_RE)
3705
3579
  if (!match) {
3706
3580
  fail(`${rel}: некоректний modeline $schema у першому рядку`)
@@ -3746,10 +3620,9 @@ function checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pa
3746
3620
  * @param {string} root корінь репозиторію
3747
3621
  * @param {(msg: string) => void} fail реєстрація помилки
3748
3622
  * @param {(msg: string) => void} pass реєстрація успіху
3749
- * @param {Set<string>} kustomizeManagedRel відносні posix-шляхи з collectKustomizeManagedRelPaths
3750
3623
  * @returns {Promise<void>}
3751
3624
  */
3752
- async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
3625
+ async function checkK8sYamlFile(abs, root, fail, pass) {
3753
3626
  const rel = (relative(root, abs) || abs).replaceAll('\\', '/')
3754
3627
  const base = basename(abs)
3755
3628
  const baseLower = base.toLowerCase()
@@ -3790,7 +3663,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
3790
3663
  )
3791
3664
  return
3792
3665
  }
3793
- checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass, kustomizeManagedRel)
3666
+ checkK8sYamlHttpBackendGroupFile(rel, baseLower, lines, fail, pass)
3794
3667
  return
3795
3668
  }
3796
3669
 
@@ -3799,7 +3672,7 @@ async function checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel) {
3799
3672
  return
3800
3673
  }
3801
3674
 
3802
- checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass, kustomizeManagedRel)
3675
+ checkK8sYamlFileWithSchemaModeline(abs, rel, baseLower, lines, fail, pass)
3803
3676
  }
3804
3677
 
3805
3678
  /**
@@ -6030,19 +5903,18 @@ async function runKustomizationImagesCleanup(kustAbs, rel, fail, pass) {
6030
5903
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
6031
5904
  */
6032
5905
  /**
6033
- * Plan B (rego-authoritative): на початку `check()` батч-викликаємо всі 9 path-фільтрованих
6034
- * rego-пакетів з `npm/policy/k8s/` через `runConftestBatch`. Пакети hasura_configmap і
5906
+ * Plan B (rego-authoritative): на початку `check()` батч-викликаємо path-фільтровані
5907
+ * rego-пакети з `npm/policy/k8s/` через `runConftestBatch`. Пакети hasura_configmap і
6035
5908
  * hasura_httproute мають cross-file gating (паруються з Hasura-Deployment) — вони запускаються
6036
5909
  * з відповідних orchestrator-функцій (`validateHasuraConfigMapRemoteSchemaPermissions`,
6037
5910
  * `validateHasuraHttpRouteCanon`). Структурна частина HPA/PDB (`k8s.hpa_pdb`) тут на всіх yaml,
6038
5911
  * env-залежні межі min/maxReplicas і expected-name — JS-cross-file у `validateDeploymentHpaPdbAndTopology`.
6039
5912
  * @param {string} root корінь репозиторію (cwd)
6040
5913
  * @param {string[]} yamlFiles абсолютні шляхи знайдених *.yaml під `…/k8s/`
6041
- * @param {Set<string>} kustomizeManagedRel відносні posix-шляхи kustomize-managed файлів
6042
5914
  * @param {(msg: string) => void} fail callback при помилці
6043
5915
  * @returns {void}
6044
5916
  */
6045
- function runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail) {
5917
+ function runAllK8sRego(root, yamlFiles, fail) {
6046
5918
  const relOf = abs => relative(root, abs).replaceAll('\\', '/') || abs
6047
5919
 
6048
5920
  const allYaml = yamlFiles
@@ -6055,7 +5927,6 @@ function runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail) {
6055
5927
  if (!K8S_BASE_SEGMENT_RE.test(r)) return false
6056
5928
  return basename(p).toLowerCase() !== 'kustomization.yaml'
6057
5929
  })
6058
- const kustomizeManagedFiles = yamlFiles.filter(p => kustomizeManagedRel.has(relOf(p)))
6059
5930
 
6060
5931
  /** @type {Array<{ ns: string, dir: string, files: string[] }>} */
6061
5932
  const targets = [
@@ -6066,8 +5937,7 @@ function runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail) {
6066
5937
  { ns: 'k8s.svc_yaml', dir: 'k8s/svc_yaml', files: svcYaml },
6067
5938
  { ns: 'k8s.svc_hl_yaml', dir: 'k8s/svc_hl_yaml', files: svcHlYaml },
6068
5939
  { ns: 'k8s.base_kustomization', dir: 'k8s/base_kustomization', files: baseKustYaml },
6069
- { ns: 'k8s.base_manifest', dir: 'k8s/base_manifest', files: baseResourceYaml },
6070
- { ns: 'k8s.kustomize_managed', dir: 'k8s/kustomize_managed', files: kustomizeManagedFiles }
5940
+ { ns: 'k8s.base_manifest', dir: 'k8s/base_manifest', files: baseResourceYaml }
6071
5941
  ]
6072
5942
 
6073
5943
  for (const t of targets) {
@@ -6103,16 +5973,14 @@ export async function check() {
6103
5973
 
6104
5974
  assertNoForbiddenK8sDevPaths(yamlFiles, root, fail)
6105
5975
 
6106
- const kustomizeManagedRel = await collectKustomizeManagedRelPaths(root, yamlFiles)
6107
-
6108
5976
  // Plan B: пер-документні структурні правила — у rego-полісі `npm/policy/k8s/*`,
6109
5977
  // викликаємо одним батчем на namespace через runConftestBatch. JS нижче робить
6110
5978
  // лише cross-file orchestration, modeline та FS-existence перевірки.
6111
- runAllK8sRego(root, yamlFiles, kustomizeManagedRel, fail)
5979
+ runAllK8sRego(root, yamlFiles, fail)
6112
5980
  pass(`Rego-полісі (npm/policy/k8s/*) виконано на ${yamlFiles.length} файл(ах)`)
6113
5981
 
6114
5982
  for (const abs of yamlFiles) {
6115
- await checkK8sYamlFile(abs, root, fail, pass, kustomizeManagedRel)
5983
+ await checkK8sYamlFile(abs, root, fail, pass)
6116
5984
  }
6117
5985
 
6118
5986
  await validateSvcYamlAndSvcHlPairs(root, yamlFiles, fail)
@@ -102,7 +102,7 @@ export function runConftestBatch(opts) {
102
102
  let parsed
103
103
  try {
104
104
  parsed = JSON.parse(result.stdout)
105
- } catch (e) {
105
+ } catch {
106
106
  throw new Error(`conftest stdout не парситься як JSON: ${(result.stdout || '').slice(0, 200)}`)
107
107
  }
108
108
  /** @type {ConftestViolation[]} */
@@ -1,31 +0,0 @@
1
- # Порт перевірки `metadataNamespaceForbiddenViolation` з
2
- # `npm/scripts/check-k8s.mjs` (k8s.mdc): для файлів, які підключено до якогось
3
- # `kustomization.yaml` через `resources` / `patches` / `…`, поле
4
- # `metadata.namespace` забороняється — namespace задає сам kustomization.
5
- #
6
- # Запуск (локально, лише для одного kustomize-managed YAML):
7
- # conftest test path/to/manifest.yaml -p npm/policy/k8s/kustomize_managed \
8
- # --namespace k8s.kustomize_managed
9
- #
10
- # JS відбирає kustomize-managed файли через `collectKustomizeManagedRelPaths`
11
- # і викликає conftest з цією намеспейс. JS authoritative
12
- # (`check-k8s.mjs`: `metadataNamespaceForbiddenViolation`,
13
- # `failIfK8sPolicyNamespaceRulesViolated`).
14
- #
15
- # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
16
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`.
17
- package k8s.kustomize_managed
18
-
19
- import rego.v1
20
-
21
- namespace_forbidden_msg := concat(" ", [
22
- "metadata.namespace заборонено — namespace задає kustomization.yaml",
23
- "(поле namespace); файл підключено через resources / patches / …",
24
- "(k8s.mdc)",
25
- ])
26
-
27
- deny contains namespace_forbidden_msg if {
28
- meta := object.get(input, "metadata", null)
29
- is_object(meta)
30
- "namespace" in object.keys(meta)
31
- }
@@ -1,30 +0,0 @@
1
- # Тести для `k8s.kustomize_managed`. Запуск:
2
- # conftest verify -p npm/policy/k8s/kustomize_managed --namespace k8s.kustomize_managed
3
- package k8s.kustomize_managed_test
4
-
5
- import rego.v1
6
-
7
- import data.k8s.kustomize_managed
8
-
9
- test_deny_metadata_with_namespace if {
10
- count(kustomize_managed.deny) > 0 with input as {
11
- "apiVersion": "v1",
12
- "kind": "ConfigMap",
13
- "metadata": {"name": "cm", "namespace": "dev"},
14
- }
15
- }
16
-
17
- test_allow_metadata_without_namespace if {
18
- count(kustomize_managed.deny) == 0 with input as {
19
- "apiVersion": "v1",
20
- "kind": "ConfigMap",
21
- "metadata": {"name": "cm"},
22
- }
23
- }
24
-
25
- test_allow_no_metadata if {
26
- count(kustomize_managed.deny) == 0 with input as {
27
- "apiVersion": "v1",
28
- "kind": "ConfigMap",
29
- }
30
- }