@nitra/cursor 1.8.208 → 1.8.210

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/CHANGELOG.md CHANGED
@@ -4,6 +4,109 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.210] - 2026-05-08
8
+
9
+ ### Added
10
+
11
+ - `js-bun-db` v1.6: правило тепер забороняє локальні pg-format-сумісні шими у
12
+ файлах з Bun SQL.
13
+ - Розділ `## pg-format: повне видалення, без шимів` у `npm/mdc/js-bun-db.mdc`:
14
+ типові ідіоми `format(...)` → tagged template, заборонений drop-in `format()`
15
+ і `pg`-сумісна `query(text, params)`-обгортка над `sql.unsafe(...)`.
16
+ - Два нові AST-детектори у `npm/scripts/utils/bun-sql-scan.mjs`:
17
+ `findPgFormatShimDefinitionInText` (функції `format` / `pgFormat` /
18
+ `sqlFormat` / `pgFmt` з `%L`/`%I`/`%s` у тілі, плюс `quoteLiteral` /
19
+ `quoteIdent` / `escapeLiteral` / `escapeIdent` без додаткової перевірки)
20
+ та `findPgFormatLikeQueryWrapperInText` (`{ query(text, params) { ...
21
+ <obj>.unsafe(...) ... } }`). Скан запускається лише у файлах з
22
+ `import { sql|SQL } from 'bun'`.
23
+ - `npm/scripts/check-js-bun-db.mjs` рапортує `pgFormatShim` / `queryWrapper` —
24
+ окремі лічильники й `pass`-рядки, без зміни існуючих перевірок.
25
+
26
+ ## [1.8.209] - 2026-05-08
27
+
28
+ ### Removed
29
+
30
+ - Дедуплікація JS-перевірок, що вже покриті Rego-полісі (запускаються через
31
+ `bun run lint-conftest`):
32
+ - `npm/scripts/check-bun.mjs` — без `checkBunfigHoisted`, `checkDevDependencies`,
33
+ `checkLintAggregate`, перевірок `pkg.packageManager` і кореневого
34
+ `pkg.dependencies`. Лишилася FS-existence (`bun.lock`, `bunfig.toml`,
35
+ `package.json`, заборонені lockfile, директорія `.yarn/`) і cross-file гейт
36
+ `lint-docker` / `lint-k8s` від `.n-cursor.json:rules`.
37
+ - `npm/scripts/check-php.mjs` — без перевірок `lint-php` скрипта і `run` у
38
+ `lint-php.yml`. Лишилися FS-existence для `composer.json`, `package.json`,
39
+ `lint-php.yml`.
40
+ - `npm/scripts/check-style-lint.mjs` — без перевірок `lint-style` через
41
+ `npx stylelint`, `@nitra/stylelint-config` у `devDependencies`,
42
+ `stylelint.extends`, `npx stylelint` у `lint-style.yml`. Лишилися VSCode-
43
+ конфіги, `.stylelintignore`, FS-existence workflow і альтернатива зовнішнього
44
+ конфіг-файлу `stylelint`.
45
+ - `npm/scripts/check-graphql.mjs` — без `checkPackageDumpSchemaScript` (структура
46
+ `scripts.dump-schema`); решта логіки (gql AST-скан, `.graphqlrc.yml`,
47
+ VSCode-розширення) лишилася.
48
+ - `npm/scripts/check-image-compress.mjs` — без `checkLintImageScript`,
49
+ `checkLintAggregateIncludesImage`, `checkMinifyImageNotInDeps`. Лишилися
50
+ `.n-minify-image.tsv` НЕ в `.gitignore` і видалення застарілого
51
+ `.minify-image-cache.tsv`.
52
+ - `npm/scripts/check-js-bun-db.mjs` — без `checkForbiddenDependencies`
53
+ (`pg`/`pg-format`/`mysql2`); AST-скан коду (`new SQL(...)` всередині функції,
54
+ `unsafe()` без маркера, динамічні `IN (…)`) лишився.
55
+ - `npm/scripts/check-text.mjs` — без `checkOxfmtRc`, `checkCspellConfig`,
56
+ `checkCspellJsonDictImports`, `checkMarkdownlintConfig`, `prettier`/`@nitra/cspell-dict`/`markdownlint-cli2`/`@nitra/*` гейт у
57
+ `checkPackageJsonTextDepsUsage`. Лишилися VSCode-конфіги, `.v8rignore`,
58
+ Prettier-файли в корені, абзац про український апостроф у `.mdc`,
59
+ складна валідація скрипта `lint-text` і виклик `bun run lint-text` у
60
+ workflow.
61
+ - `npm/scripts/check-vue.mjs` — без `checkViteVersion` (vite ≥ 8). AST-скан коду
62
+ і vite-config-перевірки лишилися.
63
+ - `npm/scripts/check-npm-module.mjs` — без `checkNpmTypesField`,
64
+ `emitTypesConfigIssues`, перевірок полів `npm-publish.yml`,
65
+ `workspaces ∋ "npm"` у кореневому `package.json`. Лишилися FS-existence,
66
+ наявність файлу зі шляху `types`, hk.pkl-перевірки, CHANGELOG-version-match,
67
+ git-dirty-bump.
68
+ - `npm/scripts/check-js-lint.mjs` — без `checkPackageJsonLintDeps`
69
+ (prettier-залежність, `@nitra/eslint-config ≥ 3.9.2`),
70
+ `checkPackageJsonTypeModule` для root, `checkEnginesNode/Bun` для root,
71
+ канонічний `lint-js`-скрипт, валідація `lint-js.yml` (`verifyLintJsWorkflowStructure`
72
+ + fallback). Лишилися — `.oxlintrc.json` canonical-snapshot, VSCode-розширення,
73
+ workspace-ітерація для `type: "module"` і engines, дубль JS-кроків у `lint.yml`,
74
+ `.jscpd.json`. Прибрано непотрібні імпорти `parseWorkflowYaml`,
75
+ `verifyLintJsWorkflowStructure` і `OXLINT_FIX_RE`.
76
+ - `npm/scripts/check-js-run.mjs` — без перевірок `bunyan` / `@nitra/bunyan` у
77
+ залежностях, canonical `jsconfig.json` через `deepEqualJson`,
78
+ `OTEL_RESOURCE_ATTRIBUTES` у `configmap.yaml`. Лишилися AST-скан коду
79
+ (bunyan, conn-aliases, process.env, setTimeout) і FS-existence для
80
+ `jsconfig.json` / `configmap.yaml`. Прибрано `CANONICAL_BACKEND_JSCONFIG`,
81
+ `deepEqualJson`.
82
+ - `npm/scripts/check-adr.mjs` — без `settingsHaveAdrHookGroup`,
83
+ `checkProjectSettings` структурного порівняння і
84
+ `checkLocalSettingsNoDuplicate`. Лишилися hash-порівняння bash-скрипта,
85
+ `.gitignore`-патерн, LLM CLI у PATH, FS-existence settings.json.
86
+ Прибрано `HOOK_COMMAND_MARKER`, `PROJECT_LOCAL_SETTINGS_PATH` (для
87
+ settings.local — Rego policy gating).
88
+ - `npm/scripts/check-ga.mjs` — без `verifyConcurrencyBlock`,
89
+ `verifyNoDirectBunOrCache`, `verifyNoRunShellLineContinuationBackslash`,
90
+ `verifyCheckoutBeforeLocalSetupBunDeps`, `validateConcurrencyOnRoot`. Тепер
91
+ усі workflow-структурні перевірки виконуються через conftest у `lint-ga.mjs`
92
+ (`ga.workflow_common`); лишилася лише git-залежна перевірка `on.*.paths`
93
+ glob-ів через `git ls-files :(glob)`. Прибрано константи
94
+ `SETUP_BUN_PATTERNS`, `FORBIDDEN_BUN_PATTERNS`, `EXPECTED_CONCURRENCY_GROUP`
95
+ і непотрібні імпорти з `gha-workflow.mjs`.
96
+
97
+ ### Changed
98
+
99
+ - Тести `check-bun.test.mjs`, `check-image-compress.test.mjs`,
100
+ `check-js-bun-db.test.mjs`, `check-js-run-fixture.test.mjs`,
101
+ `check-adr.test.mjs` — прибрано / `skip` тести, що дублювали Rego-полісі;
102
+ лишилися лише FS / cross-file сценарії.
103
+ - `npm/policy/{capacitor,js_mssql,abie,k8s,hasura}/**/*.rego` — у заголовках
104
+ policy-файлів додано позначку, що JS-чек у відповідному `check-*.mjs`
105
+ лишається authoritative (повна semver-семантика з OR-діапазонами для
106
+ `capacitor`/`js-mssql`; ширший набір полів і cross-file Kustomize-контекст
107
+ для `abie`/`k8s`; cross-file env-DNS-резолюція для `hasura`). Rego там — швидкий
108
+ гейт для одиничного файлу (наприклад через IDE).
109
+
7
110
  ## [1.8.208] - 2026-05-08
