@nitra/cursor 1.8.112 → 1.8.113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/n-cursor.js CHANGED
@@ -752,6 +752,76 @@ async function removeOrphanManagedCommandFiles(commandsDir, configSkills) {
752
752
  return removed.toSorted((a, b) => a.localeCompare(b))
753
753
  }
754
754
 
755
+ /**
756
+ * Синхронізує .claude/commands/{dirName}.md для всіх локальних скілів з .cursor/skills/
757
+ * що не керуються пакетом (відсутні в configSkills).
758
+ * @param {string[]} configSkills id керованих skills (вже оброблені syncCommands)
759
+ * @returns {Promise<{ success: number, fail: number }>} лічильники успішних і невдалих записів
760
+ */
761
+ async function syncLocalOnlySkillCommands(configSkills) {
762
+ const skillsRoot = join(cwd(), SKILLS_DIR)
763
+ if (!existsSync(skillsRoot)) return { success: 0, fail: 0 }
764
+
765
+ const commandsDir = join(cwd(), COMMANDS_DIR)
766
+ await mkdir(commandsDir, { recursive: true })
767
+
768
+ const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
769
+ const allDirNames = await listProjectSkillDirNames()
770
+ const localOnly = allDirNames.filter(d => !managedDirNames.has(d))
771
+
772
+ let success = 0
773
+ let fail = 0
774
+
775
+ for (const dirName of localOnly) {
776
+ const skillMdPath = join(skillsRoot, dirName, 'SKILL.md')
777
+ const destFile = join(commandsDir, `${dirName}.md`)
778
+
779
+ process.stdout.write(` ⬇ ${dirName} → ${COMMANDS_DIR}/${dirName}.md ... `)
780
+ try {
781
+ let desc = ''
782
+ if (existsSync(skillMdPath)) {
783
+ const raw = await readFile(skillMdPath, 'utf8')
784
+ const parsed = extractSkillDescription(raw)
785
+ if (parsed) desc = skillDescriptionSafeForMarkdownInline(parsed)
786
+ }
787
+ const header = desc ? `# ${dirName} — ${desc}\n\n` : ''
788
+ const body = `${header}Виконай інструкції зі скілу \`${SKILLS_DIR}/${dirName}/SKILL.md\`.\n`
789
+ await writeFile(destFile, body, 'utf8')
790
+ console.log(`✅`)
791
+ success++
792
+ } catch (error) {
793
+ console.log(`❌`)
794
+ console.error(` Помилка: ${errorMessage(error)}`)
795
+ fail++
796
+ }
797
+ }
798
+ return { success, fail }
799
+ }
800
+
801
+ /**
802
+ * Видаляє .claude/commands/{dirName}.md файли локальних скілів, яких більше немає в .cursor/skills/
803
+ * @param {string} commandsDir абсолютний шлях до .claude/commands
804
+ * @param {string[]} configSkills id керованих skills
805
+ * @returns {Promise<string[]>} імена видалених файлів
806
+ */
807
+ async function removeOrphanLocalSkillCommandFiles(commandsDir, configSkills) {
808
+ if (!existsSync(commandsDir)) return []
809
+
810
+ const managedDirNames = new Set(configSkills.map(s => managedSkillDirName(s)))
811
+ const allDirNames = new Set(await listProjectSkillDirNames())
812
+ const names = await readdir(commandsDir)
813
+ const removed = []
814
+
815
+ for (const name of names.filter(n => n.endsWith('.md') && !n.startsWith(RULE_PREFIX))) {
816
+ const dirName = name.slice(0, -3)
817
+ if (!managedDirNames.has(dirName) && !allDirNames.has(dirName)) {
818
+ await unlink(join(commandsDir, name))
819
+ removed.push(name)
820
+ }
821
+ }
822
+ return removed.toSorted((a, b) => a.localeCompare(b))
823
+ }
824
+
755
825
  /**
756
826
  * Людинозрозумілий текст винятку для логів.
757
827
  * @param {unknown} error виняток із catch
@@ -1031,13 +1101,19 @@ async function runSync() {
1031
1101
 
1032
1102
  await runSyncStep('❌ Commands: ', async () => {
1033
1103
  const { success: cmdOk, fail: cmdFail } = await syncCommands(skills, bundledSkillsDir)
1034
- if (skills.length > 0) {
1035
- console.log(`\n⌨️ Commands: ${cmdOk} скопійовано, ${cmdFail} з помилками`)
1104
+ const { success: localOk, fail: localFail } = await syncLocalOnlySkillCommands(skills)
1105
+ const totalOk = cmdOk + localOk
1106
+ const totalFail = cmdFail + localFail
1107
+ if (totalOk + totalFail > 0) {
1108
+ console.log(`\n⌨️ Commands: ${totalOk} скопійовано, ${totalFail} з помилками`)
1036
1109
  }
1037
- const removedCmds = await removeOrphanManagedCommandFiles(join(cwd(), COMMANDS_DIR), skills)
1110
+ const commandsDir = join(cwd(), COMMANDS_DIR)
1111
+ const removedCmds = await removeOrphanManagedCommandFiles(commandsDir, skills)
1038
1112
  logRemovedManagedItems('commands', COMMANDS_DIR, removedCmds)
1039
- if (cmdFail > 0) {
1040
- throw new Error(`Не вдалося скопіювати ${cmdFail} з ${skills.length} commands`)
1113
+ const removedLocalCmds = await removeOrphanLocalSkillCommandFiles(commandsDir, skills)
1114
+ logRemovedManagedItems('commands (local)', COMMANDS_DIR, removedLocalCmds)
1115
+ if (totalFail > 0) {
1116
+ throw new Error(`Не вдалося скопіювати ${totalFail} commands`)
1041
1117
  }
1042
1118
  })
1043
1119
 
package/mdc/ga.mdc CHANGED
@@ -4,7 +4,7 @@ alwaysApply: true
4
4
  version: '1.3'
5
5
  ---
6
6
 
7
- У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
7
+ У `.github/workflows/` лише **`.yml`**. Мають бути **`clean-ga-workflows.yml`**, **`clean-merged-branch.yml`**, **`lint-ga.yml`**, **`git-ai.yml`**. Якщо є **`apply-k8s.yml`** / **`apply-nats-consumer.yml`** — paths у тригері як у фрагментах.
8
8
 
9
9
  Повинен бути файл .github/workflows/clean-ga-workflows.yml, зі змістом:
10
10
 
@@ -110,6 +110,37 @@ jobs:
110
110
  run: bun run lint-ga
111
111
  ```
112
112
 
113
+ Повинен бути файл .github/workflows/git-ai.yml, зі змістом:
114
+
115
+ ```yaml
116
+ name: Git AI
117
+
118
+ on:
119
+ pull_request:
120
+ types: [closed]
121
+
122
+ jobs:
123
+ git-ai:
124
+ if: github.event.pull_request.merged == true
125
+ runs-on: ubuntu-latest
126
+ permissions:
127
+ contents: write
128
+
129
+ steps:
130
+ - name: Install git-ai
131
+ run: |
132
+ curl -fsSL https://usegitai.com/install.sh | bash
133
+ echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
134
+ - name: Run git-ai
135
+ id: run-git-ai
136
+ env:
137
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138
+ run: |
139
+ git config --global user.name "github-actions[bot]"
140
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
141
+ git-ai ci github run
142
+ ```
143
+
113
144
  **Локальний composite** (`uses: ./.github/actions/setup-bun-deps` або `./npm/github-actions/setup-bun-deps`): **спочатку** обов’язковий крок **`actions/checkout@v6`** (`persist-credentials: false`), інакше runner не знайде `action.yml`. Сам composite: **`actions/setup-node@v6`** (**Node 24**), **Bun**, **`actions/cache@v5`**, **`bun install --frozen-lockfile`**.
114
145
 
115
146
  ```json title=".vscode/extensions.json"
package/mdc/js-pino.mdc CHANGED
@@ -10,8 +10,6 @@ version: '1.1'
10
10
  В **/k8s/base/configmap.yaml повинен бути заданий OTEL_RESOURCE_ATTRIBUTES: 'service.name=<project_name>,service.namespace=<project_namespace>'
