@nitra/cursor 1.8.206 → 1.8.208

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 (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/mdc/js-run.mdc +49 -2
  3. package/package.json +1 -1
  4. package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
  5. package/policy/abie/http_route_base/http_route_base.rego +45 -0
  6. package/policy/adr/settings_json/settings_json.rego +31 -0
  7. package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
  8. package/policy/bun/bunfig/bunfig.rego +33 -0
  9. package/policy/bun/package_json/package_json.rego +94 -0
  10. package/policy/capacitor/package_json/package_json.rego +45 -0
  11. package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
  12. package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
  13. package/policy/ga/git_ai/git_ai.rego +0 -26
  14. package/policy/ga/lint_ga/lint_ga.rego +0 -26
  15. package/policy/ga/workflow_common/workflow_common.rego +161 -0
  16. package/policy/graphql/package_json/package_json.rego +35 -0
  17. package/policy/hasura/svc_hl/svc_hl.rego +27 -0
  18. package/policy/image_compress/package_json/package_json.rego +94 -0
  19. package/policy/js_bun_db/package_json/package_json.rego +28 -0
  20. package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
  21. package/policy/js_lint/package_json/package_json.rego +137 -0
  22. package/policy/js_mssql/package_json/package_json.rego +57 -0
  23. package/policy/js_run/configmap/configmap.rego +45 -0
  24. package/policy/js_run/jsconfig/jsconfig.rego +66 -0
  25. package/policy/js_run/package_json/package_json.rego +31 -0
  26. package/policy/k8s/manifest/manifest.rego +130 -0
  27. package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
  28. package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
  29. package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
  30. package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
  31. package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
  32. package/policy/php/package_json/package_json.rego +19 -0
  33. package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
  34. package/policy/style_lint/package_json/package_json.rego +49 -0
  35. package/policy/text/cspell/cspell.rego +91 -0
  36. package/policy/text/markdownlint/markdownlint.rego +21 -0
  37. package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
  38. package/policy/text/package_json/package_json.rego +88 -0
  39. package/policy/vue/package_json/package_json.rego +54 -0
  40. package/scripts/check-adr.mjs +3 -2
  41. package/scripts/check-bun.mjs +21 -117
  42. package/scripts/check-graphql.mjs +6 -45
  43. package/scripts/check-hasura.mjs +2 -3
  44. package/scripts/check-image-avif.mjs +3 -3
  45. package/scripts/check-image-compress.mjs +25 -132
  46. package/scripts/check-js-bun-db.mjs +3 -50
  47. package/scripts/check-js-run.mjs +84 -86
  48. package/scripts/check-k8s.mjs +4 -4
  49. package/scripts/check-npm-module.mjs +17 -8
  50. package/scripts/check-php.mjs +16 -51
  51. package/scripts/check-style-lint.mjs +28 -52
  52. package/scripts/check-text.mjs +47 -219
  53. package/scripts/check-vue.mjs +3 -16
  54. package/scripts/lint-conftest.mjs +351 -0
  55. package/scripts/lint-ga.mjs +39 -2
  56. package/scripts/run-shellcheck-text.mjs +2 -2
  57. package/scripts/utils/conn-file-rules.mjs +170 -0
@@ -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
  }
@@ -46,7 +46,6 @@ const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT
46
46
  // Дозволяємо два DNS-суфікси кластера: `<name>.internal` (GKE/GCP) і `cluster.local`
47
47
  // (стандартний k8s / Yandex Cloud). У YC namespace.yaml + cluster mode дають коротший суфікс.
48
48
  const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
49
- const CLUSTER_LOCAL_SUFFIX = 'cluster.local'
50
49
  const INTERNAL_DNS_SUFFIX = '.internal'
51
50
 
