@nitra/cursor 1.8.29 → 1.8.38
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/bin/n-cursor.js +14 -3
- package/mdc/abie.mdc +56 -0
- package/mdc/bun.mdc +9 -20
- package/mdc/js-lint.mdc +4 -4
- package/mdc/k8s.mdc +1 -1
- package/mdc/text.mdc +2 -2
- package/package.json +1 -1
- package/scripts/check-abie.mjs +468 -0
- package/scripts/check-js-lint.mjs +7 -9
- package/scripts/run-v8r.mjs +1 -1
- package/skills/n-mdc-check/SKILL.md +86 -0
package/bin/n-cursor.js
CHANGED
|
@@ -279,6 +279,16 @@ function extractSkillDescription(text) {
|
|
|
279
279
|
.trim()
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Підготовка опису skill для вставки в звичайний markdown (заголовок H1, bullet без code fence).
|
|
284
|
+
* Послідовність `<id>` сприймається markdownlint (MD033) як inline HTML — замінюємо на `{id}`.
|
|
285
|
+
* @param {string} desc один рядок з YAML frontmatter SKILL.md
|
|
286
|
+
* @returns {string} той самий рядок після заміни літералу з кутовими дужками навколо id на плейсхолдер у фігурних дужках (MD033).
|
|
287
|
+
*/
|
|
288
|
+
function skillDescriptionSafeForMarkdownInline(desc) {
|
|
289
|
+
return desc.replaceAll('<id>', '{id}')
|
|
290
|
+
}
|
|
291
|
+
|
|
282
292
|
/**
|
|
283
293
|
* Розгортає в шаблоні блок Mustache {{#section}} … {{/section}} для масиву елементів
|
|
284
294
|
* @param {string} template вихідний текст шаблону
|
|
@@ -382,7 +392,7 @@ async function buildSkillBulletItems(skillIds) {
|
|
|
382
392
|
const text = await readFile(skillMdPath, 'utf8')
|
|
383
393
|
const parsed = extractSkillDescription(text)
|
|
384
394
|
if (parsed) {
|
|
385
|
-
desc = parsed
|
|
395
|
+
desc = skillDescriptionSafeForMarkdownInline(parsed)
|
|
386
396
|
}
|
|
387
397
|
}
|
|
388
398
|
const pathLine = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
@@ -442,7 +452,7 @@ async function syncClaudeMd(configRules, configSkills) {
|
|
|
442
452
|
if (existsSync(skillMdPath)) {
|
|
443
453
|
const text = await readFile(skillMdPath, 'utf8')
|
|
444
454
|
const parsed = extractSkillDescription(text)
|
|
445
|
-
if (parsed) desc = parsed
|
|
455
|
+
if (parsed) desc = skillDescriptionSafeForMarkdownInline(parsed)
|
|
446
456
|
}
|
|
447
457
|
const ref = `- \`${SKILLS_DIR}/${dirName}/SKILL.md\``
|
|
448
458
|
lines.push(desc ? `${ref} — ${desc}` : ref, ` Команда: \`/${RULE_PREFIX}${id}\``)
|
|
@@ -559,7 +569,8 @@ async function syncCommands(configSkills) {
|
|
|
559
569
|
if (existsSync(srcSkillMd)) {
|
|
560
570
|
try {
|
|
561
571
|
const raw = await readFile(srcSkillMd, 'utf8')
|
|
562
|
-
const
|
|
572
|
+
const descRaw = extractSkillDescription(raw)
|
|
573
|
+
const desc = descRaw ? skillDescriptionSafeForMarkdownInline(descRaw) : ''
|
|
563
574
|
const header = desc ? `# ${RULE_PREFIX}${id} — ${desc}\n\n` : ''
|
|
564
575
|
const body = `${header}Виконай інструкції зі скілу \`.cursor/skills/${dirName}/SKILL.md\`.\n`
|
|
565
576
|
await writeFile(destFile, body, 'utf8')
|
package/mdc/abie.mdc
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Правила для проєктів abinbevefes
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
version: '1.0'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## k8s
|
|
8
|
+
|
|
9
|
+
Якщо в проекті є k8s deployment, рядом з ним повинен бути:
|
|
10
|
+
|
|
11
|
+
```yaml title="hc.yaml"
|
|
12
|
+
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
|
|
13
|
+
apiVersion: networking.gke.io/v1
|
|
14
|
+
kind: HealthCheckPolicy
|
|
15
|
+
metadata:
|
|
16
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
17
|
+
namespace: dev # буде замінено через kustomize
|
|
18
|
+
spec:
|
|
19
|
+
default:
|
|
20
|
+
config:
|
|
21
|
+
type: HTTP
|
|
22
|
+
httpHealthCheck:
|
|
23
|
+
requestPath: /healthz
|
|
24
|
+
port: 8080
|
|
25
|
+
targetRef:
|
|
26
|
+
group: ''
|
|
27
|
+
kind: Service
|
|
28
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
і підключення до kustomize. Але у директорії ru повинен бути файл kustomization.yaml з patch, що видаляє ресурс **HealthCheckPolicy**.
|
|
32
|
+
|
|
33
|
+
```yaml title="kustomization.yaml"
|
|
34
|
+
patches:
|
|
35
|
+
- target:
|
|
36
|
+
kind: HealthCheckPolicy
|
|
37
|
+
patch: |-
|
|
38
|
+
kind: HealthCheckPolicy
|
|
39
|
+
metadata:
|
|
40
|
+
name: НАЗВА_СЕРВІСУ_ДЛЯ_РОЗГОРТАННЯ
|
|
41
|
+
$patch: delete
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## branch
|
|
45
|
+
|
|
46
|
+
В lean-merged-branch.yml список гілок, які повинні бути:
|
|
47
|
+
|
|
48
|
+
```yaml title=".github/workflows/clean-merged-branch.yml"
|
|
49
|
+
ignore_branches: dev,ua,ru
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Перевірка
|
|
53
|
+
|
|
54
|
+
`npx @nitra/cursor check abie`
|
|
55
|
+
|
|
56
|
+
Програмна перевірка (**`check-abie.mjs`**) виконується лише якщо у **`.n-cursor.json`** у **`rules`** є **`abie`** — інакше вихід **0** без зауважень (щоб не вимагати **ua**/**ru** у репозиторіях без цього правила).
|
package/mdc/bun.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Bun як єдиний package manager у монорепо
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.6'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Проект використовує тільки Bun для керування залежностями та запуску скриптів.
|
|
@@ -32,6 +32,8 @@ version: '1.4'
|
|
|
32
32
|
- `bun add -d <pkg>` для devDependencies
|
|
33
33
|
- Для одноразових CLI-команд використовуй `bunx <tool>`.
|
|
34
34
|
|
|
35
|
+
**Не додавай** у `dependencies` / `devDependencies` пакети, які **використовуються лише як CLI** і їх достатньо викликати через **`bunx <pkg>`** (або **`npx`**, якщо в проєкті так прийнято): наприклад **oxlint**, **jscpd**, **eslint** у корені тощо. Виняток — пакет потрібен як **бібліотека** (імпорт у коді), peer для іншого пакета, або інструмент **не** покривається `bunx` у вашому CI.
|
|
36
|
+
|
|
35
37
|
Заборонено використовувати:
|
|
36
38
|
|
|
37
39
|
- `npm`
|
|
@@ -61,29 +63,16 @@ FROM oven/bun:alpine AS build-env
|
|
|
61
63
|
|
|
62
64
|
замість образу node
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
У **GitHub Actions** не вставляй у workflow окремі кроки **`actions/setup-node`**, **`oven-sh/setup-bun`**, **`actions/cache`** та **`bun install`** — їх **заборонено** дублювати в кожному job; завжди використовуй **локальний composite** (деталі й заборона дублювання — **ga.mdc**). Під капотом composite уже містить Node **24**, Bun, кеш і **`bun install --frozen-lockfile`**.
|
|
67
|
+
|
|
68
|
+
Після **`actions/checkout@v6`** (`persist-credentials: false`):
|
|
65
69
|
|
|
66
70
|
```yaml
|
|
67
|
-
- uses: actions/setup-
|
|
68
|
-
with:
|
|
69
|
-
node-version: '24'
|
|
70
|
-
|
|
71
|
-
- uses: oven-sh/setup-bun@v2
|
|
72
|
-
|
|
73
|
-
- name: Cache Bun dependencies
|
|
74
|
-
uses: actions/cache@v5
|
|
75
|
-
with:
|
|
76
|
-
path: |
|
|
77
|
-
~/.bun/install/cache
|
|
78
|
-
node_modules
|
|
79
|
-
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
|
|
80
|
-
restore-keys: |
|
|
81
|
-
${{ runner.os }}-bun-
|
|
82
|
-
|
|
83
|
-
- name: Install dependencies
|
|
84
|
-
run: bun install --frozen-lockfile
|
|
71
|
+
- uses: ./.github/actions/setup-bun-deps
|
|
85
72
|
```
|
|
86
73
|
|
|
74
|
+
Якщо в репозиторії action збережено під **`./npm/github-actions/setup-bun-deps`**, у `uses:` вкажи цей шлях замість `.github/actions/…` (**ga.mdc**).
|
|
75
|
+
|
|
87
76
|
## Перевірка
|
|
88
77
|
|
|
89
78
|
`npx @nitra/cursor check bun`
|
package/mdc/js-lint.mdc
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Перевірка JavaScript коду
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
4
|
+
version: '1.11'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js
|
|
7
|
+
**oxlint**, **ESLint**, **jscpd**. У скрипті **`lint-js`** і в CI — **`bunx oxlint`**, **`bunx eslint`**, **`bunx jscpd`** (у CI без **`--fix`** для oxlint/eslint — див. приклад workflow нижче). Без **prettier** і **@nitra/prettier-config**. Достатньо **`@nitra/eslint-config`** у devDependencies; пакети oxlint/eslint/jscpd не додавай без потреби монорепо.
|
|
8
8
|
|
|
9
9
|
```json title=".vscode/extensions.json"
|
|
10
10
|
{
|
|
@@ -19,7 +19,7 @@ version: '1.9'
|
|
|
19
19
|
```json title="package.json"
|
|
20
20
|
{
|
|
21
21
|
"scripts": {
|
|
22
|
-
"lint-js": "oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
22
|
+
"lint-js": "bunx oxlint --fix && bunx eslint --fix . && bunx jscpd ."
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@nitra/eslint-config": "^3.4.0"
|
|
@@ -38,7 +38,7 @@ version: '1.9'
|
|
|
38
38
|
"exitCode": 1,
|
|
39
39
|
"reporters": ["console"],
|
|
40
40
|
"minLines": 25,
|
|
41
|
-
"ignore": ["**/dist/**"
|
|
41
|
+
"ignore": ["**/dist/**"]
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
package/mdc/k8s.mdc
CHANGED
|
@@ -206,7 +206,7 @@ resources: {}
|
|
|
206
206
|
- Обхід з пропуском `node_modules`, `.git`, `dist`, `coverage`, `.turbo`, `.next`.
|
|
207
207
|
- **`file:`** у `$schema` — URL до apiVersion/kind не звіряється; **`https:`** — kustomization за іменем файлу → Schema Store; далі перевіряється **`EXPLICIT_K8S_SCHEMAS`** (`Map`: `apiVersion` + `kind` + `type`, для записів без `type` у маніфесті — третій компонент **`*`**); потім `v1` → yannh; група з `YANNH_GROUPS` → yannh; інакше → datree (GitHub Pages), крім рядків явної таблиці (наприклад **InfisicalSecret** — raw на `main`).
|
|
208
208
|
- У кожному YAML-документі з **`kind: Deployment`** — парсинг через **`yaml`**: у кожного елемента **`spec.template.spec.containers[]`** має існувати ключ **`resources`** зі значенням-об'єктом (допускається порожній **`{}`**); у кожного контейнера — **`imagePullPolicy: Always`**.
|
|
209
|
-
- У файлах, ім’я яких **не** `kustomization.yaml
|
|
209
|
+
- У файлах, ім’я яких **не** `kustomization.yaml`.
|
|
210
210
|
- Документи з **`kind: Ingress`** — заборонені (потрібен перехід на Gateway API, див. розділ **Ingress → Gateway API**).
|
|
211
211
|
- Якщо в будь-якому файлі під **`k8s`** є **`kind: HealthCheckPolicy`**, серед файлів має бути **`ru/kustomization.yaml`** (сегмент шляху **`ru`** перед іменем файлу), а його вміст — patch видалення **HealthCheckPolicy** з **`$patch: delete`** (див. той самий розділ).
|
|
212
212
|
- Заборона шляхів **`…/k8s/dev/…`** (окремої директорії **`dev`** під **`k8s`** не має бути).
|
package/mdc/text.mdc
CHANGED
|
@@ -28,9 +28,9 @@ version: '1.24'
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
**`package.json`:** скрипт **`lint-text`** і devDependencies **`@nitra/cspell-dict`**. Для української додай **`@cspell/dict-uk-ua`**. **`markdownlint-cli2`** викликай у `lint-text` лише через **`bunx markdownlint-cli2`**, не додавай пакет до devDependencies. **`v8r`** лише через **`
|
|
31
|
+
**`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`** не потрібний.
|
|
32
32
|
|
|
33
|
-
У v8r **немає** прапорця тихого режиму; рекомендовано скрипт **`run-v8r.mjs`** з репозиторію пакета `@nitra/cursor` (`npm/scripts/run-v8r.mjs`): один виклик у `lint-text` — під капотом послідовні **`
|
|
33
|
+
У 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/…`.
|
|
34
34
|
|
|
35
35
|
```json title="package.json"
|
|
36
36
|
{
|
package/package.json
CHANGED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Перевіряє відповідність проєкту правилу abie.mdc (проєкти abinbevefes).
|
|
3
|
+
*
|
|
4
|
+
* Застосовується лише якщо у **`.n-cursor.json`** у масиві **`rules`** є **`abie`** — інакше вихід **0**
|
|
5
|
+
* без перевірок (щоб не суперечити типовому **ga.mdc** з **`ignore_branches: main,dev`**).
|
|
6
|
+
*
|
|
7
|
+
* **Гілки:** у **`.github/workflows/clean-merged-branch.yml`** у кроці з
|
|
8
|
+
* **`phpdocker-io/github-actions-delete-abandoned-branches`** у **`with.ignore_branches`** мають бути
|
|
9
|
+
* **dev**, **ua** та **ru** (разом з іншими гілками, якщо потрібно).
|
|
10
|
+
*
|
|
11
|
+
* **k8s:** якщо під деревом із сегментом **`k8s`** є YAML з **`kind: Deployment`**, у тій самій директорії
|
|
12
|
+
* має існувати **`hc.yaml`** із **`HealthCheckPolicy`** (**`networking.gke.io/v1`**), modeline **`$schema`**
|
|
13
|
+
* як у abie.mdc, **`/healthz`**, порт **8080**, **`targetRef`** на **Service** з тим самим **`metadata.name`**.
|
|
14
|
+
* Якщо в дереві **k8s** є **HealthCheckPolicy**, перевіряється **`ru/kustomization.yaml`** з patch **`$patch: delete`**
|
|
15
|
+
* (узгоджено з **k8s.mdc** / **check-k8s.mjs**).
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync } from 'node:fs'
|
|
18
|
+
import { readFile } from 'node:fs/promises'
|
|
19
|
+
import { dirname, join, relative } from 'node:path'
|
|
20
|
+
|
|
21
|
+
import { parseAllDocuments } from 'yaml'
|
|
22
|
+
|
|
23
|
+
import { isRuKustomizationPath, pathHasK8sSegment, ruKustomizationHasHealthCheckDeletePatch } from './check-k8s.mjs'
|
|
24
|
+
import { pass } from './utils/pass.mjs'
|
|
25
|
+
import { flattenWorkflowSteps, getStepUses, parseWorkflowYaml } from './utils/gha-workflow.mjs'
|
|
26
|
+
import { walkDir } from './utils/walkDir.mjs'
|
|
27
|
+
|
|
28
|
+
const CONFIG_FILE = '.n-cursor.json'
|
|
29
|
+
|
|
30
|
+
/** Очікуваний URL **`$schema`** для **hc.yaml** (abie.mdc). */
|
|
31
|
+
export const ABIE_HC_SCHEMA_URL = 'https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json'
|
|
32
|
+
|
|
33
|
+
const MODELINE_RE = /^#\s*yaml-language-server:\s*\$schema=(\S+)\s*$/
|
|
34
|
+
|
|
35
|
+
/** Гілки, які мають бути в **`ignore_branches`** за abie.mdc. */
|
|
36
|
+
export const ABIE_REQUIRED_IGNORE_BRANCHES = ['dev', 'ua', 'ru']
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Чи увімкнено правило **abie** у конфігу репозиторію.
|
|
40
|
+
* @param {string} root корінь репозиторію (cwd)
|
|
41
|
+
* @returns {Promise<boolean>} true, якщо **rules** містить **abie**
|
|
42
|
+
*/
|
|
43
|
+
export async function isAbieRuleEnabled(root) {
|
|
44
|
+
const p = join(root, CONFIG_FILE)
|
|
45
|
+
if (!existsSync(p)) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
let raw
|
|
49
|
+
try {
|
|
50
|
+
raw = await readFile(p, 'utf8')
|
|
51
|
+
} catch {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
let cfg
|
|
55
|
+
try {
|
|
56
|
+
cfg = JSON.parse(raw)
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
const rules = cfg?.rules
|
|
61
|
+
if (!Array.isArray(rules)) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
return rules.some(r => String(r).trim().toLowerCase() === 'abie')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Розбирає **`ignore_branches`** з workflow **clean-merged-branch** (крок delete-abandoned-branches).
|
|
69
|
+
* @param {string} content вміст **.yml**
|
|
70
|
+
* @returns {string | null} рядок **ignore_branches** або **null**
|
|
71
|
+
*/
|
|
72
|
+
export function parseCleanMergedIgnoreBranches(content) {
|
|
73
|
+
const root = parseWorkflowYaml(content)
|
|
74
|
+
if (!root) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
for (const { step } of flattenWorkflowSteps(root)) {
|
|
78
|
+
const uses = getStepUses(step)
|
|
79
|
+
if (uses.includes('phpdocker-io/github-actions-delete-abandoned-branches')) {
|
|
80
|
+
const w = step.with
|
|
81
|
+
if (w && typeof w === 'object' && !Array.isArray(w)) {
|
|
82
|
+
const ib = /** @type {Record<string, unknown>} */ (w).ignore_branches
|
|
83
|
+
if (typeof ib === 'string') {
|
|
84
|
+
return ib
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Чи рядок **ignore_branches** містить усі гілки з **required** (для abie — dev, ua, ru).
|
|
94
|
+
* @param {string} ignoreBranches значення **ignore_branches**
|
|
95
|
+
* @param {string[]} required імена гілок (нижній регістр для порівняння)
|
|
96
|
+
* @returns {boolean} true, якщо всі **required** присутні як окремі токени
|
|
97
|
+
*/
|
|
98
|
+
export function ignoreBranchesIncludesRequired(ignoreBranches, required) {
|
|
99
|
+
const parts = new Set(
|
|
100
|
+
ignoreBranches
|
|
101
|
+
.split(',')
|
|
102
|
+
.map(s => s.trim().toLowerCase())
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
)
|
|
105
|
+
return required.every(r => parts.has(r.toLowerCase()))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Збирає абсолютні шляхи до **.yaml** / **.yml** під деревом, де є сегмент **k8s**.
|
|
110
|
+
* @param {string} root корінь репозиторію
|
|
111
|
+
* @returns {Promise<string[]>} відсортовані шляхи
|
|
112
|
+
*/
|
|
113
|
+
async function findK8sYamlFiles(root) {
|
|
114
|
+
/** @type {string[]} */
|
|
115
|
+
const out = []
|
|
116
|
+
await walkDir(root, p => {
|
|
117
|
+
if (!pathHasK8sSegment(p)) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (!/\.ya?ml$/iu.test(p)) {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
out.push(p)
|
|
124
|
+
})
|
|
125
|
+
return [...out].toSorted((a, b) => a.localeCompare(b))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Чи документ — **Deployment**.
|
|
130
|
+
* @param {unknown} obj корінь YAML-документа
|
|
131
|
+
* @returns {boolean} true, якщо **kind** документа — **Deployment**
|
|
132
|
+
*/
|
|
133
|
+
function isDeploymentDoc(obj) {
|
|
134
|
+
return (
|
|
135
|
+
obj !== null &&
|
|
136
|
+
typeof obj === 'object' &&
|
|
137
|
+
!Array.isArray(obj) &&
|
|
138
|
+
/** @type {Record<string, unknown>} */ (obj).kind === 'Deployment'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Директорії, де є хоча б один **Deployment** у файлах **k8s**.
|
|
144
|
+
* @param {string} root корінь cwd
|
|
145
|
+
* @param {string[]} yamlAbs абсолютні шляхи yaml під k8s
|
|
146
|
+
* @param {(msg: string) => void} fail реєстрація помилки парсингу
|
|
147
|
+
* @returns {Promise<Set<string>>} абсолютні шляхи директорій
|
|
148
|
+
*/
|
|
149
|
+
async function collectDeploymentDirs(root, yamlAbs, fail) {
|
|
150
|
+
/** @type {Set<string>} */
|
|
151
|
+
const dirs = new Set()
|
|
152
|
+
for (const abs of yamlAbs) {
|
|
153
|
+
let raw
|
|
154
|
+
let readOk = false
|
|
155
|
+
try {
|
|
156
|
+
raw = await readFile(abs, 'utf8')
|
|
157
|
+
readOk = true
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
160
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
161
|
+
}
|
|
162
|
+
if (readOk) {
|
|
163
|
+
const body = stripBom(raw)
|
|
164
|
+
const lines = body.split(/\r?\n/u)
|
|
165
|
+
const first = lines[0] ?? ''
|
|
166
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
167
|
+
/** @type {import('yaml').Document[]} */
|
|
168
|
+
let docs
|
|
169
|
+
let parseOk = false
|
|
170
|
+
try {
|
|
171
|
+
docs = parseAllDocuments(rest)
|
|
172
|
+
parseOk = true
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
175
|
+
fail(`${relative(root, abs) || abs}: YAML (${msg})`)
|
|
176
|
+
}
|
|
177
|
+
if (parseOk) {
|
|
178
|
+
for (const doc of docs) {
|
|
179
|
+
if (doc.errors.length === 0) {
|
|
180
|
+
const obj = doc.toJSON()
|
|
181
|
+
if (isDeploymentDoc(obj)) {
|
|
182
|
+
dirs.add(dirname(abs))
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return dirs
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Прибирає BOM на початку файлу.
|
|
194
|
+
* @param {string} s вміст
|
|
195
|
+
* @returns {string} той самий рядок без BOM (U+FEFF) на початку
|
|
196
|
+
*/
|
|
197
|
+
function stripBom(s) {
|
|
198
|
+
return s.startsWith('\uFEFF') ? s.slice(1) : s
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Перевіряє **hc.yaml** на відповідність abie.mdc.
|
|
203
|
+
* @param {string} raw повний текст файлу
|
|
204
|
+
* @param {string} relPath відносний шлях для повідомлень
|
|
205
|
+
* @returns {string | null} текст помилки або **null**
|
|
206
|
+
*/
|
|
207
|
+
export function validateAbieHcYaml(raw, relPath) {
|
|
208
|
+
const body = stripBom(raw)
|
|
209
|
+
const lines = body.split(/\r?\n/u)
|
|
210
|
+
if (lines.length === 0 || lines[0].trim() === '') {
|
|
211
|
+
return `${relPath}: перший рядок порожній — потрібен # yaml-language-server: $schema=… (abie.mdc)`
|
|
212
|
+
}
|
|
213
|
+
const m = lines[0].match(MODELINE_RE)
|
|
214
|
+
if (!m) {
|
|
215
|
+
return `${relPath}: перший рядок має бути modeline $schema (abie.mdc)`
|
|
216
|
+
}
|
|
217
|
+
if (m[1] !== ABIE_HC_SCHEMA_URL) {
|
|
218
|
+
return `${relPath}: $schema має бути\n ${ABIE_HC_SCHEMA_URL}\n (abie.mdc)`
|
|
219
|
+
}
|
|
220
|
+
const yamlBody = lines
|
|
221
|
+
.slice(1)
|
|
222
|
+
.join('\n')
|
|
223
|
+
.replace(/^\s*\n/u, '')
|
|
224
|
+
/** @type {import('yaml').Document[]} */
|
|
225
|
+
let docs
|
|
226
|
+
try {
|
|
227
|
+
docs = parseAllDocuments(yamlBody)
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
230
|
+
return `${relPath}: не вдалося розібрати YAML (${msg})`
|
|
231
|
+
}
|
|
232
|
+
/** @type {Record<string, unknown> | null} */
|
|
233
|
+
let policy = null
|
|
234
|
+
for (const doc of docs) {
|
|
235
|
+
if (doc.errors.length > 0) {
|
|
236
|
+
return `${relPath}: YAML: ${doc.errors.map(e => e.message).join('; ')}`
|
|
237
|
+
}
|
|
238
|
+
const obj = doc.toJSON()
|
|
239
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
240
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
241
|
+
if (rec.kind === 'HealthCheckPolicy') {
|
|
242
|
+
policy = rec
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!policy) {
|
|
248
|
+
return `${relPath}: очікується документ kind: HealthCheckPolicy (abie.mdc)`
|
|
249
|
+
}
|
|
250
|
+
if (policy.apiVersion !== 'networking.gke.io/v1') {
|
|
251
|
+
return `${relPath}: apiVersion має бути networking.gke.io/v1 (abie.mdc)`
|
|
252
|
+
}
|
|
253
|
+
const meta = policy.metadata
|
|
254
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
255
|
+
return `${relPath}: відсутній metadata (abie.mdc)`
|
|
256
|
+
}
|
|
257
|
+
const name = /** @type {Record<string, unknown>} */ (meta).name
|
|
258
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
259
|
+
return `${relPath}: metadata.name має бути непорожнім рядком (abie.mdc)`
|
|
260
|
+
}
|
|
261
|
+
const spec = policy.spec
|
|
262
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
263
|
+
return `${relPath}: відсутній spec (abie.mdc)`
|
|
264
|
+
}
|
|
265
|
+
const def = /** @type {Record<string, unknown>} */ (spec).default
|
|
266
|
+
if (def === null || typeof def !== 'object' || Array.isArray(def)) {
|
|
267
|
+
return `${relPath}: відсутній spec.default (abie.mdc)`
|
|
268
|
+
}
|
|
269
|
+
const config = /** @type {Record<string, unknown>} */ (def).config
|
|
270
|
+
if (config === null || typeof config !== 'object' || Array.isArray(config)) {
|
|
271
|
+
return `${relPath}: відсутній spec.default.config (abie.mdc)`
|
|
272
|
+
}
|
|
273
|
+
if (config.type !== 'HTTP') {
|
|
274
|
+
return `${relPath}: spec.default.config.type має бути HTTP (abie.mdc)`
|
|
275
|
+
}
|
|
276
|
+
const httpHc = /** @type {Record<string, unknown>} */ (config).httpHealthCheck
|
|
277
|
+
if (httpHc === null || typeof httpHc !== 'object' || Array.isArray(httpHc)) {
|
|
278
|
+
return `${relPath}: відсутній httpHealthCheck (abie.mdc)`
|
|
279
|
+
}
|
|
280
|
+
if (httpHc.requestPath !== '/healthz') {
|
|
281
|
+
return `${relPath}: httpHealthCheck.requestPath має бути /healthz (abie.mdc)`
|
|
282
|
+
}
|
|
283
|
+
if (httpHc.port !== 8080) {
|
|
284
|
+
return `${relPath}: httpHealthCheck.port має бути 8080 (abie.mdc)`
|
|
285
|
+
}
|
|
286
|
+
const targetRef = /** @type {Record<string, unknown>} */ (spec).targetRef
|
|
287
|
+
if (targetRef === null || typeof targetRef !== 'object' || Array.isArray(targetRef)) {
|
|
288
|
+
return `${relPath}: відсутній targetRef (abie.mdc)`
|
|
289
|
+
}
|
|
290
|
+
if (targetRef.kind !== 'Service') {
|
|
291
|
+
return `${relPath}: targetRef.kind має бути Service (abie.mdc)`
|
|
292
|
+
}
|
|
293
|
+
const svcName = targetRef.name
|
|
294
|
+
if (typeof svcName !== 'string' || svcName !== name) {
|
|
295
|
+
return `${relPath}: targetRef.name має збігатися з metadata.name (${name}) (abie.mdc)`
|
|
296
|
+
}
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Збирає відносні шляхи файлів із **HealthCheckPolicy** у дереві k8s.
|
|
302
|
+
* @param {string} root корінь
|
|
303
|
+
* @param {string[]} yamlAbs абсолютні шляхи
|
|
304
|
+
* @returns {Promise<string[]>} унікальні відносні шляхи yaml із **HealthCheckPolicy**
|
|
305
|
+
*/
|
|
306
|
+
async function collectHealthCheckPolicyRelPaths(root, yamlAbs) {
|
|
307
|
+
/** @type {string[]} */
|
|
308
|
+
const out = []
|
|
309
|
+
for (const abs of yamlAbs) {
|
|
310
|
+
const rel = relative(root, abs).replaceAll('\\', '/') || abs
|
|
311
|
+
let raw
|
|
312
|
+
try {
|
|
313
|
+
raw = await readFile(abs, 'utf8')
|
|
314
|
+
} catch {
|
|
315
|
+
raw = null
|
|
316
|
+
}
|
|
317
|
+
if (raw !== null) {
|
|
318
|
+
const body = stripBom(raw)
|
|
319
|
+
const lines = body.split(/\r?\n/u)
|
|
320
|
+
const first = lines[0] ?? ''
|
|
321
|
+
const rest = MODELINE_RE.test(first.trim()) ? lines.slice(1).join('\n') : body
|
|
322
|
+
try {
|
|
323
|
+
const docs = parseAllDocuments(rest)
|
|
324
|
+
for (const doc of docs) {
|
|
325
|
+
if (doc.errors.length === 0) {
|
|
326
|
+
const obj = doc.toJSON()
|
|
327
|
+
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
328
|
+
const rec = /** @type {Record<string, unknown>} */ (obj)
|
|
329
|
+
if (rec.kind === 'HealthCheckPolicy' && !out.includes(rel)) {
|
|
330
|
+
out.push(rel)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
/* пропускаємо пошкоджені файли — їх ловить check-k8s */
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return out
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Якщо є **HealthCheckPolicy**, вимагає **ru/kustomization.yaml** з patch видалення (як **check-k8s**).
|
|
345
|
+
* @param {string} root корінь
|
|
346
|
+
* @param {string[]} yamlFilesAbs абсолютні шляхи yaml k8s
|
|
347
|
+
* @param {string[]} healthCheckPolicyRelativePaths відносні шляхи
|
|
348
|
+
* @param {(msg: string) => void} fail callback
|
|
349
|
+
* @returns {Promise<void>}
|
|
350
|
+
*/
|
|
351
|
+
async function ensureRuKustomizationHealthCheckDelete(root, yamlFilesAbs, healthCheckPolicyRelativePaths, fail) {
|
|
352
|
+
if (healthCheckPolicyRelativePaths.length === 0) {
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
const ruAbsList = yamlFilesAbs.filter(abs => isRuKustomizationPath(relative(root, abs).replaceAll('\\', '/') || abs))
|
|
356
|
+
if (ruAbsList.length === 0) {
|
|
357
|
+
fail(
|
|
358
|
+
`Знайдено HealthCheckPolicy у ${healthCheckPolicyRelativePaths.join(', ')} — додай ru/kustomization.yaml з patch видалення (abie.mdc)`
|
|
359
|
+
)
|
|
360
|
+
return
|
|
361
|
+
}
|
|
362
|
+
for (const abs of ruAbsList) {
|
|
363
|
+
let raw
|
|
364
|
+
try {
|
|
365
|
+
raw = await readFile(abs, 'utf8')
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
368
|
+
fail(`${relative(root, abs) || abs}: не вдалося прочитати (${msg})`)
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
if (ruKustomizationHasHealthCheckDeletePatch(raw)) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
fail(
|
|
376
|
+
'Є HealthCheckPolicy, але жоден ru/kustomization.yaml не містить очікуваного patch видалення (kind: HealthCheckPolicy, metadata.name, $patch: delete) — abie.mdc'
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Перевіряє відповідність проєкту правилам abie.mdc.
|
|
382
|
+
* @returns {Promise<number>} 0 — OK, 1 — є порушення
|
|
383
|
+
*/
|
|
384
|
+
export async function check() {
|
|
385
|
+
let exitCode = 0
|
|
386
|
+
const fail = msg => {
|
|
387
|
+
console.log(` ❌ ${msg}`)
|
|
388
|
+
exitCode = 1
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const root = process.cwd()
|
|
392
|
+
const enabled = await isAbieRuleEnabled(root)
|
|
393
|
+
if (!enabled) {
|
|
394
|
+
pass(`Правило abie не увімкнено в ${CONFIG_FILE} (rules) — перевірку пропущено`)
|
|
395
|
+
return 0
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
pass('Правило abie увімкнено — виконуємо перевірки')
|
|
399
|
+
|
|
400
|
+
const cleanMergedPath = join(root, '.github/workflows/clean-merged-branch.yml')
|
|
401
|
+
if (existsSync(cleanMergedPath)) {
|
|
402
|
+
/** @type {string | undefined} */
|
|
403
|
+
let wfRaw
|
|
404
|
+
try {
|
|
405
|
+
wfRaw = await readFile(cleanMergedPath, 'utf8')
|
|
406
|
+
} catch (error) {
|
|
407
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
408
|
+
fail(`Не вдалося прочитати clean-merged-branch.yml (${msg})`)
|
|
409
|
+
}
|
|
410
|
+
if (wfRaw !== undefined) {
|
|
411
|
+
const ib = parseCleanMergedIgnoreBranches(wfRaw)
|
|
412
|
+
if (ib === null || ib.trim() === '') {
|
|
413
|
+
fail(
|
|
414
|
+
'clean-merged-branch.yml: не знайдено with.ignore_branches у кроці phpdocker-io/github-actions-delete-abandoned-branches (abie.mdc)'
|
|
415
|
+
)
|
|
416
|
+
} else if (ignoreBranchesIncludesRequired(ib, ABIE_REQUIRED_IGNORE_BRANCHES)) {
|
|
417
|
+
pass('clean-merged-branch.yml: ignore_branches містить dev, ua, ru')
|
|
418
|
+
} else {
|
|
419
|
+
fail(
|
|
420
|
+
`clean-merged-branch.yml: ignore_branches має містити dev, ua та ru (зараз: ${JSON.stringify(ib)}) — abie.mdc`
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
fail(`Відсутній ${cleanMergedPath} — потрібен для ignore_branches (abie.mdc)`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const yamlFiles = await findK8sYamlFiles(root)
|
|
429
|
+
const deploymentDirs = await collectDeploymentDirs(root, yamlFiles, fail)
|
|
430
|
+
|
|
431
|
+
if (deploymentDirs.size > 0) {
|
|
432
|
+
pass(`Знайдено Deployment у ${deploymentDirs.size} директорія(ї/й) k8s — перевіряємо hc.yaml`)
|
|
433
|
+
for (const dir of [...deploymentDirs].toSorted((a, b) => a.localeCompare(b))) {
|
|
434
|
+
const hcAbs = join(dir, 'hc.yaml')
|
|
435
|
+
const relHc = relative(root, hcAbs).replaceAll('\\', '/') || 'hc.yaml'
|
|
436
|
+
if (existsSync(hcAbs)) {
|
|
437
|
+
let hcRaw
|
|
438
|
+
let hcReadOk = false
|
|
439
|
+
try {
|
|
440
|
+
hcRaw = await readFile(hcAbs, 'utf8')
|
|
441
|
+
hcReadOk = true
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
444
|
+
fail(`${relHc}: не вдалося прочитати (${msg})`)
|
|
445
|
+
}
|
|
446
|
+
if (hcReadOk) {
|
|
447
|
+
const v = validateAbieHcYaml(hcRaw, relHc)
|
|
448
|
+
if (v === null) {
|
|
449
|
+
pass(`${relHc}: відповідає abie.mdc`)
|
|
450
|
+
} else {
|
|
451
|
+
fail(v)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
fail(
|
|
456
|
+
`${relative(root, dir) || dir}: є Deployment, але немає hc.yaml поруч — додай HealthCheckPolicy (abie.mdc)`
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
pass('Немає Deployment у дереві k8s — перевірку hc.yaml пропущено')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const healthCheckPolicyRelativePaths = await collectHealthCheckPolicyRelPaths(root, yamlFiles)
|
|
465
|
+
await ensureRuKustomizationHealthCheckDelete(root, yamlFiles, healthCheckPolicyRelativePaths, fail)
|
|
466
|
+
|
|
467
|
+
return exitCode
|
|
468
|
+
}
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { existsSync } from 'node:fs'
|
|
10
10
|
import { readFile } from 'node:fs/promises'
|
|
11
11
|
|
|
12
|
-
import { pass } from './utils/pass.mjs'
|
|
13
12
|
import { parseWorkflowYaml, verifyLintJsWorkflowStructure } from './utils/gha-workflow.mjs'
|
|
13
|
+
import { pass } from './utils/pass.mjs'
|
|
14
14
|
|
|
15
|
-
/** Очікуваний локальний
|
|
16
|
-
export const CANONICAL_LINT_JS = 'oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
15
|
+
/** Очікуваний локальний скрипт. */
|
|
16
|
+
export const CANONICAL_LINT_JS = 'bunx oxlint --fix && bunx eslint --fix . && bunx jscpd .'
|
|
17
17
|
|
|
18
18
|
/** Мінімальні рекомендації розширень редактора з js-lint.mdc (eslint, oxlint, GA). */
|
|
19
19
|
export const REQUIRED_VSCODE_EXTENSIONS = ['dbaeumer.vscode-eslint', 'github.vscode-github-actions', 'oxc.oxc-vscode']
|
|
@@ -28,14 +28,12 @@ export function normalizeLintJsScript(s) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Чи рядок `lint-js` збігається з каноном
|
|
31
|
+
* Чи рядок `lint-js` збігається з каноном (`bunx oxlint`, `bunx eslint`, `bunx jscpd`).
|
|
32
32
|
* @param {string} script значення `scripts.lint-js` з package.json
|
|
33
33
|
* @returns {boolean} true, якщо рядок канонічний
|
|
34
34
|
*/
|
|
35
35
|
export function isCanonicalLintJs(script) {
|
|
36
|
-
|
|
37
|
-
if (n.includes('bunx oxlint')) return false
|
|
38
|
-
return n === CANONICAL_LINT_JS
|
|
36
|
+
return normalizeLintJsScript(script) === CANONICAL_LINT_JS
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
/**
|
|
@@ -89,7 +87,7 @@ export async function check() {
|
|
|
89
87
|
pass(`lint-js збігається з каноном: ${CANONICAL_LINT_JS}`)
|
|
90
88
|
} else {
|
|
91
89
|
fail(
|
|
92
|
-
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (
|
|
90
|
+
`lint-js має бути рівно: "${CANONICAL_LINT_JS}" (див. js-lint.mdc / check-js-lint.mjs). Зараз: ${JSON.stringify(normalizeLintJsScript(lintJs))}`
|
|
93
91
|
)
|
|
94
92
|
}
|
|
95
93
|
} else {
|
|
@@ -182,7 +180,7 @@ export async function check() {
|
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
182
|
if (content.includes('bunx oxlint') && /bunx\s+oxlint[^\n]*--fix/u.test(content)) {
|
|
185
|
-
fail('lint-js.yml: у CI не використовуй oxlint --fix (лише bunx oxlint)')
|
|
183
|
+
fail('lint-js.yml: у CI не використовуй bunx oxlint --fix (лише bunx oxlint)')
|
|
186
184
|
}
|
|
187
185
|
if (content.includes('eslint --fix')) {
|
|
188
186
|
fail('lint-js.yml: у CI не використовуй eslint --fix (лише bunx eslint .)')
|
package/scripts/run-v8r.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Тихий запуск v8r для усіх типів файлів, які підтримує v8r (json, json5, yaml, yml, toml).
|
|
3
3
|
*
|
|
4
4
|
* Один виклик цього скрипта з `lint-text` замість чотирьох окремих викликів v8r: під капотом для
|
|
5
|
-
* кожного glob окремий `
|
|
5
|
+
* кожного glob окремий `bunx v8r`, бо v8r у одному процесі падає з кодом 98, якщо хоч один із
|
|
6
6
|
* переданих глобів не знаходить файлів — тоді решта розширень не перевіряються.
|
|
7
7
|
*
|
|
8
8
|
* Каталог схем `@nitra/cursor` (`v8r-catalog.json` у каталозі `schemas` пакета) передається в v8r
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: n-mdc-check
|
|
3
|
+
description: >-
|
|
4
|
+
Проаналізувати правило в npm/mdc і перенести максимум перевірюваної логіки в check-{id}.mjs;
|
|
5
|
+
залишити в .mdc лише те, що не автоматизується або служить контекстом для агента
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# n-mdc-check — від правила до скрипта
|
|
9
|
+
|
|
10
|
+
Навичка для роботи з правилами пакета `@nitra/cursor`: **перевірювані критерії** мають жити в **`npm/scripts/check-<id>.mjs`**, а **`npm/mdc/<id>.mdc`** (і дзеркало **`.cursor/rules/n-<id>.mdc`**) — бути **коротким орієнтиром**: навіщо правило, як виправляти порушення, яка команда підтверджує відповідність.
|
|
11
|
+
|
|
12
|
+
Канонічні вимоги до скриптів, тестів і AST — **`.cursor/rules/scripts.mdc`**.
|
|
13
|
+
|
|
14
|
+
## Коли застосовувати
|
|
15
|
+
|
|
16
|
+
- Додали або змінили вимоги в **`npm/mdc/<id>.mdc`** (або локальному **`n-<id>.mdc`**).
|
|
17
|
+
- У правилі багато «має містити», «заборонено», точних фрагментів файлів — і агенти повторюють перевірку вручну.
|
|
18
|
+
- Потрібно зменшити обсяг `.mdc` і уникнути роз’їзду тексту й фактичної перевірки.
|
|
19
|
+
|
|
20
|
+
## Ідентифікатор правила
|
|
21
|
+
|
|
22
|
+
- Файл правила в пакеті: **`npm/mdc/<id>.mdc`** → скрипт: **`npm/scripts/check-<id>.mjs`**, команда CLI: **`npx @nitra/cursor check <id>`**.
|
|
23
|
+
- CLI підхоплює всі **`check-*.mjs`** з каталогу `scripts` пакета; окремої реєстрації не потрібно.
|
|
24
|
+
- Якщо правило згадане в **`AGENTS.md`**, для нього доречна programmatic перевірка — має існувати відповідний **`check-<id>.mjs`** (коли вимоги формалізовані).
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
### 1. Базова лінія
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @nitra/cursor check <id>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Зафіксуй поточний вивід (якщо скрипт уже є).
|
|
35
|
+
|
|
36
|
+
### 2. Розбір тексту правила
|
|
37
|
+
|
|
38
|
+
Прочитай **`npm/mdc/<id>.mdc`** повністю. Для кожної вимоги виріши:
|
|
39
|
+
|
|
40
|
+
| Тип | Куди |
|
|
41
|
+
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
42
|
+
| Перевірювана структура (файли, поля JSON/YAML, наявність ключів, заборонені шляхи) | **`check-<id>.mjs`** |
|
|
43
|
+
| Семантика JS/TS у файлах | **`check-<id>.mjs`** через **Oxc AST** (`oxc-parser`), не «сліпі» regex по всьому файлу |
|
|
44
|
+
| Вміст у Vue SFC / MDX / Astro | Спочатку **виділи** валідний JS/TS-фрагмент, потім AST; regex — лише для ізоляції блоків |
|
|
45
|
+
| Не-JS конфіги, сирі workflow | Текстовий/структурний парсинг або обґрунтований regex; у JSDoc коротко **чому** не AST |
|
|
46
|
+
| Смак, архітектура, «навіщо так», поради без однозначного критерію | Залиш у **`.mdc`** |
|
|
47
|
+
|
|
48
|
+
### 3. Скрипт
|
|
49
|
+
|
|
50
|
+
- **Новий** `check-<id>.mjs`: орієнтуйся на наявні файли в **`npm/scripts/`** (наприклад **`check-bun.mjs`**) — стиль **`fail` / `pass`**, **`export async function check()`**, exit code **0 / 1**.
|
|
51
|
+
- **На початку файлу** (перед `import`) — **багаторядковий** верхній JSDoc **українською**: що перевіряє файл; між смисловими блоками — рядок `*`; перші рядки без службових префіксів — одразу зрозуміло призначення.
|
|
52
|
+
- **JSDoc** до експортованих функцій і нетривіальних внутрішніх функцій.
|
|
53
|
+
- Повідомлення **`fail`**: конкретно, що не так і як виправити (як у інших check-скриптах).
|
|
54
|
+
- Залежність **`oxc-parser`** не додавай у корінь споживача — вона в **`dependencies`** пакета **`@nitra/cursor`** (див. **dev-dep** / **scripts.mdc**).
|
|
55
|
+
|
|
56
|
+
### 4. Тести
|
|
57
|
+
|
|
58
|
+
Якщо логіка нетривіальна або її легко зламати — додай **`npm/tests/check-<id>.test.mjs`** (або розшир наявні тести). Після змін:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd npm && bun test
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 5. Спрощення `.mdc`
|
|
65
|
+
|
|
66
|
+
- Прибери з правила **дубль** детальних умов, які тепер у скрипті; залиш **короткий зміст**, мотивацію, посилання на файли/конвенції.
|
|
67
|
+
- Потрібно зберегти (або додати) секцію **«Перевірка»** з **`npx @nitra/cursor check <id>`** (або повним ім’ям правила з `AGENTS.md`).
|
|
68
|
+
- Якщо оновлюєш пакетне правило, **синхронно** онови дзеркало **`.cursor/rules/n-<id>.mdc`**, щоб текст не розходився (як прийнято в цьому репозиторії).
|
|
69
|
+
|
|
70
|
+
### 6. Верифікація та версія пакета
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @nitra/cursor check <id>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Після змін у **`npm/`** (у тому числі `skills/`, `scripts/`, `mdc/`) підвищ **patch**-версію в **`npm/package.json`** на **один** крок відносно вже запланованого релізу (див. **n-npm-module**).
|
|
77
|
+
|
|
78
|
+
## Антипатерни
|
|
79
|
+
|
|
80
|
+
- Залишати в `.mdc` великі «еталонні» YAML/JSON, якщо перевірка може читати очікувану структуру з репозиторію або константи в скрипті.
|
|
81
|
+
- Оновлювати лише текст правила без оновлення **`check-<id>.mjs`**, коли для правила вже є скрипт або з’явилась нова перевірювана умова.
|
|
82
|
+
- Дублювати однакову логіку в кількох check-скриптах замість спільної утиліти в **`npm/scripts/utils/`**.
|
|
83
|
+
|
|
84
|
+
## Команда в чаті
|
|
85
|
+
|
|
86
|
+
`/n-mdc-check` — застосуй цей workflow до правила з поточного контексту або до `<id>`, який вкаже користувач.
|