@nitra/cursor 1.8.74 → 1.8.79

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/mdc/docker.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Dockerfile — lint-docker / hadolint; перевірка check-docker
3
- version: '1.6'
3
+ version: '1.7'
4
4
  globs: "**/Dockerfile*"
5
5
  alwaysApply: false
6
6
  ---
@@ -103,14 +103,19 @@ jobs:
103
103
 
104
104
  1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
105
105
  2. **`npx @nitra/cursor check docker`** — **`check-docker.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`PATH`** або **`docker run`** з **`hadolint/hadolint:v2.12.0`**).
106
- 3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure).
106
+ 3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
107
+
108
+ ```yaml title=".hadolint.yaml"
109
+ ignored:
110
+ - DL3007
111
+ ```
107
112
 
108
113
  Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
109
114
 
110
115
  ## Агентам
111
116
 
112
117
  - Після правок у Dockerfile проганяй **`bun run lint-docker`** і/або **`check docker`**.
113
- - Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або правила в **`.hadolint.yaml`**.
118
+ - Винятки: **`# hadolint ignore=DL3008`** (або інший код) у Dockerfile або **`ignored`** у **`.hadolint.yaml`** (наприклад **DL3007** для **`:latest`** — див. вище).
114
119
  - Образи на базі Bun — див. **`n-bun.mdc`**.
115
120
 
116
121
  ## Редактор
package/mdc/k8s.mdc CHANGED
@@ -226,11 +226,7 @@ patches:
226
226
 
227
227
  **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
228
228
 
229
- ## Що саме в скрипті `check-k8s.mjs`
230
229
 
231
- Повний перелік умов, константи (**`YANNH_PIN`**, **`CLUSTER_SCOPED_KINDS`**, **`HASURA_GRAPHQL_ENGINE_IMAGE`** тощо) і допоміжні функції — у файлі скрипта; змінив вимогу для **check** — онови **JSDoc** і за потреби тести в **`npm/tests/check-k8s-schema.test.mjs`**.
232
-
233
- При зміні **PIN** версії Kubernetes узгодь **`check-k8s.mjs`**, **`run-k8s.mjs`** (**`KUBERNETES_VERSION`**, **`DATREE_CRD_SCHEMA_LOCATION`**) і цей файл (**lint-k8s**, **Визначення схеми YAML**).
234
230
 
235
231
  ## Коли застосовувати (агентам)
236
232
 
@@ -8,7 +8,7 @@ version: '1.2'
8
8
 
9
9
  - **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
10
10
  - **Форматування** узгоджуй з **`n-js-format.mdc`** (oxfmt / `.oxfmtrc.json` для css, scss тощо), щоб форматер і stylelint не суперечили один одному.
11
- - **Запуск stylelint:** лише **`npx stylelint`** (у **`lint-style`**, у CI, вручну). Не використовуй **`bunx stylelint`** і не запускай **stylelint** напряму без **`npx`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
11
+ - **Запуск stylelint:** лише **`npx stylelint`**. Локально — через скрипт **`lint-style`** (`bun run lint-style`); у **GitHub Actions** у кроці **`run`** викликай `npx stylelint '**/*.{css,scss,vue}' --fix` напряму (не через **`bun run lint-style`**). Не використовуй **`bunx stylelint`**. Після змін запускай **`bun run lint-style`** і виправляй усе, що лишилось після auto-fix; за потреби — повний набір `lint-*` (навичка **`n-fix`**).
12
12
  - **Не розширюй винятки:** не додавай зайві **`stylelint-disable`** без потреби; краще підлаштувати стилі під правила проєкту.
13
13
 
14
14
  **VSCode:** у **`.vscode/extensions.json`** рекомендуй **`stylelint.vscode-stylelint`**. У **`.vscode/settings.json`** вимкни вбудовану валідацію CSS/SCSS/Less і увімкни явні code actions:
@@ -44,7 +44,7 @@ version: '1.2'
44
44
  },
45
45
  ```
46
46
 
47
- Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі **`bun run lint-style`** (у скрипті вже **`npx stylelint`**). **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
47
+ Додай **`.github/workflows/lint-style.yml`** (лише **`.yml`**, **`ga.mdc`**): після **`checkout`** — локальний composite **`setup-bun-deps`**, далі `npx stylelint '**/*.{css,scss,vue}' --fix` у кроці **`run`**. **Не** дублюй окремі кроки **`setup-node`** / **`oven-sh/setup-bun`** / кеш / **`npm install`**.
48
48
 
49
49
  ```yaml title=".github/workflows/lint-style.yml"