52
51
  /**
@@ -149,9 +148,9 @@ async function checkEnvFile(relPath, expected, reporter) {
149
148
  const value = m[1].trim()
150
149
  const parsed = parseInternalHasuraEndpoint(value)
151
150
  if (!parsed.ok) {
152
- // eslint-disable-next-line @microsoft/sdl/no-insecure-url, sonarjs/no-clear-text-protocols -- hasura.mdc вимагає саме http:// для кластерного URL (TLS не використовується)
151
+
153
152
  const example =
154
- 'http://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
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
  )
@@ -1,121 +1,32 @@
1
1
  /**
2
- * Перевіряє відповідність репозиторію правилу `image-compress.mdc`: канонічний скрипт
3
- * `lint-image` для оптимізації raster/SVG через `@nitra/minify-image` ≥ 3.2.0 (локально).
2
+ * Перевіряє вимоги правила `image-compress.mdc` для оптимізації raster/SVG через
3
+ * `@nitra/minify-image` ≥ 3.2.0 (локально).
4
4
  *
5
- * Очікування:
6
- * - у кореневому `package.json` є скрипт `lint-image`, який викликає `npx @nitra/minify-image`
7
- * з обовʼязковими `--src=.` і `--write`. Прапорець `--avif` у `lint-image` заборонений —
8
- * AVIF-генерацію виконує окреме правило `image-avif` (інакше `bun run lint` плодив би `.avif`
9
- * для зображень, що ніде не вживаються);
10
- * - якщо в `package.json` є агрегований скрипт `lint`, він викликає `bun run lint-image`
11
- * (симетрично до `lint-text`, `lint-js`, `lint-ga`);
12
- * - `@nitra/minify-image` не оголошений у `dependencies`/`devDependencies` —
13
- * CLI запускається лише через `npx` (як `markdownlint-cli2` у `text.mdc`);
14
- * - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
15
- * в `.gitignore` — він має бути в git. Локальний mtime-кеш у
16
- * `node_modules/.cache/@nitra/minify-image/mtime.tsv` авто-gitignored через `node_modules/`,
17
- * окремої перевірки не вимагає;
18
- * - застарілий `.minify-image-cache.tsv` (з версій < 3.2) видалений з кореня — інакше
19
- * проєкт лишається у напівпереміщеному стані.
5
+ * **Що тут лишилося** (FS / cross-file):
6
+ * - наявність `package.json` у корені;
7
+ * - `.n-minify-image.tsv` (committed source of truth з sha1/originalSize/size) НЕ
8
+ * в `.gitignore` він має бути в git;
9
+ * - застарілий `.minify-image-cache.tsv` версій < 3.2) видалений з кореня та
10
+ * з `.gitignore`.
11
+ *
12
+ * **Що покрила Rego** (`bun run lint-conftest`,
13
+ * `npm/policy/image_compress/package_json/`):
14
+ * - `scripts.lint-image` викликає `npx @nitra/minify-image --src=. --write`
15
+ * без `--avif` (AVIF окреме правило `image-avif`);
16
+ * - агрегований `lint` (якщо є) містить `bun run lint-image`;
17
+ * - `@nitra/minify-image` НЕ у `dependencies` / `devDependencies` (через `npx`).
20
18
  */
21
19
  import { existsSync } from 'node:fs'
22
20
  import { readFile } from 'node:fs/promises'
23
21
 
24
22
  import { createCheckReporter } from './utils/check-reporter.mjs'
25
23
 
26
- /** Імʼя CLI-пакета: рядок у `lint-image` і заборонений у залежностях. */
27
- const MINIFY_PACKAGE_NAME = '@nitra/minify-image'
28
-
29
24
  /** Імʼя committed-кешу (sha1 + originalSize + size) у `@nitra/minify-image` ≥ 3.2.0. */
30
25
  const HASH_CACHE_FILENAME = '.n-minify-image.tsv'
31
26
 
32
27
  /** Імʼя застарілого 4-колонкового кешу (`@nitra/minify-image` < 3.2). Має бути видалений після міграції. */
33
28
  const LEGACY_CACHE_FILENAME = '.minify-image-cache.tsv'
34
29
 
