@nitra/cursor 1.8.204 → 1.8.207

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +52 -1
  2. package/bin/auto-rules.md +2 -0
  3. package/mdc/rego.mdc +77 -0
  4. package/package.json +1 -1
  5. package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
  6. package/policy/abie/http_route_base/http_route_base.rego +45 -0
  7. package/policy/adr/settings_json/settings_json.rego +31 -0
  8. package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
  9. package/policy/bun/bunfig/bunfig.rego +33 -0
  10. package/policy/bun/package_json/package_json.rego +94 -0
  11. package/policy/capacitor/package_json/package_json.rego +45 -0
  12. package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
  13. package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
  14. package/policy/ga/git_ai/git_ai.rego +83 -0
  15. package/policy/ga/lint_ga/lint_ga.rego +118 -0
  16. package/policy/ga/workflow_common/workflow_common.rego +161 -0
  17. package/policy/graphql/package_json/package_json.rego +35 -0
  18. package/policy/hasura/svc_hl/svc_hl.rego +27 -0
  19. package/policy/image_compress/package_json/package_json.rego +94 -0
  20. package/policy/js_bun_db/package_json/package_json.rego +28 -0
  21. package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
  22. package/policy/js_lint/package_json/package_json.rego +137 -0
  23. package/policy/js_mssql/package_json/package_json.rego +57 -0
  24. package/policy/js_run/configmap/configmap.rego +45 -0
  25. package/policy/js_run/jsconfig/jsconfig.rego +66 -0
  26. package/policy/js_run/package_json/package_json.rego +31 -0
  27. package/policy/k8s/manifest/manifest.rego +130 -0
  28. package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
  29. package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
  30. package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
  31. package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
  32. package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
  33. package/policy/php/package_json/package_json.rego +19 -0
  34. package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
  35. package/policy/style_lint/package_json/package_json.rego +49 -0
  36. package/policy/text/cspell/cspell.rego +91 -0
  37. package/policy/text/markdownlint/markdownlint.rego +21 -0
  38. package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
  39. package/policy/text/package_json/package_json.rego +88 -0
  40. package/policy/vue/package_json/package_json.rego +54 -0
  41. package/scripts/auto-rules.mjs +10 -0
  42. package/scripts/check-adr.mjs +7 -3
  43. package/scripts/check-bun.mjs +21 -117
  44. package/scripts/check-ga.mjs +0 -284
  45. package/scripts/check-graphql.mjs +6 -45
  46. package/scripts/check-hasura.mjs +4 -5
  47. package/scripts/check-image-avif.mjs +3 -3
  48. package/scripts/check-image-compress.mjs +25 -132
  49. package/scripts/check-js-bun-db.mjs +3 -50
  50. package/scripts/check-js-run.mjs +9 -12
  51. package/scripts/check-k8s.mjs +6 -5
  52. package/scripts/check-npm-module.mjs +17 -8
  53. package/scripts/check-php.mjs +16 -51
  54. package/scripts/check-style-lint.mjs +28 -52
  55. package/scripts/check-text.mjs +47 -219
  56. package/scripts/check-vue.mjs +3 -16
  57. package/scripts/lint-conftest.mjs +351 -0
  58. package/scripts/lint-ga.mjs +49 -2
  59. package/scripts/lint-rego.mjs +67 -21
  60. package/scripts/run-shellcheck-text.mjs +3 -6
  61. package/scripts/utils/depcheck-workflow.mjs +2 -6