11
11
  а в директоріях з kustomize повинні бути перевизначені значення OTEL_RESOURCE_ATTRIBUTES і в них service.namespace повинен відповідати namespace, в якому знаходиться дана директорія.
12
12
 
13
- Configmap повинен бути з тією самою назвою, що і deployment, якщо у deployment тільки один configmap.
14
-
15
13
  ## Перевірка
16
14
 
17
15
  `npx @nitra/cursor check js-pino`
package/mdc/k8s.mdc CHANGED
@@ -210,6 +210,10 @@ spec:
210
210
 
211
211
  **Точні умови та повідомлення `fail`** — верхній JSDoc **`npm/scripts/check-k8s.mjs`**.
212
212
 
213
+ ## ConfigMap: ім'я збігається з Deployment
214
+
215
+ Якщо в `k8s/base/` є **`configmap.yaml`** і **Deployment**, і цей Deployment посилається рівно на **один** ConfigMap — `metadata.name` ConfigMap має збігатися з `metadata.name` Deployment. Точні умови перевірки — **`check-k8s.mjs`**.
216
+
213
217
  ## Kustomize: структура каталогів (`base` / overlays)
214
218
 
215
219
  Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
@@ -3,6 +3,7 @@ description: Правила nginx для статичних файлів
3
3
  version: '1.2'