35
- /**
36
- * Перевіряє скрипт `lint-image` у `package.json`.
37
- *
38
- * Має містити виклик `npx @nitra/minify-image` з обовʼязковими прапорцями `--src=.`
39
- * і `--write` (авто-оптимізація на місці). Прапорець `--avif` у `lint-image`
40
- * заборонений — AVIF-генерацію виконує `check image-avif`, інакше `bun run lint` плодить
41
- * `.avif` для зображень, що ніде не вживаються.
42
- * @param {string|undefined} lintImage значення `scripts['lint-image']`
43
- * @param {(msg: string) => void} pass callback при успішній перевірці
44
- * @param {(msg: string) => void} fail callback при помилці
45
- * @returns {void}
46
- */
47
- function checkLintImageScript(lintImage, pass, fail) {
48
- const canonical = `npx ${MINIFY_PACKAGE_NAME} --src=. --write`
49
- if (typeof lintImage !== 'string' || !lintImage.trim()) {
50
- fail(`package.json: додай скрипт "lint-image" з \`${canonical}\` (image-compress.mdc)`)
51
- return
52
- }
53
- if (!lintImage.includes(`npx ${MINIFY_PACKAGE_NAME}`)) {
54
- fail(`package.json: lint-image має викликати \`npx ${MINIFY_PACKAGE_NAME}\` (image-compress.mdc)`)
55
- return
56
- }
57
- /** @type {{ flag: string, variants: string[], hint: string }[]} */
58
- const requiredFlags = [
59
- { flag: '--src=.', variants: ['--src=.', '--src .'], hint: '`--src=.`' },
60
- { flag: '--write', variants: ['--write'], hint: '`--write` (авто-оптимізація на місці)' }
61
- ]
62
- const missing = requiredFlags.filter(f => !f.variants.some(v => lintImage.includes(v)))
63
- if (missing.length > 0) {
64
- fail(
65
- `package.json: lint-image має містити ${missing.map(f => f.hint).join(', ')} — канонічний виклик: \`${canonical}\` (image-compress.mdc)`
66
- )
67
- return
68
- }
69
- if (lintImage.includes('--avif')) {
70
- fail(
71
- `package.json: прибери \`--avif\` з lint-image — AVIF-генерацію виконує \`npx @nitra/cursor check image-avif\` (image-compress.mdc). Канонічний виклик: \`${canonical}\``
72
- )
73
- return
74
- }
75
- pass(`package.json: lint-image викликає \`${canonical}\``)
76
- }
77
-
78
- /**
79
- * Перевіряє, що агрегований `lint` (якщо є) кличе `bun run lint-image` —
80
- * симетрично до `lint-text`, `lint-js`, `lint-ga`.
81
- * @param {string|undefined} lintAggregate значення `scripts.lint`
82
- * @param {(msg: string) => void} pass callback при успішній перевірці
83
- * @param {(msg: string) => void} fail callback при помилці
84
- * @returns {void}
85
- */
86
- function checkLintAggregateIncludesImage(lintAggregate, pass, fail) {
87
- if (typeof lintAggregate !== 'string' || !lintAggregate.trim()) {
88
- return
89
- }
90
- if (lintAggregate.includes('bun run lint-image')) {
91
- pass('package.json: агрегований `lint` викликає `bun run lint-image`')
92
- } else {
93
- fail(
94
- 'package.json: у `lint` додай `bun run lint-image` (image-compress.mdc, симетрично до lint-text / lint-js / lint-ga)'
95
- )
96
- }
97
- }
98
-
99
- /**
100
- * Забороняє `@nitra/minify-image` у `dependencies` чи `devDependencies` —
101
- * CLI завжди запускається через `npx` (як `markdownlint-cli2` у `text.mdc`).
102
- * @param {{ dependencies?: Record<string, unknown>, devDependencies?: Record<string, unknown> }} pkg розібраний package.json
103
- * @param {(msg: string) => void} pass callback при успішній перевірці
104
- * @param {(msg: string) => void} fail callback при помилці
105
- * @returns {void}
106
- */
107
- function checkMinifyImageNotInDeps(pkg, pass, fail) {
108
- const inDeps = Boolean(pkg.dependencies && MINIFY_PACKAGE_NAME in pkg.dependencies)
109
- const inDevDeps = Boolean(pkg.devDependencies && MINIFY_PACKAGE_NAME in pkg.devDependencies)
110
- if (inDeps || inDevDeps) {
111
- fail(
112
- `package.json: ${MINIFY_PACKAGE_NAME} не додавай у dependencies/devDependencies — лише через \`npx\` (image-compress.mdc)`
113
- )
114
- } else {
115
- pass(`package.json: ${MINIFY_PACKAGE_NAME} не оголошено в dependencies/devDependencies`)
116
- }
117
- }
118
-
119
30
  /**
120
31
  * Зчитує всі змістовні рядки `.gitignore` (без коментарів і порожніх). Якщо файла нема — `null`.
121
32
  * @returns {Promise<string[] | null>} список trim-нутих рядків або `null`
@@ -176,41 +87,23 @@ async function checkLegacyCacheRemoved(pass, fail) {
176
87
  }
177
88
 
178
89
  /**
179
- * Перевіряє кореневий `package.json`: скрипти, заборонені залежності, агрегований `lint`.
180
- * @param {(msg: string) => void} pass callback при успішній перевірці
181
- * @param {(msg: string) => void} fail callback при помилці
182
- * @returns {Promise<boolean>} `true`, якщо `package.json` знайдено й оброблено; `false` — нема
183
- */
184
- async function checkPackageJsonImage(pass, fail) {
185
- if (!existsSync('package.json')) {
186
- fail('package.json не знайдено в корені — додай (image-compress.mdc)')
187
- return false
188
- }
189
- const pkg = JSON.parse(await readFile('package.json', 'utf8'))
190
- const scripts = /** @type {Record<string, unknown>} */ (pkg.scripts || {})
191
- checkLintImageScript(typeof scripts['lint-image'] === 'string' ? scripts['lint-image'] : undefined, pass, fail)
192
- checkLintAggregateIncludesImage(typeof scripts.lint === 'string' ? scripts.lint : undefined, pass, fail)
193
- checkMinifyImageNotInDeps(pkg, pass, fail)
194
- return true
195
- }
196
-
197
- /**
198
- * Перевіряє відповідність проєкту правилу `image-compress.mdc`: канонічний `lint-image`
199
- * (через `npx @nitra/minify-image --src=. --write`, без `--avif`!), агрегований `lint`,
200
- * `@nitra/minify-image` не у залежностях, `.n-minify-image.tsv` НЕ в `.gitignore`,
201
- * застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image не вимагається —
202
- * лінт зображень виконується лише локально.
90
+ * Перевіряє відповідність проєкту правилу `image-compress.mdc`: `.n-minify-image.tsv` НЕ
91
+ * в `.gitignore`, застарілий `.minify-image-cache.tsv` видалений. CI-workflow для image
92
+ * не вимагається лінт зображень виконується лише локально.
203
93
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
204
94
  */