@@ -1,54 +1,30 @@
1
1
  /**
2
2
  * Перевіряє відповідність репозиторію правилам Bun (bun.mdc).
3
3
  *
4
- * Очікує наявність `bun.lock`, `bunfig.toml` з `linker = "hoisted"` у секції `[install]`,
5
- * забороняє lockfile та артефакти yarn/pnpm, директорію `.yarn` і поле `packageManager`
6
- * у кореневому `package.json`.
4
+ * **Що тут лишилося** (FS / cross-file не покривається conftest):
5
+ * - наявність `bun.lock`, `bunfig.toml`, `package.json` у корені (FS-existence);
6
+ * - заборонені lockfile та артефакти yarn/pnpm (`package-lock.json`, `yarn.lock`,
7
+ * `pnpm-lock.yaml`, `.yarnrc.yml`, директорія `.yarn/`);
8
+ * - якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, у кореневому
9
+ * `package.json` має бути відповідний скрипт `lint-docker` / `lint-k8s`
10
+ * (cross-file: два JSON-файли).
7
11
  *
8
- * У кореневому `package.json` не має бути поля **`dependencies`**; у **`devDependencies`** дозволені лише
9
- * пакети **`@nitra/*`** (наприклад **`@nitra/cspell-dict`**, **`@nitra/eslint-config`**).
10
- *
11
- * Якщо в `.n-cursor.json` у `rules` є `docker` або `k8s`, вимагає у кореневому `package.json`
12
- * відповідно скриптів `lint-docker` / `lint-k8s` (див. docker.mdc, k8s.mdc).
13
- *
14
- * Якщо в кореневому `package.json` є скрипти з префіксом `lint-`, перевіряє наявність агрегованого
15
- * скрипта `lint`, у якому через `bun run <ім’я>` викликаються всі такі скрипти, і що рядок `lint`
16
- * закінчується на `&& oxfmt .`.
12
+ * **Що покрила Rego** (`bun run lint-conftest`):
13
+ * - `npm/policy/bun/bunfig/` `[install].linker == "hoisted"` у `bunfig.toml`;
14
+ * - `npm/policy/bun/package_json/` — відсутність `packageManager` / `dependencies`
15
+ * у кореневому `package.json`, у `devDependencies` лише `@nitra/*`, агрегований
16
+ * `lint`-скрипт покриває всі `lint-*` через `bun run` і завершується `&& oxfmt .`.
17
17
  */
18
18
  import { existsSync } from 'node:fs'
19
19
  import { readFile } from 'node:fs/promises'
20
20
 
21
21
  import { createCheckReporter } from './utils/check-reporter.mjs'
22
22
 
23
- const OXFMT_END_RE = /&&[ \t]+oxfmt[ \t]+\.[ \t]*$/
24
- /** Пробіли/таби без `\s` (уникаємо super-linear backtracking у sonarjs/slow-regex). */
25
- const HOISTED_LINKER_RE = /^[ \t]*linker[ \t]*=[ \t]*"hoisted"[ \t]*$/m
26
- const INSTALL_SECTION_RE = /^[ \t]*\[install\][ \t]*$/m
27
-
28
- /**
29
- * Перевіряє `bunfig.toml` на секцію `[install]` з `linker = "hoisted"`.
30
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер
31
- */
32
- async function checkBunfigHoisted(reporter) {
33
- const { pass, fail } = reporter
34
- if (!existsSync('bunfig.toml')) {
35
- fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
36
- return
37
- }
38
- const content = await readFile('bunfig.toml', 'utf8')
39
- if (!INSTALL_SECTION_RE.test(content)) {
40
- fail('bunfig.toml: відсутня секція [install] (bun.mdc)')
41
- return
42
- }
43
- if (HOISTED_LINKER_RE.test(content)) {
44
- pass('bunfig.toml: [install] linker = "hoisted"')
45
- } else {
46
- fail('bunfig.toml: у секції [install] має бути linker = "hoisted" (bun.mdc)')
47
- }
48
- }
49
-
50
23
  /**
51
24
  * Чи ім'я пакета дозволене в кореневих `devDependencies` за bun.mdc (лише **`@nitra/*`**).
25
+ *
26
+ * Залишилася як експорт для `check-text.mjs` і тестів — `bun.package_json` Rego
27
+ * робить ту саму перевірку для check-runner-а.
52
28
  * @param {string} name ключ з поля `devDependencies`
53
29
  * @returns {boolean} true, якщо префікс дозволений
54
30
  */
@@ -76,66 +52,6 @@ async function loadNCursorRules() {
76
52
  }
77
53
  }
78
54
 