4
4
  ---
5
5
 
6
+ > **Автоміграція:** `npx @nitra/cursor check nginx-default-tpl` автоматично перейменовує `default.tpl.conf` → `default.conf.template` (або перезаписує вміст, якщо обидва файли існують). Якщо шаблон відсутній — перевірка пропускається.
6
7
 
7
8
  default.conf.template повинен виглядати так:
8
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.112",
3
+ "version": "1.8.113",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -256,7 +256,7 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
256
256
  pass('Всі workflows мають розширення .yml')
257
257
  }
258
258
 
259
- for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml']) {
259
+ for (const f of ['clean-ga-workflows.yml', 'clean-merged-branch.yml', 'lint-ga.yml', 'git-ai.yml']) {
260
260
  if (files.includes(f)) {
261
261
  pass(`${f} існує`)
262
262
  } else {
@@ -265,6 +265,66 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
265
265
  }
266
266
  }
267
267
 
268
+ /**
269
+ * Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
270
+ * @param {string} wfDir директорія workflows
271
+ * @param {(msg: string) => void} passFn callback при успішній перевірці
272
+ * @param {(msg: string) => void} failFn callback при помилці
273
+ */
274
+ async function checkGitAiWorkflow(wfDir, passFn, failFn) {
275
+ const gitAiWf = join(wfDir, 'git-ai.yml')
276
+ if (!existsSync(gitAiWf)) return
277
+ const content = await readFile(gitAiWf, 'utf8')
278
+ const root = parseWorkflowYaml(content)
279
+
280
+ if (root) {
281
+ // on.pull_request.types має містити 'closed'
282
+ const on = root.on
283
+ let hasPrClosed = false
284
+ if (on && typeof on === 'object') {
285
+ const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
286
+ if (pr && typeof pr === 'object') {
287
+ const types = /** @type {Record<string, unknown>} */ (pr).types
288
+ hasPrClosed = Array.isArray(types) && types.includes('closed')
289
+ }
290
+ }
291
+ if (hasPrClosed) {
292
+ passFn('git-ai.yml: on.pull_request.types містить closed')
293
+ } else {
294
+ failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
295
+ }
296
+
297
+ // Job if-умова: запускати лише при злитті PR
298
+ const jobs = root.jobs
299
+ let hasMergedCondition = false
300
+ if (jobs && typeof jobs === 'object') {
301
+ for (const job of Object.values(jobs)) {
302
+ if (job && typeof job === 'object') {
303
+ const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
304
+ if (ifCond.includes('merged')) {
305
+ hasMergedCondition = true
306
+ }
307
+ }
308
+ }
309
+ }
310
+ if (hasMergedCondition) {
311
+ passFn('git-ai.yml: job має умову merged')
312
+ } else {
313
+ failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
314
+ }
315
+ }
316
+
317
+ // Крок викликає git-ai ci github run
318
+ const hasGitAiRun = root
319
+ ? anyRunStepIncludes(root, 'git-ai ci github run')
320
+ : content.includes('git-ai ci github run')
321
+ if (hasGitAiRun) {
322
+ passFn('git-ai.yml: крок виконує git-ai ci github run')
323
+ } else {
324
+ failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
325
+ }
326
+ }
327
+
268
328
  /**
269
329
  * Перевіряє відповідність проєкту правилам ga.mdc
270
330
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -318,6 +378,7 @@ export async function check() {
318
378
  await checkZizmor(pass, fail)
319
379
  await checkLintGaScript(pass, fail)
320
380
  await checkLintGaWorkflow(wfDir, pass, fail)
381
+ await checkGitAiWorkflow(wfDir, pass, fail)
321
382
 
322
383
  return reporter.getExitCode()
323
384
  }
@@ -5,6 +5,8 @@
5
5
  * (`import` / `require` / динамічний `import()`); наявність `OTEL_RESOURCE_ATTRIBUTES`
6
6
  * у `k8s/base/configmap.yaml`, якщо такий файл існує.
7
7
  *
8
+ * Перевірка відповідності імені ConfigMap імені Deployment — у `check-k8s.mjs` (k8s.mdc).
9
+ *
8
10
  * Імпорти в джерелах сканує AST через `oxc-parser` (див. `utils/bunyan-imports.mjs`),
9
11
  * щоб виявити випадки на кшталт `import log from '@nitra/bunyan'`, які лишаються в коді
10
12
  * після підміни залежності.
@@ -78,7 +80,7 @@ async function checkWorkspacePackage(rootDir, fail, passFn) {
78
80
  passFn(`${label}немає імпортів '@nitra/bunyan' / 'bunyan' у джерелах`)
79
81
  }
80
82
 
81
- const configmapPath = join(rootDir, 'k8s/base/configmap.yaml')
83
+ const configmapPath = join(rootDir, 'k8s', 'base', 'configmap.yaml')
82
84
  if (existsSync(configmapPath)) {
83
85
  const content = await readFile(configmapPath, 'utf8')
84
86
  if (content.includes('OTEL_RESOURCE_ATTRIBUTES')) {
@@ -58,7 +58,7 @@
58
58
  * **Структура `HTTPRoute` для Hasura-Deployment:** звіряється канон 4 правил у **`spec.rules`** (редиректи **`<prefix>/ql`** і **`<prefix>/ql/`** на **`<prefix>/ql/console`** 302, **`PathPrefix <prefix>/ql`** + **URLRewrite** на **`/`**, окреме WebSocket-правило з **`RequestHeaderModifier`** remove **`Authorization`**). **Префікс параметризовано** (рядок перед **`/ql`** у першому Hasura-правилі). **Прив'язка** — за **`metadata.name`** у тому ж каталозі, що й **Deployment** з образом **`hasura/graphql-engine`** (див. k8s.mdc). **Додаткові правила** поверх канону дозволені.
59
59
  */