205
95
  export async function check() {
206
96
  const reporter = createCheckReporter()
207
97
  const { pass, fail } = reporter
208
98
 
209
- const pkgFound = await checkPackageJsonImage(pass, fail)
210
- if (pkgFound) {
211
- await checkHashCacheNotIgnored(pass, fail)
212
- await checkLegacyCacheRemoved(pass, fail)
99
+ if (!existsSync('package.json')) {
100
+ fail('package.json не знайдено в корені — додай (image-compress.mdc)')
101
+ return reporter.getExitCode()
213
102
  }
103
+ pass('package.json є (структуру перевіряє bun run lint-conftest → image_compress.package_json)')
104
+
105
+ await checkHashCacheNotIgnored(pass, fail)
106
+ await checkLegacyCacheRemoved(pass, fail)
214
107
 
215
108
  return reporter.getExitCode()
216
109
  }
@@ -39,18 +39,6 @@ import { findAllPackageJsonPaths } from './utils/find-package-json-paths.mjs'
39
39
  import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
40
40
  import { walkDir } from './utils/walkDir.mjs'
41
41
 
42
- /** Імена забороненої залежності у будь-якому `package.json`. */
43
- const FORBIDDEN_DEPENDENCIES = Object.freeze(['pg', 'pg-format', 'mysql2'])
44
-
45
- /**
46
- * @param {unknown} v parsed JSON
47
- * @returns {Record<string, unknown>} object або {}
48
- */
49
- function asObject(v) {
50
- if (!v || typeof v !== 'object' || Array.isArray(v)) return {}
51
- return /** @type {Record<string, unknown>} */ (v)
52
- }
53
-
54
42
  /**
55
43
  * Збирає абсолютні шляхи JS/TS джерел у репозиторії для скану Bun SQL патернів.
56
44
  * @param {string} repoRoot абсолютний шлях до кореня репозиторію
@@ -74,43 +62,6 @@ async function findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths) {
74
62
  return paths
75
63
  }
76
64
 
77
- /**
78
- * Перевіряє, чи в кореневому `package.json` присутні заборонені пакети у `dependencies`.
79
- * @param {string[]} pkgJsonPaths абсолютні шляхи всіх `package.json` у репо
80
- * @param {string} repoRoot абсолютний шлях до кореня
81
- * @param {{ pass: (m: string) => void, fail: (m: string) => void }} reporter колбеки pass і fail з перевірки
82
- * @returns {Promise<number>} кількість знайдених порушень
83
- */
84
- async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
85
- const { pass, fail } = reporter
86
- let bad = 0
87
- for (const absPath of pkgJsonPaths) {
88
- const rel = relative(repoRoot, absPath).split('\\').join('/')
89
- let parsed
90
- try {
91
- parsed = JSON.parse(await readFile(absPath, 'utf8'))
92
- } catch {
93
- fail(`js-bun-db: ${rel} — невалідний JSON`)
94
- bad++
95
- continue
96
- }
97
- const deps = asObject(parsed.dependencies)
98
- for (const name of FORBIDDEN_DEPENDENCIES) {
99
- if (Object.hasOwn(deps, name)) {
100
- bad++
101
- fail(
102
- `js-bun-db: ${rel}: dependencies.${name} — замінити на Bun native SQL ` +
103
- `(import { sql, SQL } from 'bun', https://bun.com/docs/runtime/sql) (js-bun-db.mdc)`
104
- )
105
- }
106
- }
107
- }
108
- if (bad === 0) {
109
- pass(`js-bun-db: жоден package.json не містить ${FORBIDDEN_DEPENDENCIES.join(' / ')} у dependencies`)
110
- }
111
- return bad
112
- }
113
-
114
65
  /**
115
66
  * Сканує JS/TS-джерела на небезпечні патерни Bun SQL.
116
67
  * @param {string[]} sourcePaths абсолютні шляхи джерел
@@ -233,7 +184,9 @@ export async function check() {
233
184
  return reporter.getExitCode()
234
185
  }
235
186
 
236
- await checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter)
187
+ // Перевірку `dependencies` (заборона `pg` / `pg-format` / `mysql2`) перенесено
188
+ // в Rego-полісі `npm/policy/js_bun_db/package_json/`; `bun run lint-conftest`
189
+ // запускає її по всіх workspace-`package.json`. Тут лишився лише AST-скан коду.
237
190
 
238
191
  const sourcePaths = await findAllSourcePathsForBunSqlScan(repoRoot, ignorePaths)
239
192
  if (sourcePaths.length === 0) {
@@ -12,6 +12,11 @@
12
12
  * дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
13
13
  * `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
14
14
  * (див. `utils/conn-imports-scan.mjs`);
15
+ * - «Нейминг та експорти у `#conn/`»: всередині conn-каталогу basename файла має відповідати
16
+ * канону `ql-<id>` / `(pg|mysql)-(read|write)[-<id>]`; `export default` заборонений; має бути
17
+ * іменований експорт з імʼям, що дорівнює camelCase від basename файла (`pg-write-contract.js`
18
+ * → `export const pgWriteContract`); `index.*` як reexport-барель пропускаємо
19
+ * (див. `utils/conn-file-rules.mjs`);
15
20
  * - «process.env / CheckEnv»: пряме `process.env.X` має бути замінено на `env` —
16
21
  * з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
17
22
  * `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
@@ -40,6 +45,7 @@ import {
40
45
  import { findUncheckedProcessEnvInText, isCheckEnvScanSourceFile } from './utils/check-env-scan.mjs'
41
46
  import { createCheckReporter } from './utils/check-reporter.mjs'
42
47
  import { findDepcheckViolationsForPackage, readAllWorkflowFiles } from './utils/depcheck-workflow.mjs'
48
+ import { findConnFileRuleViolations, isConnFileRulesSourceFile } from './utils/conn-file-rules.mjs'
43
49
  import {
44
50
  findConnFactoryImportsInText,
45
51
  isConnImportsScanSourceFile,
@@ -51,52 +57,10 @@ import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from '
51
57
  import { walkDir } from './utils/walkDir.mjs'
52
58
  import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
53
59
 
54
- /** Канонічний `jsconfig.json` для backend workspace-пакетів із каталогом `src/` (js-run.mdc). */
55
- const CANONICAL_BACKEND_JSCONFIG = Object.freeze({
56
- compilerOptions: Object.freeze({
57
- lib: Object.freeze(['esnext']),
58
- module: 'NodeNext',
59
- moduleResolution: 'NodeNext',
60
- target: 'esnext',
61
- checkJs: false
62
- }),
63
- include: Object.freeze(['src/**/*'])
64
- })
65
-
66
- /**
67
- * Глибока рівність для JSON-подібних значень (масиви — порядок важливий).
68
- * @param {unknown} a
69
- * @param {unknown} b
70
- * @returns {boolean}
71
- */
72
- function deepEqualJson(a, b) {
73
- if (a === b) return true
74
- if (a === null || b === null || typeof a !== typeof b) return false
75
- if (typeof a !== 'object') return false
76
- if (Array.isArray(a) !== Array.isArray(b)) return false
77
- if (Array.isArray(a)) {
78
- if (a.length !== b.length) return false
79
- for (const [i, v] of a.entries()) {
80
- if (!deepEqualJson(v, b[i])) return false
81
- }
82
- return true
83
- }
84
- const ao = /** @type {Record<string, unknown>} */ (a)
85
- const bo = /** @type {Record<string, unknown>} */ (b)
86
- const keysA = Object.keys(ao).sort()
87
- const keysB = Object.keys(bo).sort()
88
- if (keysA.length !== keysB.length) return false
89
- for (const [i, k] of keysA.entries()) {
90
- if (k !== keysB[i]) return false
91
- if (!deepEqualJson(ao[k], bo[k])) return false
92
- }
93
- return true
94
- }
95
-
96
60
  /**
97
61
  * Чи існує непорожній за змістом маркер каталогу `src/` (рекомендована структура js-run).
98
62
  * @param {string} absPackageRoot абсолютний корінь пакета
99
- * @returns {boolean}
63
+ * @returns {boolean} true, якщо `src/` існує і є каталогом
100
64
  */