79
- /**
80
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
81
- * @param {Record<string, unknown>} pkg розібраний package.json
82
- */
83
- function checkDevDependencies(reporter, pkg) {
84
- const { pass, fail } = reporter
85
- const dev = pkg.devDependencies
86
- if (dev === undefined) {
87
- pass('Кореневий package.json без devDependencies')
88
- return
89
- }
90
- if (dev === null || typeof dev !== 'object' || Array.isArray(dev)) {
91
- fail(
92
- 'Кореневий package.json: `devDependencies` має бути object з ключами пакетів і діапазонами версій (не null, не масив)'
93
- )
94
- return
95
- }
96
- const bad = Object.keys(/** @type {object} */ (dev)).filter(n => !isAllowedRootDevDependency(n))
97
- if (bad.length > 0) {
98
- fail(`Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: ${bad.join(', ')} (bun.mdc)`)
99
- return
100
- }
101
- const n = Object.keys(/** @type {object} */ (dev)).length
102
- pass(
103
- n === 0
104
- ? 'Кореневі devDependencies порожні або відсутні (лише @nitra/*)'
105
- : `Кореневі devDependencies: лише @nitra/* (${n} пак.)`
106
- )
107
- }
108
-
109
- /**
110
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
111
- * @param {Record<string, string>} scripts scripts з package.json
112
- */
113
- function checkLintAggregate(reporter, scripts) {
114
- const { pass, fail } = reporter
115
- const lintPrefixed = Object.keys(scripts).filter(name => name.startsWith('lint-'))
116
- if (lintPrefixed.length === 0) return
117
- const aggregate = typeof scripts.lint === 'string' ? scripts.lint : ''
118
- if (!aggregate.trim()) {
119
- const scriptList = lintPrefixed.map(s => `\`${s}\``).join(', ')
120
- fail(
121
- `У package.json є скрипти ${scriptList}, але немає агрегованого \`lint\` — додай скрипт, який запускає їх через \`bun run\``
122
- )
123
- return
124
- }
125
- const missing = lintPrefixed.filter(name => !aggregate.includes(`bun run ${name}`))
126
- if (missing.length > 0) {
127
- const missingList = missing.map(s => '`' + s + '`').join(', ')
128
- fail(`Скрипт \`lint\` має викликати всі lint-* через bun run; відсутньо: ${missingList}`)
129
- return
130
- }
131
- pass('package.json: агрегований `lint` покриває всі `lint-*` скрипти')
132
- if (OXFMT_END_RE.test(aggregate.trim())) {
133
- pass('package.json: `lint` завершується `&& oxfmt .`')
134
- } else {
135
- fail('Скрипт `lint` має закінчуватися на `&& oxfmt .`')
136
- }
137
- }
138
-
139
55
  /**
140
56
  * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер для збору результатів
141
57
  * @param {Record<string, string>} scripts scripts з package.json
@@ -188,34 +104,22 @@ export async function check() {
188
104
  fail('Відсутній bun.lock — запусти bun i')
189
105
  }
190
106
 
191
- await checkBunfigHoisted(reporter)
107
+ if (!existsSync('bunfig.toml')) {
108
+ fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
109
+ } else {
110
+ pass('bunfig.toml є (структуру перевіряє bun run lint-conftest → bun.bunfig)')
111
+ }
192
112
 
193
113
  const cursorRules = await loadNCursorRules()
194
114
 
195
115
  if (!existsSync('package.json')) {
116
+ fail('Відсутній package.json у корені')
196
117
  return reporter.getExitCode()
197
118
  }
198
119
 
199
120
  const pkg = JSON.parse(await readFile('package.json', 'utf8'))
200
- if (pkg.packageManager) {
201
- fail(`package.json містить поле packageManager: "${pkg.packageManager}" — видали його`)
202
- } else {
203
- pass('package.json не містить packageManager')
204
- }
205
-
206
- if (pkg.dependencies === undefined) {
207
- pass('Кореневий package.json без поля `dependencies`')
208
- } else {
209
- fail(
210
- 'Кореневий package.json не повинен містити поле `dependencies` — додай залежності в workspace-пакети (bun.mdc)'
211
- )
212
- }
213
-
214
- checkDevDependencies(reporter, pkg)
215
-
216
121
  const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
217
122
  checkCursorRuleScripts(reporter, scripts, cursorRules)
218
- checkLintAggregate(reporter, scripts)
219
123
 
220
124
  return reporter.getExitCode()
221
125
  }
@@ -24,15 +24,11 @@ import { join } from 'node:path'
24
24
 
25
25
  import { createCheckReporter } from './utils/check-reporter.mjs'
26
26
  import {
27
- anyRunStepIncludes,
28
27
  eventPathsIncludeExact,
29
28
  findForbiddenUsesOrRunPatterns,
30
29
  findRunStepsWithShellLineContinuationBackslash,
31
30
  hasAnyStepUsesContaining,
32
31
  hasCheckoutBeforeLocalSetupBunDeps,
33
- flattenWorkflowSteps,
34
- getStepRun,
35
- getStepUses,
36
32
  parseWorkflowYaml
37
33
  } from './utils/gha-workflow.mjs'
38
34
  import { resolveCmd } from './utils/resolve-cmd.mjs'
@@ -160,156 +156,6 @@ function getObjKey(obj, key) {
160
156
  : undefined
161
157
  }
162
158
 
163
- /**
164
- * Очікує, що значення є рядком рівно `expected`.
165
- * @param {unknown} v значення
166
- * @param {string} expected очікуваний рядок
167
- * @returns {boolean} true, якщо збігається
168
- */
169
- function isExactString(v, expected) {
170
- return typeof v === 'string' && v === expected
171
- }
172
-
173
- /**
174
- * Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
175
- * @param {unknown} on корінь `on:` з YAML
176
- * @param {(msg: string) => void} failFn fail
177
- */
178
- function validateLintGaOnTriggers(on, failFn) {
179
- const push = getObjKey(on, 'push')
180
- const pr = getObjKey(on, 'pull_request')
181
- const pushBranches = getObjKey(push, 'branches')
182
- const pushPaths = getObjKey(push, 'paths')
183
- const prBranches = getObjKey(pr, 'branches')
184
-
185
- if (!Array.isArray(pushBranches) || !(pushBranches.includes('dev') && pushBranches.includes('main'))) {
186
- failFn('lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)')
187
- }
188
- if (!Array.isArray(prBranches) || !(prBranches.includes('dev') && prBranches.includes('main'))) {
189
- failFn('lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)')
190
- }
191
- if (
192
- !Array.isArray(pushPaths) ||
193
- !(pushPaths.includes('.github/actions/**') && pushPaths.includes('.github/workflows/**'))
194
- ) {
195
- failFn('lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)')
196
- }
197
- }
198
-
199
- /**
200
- * Перевіряє структуру workflow `lint-ga.yml` (ga.mdc).
201
- * @param {Record<string, unknown> | null} root parsed YAML
202
- * @param {(msg: string) => void} passFn pass
203
- * @param {(msg: string) => void} failFn fail
204
- */
205
- function validateLintGaWorkflowStructure(root, passFn, failFn) {
206
- if (!root) {
207
- failFn('lint-ga.yml: YAML не вдалося розібрати (ga.mdc)')
208
- return
209
- }
210
-
211
- if (!isExactString(root.name, 'Lint GA')) {
212
- failFn('lint-ga.yml: name має бути "Lint GA" (ga.mdc)')
213
- }
214
-
215
- validateLintGaOnTriggers(root.on, failFn)
216
-
217
- validateConcurrencyOnRoot('lint-ga.yml', root, passFn, failFn)
218
-
219
- const jobs = getObjKey(root, 'jobs')
220
- const job = getObjKey(jobs, 'lint-ga')
221
- if (!job) {
222
- failFn('lint-ga.yml: jobs.lint-ga відсутній (ga.mdc)')
223
- return
224
- }
225
-
226
- if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
227
- failFn('lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)')
228
- }
229
- const perm = getObjKey(job, 'permissions')
230
- if (getObjKey(perm, 'contents') !== 'read') {
231
- failFn('lint-ga.yml: permissions мають бути contents: read (ga.mdc)')
232
- }
233
-
234
- const steps = getObjKey(job, 'steps')
235
- if (!Array.isArray(steps) || steps.length === 0) {
236
- failFn('lint-ga.yml: jobs.lint-ga.steps відсутні (ga.mdc)')
237
- return
238
- }
239
-
240
- const flat = flattenWorkflowSteps(root)
241
- const usesList = new Set(flat.map(s => getStepUses(s.step)))
242
- const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
243
-
244
- if (!usesList.has('actions/checkout@v6')) {
245
- failFn('lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)')
246
- }
247
- if (!usesList.has('./.github/actions/setup-bun-deps')) {
248
- failFn('lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)')
249
- }
250
- if (!usesList.has('astral-sh/setup-uv@v8.0.0')) {
251
- failFn('lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)')
252
- }
253
- if (runBlob.includes('bun run lint-ga')) {
254
- passFn('lint-ga.yml: структура jobs/steps OK')
255
- } else {
256
- failFn('lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)')
257
- }
258
- }
259
-
260
- /**
261
- * Перевіряє структуру workflow `git-ai.yml` (ga.mdc).
262
- * @param {Record<string, unknown> | null} root parsed YAML
263
- * @param {(msg: string) => void} passFn pass
264
- * @param {(msg: string) => void} failFn fail
265
- */
266
- function validateGitAiWorkflowStructure(root, passFn, failFn) {
267
- if (!root) {
268
- failFn('git-ai.yml: YAML не вдалося розібрати (ga.mdc)')
269
- return
270
- }
271
-
272
- if (!isExactString(root.name, 'Git AI')) {
273
- failFn('git-ai.yml: name має бути "Git AI" (ga.mdc)')
274
- }
275
-
276
- const on = root.on
277
- const pr = getObjKey(on, 'pull_request')
278
- const types = getObjKey(pr, 'types')
279
- if (!Array.isArray(types) || !types.includes('closed')) {
280
- failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
281
- }
282
-
283
- validateConcurrencyOnRoot('git-ai.yml', root, passFn, failFn)
284
-
285
- const jobs = getObjKey(root, 'jobs')
286
- const job = getObjKey(jobs, 'git-ai')
287
- if (!job) {
288
- failFn('git-ai.yml: jobs.git-ai відсутній (ga.mdc)')
289
- return
290
- }
291
-
292
- if (!String(getObjKey(job, 'if') ?? '').includes('github.event.pull_request.merged == true')) {
293
- failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
294
- }
295
-
296
- const perm = getObjKey(job, 'permissions')
297
- if (getObjKey(perm, 'contents') !== 'write') {
298
- failFn('git-ai.yml: permissions мають бути contents: write (ga.mdc)')
299
- }
300
-
301
- const flat = flattenWorkflowSteps(root)
302
- const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
303
- if (!runBlob.includes('curl -fsSL https://usegitai.com/install.sh | bash')) {
304
- failFn('git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)')
305
- }
306
- if (runBlob.includes('git-ai ci github run')) {
307
- passFn('git-ai.yml: структура jobs/steps OK')
308
- } else {
309
- failFn('git-ai.yml: має виконувати git-ai ci github run (ga.mdc)')
310
- }
311
- }
312
-
313
159
  /**
314
160
  * Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
315
161
  *
@@ -624,33 +470,6 @@ function checkShellcheckInstalled(passFn, failFn) {
624
470
  )
625
471
  }
626
472
 
627
- /**
628
- * Перевіряє lint-ga.yml workflow.
629
- * @param {string} wfDir директорія workflows
630
- * @param {(msg: string) => void} passFn callback при успішній перевірці
631
- * @param {(msg: string) => void} failFn callback при помилці
632
- */
633
- async function checkLintGaWorkflow(wfDir, passFn, failFn) {
634
- const lintGaWf = join(wfDir, 'lint-ga.yml')
635
- if (!existsSync(lintGaWf)) return
636
- const lgContent = await readFile(lintGaWf, 'utf8')
637
- const root = parseWorkflowYaml(lgContent)
638
- const hasBunRun = root ? anyRunStepIncludes(root, 'bun run lint-ga') : lgContent.includes('bun run lint-ga')
639
- const hasSetupUv = root
640
- ? hasAnyStepUsesContaining(root, ['astral-sh/setup-uv']) || lgContent.includes('astral-sh/setup-uv')
641
- : lgContent.includes('astral-sh/setup-uv')
642
- if (hasBunRun) {
643
- passFn('lint-ga.yml викликає bun run lint-ga')
644
- } else {
645
- failFn('lint-ga.yml: крок має містити bun run lint-ga')
646
- }
647
- if (hasSetupUv) {
648
- passFn('lint-ga.yml містить astral-sh/setup-uv')
649
- } else {
650
- failFn('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
651
- }
652
- }
653
-
654
473
  /**
655
474
  * Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
656
475
  * @param {string} wfDir шлях до директорії workflows
@@ -684,105 +503,6 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
684
503
  }
685
504
  }
686
505
 
687
- /**
688
- * Перевіряє, чи on.pull_request.types у parsed YAML містить 'closed'.
689
- * @param {Record<string, unknown>} root розібраний YAML workflow
690
- * @returns {boolean} true, якщо тригер pull_request має тип closed
691
- */
692
- function hasPullRequestClosedTrigger(root) {
693
- const on = root.on
694
- if (!on || typeof on !== 'object') return false
695
- const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
696
- if (!pr || typeof pr !== 'object') return false
697
- const types = /** @type {Record<string, unknown>} */ (pr).types
698
- return Array.isArray(types) && types.includes('closed')
699
- }
700
-
701
- /**
702
- * Перевіряє, чи будь-який job у parsed YAML має if-умову з 'merged'.
703
- * @param {Record<string, unknown>} root розібраний YAML workflow
704
- * @returns {boolean} true, якщо хоча б один job містить умову merged
705
- */
706
- function hasJobMergedCondition(root) {
707
- const { jobs } = root
708
- if (!jobs || typeof jobs !== 'object') return false
709
- return Object.values(jobs).some(job => {
710
- if (!job || typeof job !== 'object') return false
711
- const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
712
- return ifCond.includes('merged')
713
- })
714
- }
715
-
716
- /**
717
- * Перевіряє parsed YAML git-ai.yml: тригер closed та умова merged.
718
- * @param {Record<string, unknown>} root розібраний YAML workflow
719
- * @param {(msg: string) => void} passFn callback при успішній перевірці
720
- * @param {(msg: string) => void} failFn callback при помилці
721
- */
722
- function validateGitAiParsedYaml(root, passFn, failFn) {
723
- if (hasPullRequestClosedTrigger(root)) {
724
- passFn('git-ai.yml: on.pull_request.types містить closed')
725
- } else {
726
- failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
727
- }
728
-
729
- if (hasJobMergedCondition(root)) {
730
- passFn('git-ai.yml: job має умову merged')
731
- } else {
732
- failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
733
- }
734
- }
735
-
736
- /**
737
- * Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
738
- * @param {string} wfDir директорія workflows
739
- * @param {(msg: string) => void} passFn callback при успішній перевірці
740
- * @param {(msg: string) => void} failFn callback при помилці
741
- */
742
- async function checkGitAiWorkflow(wfDir, passFn, failFn) {
743
- const gitAiWf = join(wfDir, 'git-ai.yml')
744
- if (!existsSync(gitAiWf)) return
745
- const content = await readFile(gitAiWf, 'utf8')
746
- const root = parseWorkflowYaml(content)
747
-
748
- if (root) {
749
- validateGitAiParsedYaml(root, passFn, failFn)
750
- }
751
-
752
- const hasGitAiRun = root ? anyRunStepIncludes(root, 'git-ai ci github run') : content.includes('git-ai ci github run')
753
- if (hasGitAiRun) {
754
- passFn('git-ai.yml: крок виконує git-ai ci github run')
755
- } else {
756
- failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
757
- }
758
- }
759
-
760
- /**
761
- * Перевіряє, що “канонічні” workflows відповідають ga.mdc (структура і значення).
762
- *
763
- * Структурні валідатори `clean-ga-workflows.yml` і `clean-merged-branch.yml` мігровано в Rego-полісі
764
- * під `npm/policy/ga/clean_ga_workflows/` та `npm/policy/ga/clean_merged_branch/` (виконує conftest з
765
- * `bun run lint-ga`). Тут лишаються `lint-ga.yml` і `git-ai.yml` — їх перенесення в наступних ітераціях.
766
- * @param {string} wfDir директорія workflows
767
- * @param {(msg: string) => void} passFn pass
768
- * @param {(msg: string) => void} failFn fail
769
- */
770
- async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
771
- const paths = {
772
- lintGa: join(wfDir, 'lint-ga.yml'),
773
- gitAi: join(wfDir, 'git-ai.yml')
774
- }
775
-
776
- if (existsSync(paths.lintGa)) {
777
- const c = await readFile(paths.lintGa, 'utf8')
778
- validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
779
- }
780
- if (existsSync(paths.gitAi)) {
781
- const c = await readFile(paths.gitAi, 'utf8')
782
- validateGitAiWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
783
- }
784
- }
785
-
786
506
  /**
787
507
  * Перевіряє відповідність проєкту правилам ga.mdc
788
508
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -841,12 +561,8 @@ export async function check() {
841
561
  }
842
562
  }
843
563
 
844
- await checkCanonicalWorkflowsMatchRule(wfDir, pass, fail)
845
-
846
564
  await checkZizmor(pass, fail)
847
565
  await checkLintGaScript(pass, fail)
848
- await checkLintGaWorkflow(wfDir, pass, fail)
849
- await checkGitAiWorkflow(wfDir, pass, fail)
850
566
  checkShellcheckInstalled(pass, fail)
851
567
 
852
568
  return reporter.getExitCode()
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`**, рекомендації
3
- * **`graphql.vscode-graphql`** і скрипта **`dump-schema`** у кореневому
4
- * **`package.json`**, якщо у дереві є **`gql\`…\``**.
2
+ * Перевіряє правило graphql.mdc: наявність **`.graphqlrc.yml`** і рекомендації
3
+ * **`graphql.vscode-graphql`**, якщо у дереві є **`gql\`…\``**.
5
4
  *
6
5
  * Обхід репозиторію — **`walkDir`** від **`process.cwd()`** (пропуски як у інших check). Кандидати — **`.vue`** та **`.js`/`.ts`/`.jsx`/`.tsx`** тощо; пропуск **`.d.ts`**, **auto-imports.d.ts** тощо — **`shouldSkipFileForGqlScan`**.
7
6
  *
8
7
  * Виявлення **`gql`** — **oxc-parser** після витягування `<script>` з SFC (**`graphql-gql-scan.mjs`**). Якщо збігів немає — перевірка завершується успішно без вимог до конфігів.
8
+ *
9
+ * Перевірку `scripts.dump-schema == REQUIRED_DUMP_SCHEMA_SCRIPT` у `package.json`
10
+ * перенесено в Rego (`npm/policy/graphql/package_json/`); `bun run lint-conftest`
11
+ * запускає її окремо.
9
12
  */
