@nitra/cursor 1.8.85 → 1.8.88

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/abie.mdc CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.9'
4
+ version: '1.11'
5
5
  ---
6
6
 
7
- Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**), видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
7
+ Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** / **ru** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`filelint-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua** / **ru**), видалення **HealthCheckPolicy** у **ru**), гілки **dev**, **ua**, **ru** у **clean-merged-branch**, а також заборона артефактів **Firebase Hosting** у корені репозиторію.
8
8
 
9
9
  **`npx @nitra/cursor check abie`** виконується лише якщо в **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень.
10
10
 
@@ -12,7 +12,7 @@ version: '1.9'
12
12
 
13
13
  ## k8s: `hc.yaml` поруч із Deployment
14
14
 
15
- Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** = **`metadata.name`**.
15
+ Якщо під **`k8s`** є **Deployment**, у **тій самій директорії** має бути **`hc.yaml`** з **HealthCheckPolicy** (**`networking.gke.io/v1`**): коректний modeline **`$schema`**, **`/healthz`**, порт **8080**, **`targetRef.name`** **headless** **Service** з суфіксом **`-hl`** (узгоджено з парою **`svc.yaml`** / **`svc-hl.yaml`** у **k8s.mdc**): або **`${metadata.name}-hl`**, або те саме ім’я, якщо **`metadata.name`** уже з **`-hl`**.
16
16
 
17
17
  ```yaml title="hc.yaml"
18
18
  # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
@@ -31,13 +31,35 @@ spec:
31
31
  targetRef:
32
32
  group: ''
33
33
  kind: Service
34
- name: СЕРВІС
34
+ name: СЕРВІС-hl
35
35
  ```
36
36
 
37
37
  ## k8s: overlay **HTTPRoute** (**ua** / **ru**)
38
38
 
39
39
  За наявності **Deployment** під **k8s** і наявності **Vite** (**`vite.config.js`**, **`vite.config.mjs`** або **`vite.config.ts`** у каталозі пакета) у **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** цього пакета потрібні **inline JSON6902** у **`patches`**: **target** **`kind: HTTPRoute`**, **непорожній `name`** (як у маніфесті маршруту). Мають бути зміни **`/spec/hostnames`** (домени abie — у скрипті) та **`/spec/parentRefs/0/namespace`** (**`ua`** / **`ru`**). Для **ru** — анотація **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`** лише якщо в **тому ж** **`ru/kustomization.yaml`** є згадка **`HASURA_GRAPHQL_JWT_SECRET`** (типово patch на **ConfigMap** Hasura). Як обирати **`op`** (**add** / **replace** тощо) у patch — **k8s.mdc** (розділ про JSON patch у kustomization).
40
40
 
41
+ ### HTTPRoute: спільні сервіси **`auth-run-hl`**, **`filelint-hl`**
42
+
43
+ Ці **Service** (headless **`-hl`**) живуть у **базовому** неймспейсі **`dev`**. У маніфесті **HTTPRoute** під **`k8s`** (шар без **`ua/`** та **`ru/`** — наприклад **`…/k8s/base/hr.yaml`**) для кожного **`backendRefs`** до такого сервісу явно вкажи **`namespace: dev`** і порт **8080**:
44
+
45
+ ```yaml title="…/k8s/base/hr.yaml (фрагмент)"
46
+ spec:
47
+ rules:
48
+ - matches:
49
+ - path:
50
+ type: PathPrefix
51
+ value: /
52
+ backendRefs:
53
+ - name: auth-run-hl
54
+ namespace: dev
55
+ port: 8080
56
+ - name: filelint-hl
57
+ namespace: dev
58
+ port: 8080
59
+ ```
60
+
61
+ У **`ua/kustomization.yaml`** та **`ru/kustomization.yaml`** додай до того самого **inline** patch на **`HTTPRoute`** (той самий **`target.name`**) операції **JSON6902** з **`path`**: **`/spec/rules/<i>/backendRefs/<j>/namespace`**, де **`<i>`** / **`<j>`** — індекси відповідно до порядку **`spec.rules`** та **`backendRefs`** у base-файлі; **`value`**: **`ua`** або **`ru`**. Якщо кілька таких **`backendRefs`**, потрібна окрема операція для кожного.
62
+
41
63
  ```yaml title="…/ua/kustomization.yaml (фрагмент)"
42
64
  - target:
43
65
  kind: HTTPRoute
@@ -50,6 +72,12 @@ spec:
50
72
  - op: replace
51
73
  path: /spec/parentRefs/0/namespace
52
74
  value: ua
75
+ - op: replace
76
+ path: /spec/rules/0/backendRefs/0/namespace
77
+ value: ua
78
+ - op: replace
79
+ path: /spec/rules/0/backendRefs/1/namespace
80
+ value: ua
53
81
  ```