101
65
  function backendPackageHasSrcDir(absPackageRoot) {
102
66
  const srcPath = join(absPackageRoot, 'src')
@@ -108,40 +72,29 @@ function backendPackageHasSrcDir(absPackageRoot) {
108
72
  }
109
73
 
110
74
  /**
111
- * Перевіряє `jsconfig.json` для backend-пакетів із `src/`.
75
+ * FS-existence для `jsconfig.json` у backend-пакеті з каталогом `src/` (cross-file:
76
+ * наявність каталогу + файла). Структуру самого `jsconfig.json` (canonical
77
+ * compilerOptions і include) валідує `npm/policy/js_run/jsconfig/`; її прогоняє
78
+ * `bun run lint-conftest`.
112
79
  * @param {string} rootDir відносний шлях workspace
113
80
  * @param {string} absPackageRoot абсолютний корінь пакета
114
81
  * @param {string} label префікс `[pkg] `
115
- * @param {(msg: string) => void} fail
116
- * @param {(msg: string) => void} passFn
117
- * @returns {Promise<void>}
82
+ * @param {(msg: string) => void} fail callback для повідомлень про порушення
83
+ * @param {(msg: string) => void} passFn callback для повідомлень про успішну перевірку
84
+ * @returns {void}
118
85
  */
119
- async function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
86
+ function checkBackendJsconfigWhenSrcPresent(rootDir, absPackageRoot, label, fail, passFn) {
120
87
  if (!backendPackageHasSrcDir(absPackageRoot)) return
121
88
 
122
89
  const jcPath = join(rootDir, 'jsconfig.json')
123
- if (!existsSync(jcPath)) {
90
+ if (existsSync(jcPath)) {
91
+ passFn(`${label}jsconfig.json є (структуру перевіряє bun run lint-conftest → js_run.jsconfig)`)
92
+ } else {
124
93
  fail(
125
94
  `${label}є каталог src/, але немає jsconfig.json — додай канонічний файл з js-run.mdc ` +
126
95
  `(NodeNext, include: src/**/*).`
127
96
  )
128
- return
129
- }
130
- let parsed
131
- try {
132
- parsed = JSON.parse(await readFile(jcPath, 'utf8'))
133
- } catch {
134
- fail(`${label}jsconfig.json не вдалося розпарсити як JSON`)
135
- return
136
- }
137
- if (!deepEqualJson(parsed, CANONICAL_BACKEND_JSCONFIG)) {
138
- fail(
139
- `${label}jsconfig.json не збігається з каноном js-run.mdc — заміни на шаблон з правила ` +
140
- `(compilerOptions: lib esnext, module/moduleResolution NodeNext, target esnext, checkJs false; include: src/**/*).`
141
- )
142
- return
143
97
  }
144
- passFn(`${label}jsconfig.json узгоджено з js-run (пакет з src/)`)
145
98
  }
146
99
 
147
100
  /**
@@ -236,6 +189,52 @@ async function checkConnImports(absPackageRoot, sourcePaths, pkgJson, label, fai
236
189
  return violations
237
190
  }
238
191
 
192
+ /**
193
+ * Перевіряє правила нейминга та експортів для файлів усередині `#conn/`.
194
+ *
195
+ * Канон імені: `ql-<id>` для GraphQL, `(pg|mysql)-(read|write)[-<id>]` для БД (js-run.mdc,
196
+ * розділ «Нейминг файлів у `src/conn/`»). Експорт у файлі — лише іменований, з імʼям, що
197
+ * дорівнює camelCase від basename файла (`pg-write-contract.js` → `export const pgWriteContract`).
198
+ * @param {string} absPackageRoot абсолютний корінь пакета
199
+ * @param {string[]} sourcePaths абсолютні шляхи до файлів пакета
200
+ * @param {unknown} pkgJson розпарсений package.json пакета (або null)
201
+ * @param {string} label префікс повідомлення `[<pkg>] `
202
+ * @param {(msg: string) => void} fail callback при помилці
203
+ * @returns {Promise<number>} кількість порушень
204
+ */
205
+ async function checkConnFileNamingAndExports(absPackageRoot, sourcePaths, pkgJson, label, fail) {
206
+ const connDir = resolveConnDirFromPackageJson(pkgJson)
207
+ let violations = 0
208
+ for (const absPath of sourcePaths) {
209
+ const rel = relPosix(absPackageRoot, absPath)
210
+ if (!isInsideConnDir(rel, connDir)) continue
211
+ if (!isConnFileRulesSourceFile(rel)) continue
212
+ // пропускаємо реекспортний барель `index.*` (якщо знадобиться) і прихований .d.ts
213
+ const base = rel.slice(rel.lastIndexOf('/') + 1)
214
+ if (base.startsWith('index.')) continue
215
+
216
+ const content = await readFile(absPath, 'utf8')
217
+ for (const v of findConnFileRuleViolations(content, rel)) {
218
+ violations++
219
+ if (v.kind === 'name') {
220
+ fail(
221
+ `${label}${rel} — назва файла в '${connDir}/' не відповідає канону js-run: ` +
222
+ `'ql-<id>', 'pg-{read|write}[-<id>]' або 'mysql-{read|write}[-<id>]' (kebab-case, [a-z0-9-])`
223
+ )
224
+ } else if (v.kind === 'default-export') {
225
+ fail(`${label}${rel} — 'export default' заборонений у '${connDir}/'; зроби іменований експорт`)
226
+ } else {
227
+ const found = v.foundNames?.length ? v.foundNames.join(', ') : '—'
228
+ fail(
229
+ `${label}${rel} — очікується іменований експорт 'export const ${v.expectedName} = …' ` +
230
+ `(camelCase від назви файла); знайдено: ${found}`
231
+ )
232
+ }
233
+ }
234
+ }
235
+ return violations
236
+ }
237
+
239
238
  /**
240
239
  * Перевіряє правило «CheckEnv» для пакета.
241
240
  * @param {string} absPackageRoot абсолютний корінь пакета
@@ -323,6 +322,14 @@ async function checkWorkspacePackage(rootDir, ignorePaths, workflows, fail, pass
323
322
  passFn(`${label}імпорти підключень (bun#SQL / mssql / @nitra/graphql-request#GraphQLClient) лише в '${connDir}/'`)
324
323
  }
325
324
 
325
+ const connFileViolations = await checkConnFileNamingAndExports(absPackageRoot, sourcePaths, pkgJson, label, fail)
326
+ if (connFileViolations === 0) {
327
+ const connDir = resolveConnDirFromPackageJson(pkgJson)
328
+ passFn(
329
+ `${label}файли в '${connDir}/' дотримують канону js-run: нейминг (ql-/pg-/mysql-…) і іменований експорт у camelCase від basename`
330
+ )
331
+ }
332
+
326
333
  const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
327
334
  if (envViolations === 0) {
328
335
  passFn(
@@ -390,15 +397,11 @@ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
390
397
  const pkgPath = join(rootDir, 'package.json')
391
398
  if (!existsSync(pkgPath)) return null
392
399
  const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
393
- const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
394
- const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
395
- const allDeps = { ...deps, ...devDeps }
396
- if (allDeps['@nitra/bunyan']) {
397
- fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
398
- }
399
- if (allDeps.bunyan) {
400
- fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
401
- }
400
+ // Заборону `@nitra/bunyan` / `bunyan` у dependencies/devDependencies перенесено
401
+ // в Rego (`npm/policy/js_run/package_json/`); `bun run lint-conftest` запускає
402
+ // її по всіх workspace `package.json`. Тут лишилася лише AST-перевірка імпортів.
403
+ void label
404
+ void fail
402
405
  return pkgJson
403
406
  }
404
407
 
@@ -411,20 +414,15 @@ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
411
414
  * @param {(msg: string) => void} passFn успішне повідомлення
412
415
  * @returns {Promise<void>} завершується після перевірки configmap
413
416
  */
414
- async function checkOtelConfigmap(rootDir, label, fail, passFn) {
417
+ function checkOtelConfigmap(rootDir, label, fail, passFn) {
415
418
  const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
416
419
  if (!existsSync(configmapPath)) return
417
- const content = await readFile(configmapPath, 'utf8')
418
- if (!content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
419
- fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
420
- return
421
- }
422
- passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
423
- if (content.includes('service.name=') && content.includes('service.namespace=')) {
424
- passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
425
- } else {
426
- fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
427
- }
420
+ // Перевірку `OTEL_RESOURCE_ATTRIBUTES` має містити `service.name=` /
421
+ // `service.namespace=` перенесено в Rego (`npm/policy/js_run/configmap/`);
422
+ // `bun run lint-conftest` запускає її на всіх `k8s/base/configmap.yaml`.
423
+ void label
424
+ void fail
425
+ passFn(`${rootDir}/k8s/base/configmap.yaml є (OTEL — bun run lint-conftest → js_run.configmap)`)
428
426
  }
429
427
 
430
428
  /**