50
50
  name: StyleLint
@@ -85,7 +85,7 @@ jobs:
85
85
  - uses: ./.github/actions/setup-bun-deps
86
86
 
87
87
  - name: StyleLint
88
- run: bun run lint-style
88
+ run: npx stylelint '**/*.{css,scss,vue}' --fix
89
89
  ```
90
90
 
91
91
  У корені проєкту має бути **`.stylelintignore`**:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.74",
3
+ "version": "1.8.79",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -17,14 +17,13 @@
17
17
  * **nodeSelector (base):** якщо **Deployment** лежить у шляху з сегментом **`base`** (наприклад **`…/k8s/base/deploy.yaml`**),
18
18
  * у **`spec.template.spec.nodeSelector`** має бути **`preem`** з булевим значенням **true** або рядком **`'true'`** — overlay **ua** та **ru** далі підміняють селектор.
19
19
  *
20
- * **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у кожному **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`**
21
- * має бути inline **JSON6902** patch на **`kind: Deployment`**: для **ua** — **`op: add`**, **`path: /spec/template/spec/nodeSelector`**,
22
- * **`preem: false`**; для **ru** **`op: replace`**, той самий **path**, **`yandex.cloud/preemptible: false`** (див. abie.mdc).
20
+ * **nodeSelector (overlay):** якщо є **Deployment** під **k8s**, у **`ua`/`ru` kustomization** inline patch на **`kind: Deployment`**
21
+ * з **`path: /spec/template/spec/nodeSelector`**: **ua** **`preem: false`**; **ru** — **`yandex.cloud/preemptible: false`**.
22
+ * Узагальнені вимоги **k8s.mdc** до JSON6902 (зокрема заборона **remove** + **add** на той самий **path**) перевіряє **check-k8s.mjs**; **check-abie** — лише abie-специфічний вміст (без дублювання цього правила).
23
23
  *
24
- * **HTTPRoute (overlay):** за тієї ж умови (**Deployment** під **k8s**) у **кожному** **`ua`/`ru` kustomization** має бути
25
- * inline **JSON6902** на **`kind: HTTPRoute`** з **непорожнім `target.name`** (будь-яке ім’я): **replace** **`/spec/hostnames`**
26
- * (домени з abie.mdc), **replace** **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** також
27
- * **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
24
+ * **HTTPRoute (overlay):** тієї ж умови patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
25
+ * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**.
26
+ * Вибір **`op`** **k8s.mdc**.
28
27
  */
29
28
  import { existsSync } from 'node:fs'
30
29
  import { readFile } from 'node:fs/promises'
@@ -344,7 +343,8 @@ function stripBom(s) {
344
343
  }
345
344
 
346
345
  /**
347
- * Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**op: add**, **preem: false**).
346
+ * Чи рядок inline JSON6902 patch містить очікуваний **ua** nodeSelector (**preem: false** на **`/spec/template/spec/nodeSelector`**).
347
+ * Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
348
348
  * @param {string} patchText поле **patch** у kustomization
349
349
  * @returns {boolean} true, якщо критерії abie.mdc виконано
350
350
  */
@@ -352,9 +352,6 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
352
352
  if (typeof patchText !== 'string' || patchText.trim() === '') {
353
353
  return false
354
354
  }
355
- if (!/op:\s*add\b/u.test(patchText)) {
356
- return false
357
- }
358
355
  if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
359
356
  return false
360
357
  }
@@ -365,7 +362,8 @@ function jsonPatchTextHasUaDeploymentNodeSelector(patchText) {
365
362
  }
366
363
 
367
364
  /**
368
- * Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**op: replace**, **yandex.cloud/preemptible: false**).
365
+ * Чи рядок inline JSON6902 patch містить очікуваний **ru** nodeSelector (**yandex.cloud/preemptible: false** на **`/spec/template/spec/nodeSelector`**).
366
+ * Конкретний **`op`** не перевіряється — див. **k8s.mdc**.
369
367
  * @param {string} patchText поле **patch** у kustomization
370
368
  * @returns {boolean} true, якщо критерії abie.mdc виконано
371
369
  */