8
111
 
9
112
  ### Added
package/mdc/js-bun-db.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Використання pg / mysql2 / Bun SQL у Node.js та Bun
3
3
  alwaysApply: true
4
- version: '1.5'
4
+ version: '1.6'
5
5
  ---
6
6
 
7
7
  ## Підтримувані версії баз даних
@@ -18,6 +18,58 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
18
18
 
19
19
  `pg-format` — це ручне форматування SQL через escape (`format('... %L ...', value)`); такі рядки легко поламати неправильним типом, locale-залежним escape або забутим `%L`. Tagged template Bun SQL параметризує значення нативно (`sql\`... ${value} ...\``) і не лишає простору для injection — окремий «форматер» не потрібен.
20
20
 
21
+ ## `pg-format`: повне видалення, без шимів
22
+
23
+ Міграція з `pg-format` — це **зміна стилю запитів**, а не збереження API. У проєкті після переходу на Bun SQL **заборонено** залишати:
24
+
25
+ - функцію з іменем `format` (чи `pgFormat`, `sqlFormat`, `pgFmt`), що приймає шаблон з `%L` / `%I` / `%s` і значення;
26
+ - допоміжні `quoteLiteral`, `quoteIdent`, `escapeLiteral`, `escapeIdent` як публічні експорти модуля;
27
+ - обгортки `pgRead.query(text, params)` / `pgWrite.query(text, params)` / `db.query(text, params)`, які складають SQL-рядок (з або без `format`) і викликають `sql.unsafe(text, params)` — це повертає injection-поверхню, від якої ми йдемо, тільки під «зручним» іменем.
28
+
29
+ Замість цього всі точки використання потрібно перевести на tagged template `sql\`...\${value}...\``. Кожне `${value}` стає окремим параметром bind, без рядкового екранування.
30
+
31
+ ### Типові ідіоми `pg-format` → Bun SQL
32
+
33
+ | Було (`pg-format`) | Стало (Bun SQL) |
34
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
35
+ | `format('... WHERE id = %L', id)` | `sql\`... WHERE id = ${id}\`` |
36
+ | `format('... IN (%L)', ids)` | `sql\`... IN ${sql(ids)}\`` (з guard на пустоту перед запитом) |
37
+ | `format('INSERT ... VALUES %L', [row])` (1 рядок) | `sql\`INSERT ... VALUES (${a}, ${b}, ...)\`` |
38
+ | `format('INSERT ... VALUES %L', rows)` (N рядків) | `sql\`INSERT INTO t ${sql(rows, 'a', 'b')}\`` або `unnest($1::T[], $2::T[]) AS t(a, b)` |
39
+ | `format('MERGE ... USING (VALUES %L) AS d(...)')` | `sql\`MERGE ... USING (SELECT * FROM unnest(${arrA}::A[], ${arrB}::B[]) AS t(a, b)) AS d\`` |
40
+ | `format('... %I ...', tableName)` (whitelist) | `sql.unsafe(\`... \${tableName} ...\`)` з маркером `// allow-unsafe: <причина>` і whitelist'ом |
41
+
42
+ Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
43
+
44
+ ### Заборонений «drop-in» шим
45
+
46
+ ```javascript
47
+ // ❌ pg-format-сумісний шим, що ховає `unsafe` під «безпечним» іменем
48
+ export function format(fmt, ...args) {
49
+ let i = 0
50
+ return fmt.replaceAll(/%[LIs]/g, () => quoteLiteral(args[i++]))
51
+ }
52
+
53
+ // ❌ і його типовий call-site — той самий injection-вектор, що і прямий sql.unsafe із конкатенацією
54
+ await sql.unsafe(format('... WHERE id = %L', userId))
55
+ ```
56
+
57
+ ```javascript
58
+ // ❌ pg-сумісна обгортка над Bun SQL — ще один прихований `unsafe`
59
+ export const pgWrite = {
60
+ query(text, params) {
61
+ return sql.unsafe(text, params)
62
+ }
63
+ }
64
+ ```
65
+
66
+ ```javascript
67
+ // ✅ напряму tagged template — параметризація через wire-protocol bind
68
+ await sql`... WHERE id = ${userId}`
69
+ ```
70
+
71
+ Виняток для шиму `format()` під час поетапної міграції допускається **тільки** в окремому commit'і з TODO-маркером і дедлайном; готовий код у main з таким шимом — `fail` правила (детектори: `pg-format shim`, `query wrapper over unsafe`).
72
+
21
73
  ## Підключення (singleton + env)