10
13
  import { existsSync } from 'node:fs'
11
14
  import { readFile } from 'node:fs/promises'
@@ -25,9 +28,6 @@ export const GRAPHQL_RC_FILENAME = '.graphqlrc.yml'
25
28
 
26
29
  /** Розширення VS Code з graphql.mdc. */
27
30
  export const REQUIRED_GRAPHQL_VSCODE_EXTENSION = 'graphql.vscode-graphql'
28
- /** Команда dump-schema з graphql.mdc. */
29
- export const REQUIRED_DUMP_SCHEMA_SCRIPT =
30
- "bunx graphqurl http://localhost:4040/v1/graphql -H 'X-Hasura-Admin-Secret: secret' --introspect > schema.graphql"
31
31
 
32
32
  /**
33
33
  * Збирає абсолютні шляхи source-файлів, які підлягають скануванню на gql templates.
@@ -106,44 +106,6 @@ async function checkExtensionsRecommendation(pass, fail) {
106
106
  }
107
107
  }
108
108
 
109
- /**
110
- * Перевіряє `package.json` і значення scripts.dump-schema.
111
- * @param {(msg: string) => void} pass success-репортер
112
- * @param {(msg: string) => void} fail fail-репортер
113
- * @returns {Promise<void>}
114
- */
115
- async function checkPackageDumpSchemaScript(pass, fail) {
116
- if (!existsSync('package.json')) {
117
- fail('Відсутній package.json у корені репозиторію')
118
- return
119
- }
120
-
121
- let pkg
122
- try {
123
- pkg = JSON.parse(await readFile('package.json', 'utf8'))
124
- } catch {
125
- fail('package.json не є валідним JSON')
126
- return
127
- }
128
-
129
- const scripts = pkg.scripts
130
- if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
131
- fail('package.json: поле scripts має бути обʼєктом')
132
- return
133
- }
134
-
135
- if (!Object.hasOwn(scripts, 'dump-schema')) {
136
- fail('package.json: відсутній scripts.dump-schema (graphql.mdc)')
137
- return
138
- }
139
-
140
- if (scripts['dump-schema'] === REQUIRED_DUMP_SCHEMA_SCRIPT) {
141
- pass('package.json: scripts.dump-schema відповідає graphql.mdc')
142
- } else {
143
- fail(`package.json: scripts.dump-schema має бути "${REQUIRED_DUMP_SCHEMA_SCRIPT}" (graphql.mdc)`)
144
- }
145
- }
146
-
147
109
  /**
148
110
  * Перевіряє graphql.mdc: умовна вимога .graphqlrc.yml, graphql.vscode-graphql
149
111
  * і scripts.dump-schema за наявності gql tagged templates.
@@ -176,7 +138,6 @@ export async function check() {
176
138
  }
177
139
 
178
140
  await checkExtensionsRecommendation(pass, fail)
179
- await checkPackageDumpSchemaScript(pass, fail)
180
141
 
181
142
  return reporter.getExitCode()
182
143
  }
@@ -45,9 +45,7 @@ const ENV_FILE_RE = /\.env$/u
45
45
  const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
46
46
  // Дозволяємо два DNS-суфікси кластера: `<name>.internal` (GKE/GCP) і `cluster.local`
47
47
  // (стандартний k8s / Yandex Cloud). У YC namespace.yaml + cluster mode дають коротший суфікс.
48
- const INTERNAL_HASURA_URL_RE =
49
- /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
50
- const CLUSTER_LOCAL_SUFFIX = 'cluster.local'
48
+ const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
51
49
  const INTERNAL_DNS_SUFFIX = '.internal'
52
50
 
53
51
  /**
@@ -150,8 +148,9 @@ async function checkEnvFile(relPath, expected, reporter) {
150
148
  const value = m[1].trim()
151
149
  const parsed = parseInternalHasuraEndpoint(value)
152
150
  if (!parsed.ok) {
153
- // eslint-disable-next-line @microsoft/sdl/no-insecure-url, sonarjs/no-clear-text-protocols -- hasura.mdc вимагає саме http:// для кластерного URL (TLS не використовується)
154
- const example = 'http://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
151
+
152
+ const example =
153
+ "https://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>"
155
154
  fail(
156
155
  `${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
157
156
  )
@@ -68,6 +68,7 @@ const VUE_RASTER_STATIC_SRC_RE = /(?<![:\-_.])\bsrc\s*=\s*['"]([^'"\s]+\.(?:png|
68
68
  * є сиротами і підлягають видаленню.
69
69
  */