@@ -373,9 +371,6 @@ function jsonPatchTextHasRuDeploymentNodeSelector(patchText) {
373
371
  if (typeof patchText !== 'string' || patchText.trim() === '') {
374
372
  return false
375
373
  }
376
- if (!/op:\s*replace\b/u.test(patchText)) {
377
- return false
378
- }
379
374
  if (!/path:\s*\/spec\/template\/spec\/nodeSelector\b/u.test(patchText)) {
380
375
  return false
381
376
  }
@@ -558,11 +553,10 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
558
553
  */
559
554
  export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
560
555
  if (typeof combined !== 'string' || combined.trim() === '') {
561
- return `очікується patch target kind HTTPRoute з непорожнім target.name (replace hostnames, parentRefs namespace ${mode}; для ru — також gwin… upgradeTypes websocket) — abie.mdc`
556
+ return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — також gwin… websocket) — abie.mdc`
562
557
  }
563
- const hasHostnamesReplace = /-\s*op:\s*replace\b[\s\S]{0,200}?path:\s*\/spec\/hostnames\b/m.test(combined)
564
- if (!hasHostnamesReplace) {
565
- return 'HTTPRoute: потрібен блок op replace з path /spec/hostnames (abie.mdc)'
558
+ if (!/path:\s*\/spec\/hostnames\b/m.test(combined)) {
559
+ return 'HTTPRoute: потрібен path /spec/hostnames у patch (abie.mdc)'
566
560
  }
567
561
  const markers = mode === 'ua' ? ABIE_UA_HTTPROUTE_HOST_MARKERS : ABIE_RU_HTTPROUTE_HOST_MARKERS
568
562
  if (!markers.some(m => combined.includes(m))) {
@@ -573,7 +567,7 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode) {
573
567
  ? /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/mu.test(combined)
574
568
  : /path:\s*\/spec\/parentRefs\/0\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/mu.test(combined)
575
569
  if (!namespaceOk) {
576
- return `HTTPRoute: потрібен replace path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
570
+ return `HTTPRoute: потрібен path /spec/parentRefs/0/namespace з value ${mode} (abie.mdc)`
577
571
  }
578
572
  if (mode === 'ru' && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
579
573
  return 'HTTPRoute (ru): потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
@@ -783,7 +777,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
783
777
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
784
778
  if (uaAbsList.length === 0) {
785
779
  fail(
786
- 'Є Deployment у k8s — додай ua/kustomization.yaml з inline patch на Deployment: op add, path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
780
+ 'Є Deployment у k8s — додай ua/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, preem false (abie.mdc)'
787
781
  )
788
782
  return
789
783
  }
@@ -799,7 +793,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
799
793
  }
800
794
  if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ua')) {
801
795
  fail(
802
- `${rel}: потрібен patch target kind Deployment з op: add, path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
796
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та preem: false (abie.mdc)`
803
797
  )
804
798
  return
805
799
  }
@@ -809,7 +803,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
809
803
  const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
810
804
  if (ruAbsList.length === 0) {
811
805
  fail(
812
- 'Є Deployment у k8s — додай ru/kustomization.yaml з inline patch на Deployment: op replace, path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
806
+ 'Є Deployment у k8s — додай ru/kustomization.yaml з patch на Deployment: path /spec/template/spec/nodeSelector, yandex.cloud/preemptible false (abie.mdc)'
813
807
  )
814
808
  return
815
809
  }
@@ -825,7 +819,7 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, fail, passF
825
819
  }
