@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 +17 -2
- package/mdc/bun.mdc +1 -1
- package/mdc/graphql.mdc +1 -1
- package/mdc/js-lint.mdc +1 -1
- package/mdc/text.mdc +13 -19
- package/package.json +1 -1
- package/scripts/check-abie.mjs +286 -0
- package/scripts/check-bun.mjs +7 -7
- package/scripts/check-graphql.mjs +1 -1
- package/scripts/check-js-lint.mjs +3 -3
- package/scripts/check-text.mjs +38 -7
- package/skills/lint/SKILL.md +10 -0
package/mdc/abie.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
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`, а в
|
|
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
|
|
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
|
|
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
|
|
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": "^
|
|
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": "^
|
|
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
|
-
|
|
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. Залежності** —
|
|
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": "^
|
|
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
|
|
260
|
+
Встановлення: `bun add -d @nitra/cspell-dict@^2.0.0`.
|
|
264
261
|
|
|
265
|
-
**2. `.cspell.json`** — у полі
|
|
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
|
-
Підлаштуй
|
|
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
|
-
|
|
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
package/scripts/check-abie.mjs
CHANGED
|
@@ -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)
|
package/scripts/check-bun.mjs
CHANGED
|
@@ -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
|
-
*
|
|
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 (лише
|
|
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('@
|
|
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 дозволені лише @
|
|
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 порожні (
|
|
115
|
-
: `Кореневі devDependencies лише @
|
|
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`** тощо;
|
|
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
|
-
* Перевіряє
|
|
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') {
|
package/scripts/check-text.mjs
CHANGED
|
@@ -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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
|
280
|
-
if (
|
|
281
|
-
fail(
|
|
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
|
}
|
package/skills/lint/SKILL.md
CHANGED
|
@@ -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`**. За потреби запускай обидва.
|