70
70
  const VUE_AVIF_REF_RE = /['"]([^'"\s]+\.(?:png|jpe?g|gif)\.avif)['"]/giu
71
+ const RASTER_IMAGE_EXT_RE = /\.(?:png|jpe?g|gif)$/iu
71
72
 
72
73
  /**
73
74
  * Чи у `package.json` пакета вимкнено avif-перевірку Vue-імпортів.
@@ -113,8 +114,7 @@ function resolveImageCandidates(importPath, sourceAbsPath, packageRootAbs) {
113
114
  /** @type {string[]} */
114
115
  const candidates = []
115
116
  if (packageRootAbs) {
116
- candidates.push(join(packageRootAbs, 'public', importPath))
117
- candidates.push(join(packageRootAbs, importPath))
117
+ candidates.push(join(packageRootAbs, 'public', importPath), join(packageRootAbs, importPath))
118
118
  }
119
119
  candidates.push(join(process.cwd(), importPath))
120
120
  return candidates
@@ -291,7 +291,7 @@ async function hasAnyRasterImage(ignorePaths) {
291
291
  process.cwd(),
292
292
  absPath => {
293
293
  if (found) return
294
- if (/\.(?:png|jpe?g|gif)$/iu.test(absPath)) found = true
294
+ if (RASTER_IMAGE_EXT_RE.test(absPath)) found = true
295
295
  },
296
296
  ignorePaths
297
297
  )