54
82
 
55
83
  ```yaml title="…/ru/kustomization.yaml (фрагмент)"
@@ -64,6 +92,12 @@ spec:
64
92
  - op: replace
65
93
  path: /spec/parentRefs/0/namespace
66
94
  value: ru
95
+ - op: replace
96
+ path: /spec/rules/0/backendRefs/0/namespace
97
+ value: ru
98
+ - op: replace
99
+ path: /spec/rules/0/backendRefs/1/namespace
100
+ value: ru
67
101
  ```
68
102
 
69
103
  Якщо в цьому ж файлі є **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura з JWT), додай окремий patch на **HTTPRoute** з анотацією для WebSocket:
package/mdc/k8s.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
3
- version: '1.24'
3
+ version: '1.25'
4
4
  globs: "**/k8s/**/*.yaml"
5
5
  alwaysApply: false
6
6
  ---
@@ -253,44 +253,13 @@ spec:
253
253
 
254
254
  Для `$schema` у першому рядку див. приклад **HealthCheckPolicy** у тому ж розділі (datree CRDs-catalog).
255
255
 
256
+ **`spec.targetRef`** (типово **`kind: Service`**) має вказувати на **headless** сервіс — ім’я з суфіксом **`-hl`** (див. **«Service: `svc.yaml` і `svc-hl.yaml`»**); для проєктів **abie** точні умови — **`check-abie.mjs`** / **abie.mdc**.
257
+
256
258
  За потреби розшир **`target`** (`name`, `namespace`), щоб однозначно вказати об’єкт.
257
259
 
258
260
  **`check k8s`:** заборонено **`kind: Ingress`**.
259
261
 
260
- 3. Якщо **HTTPRoute** посилається на **Service** в іншому **namespace**, потрібен **ReferenceGrant** (дозвіл). Наприклад **HTTPRoute** в **`contract`** і **Service** в **`dev`** додай маніфест на кшталт **`base/rg.yaml`** (фрагмент нижче).
261
-
262
- ```yaml title="base/rg.yaml"
263
- # yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/gateway.networking.k8s.io/referencegrant_v1beta1.json
264
- apiVersion: gateway.networking.k8s.io/v1beta1
265
- kind: ReferenceGrant
266
- metadata:
267
- name: contract-to-dev
268
- namespace: dev
269
- spec:
270
- from:
271
- - group: gateway.networking.k8s.io
272
- kind: HTTPRoute
273
- namespace: contract
274
- to:
275
- - group: ''
276
- kind: Service
277
- # Якщо name не вказано, доступ дозволено до всіх Service в цьому namespace
278
- ```
279
-
280
- **ReferenceGrant** має бути в **namespace** тих **Service**, до яких відкривається доступ (у прикладі — **`dev`**). Якщо **`namespace:`** у **overlay** не збігається з **namespace** гранта, **не** додавай **`base/rg.yaml`** у **`resources:`** того overlay — Kustomize може перезаписати **`metadata.namespace`** гранта. Натомість застосуй **`patches`** (JSON patch), щоб явно виставити потрібний **namespace**:
281
-
282
- ```yaml title="overlay/kustomization.yaml (фрагмент)"
283
- patches:
284
- - target:
285
- kind: ReferenceGrant
286
- name: contract-to-dev
287
- patch: |-
288
- - op: replace
289
- path: /metadata/namespace
290
- value: dev
291
- ```
292
-
293
- 4. **JSON patch у kustomization:** де можливо, змінюй ресурс через **`op: replace`** (одна операція на `path`), а не пару **`remove` + `add`** на той самий шлях. **`add`** / **`remove`** лишай лише коли **`replace`** не підходить (наприклад додати новий ключ або прибрати поле без заміни).
262
+ 3. **JSON patch у kustomization:** де можливо, змінюй ресурс через **`op: replace`** (одна операція на `path`), а не пару **`remove` + `add`** на той самий шлях. **`add`** / **`remove`** лишай лише коли **`replace`** не підходить (наприклад додати новий ключ або прибрати поле без заміни).
294
263
 
