@nitra/cursor 1.8.96 → 1.8.98

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.11'
4
+ version: '1.14'
5
5
  ---
6
6
 
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** у корені репозиторію.
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**), у overlay **ru** — кожен **Service** (у т. ч. **headless** / **`-hl`**) → **`spec.type: NodePort`** через **JSON6902** у **`kustomization.yaml`**, видалення **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
 
@@ -113,6 +113,21 @@ spec:
113
113
  gwin.yandex.cloud/rules.http.upgradeTypes: "websocket"
114
114
  ```
115
115
 
116
+ ## k8s: overlay **ru** і **Service** (у т. ч. headless → NodePort)
117
+
118
+ Для кожного **Service** в YAML під **`…/k8s/…`**, де шлях файлу **не** містить **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару; у т. ч. **headless** з **`spec.clusterIP: None`** і **`-hl`**), якщо ще не **`spec.type: NodePort`** / **`LoadBalancer`** / **`ExternalName`**, у **`k8s/ru/kustomization.yaml`** того ж пакета (overlay **ru**) додай **inline** **JSON6902** у **`patches`**: **`target.kind: Service`**, **`target.name`** як у маніфеста, **`path: /spec/type`**, **`value: NodePort`**. Деталі перевірки — **`check-abie.mjs`**.
119
+
120
+ ```yaml title="…/ru/kustomization.yaml (фрагмент)"
121
+ patches:
122
+ - target:
123
+ kind: Service
124
+ name: my-app
125
+ patch: |-
126
+ - op: replace
127
+ path: /spec/type
128
+ value: NodePort
129
+ ```
130
+
116
131
  ## k8s: overlay **ru** і HealthCheckPolicy
117
132
 
118
133
  Якщо в дереві **k8s** є **HealthCheckPolicy**, у **`ru/kustomization.yaml`** має бути patch **`$patch: delete`** для політики (узгоджено з **k8s.mdc**; перевірка в **`check-k8s.mjs`**, **`ruKustomizationHasHealthCheckDeletePatch`**). Підстав реальне ім’я замість **`СЕРВІС`**:
package/mdc/bun.mdc CHANGED
@@ -50,7 +50,7 @@ Lockfile у репозиторії: `bun.lock`.
50
50
  - Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
51
51
  - У CI та локально запускати скрипти через `bun run`.
52
52
 
53
- В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` будуть тільки `@cspell/*` та `@nitra/*` модулі.
53
+ В кореневому в package.json не повинно бути `dependencies`, а в `devDependencies` тільки модулі `@nitra/*`
54
54
 
55
55
  Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn
56
56
 
package/mdc/graphql.mdc CHANGED
@@ -4,7 +4,7 @@ alwaysApply: true
4
4
  version: '1.0'
5
5
  ---
6
6
 
7
- Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\`` для GraphQL-запиту), у **корені репозиторію** має бути файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs)), а в **`.vscode/extensions.json`** у масиві **`recommendations`** — запис **`graphql.vscode-graphql`**, щоб підсвітка, навігація до схеми й діагностика працювали в редакторі.
7
+ Якщо в **`.vue`** або в **JavaScript / TypeScript** джерелах (`.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx` тощо) зустрічається **tagged template literal** з тегом **`gql`** (типово `gql\`query …\``для GraphQL-запиту), у **корені репозиторію** має бути файл **`.graphqlrc.yml`** ([GraphQL Config](https://the-guild.dev/graphql/config/docs)), а в **`.vscode/extensions.json`** у масиві **`recommendations`** — запис **`graphql.vscode-graphql`**, щоб підсвітка, навігація до схеми й діагностика працювали в редакторі.
8
8
 
9
9
  Деталі виявлення `gql` у скриптах (у т.ч. лише `<script>` у SFC) — **`npm/scripts/check-graphql.mjs`** / **`npm/scripts/utils/graphql-gql-scan.mjs`**.
10
10
 
package/mdc/js-lint.mdc CHANGED
@@ -30,7 +30,7 @@ version: '1.13'
30
30
  }
31
31
  ```
32
32
 
33
- У корені має бути **`.oxlintrc.json`** з підключенням **`@e18e/eslint-plugin`** через **`jsPlugins`** і правилом **`e18e/prefer-includes`** зі значенням **`error`**. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.5.0**), oxlint резолвить його з **`node_modules`**.
33
+ У корені має бути **`.oxlintrc.json`** з підключенням **`@e18e/eslint-plugin`** через **`jsPlugins`** і правилом **`e18e/prefer-includes`** зі значенням **`error`**. Модуль **`@e18e/eslint-plugin`** не оголошуй окремо в **`package.json`** — він уже в залежностях **`@nitra/eslint-config`** (з **3.5.0**), oxlint підвантажує його з **`node_modules`**.
34
34
 
35
35
  ```json title=".oxlintrc.json (фрагмент)"
36
36
  {
package/mdc/text.mdc CHANGED
@@ -110,7 +110,7 @@ version: '1.25'
110
110
 
111
111
  Завжди пиши **JSDoc** до функцій та методів.
112
112
 
113
- **`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**. Для української додай **`@cspell/dict-uk-ua`**. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`bunx v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
113
+ **`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`** (**`^2.0.0`** або новіший у лінії 2.x) — з **2.0.0** у пакет транзитивно входять типові **`@cspell/dict-*`**, тому **не** додавай їх окремо в корінь. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`bunx v8r`** (зазвичай **`bunx v8r`**), не в devDependencies. Окремий пакет **`markdownlint`** не потрібний.
114
114
 
115
115
  У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`bunx v8r`** для кожного типу (**json**, **json5**, **yml**, **yaml**, **toml**), бо один процес v8r з кількома глобами падає з **98**, якщо хоч один glob порожній, і тоді інші розширення не перевіряються. Вивід при кодах **0** і **98** не показується. Каталог схем **`schemas/v8r-catalog.json`** пакета `@nitra/cursor` скрипт підставляє в v8r сам. За бажання можна передати власні glob-и аргументами скрипта. Шлях до скрипта: `./npm/scripts/…`, `./scripts/…` після копіювання, або `node_modules/@nitra/cursor/scripts/…`.
116
116
 
@@ -120,7 +120,7 @@ version: '1.25'
120
120
  "lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
121
121
  },
122
122
  "devDependencies": {
123
- "@nitra/cspell-dict": "^1.0.188"
123
+ "@nitra/cspell-dict": "^2.0.0"
124
124
  }
125
125
  }