22
74
 
23
75
  Дефолтний експорт `sql` з `'bun'` сам читає змінні середовища (`DATABASE_URL`, `POSTGRES_URL`, `MYSQL_URL`, `PGHOST`/`PGUSER`/... та `MYSQL_HOST`/`MYSQL_USER`/...) і керує пулом — окремий `Pool` як у `pg` створювати не треба.
@@ -197,6 +249,8 @@ function getUser(id) {
197
249
 
198
250
  Якщо в коді з'явився `import { sql } from 'bun'`, то `pg`, `pg-format` та `mysql2` мають бути прибрані і з `dependencies`, і з імпортів — щоб не лишалось двох паралельних шляхів до БД та ручного форматування поряд із параметризованими template literal.
199
251
 
252
+ Те саме стосується **локальних шимів**: будь-який модуль, що експортує `format`, `pgRead`, `pgWrite`, `query(text, params)`, `quoteLiteral`, `quoteIdent` як обгортку над `sql.unsafe(...)`, потрібно переписати — всі call-site на tagged template, сам шим видалити (див. `## pg-format: повне видалення, без шимів`).
253
+
200
254
  ## Перевірка
201
255
 
202
256
  `npx @nitra/cursor check js-bun-db`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.208",
3
+ "version": "1.8.210",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -12,7 +12,11 @@
12
12
  # - `spec.targetRef.name` має закінчуватись на `-hl` (headless backend).
13
13
  #
14
14
  # Cross-file gating (`abie` правило в `.n-cursor.json`, парність з Deployment-каталогу,
15
- # узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`).
15
+ # узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`). JS-перевірка
16
+ # в `check-abie.mjs` (`validateAbieHcPolicy`) authoritative й тестує ширший набір полів
17
+ # (apiVersion, spec.default.config.type=="HTTP", targetRef.kind=="Service",
18
+ # обчислений `<name>-hl` суфікс); ця Rego — швидкий gate для одиничного YAML
19
+ # (наприклад через IDE).
16
20
  #
17
21
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
18
22
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
@@ -8,7 +8,8 @@
8
8
  #
9
9
  # Cross-file gating (саме шлях `…/base/…` визначає, чи застосовувати правило)
10
10
  # — у JS: conftest викликаємо лише на YAML-ах з base/. Тут — лише валідація вмісту
11
- # `spec.hostnames`.
11
+ # `spec.hostnames`. JS authoritative (`check-abie.mjs`) — ця Rego гейт для
12
+ # одиничного YAML.
12
13
  #
13
14
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
14
15
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
@@ -9,7 +9,8 @@
9
9
  # Решта логіки `check-hasura.mjs` (звірення `HASURA_GRAPHQL_ENDPOINT` в `.env`-файлах
10
10
  # з `<service>.<namespace>.svc.<cluster>` через regex по всьому дереву репо, gating
11
11
  # на `repository` у кореневому `package.json`) — у JS: вона потребує текстового
12
- # парсингу `.env`-файлів, обходу дерева й cross-file resolution.
12
+ # парсингу `.env`-файлів, обходу дерева й cross-file resolution. JS authoritative;
13
+ # ця Rego — додатковий gate (JS неявно перевіряє суфікс через звірку URL).
13
14
  #
14
15
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
15
16
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
@@ -20,6 +20,8 @@
20
20
  # HPA/PDB/topologySpreadConstraints за каталогом, BackendConfig-сепарація,
21
21
  # yaml-language-server schema modeline, namespace-перевірки за деревом
22
22
  # `…/k8s/base/`) лишається у `check-k8s.mjs`: вона потребує файлової системи.
23
+ # JS authoritative (`check-k8s.mjs` робить ці ж пер-документні перевірки в ширшому
24
+ # контексті); ця Rego — швидкий gate для одиничного маніфеста.
23
25
  #
24
26
  # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
25
27
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
@@ -23,9 +23,7 @@ import { createCheckReporter } from './utils/check-reporter.mjs'
23
23
 
24
24
  const PROJECT_HOOK_PATH = '.claude/hooks/capture-decisions.sh'
25
25
  const PROJECT_SETTINGS_PATH = '.claude/settings.json'
26
- const PROJECT_LOCAL_SETTINGS_PATH = '.claude/settings.local.json'
27
26
  const PROJECT_LOG_PATH = '.claude/hooks/capture-decisions.log'
28
- const HOOK_COMMAND_MARKER = '.claude/hooks/capture-decisions.sh'
29
27
  const EOL_RE = /\r?\n/u
30
28
 
31
29
  const here = dirname(fileURLToPath(import.meta.url))
@@ -81,93 +79,18 @@ async function checkHookScript(reporter) {
81
79
  }
82
80
 
83
81
  /**
84
- * Знаходить у `hooks.Stop` групу, де `command` будь-якого hook-а містить маркер.
85
- * @param {unknown} settings розпарсений `.claude/settings.json`
86
- * @returns {boolean} `true`, якщо знайдено хоч одну групу з маркером
82
+ * FS-existence для project-shared `.claude/settings.json` і
83
+ * `.claude/settings.local.json`. Структуру (`hooks.Stop[]` містить групу з
84
+ * `capture-decisions.sh`; `settings.local.json` не дублює) валідують
85
+ * `npm/policy/adr/settings_json/` і `npm/policy/adr/settings_local_json/`.
86
+ * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер
87
87
  */
88
- function settingsHaveAdrHookGroup(settings) {
89
- if (!settings || typeof settings !== 'object') {
90
- return false
91
- }
92
- const hooks = /** @type {Record<string, unknown>} */ (settings).hooks
93
- if (!hooks || typeof hooks !== 'object') {
94
- return false
95
- }
96
- const stopGroups = /** @type {Record<string, unknown>} */ (hooks).Stop
97
- if (!Array.isArray(stopGroups)) {
98
- return false
99
- }
100
- return stopGroups.some(group => {
101
- const inner = group && typeof group === 'object' ? /** @type {Record<string, unknown>} */ (group).hooks : null
102
- if (!Array.isArray(inner)) {
103
- return false
104
- }
105
- return inner.some(h => {
106
- const cmd = h && typeof h === 'object' ? /** @type {Record<string, unknown>} */ (h).command : null
107
- return typeof cmd === 'string' && cmd.includes(HOOK_COMMAND_MARKER)
108
- })
109
- })
110
- }
111
-
112
- /**
113
- * Зчитує JSON-файл або повертає `undefined`, якщо файл відсутній чи невалідний.
114
- * @param {string} path відносний шлях до JSON-файлу
115
- * @returns {Promise<unknown | undefined>} розпарсений вміст або `undefined`
116
- */
117
- async function readJsonOrUndefined(path) {
118
- if (!existsSync(path)) {
119
- return
120
- }
121
- try {
122
- return JSON.parse(await readFile(path, 'utf8'))
123
- } catch {
124
- return
125
- }
126
- }
127
-
128
- /**
129
- * Перевіряє project-shared `.claude/settings.json` на наявність ADR Stop-hook'а.
130
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
131
- * @returns {Promise<void>}
132
- */
133
- async function checkProjectSettings(reporter) {
88
+ function checkProjectSettings(reporter) {
134
89
  const { pass, fail } = reporter
135
- const settings = await readJsonOrUndefined(PROJECT_SETTINGS_PATH)
136
- if (settings === undefined) {
137
- fail(`${PROJECT_SETTINGS_PATH} не існує або невалідний — запусти \`npx @nitra/cursor\``)
138
- return
139
- }
140
- if (settingsHaveAdrHookGroup(settings)) {
141
- pass(`${PROJECT_SETTINGS_PATH} містить ADR Stop-hook (capture-decisions.sh)`)
142
- } else {
143
- fail(
144
- `${PROJECT_SETTINGS_PATH}: у hooks.Stop немає групи з \`${HOOK_COMMAND_MARKER}\` — переконайся, що "adr" у rules і запусти \`npx @nitra/cursor\``
145
- )
146
- }
147
- }
148
-
149
- /**
150
- * Перевіряє, що `.claude/settings.local.json` не дублює ADR Stop-hook (project-shared — джерело правди).
151
- * @param {import('./utils/check-reporter.mjs').CheckReporter} reporter репортер для збору результатів
152
- * @returns {Promise<void>}
153
- */
154
- async function checkLocalSettingsNoDuplicate(reporter) {
155
- const { pass, fail } = reporter
156
- if (!existsSync(PROJECT_LOCAL_SETTINGS_PATH)) {
157
- pass(`${PROJECT_LOCAL_SETTINGS_PATH} відсутній — дубля немає`)
158
- return
159
- }
160
- const local = await readJsonOrUndefined(PROJECT_LOCAL_SETTINGS_PATH)
161
- if (local === undefined) {
162
- pass(`${PROJECT_LOCAL_SETTINGS_PATH} нечитабельний — дубля немає`)
163
- return
164
- }
165
- if (settingsHaveAdrHookGroup(local)) {
166
- fail(
167
- `${PROJECT_LOCAL_SETTINGS_PATH} містить дубль ADR Stop-hook (capture-decisions.sh) — прибери, бо project-shared settings.json уже керує цим`
168
- )
90
+ if (existsSync(PROJECT_SETTINGS_PATH)) {
91
+ pass(`${PROJECT_SETTINGS_PATH} є (Stop-hook перевіряє bun run lint-conftest → adr.settings_json)`)
169
92
  } else {
170
- pass(`${PROJECT_LOCAL_SETTINGS_PATH} не дублює ADR Stop-hook`)
93
+ fail(`${PROJECT_SETTINGS_PATH} не існує запусти \`npx @nitra/cursor\``)
171
94
  }
172
95
  }
173
96
 
@@ -244,8 +167,7 @@ function checkLlmCliAvailable(reporter) {
244
167
  export async function check() {
245
168
  const reporter = createCheckReporter()
246
169
  await checkHookScript(reporter)
247
- await checkProjectSettings(reporter)
248
- await checkLocalSettingsNoDuplicate(reporter)
170
+ checkProjectSettings(reporter)
249
171
  await checkGitignore(reporter)
250
172
  checkLlmCliAvailable(reporter)
251
173
  return reporter.getExitCode()
@@ -4,18 +4,15 @@
4
4
  * Workflows лише з розширенням `.yml`, наявність clean/lint workflow, конфіг zizmor з ref-pin,
5
5
  * відсутність MegaLinter, коректний скрипт `lint-ga` у `package.json`, виклик у `lint-ga.yml`,
6
6
  * наявність composite `.github/actions/setup-bun-deps/action.yml` (його записує npx `\@nitra/cursor`),
7
- * `\.vscode/settings.json` — `editor.defaultFormatter` **oxc** для `[github-actions-workflow]`,
8
- * перед `uses: ./…/setup-bun-deps` у workflow — `actions/checkout` (runner інакше не бачить локальний action).
7
+ * `\.vscode/settings.json` — `editor.defaultFormatter` **oxc** для `[github-actions-workflow]`.
9
8
  *
10
- * Також перевіряє, що ключові workflow (`clean-ga-workflows.yml`, `clean-merged-branch.yml`, `lint-ga.yml`, `git-ai.yml`)
11
- * мають структуру й значення, узгоджені з `npm/mdc/ga.mdc`. Для цих файлів перевірка виконується структурно
12
- * (після YAML parse), щоб не залежати від форматування/відступів.
13
- *
14
- * Заборонено дублювати кроки встановлення Bun та кешування безпосередньо у workflow файлах
15
- * (oven-sh/setup-bun, actions/cache, bun install). Перевірки `uses`/`run` виконуються після **YAML parse**
16
- * (`yaml`), щоб не спрацьовувати на випадкові збіги в коментарях або поза кроками.
17
- *
18
- * У `run:` заборонено shell-продовження рядків через `\\` перед переносом; довгі команди — через folded block `>-`.
9
+ * Структурні поля 4 канонічних workflow (`clean-ga-workflows.yml`, `clean-merged-branch.yml`,
10
+ * `lint-ga.yml`, `git-ai.yml`) і УНІВЕРСАЛЬНІ перевірки для всіх `.github/workflows/*.yml`
11
+ * (`concurrency`, заборонені `oven-sh/setup-bun` / `actions/cache` / `bun install` у `uses`/`run`,
12
+ * shell-продовження `\` у `run`, обов'язковий `actions/checkout@v6` перед локальним
13
+ * `setup-bun-deps`) у Rego-полісі під `npm/policy/ga/` і запускаються через
14
+ * `bun run lint-ga` (`runConftestStep` у `lint-ga.mjs`). Тут лишилася лише git-залежна
15
+ * перевірка `on.*.paths` glob-ів через `git ls-files :(glob)`.
19
16
  */
20
17
  import { existsSync } from 'node:fs'
21
18
  import { readdir, readFile } from 'node:fs/promises'
@@ -23,14 +20,7 @@ import { execFileSync } from 'node:child_process'
23
20
  import { join } from 'node:path'
24
21
 
25
22
  import { createCheckReporter } from './utils/check-reporter.mjs'
26
- import {
27
- eventPathsIncludeExact,
28
- findForbiddenUsesOrRunPatterns,
29
- findRunStepsWithShellLineContinuationBackslash,
30
- hasAnyStepUsesContaining,
31
- hasCheckoutBeforeLocalSetupBunDeps,
32
- parseWorkflowYaml
33
- } from './utils/gha-workflow.mjs'
23
+ import { eventPathsIncludeExact, parseWorkflowYaml } from './utils/gha-workflow.mjs'
34
24
  import { resolveCmd } from './utils/resolve-cmd.mjs'
35
25
 
36
26
  /** Шаблони наявності MegaLinter у вмісті workflow */
@@ -41,22 +31,9 @@ const MEGALINTER_CONFIG_NAMES = ['.mega-linter.yml', '.megalinter.yaml', '.mega-
41
31
 
42
32
  const N_CURSOR_LINT_GA_RE = /\bn-cursor\s+lint-ga\b/
43
33
 
44
- /** Локальні composite setup-bun-deps (ga.mdc). */
45
- const SETUP_BUN_PATTERNS = ['./.github/actions/setup-bun-deps', './npm/github-actions/setup-bun-deps']
46
-
47
- /** Заборонені підрядки лише в кроках uses/run. */
48
- const FORBIDDEN_BUN_PATTERNS = [
49
- { pattern: 'oven-sh/setup-bun', msg: 'використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun' },
50
- { pattern: 'actions/cache', msg: 'використовуй .github/actions/setup-bun-deps замість actions/cache' },
51
- { pattern: 'bun install', msg: 'використовуй .github/actions/setup-bun-deps замість bun install' }
52
- ]
53
-
54
34
  /** Обовʼязкові workflow-файли (ga.mdc). */
55
35
  const REQUIRED_WORKFLOWS = ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']
56
36
 
57
- /** Канонічне значення `concurrency.group` (ga.mdc). Збирається з фрагментів, щоб не плодити expression-токени в коді. */
58
- const EXPECTED_CONCURRENCY_GROUP = ['$', '{{ github.ref }}-$', '{{ github.workflow }}'].join('')
59
-
60
37
  /**
61
38
  * Повертає true, якщо glob у GitHub Actions `on.*.paths` матчитсья хоча б на один tracked файл у репозиторії.
62
39
  *
@@ -156,162 +133,6 @@ function getObjKey(obj, key) {
156
133
  : undefined
157
134
  }
158
135
 
159
- /**
160
- * Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
161
- *
162
- * Використовується в канонічних структурних валідаторах (clean-ga-workflows, clean-merged-branch,
163
- * lint-ga, git-ai), де root уже отримано через `parseWorkflowYaml`. Логіка ідентична
164
- * `verifyConcurrencyBlock`, але без повторного парсингу.
165
- * @param {string} relPath шлях для повідомлень
166
- * @param {Record<string, unknown>} root parsed YAML workflow
167
- * @param {(msg: string) => void} passFn pass
168
- * @param {(msg: string) => void} failFn fail
169
- * @returns {void}
170
- */
171
- function validateConcurrencyOnRoot(relPath, root, passFn, failFn) {
172
- const conc = getObjKey(root, 'concurrency')
173
- if (!conc || typeof conc !== 'object') {
174
- failFn(
175
- `${relPath}: відсутня секція concurrency — додай concurrency.group: ${EXPECTED_CONCURRENCY_GROUP} і cancel-in-progress: true (ga.mdc)`
176
- )
177
- return
178
- }
179
- const group = getObjKey(conc, 'group')
180
- const cancel = getObjKey(conc, 'cancel-in-progress')
181
- if (group !== EXPECTED_CONCURRENCY_GROUP) {
182
- failFn(`${relPath}: concurrency.group має бути ${EXPECTED_CONCURRENCY_GROUP} (ga.mdc)`)
183
- return
184
- }
185
- if (cancel !== true) {
186
- failFn(`${relPath}: concurrency.cancel-in-progress має бути true (ga.mdc)`)
187
- return
188
- }
189
- passFn(`${relPath}: concurrency блок OK`)
190
- }
191
-
192
- /**
193
- * Перевіряє, що workflow містить блок `concurrency` з канонічними `group` і `cancel-in-progress: true` (ga.mdc).
194
- *
195
- * Без винятків — застосовується до всіх workflow у `.github/workflows/*.yml`, включно з scheduled cleanup,
196
- * `pull_request: types: [closed]` та publish-воркфлоу. Делегує логіку `validateConcurrencyOnRoot`,
197
- * додаючи лише крок парсингу YAML; якщо парсинг провалився — мовчки виходить (синтаксичні проблеми
198
- * ловлять інші перевірки).
199
- * @param {string} relPath шлях для повідомлень
200
- * @param {string} content вміст YAML
201
- * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
202
- * @param {(msg: string) => void} passFn реєструє успішну перевірку
203
- * @returns {void}
204
- */
205
- function verifyConcurrencyBlock(relPath, content, failFn, passFn) {
206
- const root = parseWorkflowYaml(content)
207
- if (!root) {
208
- return
209
- }
210
- validateConcurrencyOnRoot(relPath, root, passFn, failFn)
211
- }
212
-
213
- /**
214
- * Якщо workflow викликає локальний setup-bun-deps, раніше у файлі має бути `actions/checkout@v…` (ga.mdc).
215
- * Fallback: сирий текст, якщо YAML не вдається розібрати.
216
- * @param {string} relPath шлях для повідомлень
217
- * @param {string} content вміст YAML
218
- * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
219
- * @param {(msg: string) => void} passFn реєструє успішну перевірку
220
- * @returns {void}
221
- */
222
- function verifyCheckoutBeforeLocalSetupBunDeps(relPath, content, failFn, passFn) {
223
- const root = parseWorkflowYaml(content)
224
- if (root) {
225
- if (!hasAnyStepUsesContaining(root, SETUP_BUN_PATTERNS)) {
226
- return
227
- }
228
- if (!hasCheckoutBeforeLocalSetupBunDeps(root, SETUP_BUN_PATTERNS)) {
229
- failFn(
230
- `${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
231
- )
232
- return
233
- }
234
- passFn(`${relPath}: перед setup-bun-deps є checkout`)
235
- return
236
- }
237
- let idxSetup = -1
238
- for (const p of SETUP_BUN_PATTERNS) {
239
- const i = content.indexOf(p)
240
- if (i !== -1 && (idxSetup === -1 || i < idxSetup)) {
241
- idxSetup = i
242
- }
243
- }
244
- if (idxSetup === -1) {
245
- return
246
- }
247
- const idxCheckout = content.indexOf('actions/checkout@v')
248
- if (idxCheckout === -1 || idxCheckout > idxSetup) {
249
- failFn(
250
- `${relPath}: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 — інакше runner не знайде action.yml (ga.mdc)`
251
- )
252
- return
253
- }
254
- passFn(`${relPath}: перед setup-bun-deps є checkout`)
255
- }
256
-
257
- /**
258
- * Перевіряє заборонені кроки Bun/cache/install у `uses` та `run`.
259
- * @param {string} relPath шлях для повідомлень
260
- * @param {string} content вміст YAML
261
- * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
262
- * @param {(msg: string) => void} passFn реєструє успішну перевірку
263
- * @returns {void}
264
- */
265
- function verifyNoDirectBunOrCache(relPath, content, failFn, passFn) {
266
- const root = parseWorkflowYaml(content)
267
- if (root) {
268
- const hits = findForbiddenUsesOrRunPatterns(root, FORBIDDEN_BUN_PATTERNS)
269
- if (hits.length === 0) {
270
- passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
271
- } else {
272
- for (const h of hits) {
273
- failFn(`${relPath}: ${h.msg} (ga.mdc)`)
274
- }
275
- }
276
- return
277
- }
278
- let foundForbidden = false
279
- for (const { pattern, msg } of FORBIDDEN_BUN_PATTERNS) {
280
- if (content.includes(pattern)) {
281
- failFn(`${relPath}: ${msg} (ga.mdc)`)
282
- foundForbidden = true
283
- }
284
- }
285
- if (!foundForbidden) {
286
- passFn(`${relPath}: не містить заборонених кроків setup-bun/cache/install`)
287
- }
288
- }
289
-
290
- /**
291
- * У кроках `run` заборонено shell-продовження через `\\` перед переносом; замість `run: |` з `\\` використовуй `run: >-`.
292
- * @param {string} relPath шлях для повідомлень
293
- * @param {string} content вміст YAML
294
- * @param {(msg: string) => void} failFn реєструє порушення (exit 1)
295
- * @param {(msg: string) => void} passFn реєструє успішну перевірку
296
- * @returns {void}
297
- */
298
- function verifyNoRunShellLineContinuationBackslash(relPath, content, failFn, passFn) {
299
- const root = parseWorkflowYaml(content)
300
- if (!root) {
301
- return
302
- }
303
- const hits = findRunStepsWithShellLineContinuationBackslash(root)
304
- if (hits.length === 0) {
305
- passFn(String.raw`${relPath}: run без shell-продовження через \ (ga.mdc)`)
306
- return
307
- }
308
- for (const h of hits) {
309
- failFn(
310
- String.raw`${relPath}: job ${h.jobId}, крок ${h.stepIndex + 1}: у run заборонено продовження рядків через зворотний сліш; довгі команди оформи як folded block (run: >-) без \ на кінцях рядків (ga.mdc)`
311
- )
312
- }
313
- }
314
-
315
136
  /**
316
137
  * Перевіряє apply-workflow на наявність paths trigger.
317
138
  * @param {string} wfDir директорія workflows
@@ -549,12 +370,13 @@ export async function check() {
549
370
  const ymlWorkflows = files.filter(f => f.endsWith('.yml'))
550
371
  await checkMegalinter(wfDir, ymlWorkflows, pass, fail)
551
372
 
373
+ // Універсальні структурні перевірки (concurrency, заборонені setup-bun/cache,
374
+ // shell line-continuation `\`, checkout перед локальним setup-bun-deps)
375
+ // перенесено в Rego (`npm/policy/ga/workflow_common/`); їх запускає
376
+ // `bun run lint-ga` через conftest. Тут лишилася лише git-залежна перевірка
377
+ // `on.push.paths` glob-ів (вимагає `git ls-files`).
552
378
  for (const f of ymlWorkflows) {
553
379
  const content = await readFile(join(wfDir, f), 'utf8')
554
- verifyCheckoutBeforeLocalSetupBunDeps(`${wfDir}/${f}`, content, fail, pass)
555
- verifyNoDirectBunOrCache(`${wfDir}/${f}`, content, fail, pass)
556
- verifyNoRunShellLineContinuationBackslash(`${wfDir}/${f}`, content, fail, pass)
557
- verifyConcurrencyBlock(`${wfDir}/${f}`, content, fail, pass)
558
380
  const parsed = parseWorkflowYaml(content)
559
381
  if (parsed) {
560
382
  verifyWorkflowEventPathsGlobsExist(`${wfDir}/${f}`, parsed, pass, fail)