826
820
  if (!kustomizationHasAbieDeploymentNodeSelectorPatch(raw, 'ru')) {
827
821
  fail(
828
- `${rel}: потрібен patch target kind Deployment з op: replace, path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
822
+ `${rel}: потрібен patch target kind Deployment: path /spec/template/spec/nodeSelector та yandex.cloud/preemptible: false (abie.mdc)`
829
823
  )
830
824
  return
831
825
  }
@@ -40,6 +40,9 @@
40
40
  * Структура **Kustomize** (див. k8s.mdc): заборона шляхів **`…/k8s/dev/…`**; у **`k8s/base/kustomization.yaml`**
41
41
  * завжди має бути непорожнє поле **`namespace:`** (перевірка, якщо файл існує).
42
42
  *
43
+ * **Inline JSON6902** у **`patches`** (і зовнішні файли з **`patches[].path`** під **`k8s`**, якщо вміст — масив JSON Patch): не допускається пара **`remove`** і **`add`**
44
+ * на один і той самий **`path`** у межах одного фрагмента — потрібен **`op: replace`** (k8s.mdc). **check-k8s** це перевіряє.
45
+ *
43
46
  * Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
44
47
  * **`apiVersion`, `kind`, `type`** (для CRD без поля `type` у маніфесті — зірочка **`*`** як третій
45
48
  * компонент). Спочатку шукається збіг за фактичним `type`, потім за **`*`**.
@@ -679,6 +682,224 @@ export function ruKustomizationHasHealthCheckDeletePatch(raw) {
679
682
  return true
680
683
  }
681
684
 
685
+ /**
686
+ * Чи абсолютний шлях лежить усередині кореня репозиторію (без виходу через `..`).
687
+ * @param {string} rootAbs абсолютний корінь
688
+ * @param {string} fileAbs абсолютний шлях до файлу
689
+ * @returns {boolean} true, якщо `fileAbs` усередині `rootAbs`
690
+ */
691
+ function resolvedFilePathIsUnderRoot(rootAbs, fileAbs) {
692
+ const r = resolve(rootAbs)
693
+ const f = resolve(fileAbs)
694
+ const rel = relative(r, f).replaceAll('\\', '/')
695
+ if (rel === '') {
696
+ return true
697
+ }
698
+ return !rel.startsWith('../') && rel !== '..'
699
+ }
700
+
701
+ /**
702
+ * Нормалізує **`path`** з операції JSON Patch (RFC 6902).
703
+ * @param {string} p значення поля **path**
704
+ * @returns {string} обрізаний рядок
705
+ */
706
+ function normalizeJsonPatchPath(p) {
707
+ return typeof p === 'string' ? p.trim() : ''
708
+ }
709
+
710
+ /**
711
+ * Витягує пари **op** / **path** з масиву операцій JSON6902.
712
+ * @param {unknown[]} arr корінь-масив з YAML/JSON
713
+ * @returns {Array<{ op: string, path: string }>} **op** у нижньому регістрі
714
+ */
715
+ function extractJson6902OpsFromArray(arr) {
716
+ /** @type {Array<{ op: string, path: string }>} */
717
+ const out = []
718
+ for (const item of arr) {
719
+ if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
720
+ const rec = /** @type {Record<string, unknown>} */ (item)
721
+ const op = rec.op
722
+ const path = rec.path
723
+ if (typeof op === 'string' && typeof path === 'string') {
724
+ const p = normalizeJsonPatchPath(path)
725
+ if (p !== '') {
726
+ out.push({ op: op.trim().toLowerCase(), path: p })
727
+ }
728
+ }
729
+ }
730
+ }
731
+ return out
732
+ }
733
+
734
+ /**
735
+ * Витягує операції JSON6902 з тексту inline **patch** або окремого файлу patch (YAML-масив або JSON-масив).
736
+ * Інший вміст (strategic merge, `$patch: delete` тощо) дає порожній масив.
737
+ * @param {string} patchText вміст поля **patch** або файлу
738
+ * @returns {Array<{ op: string, path: string }>}
739
+ */
740
+ export function collectJson6902OperationsFromPatchText(patchText) {
741
+ const t = typeof patchText === 'string' ? patchText.trim() : ''
742
+ if (t === '') {
743
+ return []
744
+ }
745
+ try {
746
+ const docs = parseAllDocuments(t)
747
+ for (const d of docs) {
748
+ if (d.errors.length > 0) {
749
+ continue
750
+ }
751
+ const j = d.toJSON()
752
+ if (Array.isArray(j)) {
753
+ return extractJson6902OpsFromArray(j)
754
+ }
755
+ }
756
+ } catch {
757
+ /* пробуємо JSON */
758
+ }
759
+ if (t.startsWith('[')) {
760
+ try {
761
+ const j = JSON.parse(t)
762
+ if (Array.isArray(j)) {
763
+ return extractJson6902OpsFromArray(j)
764
+ }
765
+ } catch {
766
+ /* ignore */
767
+ }
768
+ }
769
+ return []
770
+ }
771
+
772
+ /**
773
+ * Шляхи JSON Patch, де в одному наборі операцій є і **remove**, і **add** (k8s.mdc: краще **replace**).
774
+ * @param {Array<{ op: string, path: string }>} ops нормалізовані **op**
775
+ * @returns {string[]} унікальні **path** з порушенням (відсортовано)
776
+ */
777
+ export function json6902PathsWithRemoveAndAddOnSamePath(ops) {
778
+ /** @type {Map<string, Set<string>>} */
779
+ const byPath = new Map()
780
+ for (const { op, path } of ops) {
781
+ if (!path) {
782
+ continue
783
+ }
784
+ if (!byPath.has(path)) {
785
+ byPath.set(path, new Set())
786
+ }
787
+ byPath.get(path).add(op)
788
+ }
789
+ /** @type {string[]} */
790
+ const out = []
791
+ for (const [path, set] of byPath) {
792
+ if (set.has('remove') && set.has('add')) {
793
+ out.push(path)
794
+ }
795
+ }
796
+ return out.toSorted((a, b) => a.localeCompare(b))
797
+ }
798
+
799
+ /**
800
+ * Перевіряє всі **`kustomization.yaml`** під **`k8s`**: у inline **`patch`** і у зовнішніх patch-файлах не має бути **remove** і **add** на той самий **path**.
801
+ * @param {string} root корінь репозиторію
802
+ * @param {string[]} yamlFilesAbs абсолютні шляхи до yaml під k8s
803
+ * @param {(msg: string) => void} fail реєстрація порушення
804
+ * @returns {Promise<void>}
805
+ */
806
+ async function validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFilesAbs, fail) {
807
+ const rootNorm = resolve(root)
808
+ for (const kustAbs of yamlFilesAbs) {
809
+ if (basename(kustAbs).toLowerCase() !== 'kustomization.yaml') {
810
+ continue
811
+ }
812
+ const rel = (relative(root, kustAbs) || kustAbs).replaceAll('\\', '/')
813
+ let raw
814
+ try {
815
+ raw = await readFile(kustAbs, 'utf8')
816
+ } catch (error) {
817
+ const msg = error instanceof Error ? error.message : String(error)
818
+ fail(`${rel}: не вдалося прочитати для перевірки JSON6902 (${msg})`)
819
+ continue
820
+ }
821
+ const lines = toLines(raw)
822
+ const body = lines.length > 0 && MODELINE_RE.test(lines[0]) ? yamlBodyAfterModeline(lines) : lines.join('\n')
823
+ /** @type {import('yaml').Document[]} */
824
+ let docs
825
+ try {
826
+ docs = parseAllDocuments(body)
827
+ } catch {
828
+ continue
829
+ }
830
+ for (const doc of docs) {
831
+ if (doc.errors.length > 0) {
832
+ continue
833
+ }
834
+ const rootObj = doc.toJSON()
835
+ if (rootObj === null || typeof rootObj !== 'object' || Array.isArray(rootObj)) {
836
+ continue
837
+ }
838
+ const rec = /** @type {Record<string, unknown>} */ (rootObj)
839
+ if (rec.kind !== 'Kustomization') {
840
+ continue
841
+ }
842
+ const patches = rec.patches
843
+ if (!Array.isArray(patches)) {
844
+ continue
845
+ }
846
+ let patchIdx = 0
847
+ for (const p of patches) {
848
+ patchIdx++
849
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) {
850
+ continue
851
+ }
852
+ const pr = /** @type {Record<string, unknown>} */ (p)
853
+ if (typeof pr.patch === 'string' && pr.patch.trim() !== '') {
854
+ const ops = collectJson6902OperationsFromPatchText(pr.patch)
855
+ const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
856
+ if (bad.length > 0) {
857
+ fail(
858
+ `${rel}: patches[${patchIdx}] inline JSON6902: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
859
+ )
860
+ }
861
+ }
862
+ if (typeof pr.path === 'string' && pr.path.trim() !== '') {
863
+ const patchRef = pr.path.trim()
864
+ const resolved = resolve(dirname(kustAbs), patchRef)
865
+ if (!resolvedFilePathIsUnderRoot(rootNorm, resolved)) {
866
+ continue
867
+ }
868
+ if (!existsSync(resolved)) {
869
+ continue
870
+ }
871
+ let st
872
+ try {
873
+ st = await stat(resolved)
874
+ } catch {
875
+ continue
876
+ }
877
+ if (!st.isFile()) {
878
+ continue
879
+ }
880
+ let pRaw
881
+ try {
882
+ pRaw = await readFile(resolved, 'utf8')
883
+ } catch {
884
+ continue
885
+ }
886
+ const ops = collectJson6902OperationsFromPatchText(pRaw)
887
+ if (ops.length === 0) {
888
+ continue
889
+ }
890
+ const bad = json6902PathsWithRemoveAndAddOnSamePath(ops)
891
+ if (bad.length > 0) {
892
+ const relPatch = (relative(root, resolved) || patchRef).replaceAll('\\', '/')
893
+ fail(
894
+ `${rel}: patch-файл «${relPatch}»: один path має і remove, і add — оформи як op: replace (k8s.mdc): ${bad.join(', ')}`
895
+ )
896
+ }
897
+ }
898
+ }
899
+ }
900
+ }
901
+ }
902
+
682
903
  /**
683
904
  * Шукає **Ingress** у розібраних документах; реєструє порушення.
684
905
  * @param {string} rel відносний шлях до файлу
@@ -1510,6 +1731,8 @@ export async function check() {
1510
1731
 
1511
1732
  await validateKustomizationIncludesSvcHlWithSvc(root, yamlFiles, fail)
1512
1733
 
1734
+ await validateKustomizationJson6902NoRemoveAddSamePath(root, yamlFiles, fail)
1735
+
1513
1736
  await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
1514
1737
 
1515
1738
  return reporter.getExitCode()
@@ -2,7 +2,7 @@
2
2
  * Перевіряє CSS/SCSS лінт за правилом style-lint.mdc.
3
3
  *
4
4
  * Очікування: `@nitra/stylelint-config`, `lint-style` через `npx stylelint`, `.stylelintignore`,
5
- * workflow `lint-style.yml` (у `run` — `npx stylelint` або `bun run lint-style`), VSCode stylelint,
5
+ * workflow `lint-style.yml` (у `run` — лише `npx stylelint`, не `bun run lint-style`), VSCode stylelint,
6
6
  * `css.validate` / `scss.validate` / `less.validate`: false.
7
7
  */