126
126
  ```
@@ -235,17 +235,16 @@ jobs:
235
235
  "lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
236
236
  },
237
237
  "devDependencies": {
238
- "@nitra/cspell-dict": "^1.0.188",
239
- "@cspell/dict-uk-ua": "^4.0.6"
238
+ "@nitra/cspell-dict": "^2.0.0"
240
239
  }
241
240
  }
242
241
  ```
243
242
 
244
- ## Проєкт з українською мовою
243
+ ## Проєкт з українською та російською (і суміжні мови)
245
244
 
246
- Якщо в репозиторії є українська документація, коментарі або рядки в кодіпотрібен **окремий словник** `@cspell/dict-ru_ru`, інакше cspell не перевірятиме російський правопис коректно.
245
+ У **`@nitra/cspell-dict`** від **2.0.0** уже зібрані залежності на **`@cspell/dict-uk-ua`**, **`@cspell/dict-ru_ru`** та інші типові словники **не** дублюй їх у кореневому `package.json` і **не** імпортуй **`@cspell/dict-*/cspell-ext.json`** у `.cspell.json`.
247
246
 
248
- **1. Залежності** — додай пакет словника поруч із `@nitra/cspell-dict`:
247
+ **1. Залежності** — лише корпоративний пакет:
249
248
 
250
249
  ```json title="package.json"
251
250
  {
@@ -253,31 +252,26 @@ jobs:
253
252
  "lint-text": "npx cspell . && bunx markdownlint-cli2 --fix \"**/*.md\" \"**/*.mdc\" && bun ./npm/scripts/run-v8r.mjs"
254
253
  },
255
254
  "devDependencies": {
256
- "@nitra/cspell-dict": "^1.0.188",
257
- "@cspell/dict-uk-ua": "^4.0.6",
258
- "@cspell/dict-ru_ru": "^2.3.2"
255
+ "@nitra/cspell-dict": "^2.0.0"
259
256
  }
260
257
  }
261
258
  ```
262
259
 
263
- Встановлення: `bun add -d @cspell/dict-uk-ua` (або `npm i -D @cspell/dict-uk-ua`).
260
+ Встановлення: `bun add -d @nitra/cspell-dict@^2.0.0`.
264
261
 
265
- **2. `.cspell.json`** — у полі `language` має бути `uk` (разом з іншими мовами через кому), у `import` підключення розширення українського словника:
262
+ **2. `.cspell.json`** — у полі **`language`** додай потрібні коди (наприклад **`uk`**, **`ru-ru`** разом з **`en`** та **`nitra`**). У **`import`** залиш лише **`@nitra/cspell-dict/cspell-ext.json`**:
266
263
 
267
264
  ```json title=".cspell.json"
268
265
  {
269
266
  "version": "0.2",
270
- "language": "en,uk,nitra",
267
+ "language": "en,uk,ru-ru,nitra",
271
268
  "ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "report", "*.svg", "**/k8s/**/*.yaml"],
272
- "import": [
273
- "@nitra/cspell-dict/cspell-ext.json",
274
- "@cspell/dict-uk-ua/cspell-ext.json"
275
- ],
269
+ "import": ["@nitra/cspell-dict/cspell-ext.json"],
276
270
  "words": []
277
271
  }
278
272
  ```
279
273
 
