@nitra/cursor 1.8.155 → 1.8.157

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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Перевіряє правило hasura.mdc для проєктів **nitra** і **abie**: значення
3
+ * `HASURA_GRAPHQL_ENDPOINT` у `*.env` має бути **внутрішнім** кластерним URL,
4
+ * а не публічним доменом.
5
+ *
6
+ * Запускається лише якщо в кореневому `package.json` поле `repository`
7
+ * вказує на `https://github.com/nitra/...` або `https://github.com/abinbevefes/...`
8
+ * (інші репозиторії пропускаються без помилок — як у check-abie).
9
+ *
10
+ * Очікуваний формат URL:
11
+ * `http://<service>.<namespace>.svc.<cluster>.internal:<port>`
12
+ *
13
+ * приклад: `http://contract-h.ua-contract.svc.abie-ua.internal:8080`
14
+ *
15
+ * Сегменти беруться з `hasura/k8s/base/svc-hl.yaml` (`metadata.name` —
16
+ * має закінчуватись на `-h`, headless-сервіс) і `hasura/k8s/base/namespace.yaml`
17
+ * (`metadata.name` — namespace). Якщо ці YAML є в репозиторії, у URL додатково
18
+ * звіряються конкретні `<service>` і `<namespace>` з ними.
19
+ *
20
+ * Скануються всі файли `*.env` (наприклад `dev.env`, `production.env`); файл
21
+ * `.env` без префікса також враховується. Пропускаються `node_modules`,
22
+ * `.git`, `dist`, `coverage`, `.turbo`, `.next` (як у `walkDir`).
23
+ */
24
+ import { existsSync } from 'node:fs'
25
+ import { readFile } from 'node:fs/promises'
26
+ import { basename, join, relative } from 'node:path'
27
+
28
+ import { parseAllDocuments } from 'yaml'
29
+
30
+ import { getRepositoryUrl } from './auto-rules.mjs'
31
+ import { createCheckReporter } from './utils/check-reporter.mjs'
32
+ import { walkDir } from './utils/walkDir.mjs'
33
+
34
+ const NITRA_REPOSITORY_URL_MARKER = 'https://github.com/nitra/'
35
+ const ABIE_REPOSITORY_URL_MARKER = 'https://github.com/abinbevefes/'
36
+
37
+ const HASURA_BASE_DIR = 'hasura/k8s/base'
38
+ const HASURA_SVC_HL_FILE = `${HASURA_BASE_DIR}/svc-hl.yaml`
39
+ const HASURA_NAMESPACE_FILE = `${HASURA_BASE_DIR}/namespace.yaml`
40
+
41
+ const ENV_FILE_RE = /\.env$/u
42
+ const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
43
+
44
+ /**
45
+ * Розбір значення `HASURA_GRAPHQL_ENDPOINT` як внутрішнього кластерного URL.
46
+ * Дозволяє лише `http://` (TLS усередині кластера зайвий), вимагає сегментів
47
+ * `<service>.<namespace>.svc.<cluster>.internal` та явного порту.
48
+ * @param {string} url значення з `.env` (без огорнутих лапок)
49
+ * @returns {{ ok: true, service: string, namespace: string, cluster: string, port: string } | { ok: false }}
50
+ * деталі URL або фейл, якщо формат не відповідає внутрішньому кластерному URL
51
+ */
52
+ export function parseInternalHasuraEndpoint(url) {
53
+ const trimmed = url.trim()
54
+ const m = trimmed.match(/^http:\/\/([^./]+)\.([^./]+)\.svc\.([^./]+)\.internal:(\d+)\/?$/u)
55
+ if (!m) {
56
+ return { ok: false }
57
+ }
58
+ return { ok: true, service: m[1], namespace: m[2], cluster: m[3], port: m[4] }
59
+ }
60
+
61
+ /**
62
+ * Зчитує `metadata.name` з першого документа YAML, який має заданий `kind`.
63
+ * @param {string} absPath абсолютний шлях до YAML
64
+ * @param {string} kind очікуваний `kind` (наприклад `Service`, `Namespace`)
65
+ * @returns {Promise<string | null>} ім'я ресурсу або null, якщо файл/документ відсутній
66
+ */
67
+ async function readYamlMetadataName(absPath, kind) {
68
+ if (!existsSync(absPath)) {
69
+ return null
70
+ }
71
+ let docs
72
+ try {
73
+ docs = parseAllDocuments(await readFile(absPath, 'utf8'))
74
+ } catch {
75
+ return null
76
+ }
77
+ for (const doc of docs) {
78
+ const obj = doc.toJS()
79
+ if (obj && typeof obj === 'object' && obj.kind === kind && obj.metadata?.name) {
80
+ return String(obj.metadata.name)
81
+ }
82
+ }
83
+ return null
84
+ }
85
+
86
+ /**
87
+ * Чи відносний шлях вказує на `*.env` (включно з `.env`).
88
+ * @param {string} relPath posix-шлях відносно кореня
89
+ * @returns {boolean} true для файлів виду `.env`, `dev.env`, `nitra.env`
90
+ */
91
+ export function isEnvFile(relPath) {
92
+ return ENV_FILE_RE.test(relPath)
93
+ }
94
+
95
+ /**
96
+ * Збирає всі `*.env` файли в дереві, окрім службових каталогів.
97
+ * @param {string} root абсолютний шлях кореня
98
+ * @returns {Promise<string[]>} відсортовані posix-шляхи відносно кореня
99
+ */
100
+ async function collectEnvFiles(root) {
101
+ /** @type {string[]} */
102
+ const out = []
103
+ await walkDir(root, absPath => {
104
+ const rel = relative(root, absPath).split('\\').join('/')
105
+ if (isEnvFile(rel)) {
106
+ out.push(rel)
107
+ }
108
+ })
109
+ return out.toSorted((a, b) => a.localeCompare(b))
110
+ }
111
+
112
+ /**
113
+ * Перевіряє один `.env` файл на коректність `HASURA_GRAPHQL_ENDPOINT`.
114
+ * Якщо в файлі немає змінної — вважаємо OK.
115
+ * @param {string} relPath відносний шлях файла
116
+ * @param {{ service: string | null, namespace: string | null }} expected очікувані сегменти з YAML
117
+ * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер
118
+ * @returns {Promise<void>}
119
+ */
120
+ async function checkEnvFile(relPath, expected, reporter) {
121
+ const { pass, fail } = reporter
122
+ const content = await readFile(relPath, 'utf8')
123
+ const m = content.match(HASURA_ENDPOINT_LINE_RE)
124
+ if (!m) {
125
+ return
126
+ }
127
+ const value = m[1].trim()
128
+ const parsed = parseInternalHasuraEndpoint(value)
129
+ if (!parsed.ok) {
130
+ fail(
131
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ` +
132
+ `https://<service>.<namespace>.svc.<cluster>.internal:<port> (hasura.mdc)`
133
+ )
134
+ return
135
+ }
136
+ if (expected.service && parsed.service !== expected.service) {
137
+ fail(
138
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT — сервіс "${parsed.service}" не збігається з ` +
139
+ `metadata.name "${expected.service}" із ${HASURA_SVC_HL_FILE} (hasura.mdc)`
140
+ )
141
+ return
142
+ }
143
+ if (expected.namespace && parsed.namespace !== expected.namespace) {
144
+ fail(
145
+ `${relPath}: HASURA_GRAPHQL_ENDPOINT — namespace "${parsed.namespace}" не збігається з ` +
146
+ `metadata.name "${expected.namespace}" із ${HASURA_NAMESPACE_FILE} (hasura.mdc)`
147
+ )
148
+ return
149
+ }
150
+ pass(`${relPath}: HASURA_GRAPHQL_ENDPOINT — внутрішній кластерний URL`)
151
+ }
152
+
153
+ /**
154
+ * Зчитує URL репозиторію з кореневого `package.json` (або null, якщо файла немає / не валідний).
155
+ * @returns {Promise<string | null>} URL з поля `repository`
156
+ */
157
+ async function readRootRepositoryUrl() {
158
+ if (!existsSync('package.json')) {
159
+ return null
160
+ }
161
+ try {
162
+ const pkg = JSON.parse(await readFile('package.json', 'utf8'))
163
+ return getRepositoryUrl(pkg?.repository)
164
+ } catch {
165
+ return null
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Чи URL репозиторію вказує на nitra або abie (за маркерами hasura.mdc).
171
+ * @param {string | null} url значення з `package.json` `repository`
172
+ * @returns {boolean} true для nitra/abie проєктів
173
+ */
174
+ export function isNitraOrAbieRepository(url) {
175
+ if (typeof url !== 'string') {
176
+ return false
177
+ }
178
+ const lc = url.toLowerCase()
179
+ return lc.includes(NITRA_REPOSITORY_URL_MARKER) || lc.includes(ABIE_REPOSITORY_URL_MARKER)
180
+ }
181
+
182
+ /**
183
+ * Перевіряє hasura.mdc для поточного робочого каталогу.
184
+ * @returns {Promise<number>} 0 — OK / правило не застосовується, 1 — порушення
185
+ */
186
+ export async function check() {
187
+ const reporter = createCheckReporter()
188
+ const { pass } = reporter
189
+
190
+ const repositoryUrl = await readRootRepositoryUrl()
191
+ if (!isNitraOrAbieRepository(repositoryUrl)) {
192
+ pass('Пропущено: репозиторій не nitra і не abie (hasura.mdc застосовується лише до них)')
193
+ return reporter.getExitCode()
194
+ }
195
+
196
+ const root = process.cwd()
197
+ const expected = {
198
+ service: await readYamlMetadataName(join(root, HASURA_SVC_HL_FILE), 'Service'),
199
+ namespace: await readYamlMetadataName(join(root, HASURA_NAMESPACE_FILE), 'Namespace')
200
+ }
201
+
202
+ const envFiles = await collectEnvFiles(root)
203
+ if (envFiles.length === 0) {
204
+ pass('Не знайдено жодного *.env файла — нічого перевіряти')
205
+ return reporter.getExitCode()
206
+ }
207
+
208
+ for (const rel of envFiles) {
209
+ await checkEnvFile(rel, expected, reporter)
210
+ }
211
+
212
+ // Якщо у файлах не було жодної згадки HASURA_GRAPHQL_ENDPOINT — повідом про це.
213
+ const exit = reporter.getExitCode()
214
+ if (exit === 0) {
215
+ const names = envFiles.map(p => basename(p)).join(', ')
216
+ pass(`Перевірено ${envFiles.length} *.env файл(ів): ${names}`)
217
+ }
218
+ return exit
219
+ }
@@ -124,11 +124,8 @@ async function checkForbiddenDependencies(pkgJsonPaths, repoRoot, reporter) {
124
124
  */
125
125
  async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
126
126
  const { fail } = reporter
127
+ const counts = { perRequest: 0, unsafeCall: 0, dynamicList: 0, inListGuard: 0 }
127
128
  let hasBunSqlImport = false
128
- let perRequest = 0
129
- let unsafeCall = 0
130
- let dynamicList = 0
131
- let inListGuard = 0
132
129
 
133
130
  for (const absPath of sourcePaths) {
134
131
  const rel = relative(repoRoot, absPath).split('\\').join('/')
@@ -136,50 +133,72 @@ async function scanSourcesForBunSqlPatterns(sourcePaths, repoRoot, reporter) {
136
133
  if (!hasBunSqlImport && textHasBunSqlImport(content)) {
137
134
  hasBunSqlImport = true
138
135
  }
136
+ scanFileForBunSqlPatterns(content, rel, fail, counts)
137
+ }
139
138
 
140
- for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
141
- perRequest++
142
- fail(
143
- `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
144
- `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
145
- )
146
- }
147
- for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
148
- unsafeCall++
149
- fail(
150
- `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
151
- `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
152
- )
153
- }
154
- for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
155
- dynamicList++
156
- fail(
157
- `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
158
- `у IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
159
- )
160
- }
161
- for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
162
- inListGuard++
163
- if (v.reason === 'missing_guard') {
164
- fail(
165
- `js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
166
- throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
167
- )
168
- } else if (v.reason === 'sql_helper_not_var') {
169
- fail(
170
- `js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
171
- `після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
172
- )
173
- } else {
174
- fail(
175
- `js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
176
- `і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
177
- )
178
- }
179
- }
139
+ return { hasBunSqlImport, ...counts }
140
+ }
141
+
142
+ /**
143
+ * Сканує один файл усіма AST-сканерами bun-sql і реєструє знайдені порушення.
144
+ * @param {string} content вміст файлу
145
+ * @param {string} rel posix-шлях відносно `repoRoot`
146
+ * @param {(msg: string) => void} fail callback при помилці
147
+ * @param {{ perRequest: number, unsafeCall: number, dynamicList: number, inListGuard: number }} counts акумулятори
148
+ * @returns {void}
149
+ */
150
+ function scanFileForBunSqlPatterns(content, rel, fail, counts) {
151
+ for (const v of findBunSqlPerRequestConnectionInText(content, rel)) {
152
+ counts.perRequest++
153
+ fail(
154
+ `js-bun-db: ${rel}:${v.line} — не створюй new SQL(...) всередині функцій; ` +
155
+ `тримай singleton на рівні модуля (js-bun-db.mdc): ${v.snippet}`
156
+ )
157
+ }
158
+ for (const v of findUnsafeBunSqlUnsafeCallInText(content, rel)) {
159
+ counts.unsafeCall++
160
+ fail(
161
+ `js-bun-db: ${rel}:${v.line} — sql.unsafe(\`...\${...}...\`) недопустимо: ` +
162
+ `використовуй tagged template sql\`...\${value}...\` або sql.unsafe('static', [params]) (js-bun-db.mdc): ${v.snippet}`
163
+ )
164
+ }
165
+ for (const v of findUnsafeBunSqlDynamicSqlListInText(content, rel)) {
166
+ counts.dynamicList++
167
+ fail(
168
+ `js-bun-db: ${rel}:${v.line} — заборонено підставляти у SQL динамічні списки через .join(',') ` +
169
+ IN (...) / VALUES (...); використовуй sql([...]) (js-bun-db.mdc): ${v.snippet}`
170
+ )
171
+ }
172
+ for (const v of findUnsafeBunSqlInListMissingEmptyGuardInText(content, rel)) {
173
+ counts.inListGuard++
174
+ fail(messageForBunSqlInListGuard(rel, v))
180
175
  }
176
+ }
181
177
 
182
- return { hasBunSqlImport, perRequest, unsafeCall, dynamicList, inListGuard }
178
+ /**
179
+ * Будує повідомлення `fail` для порушення `findUnsafeBunSqlInListMissingEmptyGuardInText`
180
+ * залежно від `reason` (різні діагностики однакового сімейства).
181
+ * @param {string} rel posix-шлях відносно кореня репо
182
+ * @param {{ line: number, snippet: string, name?: string, reason: string }} v порушення
183
+ * @returns {string} готове повідомлення для `fail`
184
+ */
185
+ function messageForBunSqlInListGuard(rel, v) {
186
+ if (v.reason === 'missing_guard') {
187
+ return (
188
+ `js-bun-db: ${rel}:${v.line} — перед IN-списком ${JSON.stringify(v.name)} потрібна перевірка на пустоту ` +
189
+ `з throw (наприклад if (!${v.name}.length) throw ...), інакше можливі некоректні запити (js-bun-db.mdc): ${v.snippet}`
190
+ )
191
+ }
192
+ if (v.reason === 'sql_helper_not_var') {
193
+ return (
194
+ `js-bun-db: ${rel}:${v.line} — IN-список у \${sql(...)} має підставлятись зі змінної (Identifier) ` +
195
+ `після валідації на пустоту + throw (js-bun-db.mdc): ${v.snippet}`
196
+ )
197
+ }
198
+ return (
199
+ `js-bun-db: ${rel}:${v.line} — значення для IN (...) у template literal треба винести в окрему змінну ` +
200
+ `і перевірити на пустоту (throw), не підставляти вираз напряму (js-bun-db.mdc): ${v.snippet}`
201
+ )
183
202
  }
184
203
 
185
204
  /**
@@ -4,7 +4,8 @@
4
4
  * Канонічний `lint-js`, flat ESLint з getConfig і ignore для auto-imports, рекомендації VSCode,
5
5
  * `.oxlintrc.json` має збігатися з каноном oxlint у пакеті (`npm/scripts/utils/oxlint-canonical.json`):
6
6
  * plugins, jsPlugins, categories, усі правила з канону (додаткові записи в `rules` дозволені), settings, env,
7
- * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.6.12** (транзитивний
7
+ * globals, ignorePatterns. `@nitra/eslint-config` у devDependencies мінімум **3.8.0** (з цієї версії
8
+ * правило `no-restricted-syntax` для `ForInStatement` забороняє `for...in`; також тягне транзитивний
8
9
  * `@e18e/eslint-plugin` для oxlint), `.jscpd.json` (gitignore, exitCode, reporters, minLines), workflow
9
10
  * `lint-js.yml` (checkout@v6, setup-bun-deps, bunx без --fix), без prettier, `engines.node` >= 24,
10
11
  * `engines.bun` >= 1.3, `"type": "module"` у кореневому і всіх workspace `package.json`. Дубль перевірки JS у `lint.yml` — заборонено.
@@ -53,11 +54,12 @@ export function isCanonicalLintJs(script) {
53
54
  }
54
55
 
55
56
  /**
56
- * Чи діапазон `@nitra/eslint-config` у `package.json` передбачає версію з транзитивним `@e18e/eslint-plugin` (>=3.6.12).
57
+ * Чи діапазон `@nitra/eslint-config` у `package.json` задовольняє мінімум `>= 3.8.0`
58
+ * (заборона `for...in` через `no-restricted-syntax` + транзитивний `@e18e/eslint-plugin` для oxlint).
57
59
  * @param {unknown} versionSpec значення `devDependencies['@nitra/eslint-config']`
58
- * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.6.12
60
+ * @returns {boolean} true для `workspace:*` або першої semver у рядку >= 3.8.0
59
61
  */
60
- export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
62
+ export function nitraEslintConfigMeetsMinVersion(versionSpec) {
61
63
  const s = String(versionSpec).trim()
62
64
  if (s.startsWith('workspace:')) {
63
65
  return true
@@ -70,7 +72,7 @@ export function nitraEslintConfigDeclaresE18eTransitive(versionSpec) {
70
72
  if ([major, minor, patch].some(n => Number.isNaN(n))) {
71
73
  return false
72
74
  }
73
- return major > 3 || (major === 3 && minor > 5) || (major === 3 && minor === 5 && patch >= 0)
75
+ return major > 3 || (major === 3 && minor >= 8 && patch >= 0)
74
76
  }
75
77
 
76
78
  /**
@@ -234,13 +236,13 @@ function checkPackageJsonLintDeps(pkg, passFn, failFn) {
234
236
  const nitraEslint = pkg.devDependencies?.['@nitra/eslint-config']
235
237
  if (nitraEslint) {
236
238
  passFn('@nitra/eslint-config є в devDependencies')
237
- if (nitraEslintConfigDeclaresE18eTransitive(nitraEslint)) {
239
+ if (nitraEslintConfigMeetsMinVersion(nitraEslint)) {
238
240
  passFn(
239
- '@nitra/eslint-config: мінімум 3.6.12 (транзитивний @e18e/eslint-plugin для oxlint jsPlugins, js-lint.mdc)'
241
+ '@nitra/eslint-config: мінімум 3.8.0 (no-restricted-syntax для ForInStatement + @e18e/eslint-plugin транзитивно, js-lint.mdc)'
240
242
  )
241
243
  } else {
242
244
  failFn(
243
- '@nitra/eslint-config: онови до мінімум "^3.6.12" — з цієї версії постачається @e18e/eslint-plugin для .oxlintrc.json (js-lint.mdc)'
245
+ '@nitra/eslint-config: онови до мінімум "^3.8.0" — з цієї версії правило no-restricted-syntax забороняє for...in (плюс транзитивний @e18e/eslint-plugin для oxlint, js-lint.mdc)'
244
246
  )
245
247
  }
246
248
  } else {
@@ -12,10 +12,12 @@
12
12
  * дозволені лише у каталозі conn (за замовчуванням `src/conn/`; за наявності
13
13
  * `package.json#imports['#conn/*']` — у його цільовому каталозі); поза ним — порушення
14
14
  * (див. `utils/conn-imports-scan.mjs`);
15
- * - «CheckEnv»: кожне `process.env.X` (включно з `process.env['X']` і деструктуризацією
16
- * `const { X } = process.env`) має бути закрите літеральним викликом `checkEnv(['X', ...])`
17
- * у тому ж файлі або коментарем `// @nitra/cursor ignore-next-line checkEnv` на попередньому
18
- * рядку (див. `utils/check-env-scan.mjs`).
15
+ * - «process.env / CheckEnv»: пряме `process.env.X` має бути замінено на `env`
16
+ * з `@nitra/check-env` (для обов'язкових змінних, із `checkEnv([...])`) або з
17
+ * `node:process` (для опційних). Коли `env` імпортовано з `@nitra/check-env`,
18
+ * кожен `env.X` має бути закритий літеральним викликом `checkEnv(['X', ...])`
19
+ * у тому ж файлі або коментарем `// \@nitra/cursor ignore-next-line checkEnv`
20
+ * на попередньому рядку (див. `utils/check-env-scan.mjs`).
19
21
  */
20
22
  import { existsSync } from 'node:fs'
21
23
  import { readFile } from 'node:fs/promises'
@@ -134,9 +136,11 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
134
136
  const content = await readFile(absPath, 'utf8')
135
137
  for (const v of findUncheckedProcessEnvInText(content, rel)) {
136
138
  violations++
137
- fail(
138
- `${label}${rel}:${v.line} process.env.${v.name} без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
139
- )
139
+ const message =
140
+ v.kind === 'process-env'
141
+ ? `${label}${rel}:${v.line} — process.env.${v.name}: заміни на env з '@nitra/check-env' (обов'язкова змінна + checkEnv(['${v.name}'])) або з 'node:process' (опційна)`
142
+ : `${label}${rel}:${v.line} — env.${v.name} (з '@nitra/check-env') без checkEnv(['${v.name}']) (або '// @nitra/cursor ignore-next-line checkEnv' попереду)`
143
+ fail(message)
140
144
  }
141
145
  }
142
146
  return violations
@@ -152,22 +156,7 @@ async function checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail) {
152
156
  async function checkWorkspacePackage(rootDir, fail, passFn) {
153
157
  const label = `[${rootDir}] `
154
158
  const absPackageRoot = join(process.cwd(), rootDir)
155
- /** @type {unknown} */
156
- let pkgJson = null
157
- const pkgPath = join(rootDir, 'package.json')
158
- if (existsSync(pkgPath)) {
159
- pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
160
- const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
161
- const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
162
- const allDeps = { ...(deps || {}), ...(devDeps || {}) }
163
-
164
- if (allDeps['@nitra/bunyan']) {
165
- fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
166
- }
167
- if (allDeps.bunyan) {
168
- fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
169
- }
170
- }
159
+ const pkgJson = await loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail)
171
160
 
172
161
  const importViolations = await checkBunyanImports(absPackageRoot, label, fail)
173
162
  if (importViolations === 0) {
@@ -184,22 +173,59 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
184
173
 
185
174
  const envViolations = await checkProcessEnvUsage(absPackageRoot, sourcePaths, label, fail)
186
175
  if (envViolations === 0) {
187
- passFn(`${label}усі process.env.* закриті checkEnv(['…']) або '// @nitra/cursor ignore-next-line checkEnv'`)
176
+ passFn(
177
+ `${label}немає прямого process.env.*; усі env.* з '@nitra/check-env' закриті checkEnv(['…']) (або '// @nitra/cursor ignore-next-line checkEnv')`
178
+ )
188
179
  }
189
180
 
181
+ await checkOtelConfigmap(rootDir, label, fail, passFn)
182
+ }
183
+
184
+ /**
185
+ * Завантажує `package.json` пакета (якщо є) і реєструє порушення для bunyan-залежностей.
186
+ * @param {string} rootDir відносний шлях workspace
187
+ * @param {string} label префікс повідомлення `[<pkg>] `
188
+ * @param {(msg: string) => void} fail callback при помилці
189
+ * @returns {Promise<unknown>} розпарсений package.json або null
190
+ */
191
+ async function loadPackageJsonAndCheckBunyanDeps(rootDir, label, fail) {
192
+ const pkgPath = join(rootDir, 'package.json')
193
+ if (!existsSync(pkgPath)) return null
194
+ const pkgJson = JSON.parse(await readFile(pkgPath, 'utf8'))
195
+ const deps = /** @type {Record<string, unknown>} */ (pkgJson).dependencies
196
+ const devDeps = /** @type {Record<string, unknown>} */ (pkgJson).devDependencies
197
+ const allDeps = { ...deps, ...devDeps }
198
+ if (allDeps['@nitra/bunyan']) {
199
+ fail(`${label}@nitra/bunyan знайдено — замінити на @nitra/pino`)
200
+ }
201
+ if (allDeps.bunyan) {
202
+ fail(`${label}bunyan знайдено — замінити на @nitra/pino`)
203
+ }
204
+ return pkgJson
205
+ }
206
+
207
+ /**
208
+ * Перевіряє вміст `k8s/base/configmap.yaml` пакета на наявність OTEL_RESOURCE_ATTRIBUTES
209
+ * з обов'язковими `service.name=` та `service.namespace=` всередині.
210
+ * @param {string} rootDir відносний шлях workspace
211
+ * @param {string} label префікс повідомлення `[<pkg>] `
212
+ * @param {(msg: string) => void} fail callback при помилці
213
+ * @param {(msg: string) => void} passFn успішне повідомлення
214
+ * @returns {Promise<void>} завершується після перевірки configmap
215
+ */
216
+ async function checkOtelConfigmap(rootDir, label, fail, passFn) {
190
217
  const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
191
- if (existsSync(configmapPath)) {
192
- const content = await readFile(configmapPath, 'utf8')
193
- if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
194
- passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
195
- if (content.includes('service.name=') && content.includes('service.namespace=')) {
196
- passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
197
- } else {
198
- fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
199
- }
200
- } else {
201
- fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
202
- }
218
+ if (!existsSync(configmapPath)) return
219
+ const content = await readFile(configmapPath, 'utf8')
220
+ if (!content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
221
+ fail(`${label}k8s/base/configmap.yaml не містить OTEL_RESOURCE_ATTRIBUTES`)
222
+ return
223
+ }
224
+ passFn(`${label}k8s/base/configmap.yaml містить OTEL_RESOURCE_ATTRIBUTES`)
225
+ if (content.includes('service.name=') && content.includes('service.namespace=')) {
226
+ passFn(`${label}OTEL_RESOURCE_ATTRIBUTES містить service.name та service.namespace`)
227
+ } else {
228
+ fail(`${label}OTEL_RESOURCE_ATTRIBUTES має містити service.name=<name>,service.namespace=<namespace>`)
203
229
  }
204
230
  }
205
231