60
60
  import { existsSync } from 'node:fs'
61
- import { readFile, stat, unlink } from 'node:fs/promises'
61
+ import { readFile, readdir, stat, unlink } from 'node:fs/promises'
62
62
  import { basename, dirname, join, relative, resolve } from 'node:path'
63
63
 
64
64
  import { parseAllDocuments } from 'yaml'
@@ -1959,6 +1959,88 @@ export function isHasuraDeploymentManifest(manifest) {
1959
1959
  return containerListHasHasuraImage(p.containers) || containerListHasHasuraImage(p.initContainers)
1960
1960
  }
1961
1961
 
1962
+ const K8S_YAML_EXT_RE = /\.ya?ml$/iu
1963
+
1964
+ /**
1965
+ * Знаходить перший документ **Deployment** серед YAML-файлів каталогу (для перевірки імені ConfigMap, js-pino.mdc).
1966
+ * @param {string} dirPath абсолютний шлях до каталогу
1967
+ * @returns {Promise<Record<string, unknown> | null>} об'єкт Deployment або null
1968
+ */
1969
+ export async function findDeploymentDocInDir(dirPath) {
1970
+ let entries
1971
+ try {
1972
+ entries = await readdir(dirPath)
1973
+ } catch {
1974
+ return null
1975
+ }
1976
+ for (const entry of entries) {
1977
+ if (!K8S_YAML_EXT_RE.test(entry)) continue
1978
+ let raw
1979
+ try {
1980
+ raw = await readFile(join(dirPath, entry), 'utf8')
1981
+ } catch {
1982
+ continue
1983
+ }
1984
+ let docs
1985
+ try {
1986
+ docs = parseAllDocuments(raw)
1987
+ } catch {
1988
+ continue
1989
+ }
1990
+ for (const doc of docs) {
1991
+ if (doc.errors.length > 0) continue
1992
+ const obj = doc.toJSON()
1993
+ if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
1994
+ const rec = /** @type {Record<string, unknown>} */ (obj)
1995
+ if (rec.kind === 'Deployment') return rec
1996
+ }
1997
+ }
1998
+ }
1999
+ return null
2000
+ }
2001
+
2002
+ /**
2003
+ * Збирає унікальні імена **ConfigMap**, на які посилається **Deployment**
2004
+ * через `spec.template.spec.containers[*].envFrom[*].configMapRef.name`
2005
+ * та `spec.template.spec.volumes[*].configMap.name` (для перевірки js-pino.mdc).
2006
+ * @param {Record<string, unknown>} deployment об'єкт Deployment
2007
+ * @returns {Set<string>} унікальні імена ConfigMap
2008
+ */
2009
+ export function collectDeploymentConfigMapRefs(deployment) {
2010
+ /** @type {Set<string>} */
2011
+ const names = new Set()
2012
+ const spec = deployment.spec
2013
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) return names
2014
+ const template = /** @type {Record<string, unknown>} */ (spec).template
2015
+ if (template === null || typeof template !== 'object' || Array.isArray(template)) return names
2016
+ const podSpec = /** @type {Record<string, unknown>} */ (template).spec
2017
+ if (podSpec === null || typeof podSpec !== 'object' || Array.isArray(podSpec)) return names
2018
+ const ps = /** @type {Record<string, unknown>} */ (podSpec)
2019
+ for (const c of Array.isArray(ps.containers) ? /** @type {unknown[]} */ (ps.containers) : []) {
2020
+ if (c === null || typeof c !== 'object' || Array.isArray(c)) continue
2021
+ const envFrom = /** @type {Record<string, unknown>} */ (c).envFrom
2022
+ for (const ef of Array.isArray(envFrom) ? /** @type {unknown[]} */ (envFrom) : []) {
2023
+ if (ef !== null && typeof ef === 'object' && !Array.isArray(ef)) {
2024
+ const cmr = /** @type {Record<string, unknown>} */ (ef).configMapRef
2025
+ if (cmr !== null && typeof cmr === 'object' && !Array.isArray(cmr)) {
2026
+ const n = /** @type {Record<string, unknown>} */ (cmr).name
2027
+ if (typeof n === 'string' && n.trim() !== '') names.add(n)
2028
+ }
2029
+ }
2030
+ }
2031
+ }
2032
+ for (const v of Array.isArray(ps.volumes) ? /** @type {unknown[]} */ (ps.volumes) : []) {
2033
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
2034
+ const cm = /** @type {Record<string, unknown>} */ (v).configMap
2035
+ if (cm !== null && typeof cm === 'object' && !Array.isArray(cm)) {
2036
+ const n = /** @type {Record<string, unknown>} */ (cm).name
2037
+ if (typeof n === 'string' && n.trim() !== '') names.add(n)
2038
+ }
2039
+ }
2040
+ }
2041
+ return names
2042
+ }
2043
+
1962
2044
  /**
1963
2045
  * Чи **Service** містить заборонені анотації GKE у **`metadata.annotations`** (k8s.mdc).
1964
2046
  * @param {unknown} manifest корінь YAML-документа
@@ -3160,6 +3242,69 @@ async function ensureBaseKustomizationHasNamespace(root, yamlFiles, fail) {
3160
3242
  }
3161
3243
  }
3162
3244
 
3245
+ const CONFIGMAP_BASE_PATH_RE = /\/k8s\/base\/configmap\.yaml$/u
3246
+
3247
+ /**
3248
+ * Якщо в `k8s/base/` є `configmap.yaml` і Deployment посилається рівно на один ConfigMap —
3249
+ * `metadata.name` ConfigMap має збігатися з `metadata.name` Deployment (k8s.mdc).
3250
+ * @param {string} root корінь репозиторію
3251
+ * @param {string[]} yamlFilesAbs yaml під k8s
3252
+ * @param {(msg: string) => void} fail callback при помилці
3253
+ * @param {(msg: string) => void} passFn callback при успіху
3254
+ */
3255
+ async function validateConfigMapNameMatchesDeployment(root, yamlFilesAbs, fail, passFn) {
3256
+ const cmFiles = yamlFilesAbs.filter(abs => {
3257
+ const rel = relative(root, abs).replaceAll('\\', '/')
3258
+ return CONFIGMAP_BASE_PATH_RE.test(`/${rel}`) || rel === 'k8s/base/configmap.yaml'
3259
+ })
3260
+ for (const cmAbs of cmFiles) {
3261
+ const rel = relative(root, cmAbs).replaceAll('\\', '/') || cmAbs
3262
+ let raw
3263
+ try {
3264
+ raw = await readFile(cmAbs, 'utf8')
3265
+ } catch {
3266
+ continue
3267
+ }
3268
+ let cmName = null
3269
+ try {
3270
+ for (const doc of parseAllDocuments(raw)) {
3271
+ if (doc.errors.length > 0) continue
3272
+ const obj = doc.toJSON()
3273
+ if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
3274
+ const rec = /** @type {Record<string, unknown>} */ (obj)
3275
+ if (rec.kind === 'ConfigMap') {
3276
+ const meta = rec.metadata
3277
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
3278
+ const n = /** @type {Record<string, unknown>} */ (meta).name
3279
+ if (typeof n === 'string' && n.trim() !== '') cmName = n
3280
+ }
3281
+ break
3282
+ }
3283
+ }
3284
+ }
3285
+ } catch {
3286
+ continue
3287
+ }
3288
+ if (cmName === null) continue
3289
+ const deployment = await findDeploymentDocInDir(dirname(cmAbs))
3290
+ if (deployment === null) continue
3291
+ const meta = deployment.metadata
3292
+ const deployName =
3293
+ meta !== null && typeof meta === 'object' && !Array.isArray(meta)
3294
+ ? /** @type {Record<string, unknown>} */ (meta).name
3295
+ : null
3296
+ const cmRefs = collectDeploymentConfigMapRefs(deployment)
3297
+ if (cmRefs.size !== 1 || typeof deployName !== 'string') continue
3298
+ if (cmName === deployName) {
3299
+ passFn(`${rel}: metadata.name '${cmName}' збігається з Deployment (k8s.mdc)`)
3300
+ } else {
3301
+ fail(
3302
+ `${rel}: metadata.name '${cmName}' має збігатися з назвою Deployment '${deployName}' — Deployment посилається рівно на один ConfigMap (k8s.mdc)`
3303
+ )
3304
+ }
3305
+ }
3306
+ }
3307
+
3163
3308
  /**
3164
3309
  * Перевіряє відповідність проєкту правилам k8s.mdc.
3165
3310
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
@@ -3201,5 +3346,7 @@ export async function check() {
3201
3346
 
3202
3347
  await ensureBaseKustomizationHasNamespace(root, yamlFiles, fail)
3203
3348
 
3349
+ await validateConfigMapNameMatchesDeployment(root, yamlFiles, fail, pass)
3350
+
3204
3351
  return reporter.getExitCode()
3205
3352
  }