8
8
  import { existsSync } from 'node:fs'
@@ -60,13 +60,13 @@ export async function check() {
60
60
  const content = await readFile('.github/workflows/lint-style.yml', 'utf8')
61
61
  pass('lint-style.yml існує')
62
62
  const root = parseWorkflowYaml(content)
63
- const ok = root
64
- ? anyRunStepIncludesStylelint(root)
65
- : content.includes('npx stylelint') || content.includes('bun run lint-style')
63
+ const ok = root ? anyRunStepIncludesStylelint(root) : content.includes('npx stylelint')
66
64
  if (ok) {
67
- pass('lint-style.yml містить npx stylelint або bun run lint-style у кроці run')
65
+ pass('lint-style.yml містить npx stylelint у кроці run')
68
66
  } else {
69
- fail('lint-style.yml має містити npx stylelint або bun run lint-style у кроці run')
67
+ fail(
68
+ "lint-style.yml має викликати stylelint у CI через npx — наприклад: npx stylelint '**/*.{css,scss,vue}' --fix"
69
+ )
70
70
  }
71
71
  } else {
72
72
  fail('.github/workflows/lint-style.yml не існує — створи його')
@@ -341,11 +341,10 @@ export function anyRunStepIncludes(root, needle) {
341
341
  }
342
342
 
343
343
  /**
344
- * Чи викликається stylelint коректно: `npx stylelint` у run або `bun run lint-style`
345
- * (скрипт `lint-style` у package.json має містити `npx stylelint`).
344
+ * Чи викликається stylelint у workflow через `npx stylelint` у кроці `run` (вимога для CI).
346
345
  * @param {Record<string, unknown>} root корінь workflow
347
346
  * @returns {boolean} `true`, якщо умова виконана
348
347
  */
349
348
  export function anyRunStepIncludesStylelint(root) {
350
- return anyRunStepIncludes(root, 'npx stylelint') || anyRunStepIncludes(root, 'bun run lint-style')
349
+ return anyRunStepIncludes(root, 'npx stylelint')
351
350
  }