295
264
  ```yaml title="overlay/kustomization.yaml (фрагмент)"
296
265
  patches:
@@ -308,7 +277,7 @@ patches:
308
277
 
309
278
  **`npx @nitra/cursor check k8s`** — програмні критерії в **JSDoc на початку** **`npm/scripts/check-k8s.mjs`**. Якщо під **`k8s`** немає **`*.yaml`** — крок пропущено. Канон **`$schema`** для редактора — розділ **«Визначення схеми YAML`** нижче.
310
279
 
311
- **Не входить у check k8s:** наприклад **ReferenceGrant** і доступ між **namespace** (лише рекомендації тут), повна структура **`HTTPRoute`** для **Hasura** (канон — у розділі про **`hasura/graphql-engine`**), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
280
+ **Не входить у check k8s:** наприклад повна структура **`HTTPRoute`** для **Hasura** (канон — у розділі про **`hasura/graphql-engine`**), **kubeconform** / **kubescape** — це **`bun run lint-k8s`**.
312
281
 
313
282
  ## Коли застосовувати (агентам)
314
283
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.85",
3
+ "version": "1.8.88",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
14
14
  * має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
15
- * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
15
+ * як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **headless Service** (ім’я з суфіксом **`-hl`**):
16
+ * якщо **`metadata.name`** уже закінчується на **`-hl`**, **`targetRef.name`** має збігатися з ним; інакше **`targetRef.name`** = **`${metadata.name}-hl`**.
16
17
  * Загальні вимоги до **`# yaml-language-server: $schema`** для інших YAML під **`k8s`** — у **check-k8s.mjs** / **k8s.mdc** (наприклад **HttpBackendGroup** `alb.yc.io/v1alpha1` — **без** modeline).
17
18
  * Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
18
19
  * (логіка вмісту — **`ruKustomizationHasHealthCheckDeletePatch`** у **check-k8s.mjs**, узгоджено з **k8s.mdc**).
@@ -28,6 +29,8 @@
28
29
  * — тоді в **`ua`/`ru` kustomization** потрібен patch на **`kind: HTTPRoute`**, **непорожній `target.name`**: **`/spec/hostnames`**
29
30
  * (домени abie.mdc), **`/spec/parentRefs/0/namespace`** (**ua** / **ru**); для **ru** — **`gwin.yandex.cloud/rules.http.upgradeTypes: websocket`**,
30
31
  * якщо в тому ж **`kustomization.yaml`** згадується **`HASURA_GRAPHQL_JWT_SECRET`** (Hasura + JWT).