280
- Підлаштуй `language` під проєкт (наприклад додай `ru-ru`, якщо потрібна перевірка російською). Порядок у `import` може впливати на пріоритет словників — тримай корпоративний `@nitra/cspell-dict` там, де зручно для ваших правил.
274
+ Підлаштуй **`language`** під проєкт. Порядок у **`import`** може впливати на пріоритет словників — тримай корпоративний **`@nitra/cspell-dict`** першим, якщо додаєш інші розширення (рідко).
281
275
 
282
276
  **Український апостроф:** у словах не використовуй прямий символ `'` (U+0027); потрібен типографський апостроф `’` (U+2019). Якщо після цього cspell досі підсвічує слово як невідоме — додай його до масиву `words` у `.cspell.json`.
283
277
 
@@ -293,7 +287,7 @@ jobs:
293
287
 
294
288
  ## Інші мови
295
289
 
296
- Для іншої мови встанови відповідний пакет `@cspell/dict-*`, додай його `cspell-ext.json` у `import` і код мови в `language`. Огляд словників: [streetsidesoftware/cspell-dicts](https://github.com/streetsidesoftware/cspell-dicts).
290
+ Якщо потрібна мова вже є в залежностях **`@nitra/cspell-dict`** додай лише код у **`language`**, без окремих **`@cspell/dict-*`** у споживачі. Якщо мови немає в корпоративному пакеті — розширюй **`@nitra/cspell-dict`**, а не підключай **`@cspell/dict-*`** у корені репозиторію-споживача. Огляд upstream-словників: [streetsidesoftware/cspell-dicts](https://github.com/streetsidesoftware/cspell-dicts).
297
291
 
298
292
  ## Перевірка
299
293
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.96",
3
+ "version": "1.8.98",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -32,6 +32,9 @@
32
32
  * **Спільні бекенди (`auth-run-hl`, `filelint-hl`):** у **HTTPRoute** під **`k8s`** поза overlay **ua** та **ru** (шлях не містить **`k8s/ua/`** чи **`k8s/ru/`**) кожен такий **`backendRefs`** має **`namespace: dev`** і порт **8080**;
33
33
  * у patch overlay **ua** та **ru** — по одному **JSON6902** на **`/spec/rules/…/backendRefs/…/namespace`** з **`value`**: **ua** або **ru** (кількість patch-ів = кількість таких **`backendRefs`** у пакеті).
34
34
  * Вибір **`op`** — **k8s.mdc**.
35
+ *
36
+ * **Service (overlay ru):** для кожного **Service**, оголошеного в YAML під **`…/k8s/…`**, де шлях **не** проходить через **`k8s/ua/`** чи **`k8s/ru/`** (маніфести base / спільного шару, у т. ч. **headless** з **`clusterIP: None`** і **`-hl`**), якщо ще не **NodePort** / **LoadBalancer** / **ExternalName**,
37
+ * у файлі **`k8s/ru/kustomization.yaml`** того ж пакета (overlay середовища **ru**) — inline **JSON6902** на **`kind: Service`** з тим самим **`target.name`**: **`path: /spec/type`**, **`value: NodePort`**.
35
38
  */
36
39
  import { existsSync } from 'node:fs'
37
40
  import { readFile } from 'node:fs/promises'
@@ -295,6 +298,286 @@ function isDeploymentDoc(obj) {
295
298
  )
296
299
  }
297
300
 
301
+ /**
302
+ * Чи документ — **Service**.
303
+ * @param {unknown} obj корінь YAML-документа
304
+ * @returns {boolean} true, якщо **kind** — **Service**
305
+ */
306
+ function isServiceDoc(obj) {
307
+ return (
308
+ obj !== null &&
309
+ typeof obj === 'object' &&
310
+ !Array.isArray(obj) &&
311
+ /** @type {Record<string, unknown>} */ (obj).kind === 'Service'
312
+ )
313
+ }
314
+
315
+ /**
316
+ * Чи відносний шлях до YAML під **`…/k8s/…`** не в каталозі overlay **`k8s/ua/`** чи **`k8s/ru/`** у репозиторії (після **`k8s/`** одразу не йде **`ua/`** чи **`ru/`**).
317
+ * @param {string} relFromRoot шлях від кореня
318
+ * @returns {boolean} true для base / спільних маніфестів; **false** для файлів усередині **`k8s/ua/…`** або **`k8s/ru/…`**
319
+ */
320
+ function k8sYamlRelOutsideUaRuOverlays(relFromRoot) {
321
+ const norm = relFromRoot.replaceAll('\\', '/')
322
+ const idx = norm.indexOf('/k8s/')
323
+ if (idx === -1) {
324
+ return false
325
+ }
326
+ const after = norm.slice(idx + '/k8s/'.length)
327
+ return after.length > 0 && !after.startsWith('ua/') && !after.startsWith('ru/')
328
+ }
329
+
330
+ /**
331
+ * Каталог пакета з відносного шляху **`…/k8s/…`** (частина до **`/k8s/`**).
332
+ * @param {string} root корінь репозиторію
333
+ * @param {string} relFromRoot відносний шлях
334
+ * @returns {string | null} абсолютний шлях до каталогу пакета або **null**
335
+ */
336
+ function abiePackageDirFromK8sYamlRel(root, relFromRoot) {
337
+ const norm = relFromRoot.replaceAll('\\', '/')
338
+ const m = norm.match(/^(.+)\/k8s\//u)
339
+ return m ? join(root, m[1]) : null
340
+ }
341
+
342
+ /**
343
+ * Чи **Service** у base-шарі abie потребує в **ru** patch **`spec.type: NodePort`** (у т. ч. **headless**; не вже **NodePort** / **LoadBalancer** / **ExternalName**).
344
+ * @param {unknown} obj корінь YAML (**Service**)
345
+ * @returns {boolean} true, якщо для overlay **ru** очікується **NodePort**
346
+ */
347
+ export function serviceDocumentRequiresAbieRuNodePortOverlay(obj) {
348
+ if (!isServiceDoc(obj)) {
349
+ return false
350
+ }
351
+ const rec = /** @type {Record<string, unknown>} */ (obj)
352
+ const meta = rec.metadata
353
+ if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
354
+ return false
355
+ }
356
+ const name = /** @type {Record<string, unknown>} */ (meta).name
357
+ if (typeof name !== 'string' || name.trim() === '') {
358
+ return false
359
+ }
360
+ const spec = rec.spec
361
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
362
+ return true
363
+ }
364
+ const sp = /** @type {Record<string, unknown>} */ (spec)
365
+ const t = sp.type
366
+ if (t === 'NodePort' || t === 'LoadBalancer' || t === 'ExternalName') {
367
+ return false
368
+ }
369
+ return true
370
+ }
371
+
372
+ /**
373
+ * Чи фрагмент **JSON6902** у **`patch`** задає **`/spec/type`** зі значенням **NodePort** (abie overlay **ru**).
374
+ * @param {string} patchText поле **patch** у kustomization
375
+ * @returns {boolean} true, якщо знайдено **path** і **value**
376
+ */
377
+ export function jsonPatchTextSetsServiceTypeNodePort(patchText) {
378
+ if (typeof patchText !== 'string' || patchText.trim() === '') {
379
+ return false
380
+ }
381
+ if (!/path:\s*\/spec\/type\b/u.test(patchText)) {
382
+ return false
383
+ }
384
+ if (!/value:\s*['"]?NodePort['"]?(?:\s|$)/iu.test(patchText)) {
385
+ return false
386
+ }
387
+ return true
388
+ }
389
+
390
+ /**
391
+ * З одного документа **Kustomization** збирає пари **Service name → patch text** для **inline patches** з **target.kind: Service**.
392
+ * @param {import('yaml').Document} doc документ після **parseAllDocuments**
393
+ * @returns {Map<string, string>} ім’я сервісу → текст **patch**
394
+ */
395
+ function collectAbieServicePatchTextsByNameFromKustomizationDoc(doc) {
396
+ /** @type {Map<string, string>} */
397
+ const out = new Map()
398
+ if (doc.errors.length > 0) {
399
+ return out
400
+ }
401
+ const root = doc.toJSON()
402
+ if (root === null || typeof root !== 'object' || Array.isArray(root)) {
403
+ return out
404
+ }
405
+ const rec = /** @type {Record<string, unknown>} */ (root)
406
+ if (rec.kind !== 'Kustomization') {
407
+ return out
408
+ }
409
+ const patches = rec.patches
410
+ if (!Array.isArray(patches)) {
411
+ return out
412
+ }
413
+ for (const p of patches) {
414
+ if (p !== null && typeof p === 'object' && !Array.isArray(p)) {
415
+ const pr = /** @type {Record<string, unknown>} */ (p)
416
+ const target = pr.target
417
+ if (target !== null && typeof target === 'object' && !Array.isArray(target)) {
418
+ const tg = /** @type {Record<string, unknown>} */ (target)
419
+ if (tg.kind === 'Service' && typeof tg.name === 'string' && tg.name.trim() !== '') {
420
+ const patchStr = pr.patch
421
+ if (typeof patchStr === 'string' && patchStr.trim() !== '') {
422
+ const prev = out.get(tg.name)
423
+ out.set(tg.name, prev === undefined ? patchStr : `${prev}\n${patchStr}`)
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ return out
430
+ }
431
+
432
+ /**
433
+ * Імена **Service**, для яких у **ru/kustomization.yaml** немає очікуваного patch **`/spec/type` → NodePort** (abie.mdc).
434
+ * @param {string} raw повний текст **kustomization.yaml**
435
+ * @param {Iterable<string>} serviceNames імена **metadata.name** з base-шару
436
+ * @returns {string[]} відсортовані імена без коректного patch
437
+ */
438
+ export function getMissingAbieRuServiceNodePortPatchServiceNames(raw, serviceNames) {
439
+ const req = [...new Set([...serviceNames].filter(n => typeof n === 'string' && n.trim() !== ''))].toSorted((a, b) =>
440
+ a.localeCompare(b)
441
+ )
442
+ if (req.length === 0) {
443
+ return []
444
+ }
445
+ const body = stripBom(raw)
446
+ const lines = body.split(/\r?\n/u)
447
+ const first = lines[0] ?? ''
448
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
449
+ /** @type {import('yaml').Document[]} */
450
+ let docs
451
+ try {
452
+ docs = parseAllDocuments(rest)
453
+ } catch {
454
+ return req
455
+ }
456
+ /** @type {Map<string, string>} */
457
+ const byName = new Map()
458
+ for (const doc of docs) {
459
+ const chunk = collectAbieServicePatchTextsByNameFromKustomizationDoc(doc)
460
+ for (const [k, v] of chunk) {
461
+ const prev = byName.get(k)
462
+ byName.set(k, prev === undefined ? v : `${prev}\n${v}`)
463
+ }
464
+ }
465
+ return req.filter(n => {
466
+ const pt = byName.get(n)
467
+ return pt === undefined || !jsonPatchTextSetsServiceTypeNodePort(pt)
468
+ })
469
+ }
470
+
471
+ /**
472
+ * Для кожного пакета збирає імена **Service**, які в overlay **ru** мають стати **NodePort** (abie.mdc).
473
+ * @param {string} root корінь репозиторію
474
+ * @param {string[]} yamlAbs абсолютні шляхи yaml під **k8s**
475
+ * @param {(msg: string) => void} fail реєстрація помилки читання/парсингу
476
+ * @returns {Promise<Map<string, Set<string>>>} **pkgAbs** → множина імен **Service**
477
+ */
478
+ async function collectAbieRuNodePortServiceNamesByPackage(root, yamlAbs, fail) {
479
+ /** @type {Map<string, Set<string>>} */
480
+ const map = new Map()
481
+ for (const abs of yamlAbs) {
482
+ const rel = relative(root, abs).replaceAll('\\', '/') || abs
483
+ if (k8sYamlRelOutsideUaRuOverlays(rel)) {
484
+ const pkgAbs = abiePackageDirFromK8sYamlRel(root, rel)
485
+ if (pkgAbs) {
486
+ let raw
487
+ let readOk = false
488
+ try {
489
+ raw = await readFile(abs, 'utf8')
490
+ readOk = true
491
+ } catch (error) {
492
+ const msg = error instanceof Error ? error.message : String(error)
493
+ fail(`${rel}: не вдалося прочитати (${msg})`)
494
+ }
495
+ if (readOk) {
496
+ const body = stripBom(raw)
497
+ const lines = body.split(/\r?\n/u)
498
+ const first = lines[0] ?? ''
499
+ const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
500
+ /** @type {import('yaml').Document[]} */
501
+ let docs
502
+ let parseOk = false
503
+ try {
504
+ docs = parseAllDocuments(rest)
505
+ parseOk = true
506
+ } catch (error) {
507
+ const msg = error instanceof Error ? error.message : String(error)
508
+ fail(`${rel}: YAML (${msg})`)
509
+ }
510
+ if (parseOk) {
511
+ for (const doc of docs) {
512
+ if (doc.errors.length === 0) {
513
+ const obj = doc.toJSON()
514
+ if (serviceDocumentRequiresAbieRuNodePortOverlay(obj)) {
515
+ const rec = /** @type {Record<string, unknown>} */ (obj)
516
+ const meta = /** @type {Record<string, unknown>} */ (rec.metadata)
517
+ const n = meta.name
518
+ if (typeof n === 'string' && n.trim() !== '') {
519
+ let s = map.get(pkgAbs)
520
+ if (!s) {
521
+ s = new Set()
522
+ map.set(pkgAbs, s)
523
+ }
524
+ s.add(n)
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+ }
534
+ return map
535
+ }
536
+
537
+ /**
538
+ * У **`k8s/ru/kustomization.yaml`** для кожного **Service** з YAML **`k8s`**, шлях якого без сегментів **`k8s/ua/`** та **`k8s/ru/`** (у т. ч. **headless** / **`-hl`**) — **JSON6902** **`/spec/type` → NodePort**, якщо ще не **NodePort** / **LoadBalancer** / **ExternalName** (abie.mdc).
539
+ * @param {string} root корінь
540
+ * @param {string[]} yamlFilesAbs yaml під **k8s**
541
+ * @param {(msg: string) => void} fail callback
542
+ * @param {(msg: string) => void} passFn успішне повідомлення
543
+ * @returns {Promise<void>}
544
+ */
545
+ async function ensureRuAbieServiceNodePortPatches(root, yamlFilesAbs, fail, passFn) {
546
+ const byPkg = await collectAbieRuNodePortServiceNamesByPackage(root, yamlFilesAbs, fail)
547
+ const entries = [...byPkg.entries()].filter(([, names]) => names.size > 0)
548
+ if (entries.length === 0) {
549
+ passFn('Немає Service у шарі k8s без k8s/ua/ та k8s/ru/ — patch NodePort у k8s/ru/ не вимагається (abie.mdc)')
550
+ return
551
+ }
552
+ for (const [pkgAbs, names] of entries.toSorted((a, b) => a[0].localeCompare(b[0]))) {
553
+ const relPkg = relative(root, pkgAbs).replaceAll('\\', '/') || pkgAbs
554
+ const ruAbs = join(pkgAbs, 'k8s', 'ru', 'kustomization.yaml')
555
+ if (!existsSync(ruAbs)) {
556
+ fail(
557
+ `${relPkg}/k8s: є Service (у т. ч. headless), для overlay ru потрібен patch /spec/type → NodePort: ${[...names].toSorted((a, b) => a.localeCompare(b)).join(', ')} — додай ru/kustomization.yaml (abie.mdc)`
558
+ )
559
+ return
560
+ }
561
+ const relRu = relative(root, ruAbs).replaceAll('\\', '/') || ruAbs
562
+ let raw
563
+ try {
564
+ raw = await readFile(ruAbs, 'utf8')
565
+ } catch (error) {
566
+ const msg = error instanceof Error ? error.message : String(error)
567
+ fail(`${relRu}: не вдалося прочитати (${msg})`)
568
+ return
569
+ }
570
+ const missing = getMissingAbieRuServiceNodePortPatchServiceNames(raw, names)
571
+ if (missing.length > 0) {
572
+ fail(
573
+ `${relRu}: для kind: Service потрібен inline JSON6902 з path /spec/type та value NodePort (ім’я target: ${missing.join(', ')}) — abie.mdc`
574
+ )
575
+ return
576
+ }
577
+ passFn(`${relRu}: patch Service → NodePort (ru) відповідає abie.mdc`)
578
+ }
579
+ }
580
+
298
581
  /**
299
582
  * Директорії, де є хоча б один **Deployment** у файлах **k8s**.
300
583
  * @param {string} root корінь cwd
@@ -1273,6 +1556,9 @@ export async function check() {
1273
1556
  const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
1274
1557
  await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
1275
1558
 
1559
+ pass('Перевіряємо Service → NodePort у ru/kustomization (abie.mdc)')
1560
+ await ensureRuAbieServiceNodePortPatches(root, yamlFiles, fail, pass)
1561
+
1276
1562
  if (deploymentDirs.size > 0) {
1277
1563
  pass('Є Deployment — перевіряємо nodeSelector у ua/ru kustomization (abie.mdc)')
1278
1564
  await ensureUaRuAbieNodeSelectorPatches(root, yamlFiles, deploymentDirs, fail, pass)
@@ -4,8 +4,8 @@
4
4
  * Очікує наявність `bun.lock`, забороняє lockfile та артефакти yarn/pnpm, директорію `.yarn`
5
5
  * і поле `packageManager` у кореневому `package.json`.
6
6
  *
7
- * У кореневому `package.json` не має бути поля **`dependencies`**; у **`devDependencies`** дозволені
8
- * лише пакети з префіксами **`@cspell/`** та **`@nitra/`** (інші залежності — у workspace-пакетах).
7
+ * У кореневому `package.json` не має бути поля **`dependencies`**; у **`devDependencies`** дозволені лише
8
+ * пакети **`@nitra/*`** (наприклад **`@nitra/cspell-dict`**, **`@nitra/eslint-config`**).
9
9
  *
10
10
  * Якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, вимагає у кореневому `package.json`
11
11
  * відповідно скриптів `lint-docker` / `lint-k8s` (див. docker.mdc, k8s.mdc).
@@ -20,12 +20,12 @@ import { readFile } from 'node:fs/promises'
20
20
  import { createCheckReporter } from './utils/check-reporter.mjs'
21
21
 
22
22
  /**
23
- * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише `@cspell/*` та `@nitra/*`).
23
+ * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише **`@nitra/*`**).
24
24
  * @param {string} name ключ з поля `devDependencies`
25
25
  * @returns {boolean} true, якщо префікс дозволений
26
26
  */
27
27
  export function isAllowedRootDevDependency(name) {
28
- return name.startsWith('@cspell/') || name.startsWith('@nitra/')
28
+ return name.startsWith('@nitra/')
29
29
  }
30
30
 
31
31
  /**
@@ -105,14 +105,14 @@ export async function check() {
105
105
  const bad = Object.keys(dev).filter(n => !isAllowedRootDevDependency(n))
106
106
  if (bad.length > 0) {
107
107
  fail(
108
- `Кореневі devDependencies дозволені лише @cspell/* та @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`
108
+ `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`
109
109
  )
110
110
  } else {
111
111
  const n = Object.keys(dev).length
112
112
  pass(
113
113
  n === 0
114
- ? 'Кореневі devDependencies порожні (дозволені лише @cspell/* та @nitra/*)'
115
- : `Кореневі devDependencies лише @cspell/* та @nitra/* (${n} пак.)`
114
+ ? 'Кореневі devDependencies порожні або відсутні (лише @nitra/*)'
115
+ : `Кореневі devDependencies: лише @nitra/* (${n} пак.)`
116
116
  )
117
117
  }
118
118
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`** і рекомендації **`graphql.vscode-graphql`**, якщо у дереві є **`gql\`…\``**.
3
3
  *
4
- * Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; ігнор **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
4
+ * Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; пропуск **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
5
5
  *
6
6
  * Виявлення **`gql`** — **oxc-parser** після витягування `<script>` з SFC (**`graphql-gql-scan.mjs`**). Якщо збігів немає — перевірка завершується успішно без вимог до конфігів.
7
7
  */
@@ -59,7 +59,7 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
59
59
  }
60
60
 
61
61
  /**
62
- * Перевіряє обов’язкові поля `.oxlintrc.json` для плагіна e18e (js-lint.mdc).
62
+ * Перевіряє потрібні поля `.oxlintrc.json` для розширення e18e (js-lint.mdc).
63
63
  *
64
64
  * @param {unknown} cfg корінь JSON-конфігурації oxlint
65
65
  * @returns {{ ok: boolean, failures: string[] }} `ok` і перелік повідомлень для `fail`
@@ -67,7 +67,7 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
67
67
  export function verifyOxlintRcE18e(cfg) {
68
68
  const failures = []
69
69
  if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) {
70
- return { ok: false, failures: ['.oxlintrc.json: корінь має бути об’єктом'] }
70
+ return { ok: false, failures: ['.oxlintrc.json: корінь має бути значенням типу object'] }
71
71
  }
72
72
  const o = /** @type {Record<string, unknown>} */ (cfg)
73
73
  const jsPlugins = o.jsPlugins
@@ -76,7 +76,7 @@ export function verifyOxlintRcE18e(cfg) {
76
76
  }
77
77
  const rules = o.rules
78
78
  if (!rules || typeof rules !== 'object' || Array.isArray(rules)) {
79
- failures.push('.oxlintrc.json: поле rules має бути об’єктом')
79
+ failures.push('.oxlintrc.json: поле rules має бути значенням типу object')
80
80
  } else {
81
81
  const r = /** @type {Record<string, unknown>} */ (rules)
82
82
  if (r['e18e/prefer-includes'] !== 'error') {
@@ -4,7 +4,8 @@
4
4
  * oxfmt: `.oxfmtrc.json` з обовʼязковими ключами, VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
5
5
  * відсутність Prettier у конфігах і залежностях.
6
6
  *
7
- * cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json), заборона
7
+ * cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json); у кореневих **`devDependencies`**
8
+ * дозволені лише **`@nitra/*`** (як у bun.mdc), зокрема **`@nitra/cspell-dict` ^2.0.0+**; без імпорту **`@cspell/dict-*`** у `.cspell.json`, заборона
8
9
  * `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
9
10
  * `.v8rignore` (vscode JSON),
10
11
  * workflow `lint-text.yml`, розширення VSCode (markdownlint, oxc).
@@ -15,12 +16,27 @@
15
16
  import { existsSync } from 'node:fs'
16
17
  import { readFile } from 'node:fs/promises'
17
18
 
19
+ import { isAllowedRootDevDependency } from './check-bun.mjs'
18
20
  import { createCheckReporter } from './utils/check-reporter.mjs'
19
21
  import { anyRunStepIncludes, parseWorkflowYaml } from './utils/gha-workflow.mjs'
20
22
 
21
23
  /** Заголовок абзацу про апостроф у text.mdc / n-text.mdc. */
22
24
  const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
23
25
 
26
+ /**
27
+ * Чи діапазон версії @nitra/cspell-dict у package.json означає лінію 2.0.0+ (з цієї версії словники входять у пакет).
28
+ * @param {string|undefined} range наприклад "^2.0.0"
29
+ * @returns {boolean}
30
+ */
31
+ function cspellDictVersionAtLeast200(range) {
32
+ if (typeof range !== 'string' || !range.trim()) return false
33
+ const cleaned = range.trim().replace(/^workspace:\*/, '').replace(/^[\^~>=<]+\s*/, '')
34
+ const m = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/)
35
+ if (!m) return false
36
+ const major = Number(m[1])
37
+ return major >= 2
38
+ }
39
+
24
40
  /**
25
41
  * Перевіряє абзац про український апостроф у вмісті правила text.
26
42
  * @param {string} filePath шлях до файлу (для повідомлень)
@@ -217,11 +233,22 @@ export async function check() {
217
233
  if (pkg.prettier) fail('package.json містить поле "prettier" — видали його')
218
234
 
219
235
  const devDeps = pkg.devDependencies || {}
236
+ const nonNitraDev = Object.keys(devDeps).filter(n => !isAllowedRootDevDependency(n))
237
+ if (nonNitraDev.length > 0) {
238
+ fail(
239
+ `Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${nonNitraDev.join(', ')} (bun.mdc)`
240
+ )
241
+ } else {
242
+ pass('Кореневі devDependencies лише @nitra/*')
243
+ }
220
244
 
221
- if (devDeps['@nitra/cspell-dict']) {
222
- pass('@nitra/cspell-dict є в devDependencies')
245
+ const cspellRange = devDeps['@nitra/cspell-dict']
246
+ if (!cspellRange) {
247
+ fail('@nitra/cspell-dict у devDependencies обовʼязковий для cspell — bun add -d @nitra/cspell-dict@^2.0.0')
248
+ } else if (!cspellDictVersionAtLeast200(cspellRange)) {
249
+ fail('@nitra/cspell-dict має бути ^2.0.0 або новіший (словники зібрані в пакеті з 2.x)')
223
250
  } else {
224
- fail('@nitra/cspell-dict відсутній — bun add -d @nitra/cspell-dict')
251
+ pass('@nitra/cspell-dict ^2.0.0+')
225
252
  }
226
253
 
227
254
  const rootDeps = pkg.dependencies || {}
@@ -276,9 +303,13 @@ export async function check() {
276
303
 
277
304
  if (existsSync('.cspell.json')) {
278
305
  const cfg = JSON.parse(await readFile('.cspell.json', 'utf8'))
279
- const hasUkImport = (cfg.import || []).some(i => i.includes('@cspell/dict-uk-ua'))
280
- if (hasUkImport && !devDeps['@cspell/dict-uk-ua']) {
281
- fail('.cspell.json імпортує @cspell/dict-uk-ua, але пакет відсутній в devDependencies')
306
+ const dictImports = (cfg.import || []).filter(i => typeof i === 'string' && i.includes('@cspell/dict-'))
307
+ if (dictImports.length > 0) {
308
+ fail(
309
+ `.cspell.json не має імпортувати @cspell/dict-* (${dictImports.join(', ')}) — використовуй лише @nitra/cspell-dict/cspell-ext.json`
310
+ )
311
+ } else {
312
+ pass('.cspell.json без прямих імпортів @cspell/dict-*')
282
313
  }
283
314
  }
284
315
  }
@@ -26,6 +26,7 @@ bun run lint
26
26
  2. **Якщо exit code не 0** — проаналізуй вивід (останній упавший крок у ланцюжку **`lint`** часто видно з stderr / логів):
27
27
  - Де скрипт уже робить **auto-fix** (**`--fix`**, **`markdownlint-cli2 --fix`**, **`oxfmt`** тощо) — перезапусти **`bun run lint`** після змін файлів.
28
28
  - Де auto-fix **немає** (наприклад, **jscpd**, **cspell**, **zizmor**, перевірки без прапорця fix) — виправ код, конфіги або винятки **узгоджено з** `.cursor/rules/` (не розширюй ignore лише щоб приховати проблему без причини).
29
+ - Якщо спрацьовує **`sonarjs/cognitive-complexity`** — див. окремий блок нижче.
29
30
 
30
31
  3. **Цикл** — повторюй кроки 1–2, доки **`bun run lint`** не завершиться успішно. Після суттєвих правок за потреби ще раз **`bun run lint`**, щоб переконатися, що не зламав наступний крок у скрипті **`lint`**.
31
32
 
@@ -37,6 +38,15 @@ bun run lint
37
38
 
38
39
  5. **Результат** — коротко опиши, що саме виправлено; якщо щось блокує нульовий exit code — залиш чітке пояснення й наступні кроки для людини.
39
40
 
41
+ ## sonarjs/cognitive-complexity
42
+
43
+ - **Не** додавай **`eslint-disable`** (у т.ч. на **`sonarjs/cognitive-complexity`**) чи інші коментарі-винятки лише щоб приховати порушення — потрібен саме **рефакторинг коду**, щоб зменшити **cognitive complexity**.
44
+ - **Перед будь-яким рефакторингом** перевір, чи є **тести**, які покривають змінювану поведінку:
45
+ - **unit** — **`bun test`** (або скрипт тестів у відповідному пакеті репозиторію);
46
+ - **e2e** — **Playwright**, якщо в проєкті він використовується для UI/потоків.
47
+ - Якщо тестів **немає** або вони **не покривають** блок, який змінюєш — **спочатку** додай/розшир тести, переконайся, що вони стабільно проходять, **потім** роби рефакторинг, **потім** знову прогони тести й **`bun run lint`**, щоб підтвердити, що функціональність коректна й лінт чистий.
48
+ - Якщо після рефакторингу тести або лінт падають — **не** залишай «половинчастий» рефакторинг: відкотись або доведи зміни до зеленого стану.
49
+
40
50
  ## Примітка
41
51
 
42
52
  Цей скіл **не** замінює **`npx @nitra/cursor check`**: **`lint`** перевіряє лінтери/формат у **`package.json`**, а **`check`** — програмні правила пакета **`@nitra/cursor`**. За потреби запускай обидва.