32
+ * **Спільні бекенди (`auth-run-hl`, `filelint-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** та **ru** (шлях не містить **`k8s/ua/`** чи **`k8s/ru/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
33
+ * у patch overlay **ua** та **ru** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** або **ru** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
31
34
  * Вибір **`op`** — **k8s.mdc**.
32
35
  */
33
36
  import { existsSync } from 'node:fs'
@@ -46,6 +49,14 @@ const CONFIG_FILE = '.n-cursor.json'
46
49
  /** Маркер у kustomization.yaml: якщо зустрічається у файлі — для overlay ru у patch HTTPRoute потрібна анотація gwin…websocket. */
47
50
  const HASURA_JWT_SECRET_IN_KUSTOMIZATION = 'HASURA_GRAPHQL_JWT_SECRET'
48
51
 
52
+ /**
53
+ * Спільні **Service** (**`-hl`**) у **dev**: у base-**HTTPRoute** обов’язково **`namespace: dev`**, у overlay — patch **`…/backendRefs/…/namespace`** (abie.mdc).
54
+ * Експорт для споживачів / тестів.
55
+ */
56
+ export const ABIE_SHARED_CROSS_NS_BACKEND_NAMES = Object.freeze(['auth-run-hl', 'filelint-hl'])
57
+
58
+ const ABIE_SHARED_CROSS_NS_BACKEND_SET = new Set(ABIE_SHARED_CROSS_NS_BACKEND_NAMES)
59
+
49
60
  /** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
50
61
  export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
51
62
 
@@ -528,6 +539,136 @@ export function kustomizationHasAbieDeploymentNodeSelectorPatch(raw, mode) {
528
539
  return false
529
540
  }
530
541
 
542
+ /**
543
+ * Чи YAML відносно кореня належить до **`${pkgRel}/k8s/**`** поза піддеревами **`ua/`** та **`ru/`** (base-шар abie).
544
+ * @param {string} relFromRoot відносний шлях від кореня
545
+ * @param {string} pkgRelFromRoot каталог пакета відносно кореня (без завершального слеша після імені пакета)
546
+ * @returns {boolean}
547
+ */
548
+ export function isK8sYamlInAbiePackageExcludingUaRuOverlays(relFromRoot, pkgRelFromRoot) {
549
+ const normRel = relFromRoot.replaceAll('\\', '/')
550
+ const pkg = pkgRelFromRoot.replaceAll('\\', '/').replace(/\/$/u, '')
551
+ const prefix = `${pkg}/k8s/`
552
+ if (!normRel.startsWith(prefix)) {
553
+ return false
554
+ }
555
+ const after = normRel.slice(prefix.length)
556
+ return !after.startsWith('ua/') && !after.startsWith('ru/')
557
+ }
558
+
559
+ /**
560
+ * З HTTPRoute-документа рахує **`backendRefs`** до **`auth-run-hl`** / **`filelint-hl`** і порушення **`namespace: dev`**.
561
+ * @param {unknown} obj корінь YAML
562
+ * @param {string} rel відносний шлях (повідомлення)
563
+ * @returns {{ refCount: number, errors: string[] }}
564
+ */
565
+ function httpRouteDocSharedCrossNsBackendStats(obj, rel) {
566
+ /** @type {string[]} */
567
+ const errors = []
568
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
569
+ return { refCount: 0, errors }
570
+ }
571
+ const rec = /** @type {Record<string, unknown>} */ (obj)
572
+ if (rec.kind !== 'HTTPRoute') {
573
+ return { refCount: 0, errors }
574
+ }
575
+ const spec = rec.spec
576
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
577
+ return { refCount: 0, errors }
578
+ }
579
+ const rules = /** @type {Record<string, unknown>} */ (spec).rules
580
+ if (!Array.isArray(rules)) {
581
+ return { refCount: 0, errors }
582
+ }
583
+ let refCount = 0
584
+ for (const rule of rules) {
585
+ if (rule === null || typeof rule !== 'object' || Array.isArray(rule)) {
586
+ continue
587
+ }
588
+ const brs = /** @type {Record<string, unknown>} */ (rule).backendRefs
589
+ if (!Array.isArray(brs)) {
590
+ continue
591
+ }
592
+ for (const br of brs) {
593
+ if (br === null || typeof br !== 'object' || Array.isArray(br)) {
594
+ continue
595
+ }
596
+ const brRec = /** @type {Record<string, unknown>} */ (br)
597
+ const name = brRec.name
598
+ if (typeof name !== 'string' || !ABIE_SHARED_CROSS_NS_BACKEND_SET.has(name)) {
599
+ continue
600
+ }
601
+ refCount++
602
+ const ns = brRec.namespace
603
+ if (typeof ns !== 'string' || ns !== 'dev') {
604
+ errors.push(`${rel}: HTTPRoute backendRefs до ${name} має містити namespace: dev (abie.mdc)`)
605
+ }
606
+ }
607
+ }
608
+ return { refCount, errors }
609
+ }
610
+
611
+ /**
612
+ * З YAML під **k8s** пакета (без overlay **ua** та **ru**) збирає кількість **`backendRefs`** до **`auth-run-hl`** і **`filelint-hl`** і порушення **`namespace: dev`**.
613
+ * @param {string} root корінь репозиторію
614
+ * @param {string} pkgAbs абсолютний шлях до каталогу пакета
615
+ * @param {string[]} yamlFilesAbs усі **yaml** під **k8s** (як **findK8sYamlFiles**)
616
+ * @returns {Promise<{ refCount: number, baseErrors: string[] }>}
617
+ */
618
+ export async function analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs) {
619
+ const pkgRel = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
620
+ let refCount = 0
621
+ /** @type {string[]} */
622
+ const baseErrors = []
623
+ for (const abs of yamlFilesAbs) {
624
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
625
+ if (!isK8sYamlInAbiePackageExcludingUaRuOverlays(rel, pkgRel)) {
626
+ continue
627
+ }
628
+ let raw
629
+ try {
630
+ raw = await readFile(abs, 'utf8')
631
+ } catch {
632
+ continue
633
+ }
634
+ const body = stripBom(raw)
635
+ const lines = body.split(/\r?\n/u)
636
+ const first = lines[0] ?? ''
637
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
638
+ /** @type {import('yaml').Document[]} */
639
+ let docs
640
+ try {
641
+ docs = parseAllDocuments(rest)
642
+ } catch {
643
+ continue
644
+ }
645
+ for (const doc of docs) {
646
+ if (doc.errors.length > 0) {
647
+ continue
648
+ }
649
+ const obj = doc.toJSON()
650
+ const st = httpRouteDocSharedCrossNsBackendStats(obj, rel)
651
+ refCount += st.refCount
652
+ baseErrors.push(...st.errors)
653
+ }
654
+ }
655
+ return { refCount, baseErrors }
656
+ }
657
+
658
+ /**
659
+ * Рахує операції JSON6902 з **`path`**: **`/spec/rules/…/backendRefs/…/namespace`** та **`value`** overlay.
660
+ * @param {string} combined сукупний текст patch **HTTPRoute**
661
+ * @param {'ua' | 'ru'} mode overlay
662
+ * @returns {number}
663
+ */
664
+ function countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode) {
665
+ const re =
666
+ mode === 'ua'
667
+ ? /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ua['"]?(?:\s|$)/gmu
668
+ : /path:\s*\/spec\/rules\/\d+\/backendRefs\/\d+\/namespace\b[\s\S]{0,200}?value:\s*['"]?ru['"]?(?:\s|$)/gmu
669
+ return [...combined.matchAll(re)].length
670
+ }
671
+
531
672
  /** Домени **hostnames** для overlay **ua** (підрядки у JSON6902-тексті patch), abie.mdc. */
532
673
  const ABIE_UA_HTTPROUTE_HOST_MARKERS = ['abie.app', 'vybeerai.com.ua', '*.abie.app', '*.vybeerai.com.ua']
533
674
 
@@ -610,9 +751,15 @@ export function getCombinedNginxRunPatchTextFromKustomization(raw) {
610
751
  * @param {string} combined текст одного або кількох inline **patch**, розділених символом нового рядка
611
752
  * @param {'ua' | 'ru'} mode **ua** або **ru**
612
753
  * @param {string} [fullKustomizationRaw] повний текст **kustomization.yaml** — для **ru** визначає, чи потрібна анотація **gwin…websocket** (лише якщо є **`HASURA_GRAPHQL_JWT_SECRET`**)
754
+ * @param {number} [sharedCrossNsBackendRefCount] скільки **`backendRefs`** до **`auth-run-hl`** і **`filelint-hl`** у base **HTTPRoute** пакета — стільки ж patch-ів **`…/backendRefs/…/namespace`** з **`value`** overlay
613
755
  * @returns {string | null} повідомлення про помилку або **null**
614
756
  */
615
- export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomizationRaw) {
757
+ export function validateAbieNginxRunHttpRoutePatches(
758
+ combined,
759
+ mode,
760
+ fullKustomizationRaw,
761
+ sharedCrossNsBackendRefCount = 0
762
+ ) {
616
763
  if (typeof combined !== 'string' || combined.trim() === '') {
617
764
  return `очікується patch target kind HTTPRoute з непорожнім target.name (hostnames, parentRefs namespace ${mode}; для ru — gwin… websocket лише за наявності HASURA_GRAPHQL_JWT_SECRET у файлі) — abie.mdc`
618
765
  }
@@ -637,6 +784,16 @@ export function validateAbieNginxRunHttpRoutePatches(combined, mode, fullKustomi
637
784
  if (ruNeedsWebsocket && !/gwin\.yandex\.cloud\/rules\.http\.upgradeTypes:\s*['"]?websocket['"]?/m.test(combined)) {
638
785
  return 'HTTPRoute (ru): за наявності HASURA_GRAPHQL_JWT_SECRET у kustomization потрібна анотація gwin.yandex.cloud/rules.http.upgradeTypes: websocket (abie.mdc)'
639
786
  }
787
+ const sharedCount =
788
+ typeof sharedCrossNsBackendRefCount === 'number' && Number.isFinite(sharedCrossNsBackendRefCount)
789
+ ? Math.max(0, Math.floor(sharedCrossNsBackendRefCount))
790
+ : 0
791
+ if (sharedCount > 0) {
792
+ const patchHits = countAbieHttpRouteBackendRefNamespacePatchesInCombined(combined, mode)
793
+ if (patchHits < sharedCount) {
794
+ return `HTTPRoute: для backendRefs до спільних сервісів auth-run-hl, filelint-hl очікується ${sharedCount} JSON6902 patch(ів) з path /spec/rules/…/backendRefs/…/namespace та value ${mode} (зараз ${patchHits}) — abie.mdc`
795
+ }
796
+ }
640
797
  return null
641
798
  }
642
799
 
@@ -744,8 +901,9 @@ export function validateAbieHcYaml(raw, relPath) {
744
901
  return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
745
902
  }
746
903
  const svcName = targetRef.name
747
- if (typeof svcName !== 'string' || svcName !== name) {
748
- return `${relPath}: targetRef.name має збігатися з metadata.name (${name}) (abie.mdc)`
904
+ const expectedHl = name.endsWith('-hl') ? name : `${name}-hl`
905
+ if (typeof svcName !== 'string' || svcName !== expectedHl) {
906
+ return `${relPath}: targetRef.name має посилатися на headless Service (очікується ${expectedHl}, суфікс -hl) (abie.mdc)`
749
907
  }
750
908
  return null
751
909
  }
@@ -912,6 +1070,21 @@ async function ensureUaRuAbieNodeSelectorPatches(root, yamlFilesAbs, deploymentD
912
1070
  * @returns {Promise<void>}
913
1071
  */
914
1072
  async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn) {
1073
+ /** @type {Map<string, Promise<{ refCount: number, baseErrors: string[] }>>} */
1074
+ const sharedBackendAnalysisByPkg = new Map()
1075
+ /**
1076
+ * @param {string} pkgAbs
1077
+ * @returns {Promise<{ refCount: number, baseErrors: string[] }>}
1078
+ */
1079
+ const getSharedBackendAnalysis = pkgAbs => {
1080
+ let p = sharedBackendAnalysisByPkg.get(pkgAbs)
1081
+ if (!p) {
1082
+ p = analyzeAbieSharedBackendRefsInPackageK8s(root, pkgAbs, yamlFilesAbs)
1083
+ sharedBackendAnalysisByPkg.set(pkgAbs, p)
1084
+ }
1085
+ return p
1086
+ }
1087
+
915
1088
  const uaAbsList = yamlFilesAbs.filter(abs => isUaKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
916
1089
  if (uaAbsList.length === 0) {
917
1090
  passFn(
@@ -921,6 +1094,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
921
1094
  for (const abs of uaAbsList) {
922
1095
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
923
1096
  if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
1097
+ const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
1098
+ if (!pkgAbs) {
1099
+ fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
1100
+ return
1101
+ }
1102
+ const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
1103
+ for (const err of sharedAnalysis.baseErrors) {
1104
+ fail(err)
1105
+ return
1106
+ }
924
1107
  let raw
925
1108
  try {
926
1109
  raw = await readFile(abs, 'utf8')
@@ -930,7 +1113,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
930
1113
  return
931
1114
  }
932
1115
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
933
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua')
1116
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ua', raw, sharedAnalysis.refCount)
934
1117
  if (v !== null) {
935
1118
  fail(`${rel}: ${v}`)
936
1119
  return
@@ -950,6 +1133,16 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
950
1133
  for (const abs of ruAbsList) {
951
1134
  const rel = relative(root, abs).replaceAll('\\', '/') || abs
952
1135
  if (abieOverlayRequiresHttpRouteByVite(root, abs)) {
1136
+ const pkgAbs = abiePackageDirFromK8sOverlay(root, abs)
1137
+ if (!pkgAbs) {
1138
+ fail(`${rel}: внутрішня помилка abie overlay (немає каталогу пакета)`)
1139
+ return
1140
+ }
1141
+ const sharedAnalysis = await getSharedBackendAnalysis(pkgAbs)
1142
+ for (const err of sharedAnalysis.baseErrors) {
1143
+ fail(err)
1144
+ return
1145
+ }
953
1146
  let raw
954
1147
  try {
955
1148
  raw = await readFile(abs, 'utf8')
@@ -959,7 +1152,7 @@ async function ensureUaRuAbieHttpRoutePatches(root, yamlFilesAbs, fail, passFn)
959
1152
  return
960
1153
  }
961
1154
  const combined = getCombinedNginxRunPatchTextFromKustomization(raw)
962
- const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw)
1155
+ const v = validateAbieNginxRunHttpRoutePatches(combined, 'ru', raw, sharedAnalysis.refCount)
963
1156
  if (v !== null) {
964
1157
  fail(`${rel}: ${v}`)
965
1158
  return
@@ -1148,6 +1148,9 @@ export function serviceSvcHlYamlHeadlessViolation(manifest) {
1148
1148
 
1149
1149
  /**
1150
1150
  * Чи об’єкт схожий на **backendRef** до **Kubernetes Service** у Gateway API.
1151
+ *
1152
+ * Вимагає числовий **`port`**, щоб не плутати з **`HTTPHeaderMatch`** тощо (там теж є **`name`**, але без **`port`**).
1153
+ *
1151
1154
  * @param {unknown} obj вузол у дереві **`spec`**
1152
1155
  * @returns {boolean} true, якщо враховуємо поле **`name`** як посилання на Service
1153
1156
  */
@@ -1155,6 +1158,7 @@ function isGatewayApiBackendRefToService(obj) {
1155
1158
  if (obj === null || obj === undefined || typeof obj !== 'object' || Array.isArray(obj)) return false
1156
1159
  const o = /** @type {Record<string, unknown>} */ (obj)
1157
1160
  if (typeof o.name !== 'string') return false
1161
+ if (typeof o.port !== 'number') return false
1158
1162
  const kind = o.kind
1159
1163
  if (kind !== undefined && kind !== 'Service') return false
1160
1164
  const group = o.group