@nitra/cursor 1.8.135 → 1.8.138

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/mdc/capacitor.mdc CHANGED
@@ -11,8 +11,28 @@ version: '1.0'
11
11
  **`*`**, `latest` і діапазони, де можлива 7-мажор, — неприйнятні. Програма перевірки — **`check-capacitor.mjs`**
12
12
  (репозиторій **@nitra/cursor**).
13
13
 
14
- ## iOS: лише SPM
14
+ ## iOS: зазвичай лише SPM, виняток Podfile
15
15
 
16
- Нативний iOS-шар **не** повинен використовувати **CocoaPods** (наприклад файл **`Podfile`**
17
- поза каталогом `Pods/`) **Swift Package Manager (SPM)**. Якщо каталога **`ios/`** немає, перевірка iOS
18
- у цьому кроці пропускається.
16
+ - **Правило за замовчуванням:** не залишай `Podfile` (поза `Pods/`) у вихідному iOS-шарі, **якщо** уся потрібна
17
+ iOS-функціональність (нативні плагіни/модулі) може працювати **лише** через **SPM** (Swift Package Manager).
18
+
19
+ - **Плагіни зі скоупу @nitra/:** за політикою вони **підтримуюють SPM**; **перевіряти** їх на **SPM** **не
20
+ потрібно** (і **check** цього **не** робить — **немає** обходу `package.json` на предмет **@nitra/**).
21
+
22
+ - **Коли `Podfile` дозволений:** якщо **не** вся потрібна iOS-функціональність **поза** **@nitra/** (сторонні
23
+ **Capacitor**-плагіни, інша нативна залежність) **доступна** через **SPM** — `Podfile` **дозволяється**,
24
+ але це **обов’язково** треба явно задати в кореневому **`package.json`** або в
25
+ **`capacitor.config.json` / `capacitor.config.ts` / `capacitor.config.mjs`**
26
+
27
+ - **`"iosCocoaPodsBecausePluginsLackSpm": true`** (семантика: **не** **вся** потрібна нативна частина
28
+ **поза** **@nitra/** **на** **SPM**; **@nitra/** у це **не** входить);
29
+ - або **`"iosCocoaPodsAllowed": true`** (короткий **alias** для того самого **винятку**);
30
+
31
+ Без **одного** з цих прапорів `true` наявний **Podfile** поза **`Pods/`** вважається **порушенням** правила **«лише** **SPM**».
32
+
33
+ - Перевірка читає **лише** кореневі файли: **`package.json`**, потім **capacitor-конфіги** у **корені** (див. вище).
34
+ У **`.ts` / `.mjs`**: шукається блок **nitra** `{ ... }` і **на його тілі** перевіряються ці **boolean**-поля.
35
+
36
+ ## Перевірка
37
+
38
+ `npx @nitra/cursor check capacitor` (коли **check-скрипт** підключено до цієї **ruleset**).
package/mdc/docker.mdc CHANGED
@@ -13,10 +13,13 @@ alwaysApply: false
13
13
 
14
14
  Також Dockerfile/Containerfile **має бути multistage build**: окремий build stage (залежності/компіляція) і окремий runtime stage. У фінальному stage дозволені лише мінімальні базові образи:
15
15
 
16
- - **backend**: `mirror.gcr.io/library/alpine:*`
17
- - **frontend**: `mirror.gcr.io/library/nginx:*` aбо `mirror.gcr.io/openresty/openresty:*`
16
+ - **backend (типово)**: `mirror.gcr.io/library/alpine:*`
17
+ - **ультра-легкі (glibc / одна статична збірка)**: `scratch` тільки як `FROM scratch` (офіційний порожній базовий шар), коли весь **runtime** уже в `COPY --from=…`
18
+ - **glibc, Debian (slim)**: `mirror.gcr.io/library/debian:*` **лише** з тегом, у якому є `slim` (наприклад `bookworm-slim`, `trixie-slim`), а не `bookworm` без `slim`
19
+ - **виняток (інтерпретовані стеки)**: `mirror.gcr.io/library/php:*` або `mirror.gcr.io/library/python:*` — якщо сервіс має крутитися в офіційному runtime PHP чи Python, а не як один бінарник на Alpine; інакше лишай **alpine** у фінальному stage
20
+ - **frontend**: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
18
21
 
19
- Це гарантує, що результуючий образ містить лише runtime (alpine) або nginx, без build tooling і node_modules.
22
+ Це стримує зайвий build tooling (Bun, **node_modules** зі збірки) у фінальному образі; для **alpine** / **nginx** / **openresty** у **runtime** лишаються лише відповідні вимоги, для **php** / **python** (виняток) цільовий інтерпретований **stack**; **scratch** і **debian** з тегом **`*slim*`** — коли glibc і мінімальне оточення Debian важливіші за musl в **alpine**.
20
23
 
21
24
  ## компіляція
22
25
 
@@ -182,7 +185,10 @@ jobs:
182
185
  ```yaml title=".hadolint.yaml"
183
186
  ignored:
184
187
  - DL3007
188
+ - DL3018
185
189
  ```
190
+ Де DL3007 - «Не використовуй тег latest у FROM»
191
+ Де DL3018 - «Піни версії пакетів у apk add»
186
192
 
187
193
  Якщо немає файлів у межах відповідного набору (**`lint-docker`** або **`check docker`**) — перевірка пропускається (exit 0).
188
194
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.135",
3
+ "version": "1.8.138",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -14,10 +14,12 @@
14
14
  * варто задати явний діапазон, наприклад **`^8.0.0`**. Якщо оголошено `capacitor.config.*` без жодного
15
15
  * **`@capacitor/core`** у дереві `package.json` — також помилка.
16
16
  *
17
- * **iOS лише через SPM (Swift Package Manager):** якщо в корні є каталог **`ios/`** у ньому **не** має
18
- * бути файлів **Podfile** (CocoaPods) **поза** каталогом **Pods** (тобто не використовувати **Podfile**
19
- * у вихідному iOS-шарі; присутній **Podfile** — порушення). Якщо **немає** `ios/` — вимогу iOS у цьому
20
- * прогоні пропущено.
17
+ * **iOS:** зазвичай **без** **Podfile** поза **Pods** (тільки **SPM**). **@nitra/**-плагіни за політикою **SPM**
18
+ * їх **не** перелічуємо й **не** перевіряємо. Якщо **Podfile** є, його можна зареєструвати як **виняток**
19
+ * (див. **capacitor.mdc**): у кореневому **`package.json`** або
20
+ * **`capacitor.config.json` / `capacitor.config.ts` / `capacitor.config.mjs`**, об’єкт **nitra** з
21
+ * **`iosCocoaPodsBecausePluginsLackSpm: true`** (або **`iosCocoaPodsAllowed: true`**); тоді **Podfile** **не** fail.
22
+ * Якщо **`ios/`** **немає** — iOS-умови не застосовуються.
21
23
  */
22
24
  import { existsSync } from 'node:fs'
23
25
  import { readdir, readFile } from 'node:fs/promises'
@@ -41,9 +43,11 @@ const IGNORED_DIRS_FOR_PACKAGE_JSON = new Set([
41
43
  ])
42
44
 
43
45
  /** `||` у діапазоні npm-версій */
46
+ // eslint-disable-next-line sonarjs/slow-regex -- короткі **semver**-підрядки у **package.json**
44
47
  const NPM_OR_PARTS_RE = /\s*\|\|\s*/
45
48
 
46
49
  /** `a - b` (діапазон діапазонів) */
50
+ // eslint-disable-next-line sonarjs/slow-regex -- форма **X - Y** у **npm**-range
47
51
  const NPM_HYPHEN_RANGE_RE = /^(.+?)\s+-\s+(.+)$/
48
52
 
49
53
  const FIRST_VERSION_NUM_RE = /^(?:v)?(\d+)/i
@@ -52,6 +56,14 @@ const PREFIX_GEQ_RE = /^>=\s*/u
52
56
  const PREFIX_GT_RE = /^>\s*/u
53
57
  const STRIP_CARET_TILDE_EQ_RE = /^[=^~]+\s*/u
54
58
 
59
+ /**
60
+ * Початок блока **nitra: {** у **.ts** / **.mjs** (**capacitor.config**; ключ **nitra**).
61
+ */
62
+ const RE_NITRA_CONFIG_OBJECT_LEAD_IN = /\bnitra\b\s*:\s*\{|[\u0027"]nitra[\u0027"]\s*:\s*\{/
63
+
64
+ const RE_COCOAPODS_EXEMPT_SPM = /\biosCocoaPodsBecausePluginsLackSpm\s*:\s*true\b/
65
+ const RE_COCOAPODS_EXEMPT_ALLOW = /\biosCocoaPodsAllowed\s*:\s*true\b/
66
+
55
67
  /**
56
68
  * Мінімальний **major** (нижня межа) для **однієї** OR-частини діапазону npm (без `||` всередині).
57
69
  * @param {string} segment одна частина після `||` або весь рядок
@@ -158,6 +170,22 @@ function reportOneCapacitorCoreRange(fail, pass, rel, range) {
158
170
  }
159
171
  }
160
172
 
173
+ /**
174
+ * @param {string} rel відносний шлях `package.json`
175
+ * @param {Record<string, unknown>} obj `dependencies` / `devDependencies` / **…**
176
+ * @param {{ byPath: Map<string, string>, anyCapacitor: boolean }} out накопичувач **@capacitor** у дереві
177
+ */
178
+ function recordCapacitorFromDependencyObject(rel, obj, out) {
179
+ for (const [name, val] of Object.entries(obj)) {
180
+ if (typeof name === 'string' && name.startsWith('@capacitor/')) {
181
+ out.anyCapacitor = true
182
+ }
183
+ if (name === '@capacitor/core' && typeof val === 'string' && val !== '') {
184
+ out.byPath.set(rel, val)
185
+ }
186
+ }
187
+ }
188
+
161
189
  /**
162
190
  * @param {string} absPath шлях до `package.json`
163
191
  * @param {string} root корінь репозиторію
@@ -181,15 +209,7 @@ export async function recordCapacitorFromOnePackageJson(absPath, root, out) {
181
209
  for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
182
210
  const rec = pkg?.[block]
183
211
  if (rec !== null && rec !== undefined && typeof rec === 'object' && !Array.isArray(rec)) {
184
- const obj = /** @type {Record<string, unknown>} */ (rec)
185
- for (const [name, val] of Object.entries(obj)) {
186
- if (typeof name === 'string' && name.startsWith('@capacitor/')) {
187
- out.anyCapacitor = true
188
- }
189
- if (name === '@capacitor/core' && typeof val === 'string' && val !== '') {
190
- out.byPath.set(rel, val)
191
- }
192
- }
212
+ recordCapacitorFromDependencyObject(rel, /** @type {Record<string, unknown>} */ (rec), out)
193
213
  }
194
214
  }
195
215
  }
@@ -269,18 +289,18 @@ export async function walkIosForPodfileSkipPods(root, dir, onPodfileRelative) {
269
289
  return false
270
290
  }
271
291
  for (const e of entries) {
272
- if (e.name !== 'Pods' && e.name !== 'build' && e.name !== 'DerivedData') {
292
+ if (e.name === 'Pods' || e.name === 'build' || e.name === 'DerivedData') {
293
+ // skip
294
+ } else if (e.isFile() === true && e.name === 'Podfile') {
295
+ const abs = join(dir, e.name)
296
+ onPodfileRelative((relative(root, abs) || abs).replaceAll('\\', '/'))
297
+ return true
298
+ } else if (e.isDirectory() === true) {
273
299
  const abs = join(dir, e.name)
274
- if (e.isFile() && e.name === 'Podfile') {
275
- onPodfileRelative((relative(root, abs) || abs).replaceAll('\\', '/'))
300
+ const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
301
+ if (found === true) {
276
302
  return true
277
303
  }
278
- if (e.isDirectory()) {
279
- const found = await walkIosForPodfileSkipPods(root, abs, onPodfileRelative)
280
- if (found) {
281
- return true
282
- }
283
- }
284
304
  }
285
305
  }
286
306
  return false
@@ -304,6 +324,112 @@ export async function findFirstPodfileUnderIosExcludingPods(root) {
304
324
  return first
305
325
  }
306
326
 
327
+ /**
328
+ * Чи дозволяє об’єкт **nitra** використання **Podfile** (CocoaPods) на iOS (див. **capacitor.mdc**; **@nitra/**
329
+ * **SPM** **не** аналізуємо).
330
+ * @param {unknown} o об’єкт **nitra** з **package.json** / **capacitor.config**
331
+ * @returns {boolean} **true** якщо **Podfile** дозволено (один з прапорів **true**)
332
+ */
333
+ export function nitrAObjectAllowsIosCocoaPods(o) {
334
+ if (o === null || o === undefined || typeof o !== 'object' || Array.isArray(o)) {
335
+ return false
336
+ }
337
+ const r = /** @type {Record<string, unknown>} */ (o)
338
+ if (r.iosCocoaPodsBecausePluginsLackSpm === true) {
339
+ return true
340
+ }
341
+ if (r.iosCocoaPodsAllowed === true) {
342
+ return true
343
+ }
344
+ return false
345
+ }
346
+
347
+ /**
348
+ * Витягає вихідний текст тіла **{ ... }** після **nitra:** / **"nitra":** у **config**-файлі (**ts** / **mjs**)
349
+ * (перша відповідність, баланс фігурних дужок).
350
+ * @param {string} source вміст **.ts** / **.mjs** config
351
+ * @returns {string | null} фрагмент **`{...}`** після **nitra:** або **null**
352
+ */
353
+ function extractNitraObjectBodySource(source) {
354
+ const m = RE_NITRA_CONFIG_OBJECT_LEAD_IN.exec(source)
355
+ if (!m) {
356
+ return null
357
+ }
358
+ const openBrace = m.index + m[0].length - 1
359
+ let d = 0
360
+ for (let i = openBrace; i < source.length; i += 1) {
361
+ const c = source[i]
362
+ if (c === '{') {
363
+ d += 1
364
+ } else if (c === '}') {
365
+ d -= 1
366
+ if (d === 0) {
367
+ return source.slice(openBrace, i + 1)
368
+ }
369
+ }
370
+ }
371
+ return null
372
+ }
373
+
374
+ /**
375
+ * @param {string} objectBody фрагмент **`{ ... }`** (текст)
376
+ * @returns {boolean} **true**, якщо в тілі є **iosCocoaPods**…**:** **true**
377
+ */
378
+ function nitraObjectBodyStringAllowsCocoaPodsExempt(objectBody) {
379
+ return (
380
+ RE_COCOAPODS_EXEMPT_SPM.test(objectBody) === true || RE_COCOAPODS_EXEMPT_ALLOW.test(objectBody) === true
381
+ )
382
+ }
383
+
384
+ /**
385
+ * @param {string} absPath повний шлях до **JSON**-файла
386
+ * @returns {Promise<boolean>} **true**, якщо `obj.nitra` (ключ **nitra** у JSON) — виняток
387
+ */
388
+ async function pathJsonShowsNitraCocoapodsExempt(absPath) {
389
+ if (existsSync(absPath) !== true) {
390
+ return false
391
+ }
392
+ try {
393
+ const t = await readFile(absPath, 'utf8')
394
+ const j = /** @type {Record<string, unknown>} */ (JSON.parse(t))
395
+ return nitrAObjectAllowsIosCocoaPods(j['nitra']) === true
396
+ } catch {
397
+ return false
398
+ }
399
+ }
400
+
401
+ /**
402
+ * @param {string} root корінь репозиторію
403
+ * @returns {Promise<boolean>} **true**, якщо **.ts** / **.mjs** містить валідний виняток **nitra**
404
+ */
405
+ async function capacitorConfigTsMjsNitraCocoapodsExempt(root) {
406
+ for (const name of ['capacitor.config.ts', 'capacitor.config.mjs']) {
407
+ const p = join(root, name)
408
+ if (existsSync(p) === true) {
409
+ const text = await readFile(p, 'utf8')
410
+ const body = extractNitraObjectBodySource(text)
411
+ if (body !== null && nitraObjectBodyStringAllowsCocoaPodsExempt(body) === true) {
412
+ return true
413
+ }
414
+ }
415
+ }
416
+ return false
417
+ }
418
+
419
+ /**
420
+ * @param {string} root корінь репозиторію (**process.cwd()** у **check**)
421
+ * @returns {Promise<boolean>} **true** якщо **Podfile** виправдано **nitra**-конфігом
422
+ */
423
+ async function isIosCocoaPodsExemptByNitraConfig(root) {
424
+ if ((await pathJsonShowsNitraCocoapodsExempt(join(root, 'package.json'))) === true) {
425
+ return true
426
+ }
427
+ if ((await pathJsonShowsNitraCocoapodsExempt(join(root, 'capacitor.config.json'))) === true) {
428
+ return true
429
+ }
430
+ return capacitorConfigTsMjsNitraCocoapodsExempt(root)
431
+ }
432
+
307
433
  /**
308
434
  * @returns {Promise<number>} **0** — **ok**; **1** — **fail** (див. **capacitor.mdc**)
309
435
  */
@@ -340,9 +466,13 @@ export async function check() {
340
466
  } else {
341
467
  pass('каталог ios/ не знайдено — вимогу iOS/SPM пропущено')
342
468
  }
469
+ } else if ((await isIosCocoaPodsExemptByNitraConfig(root)) === true) {
470
+ pass(
471
+ `iOS: Podfile «${podfileRel}» — дозволено виняток (nitra.iosCocoaPodsBecausePluginsLackSpm / iosCocoaPodsAllowed, capacitor.mdc)`
472
+ )
343
473
  } else {
344
474
  fail(
345
- `iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods (прибери Podfile, capacitor.mdc)`
475
+ `iOS: знайдено Podfile «${podfileRel}» — для Capacitor використовуй лише SPM, без CocoaPods, або додай виняток nitra (capacitor.mdc)`
346
476
  )
347
477
  }
348
478
 
@@ -5,8 +5,10 @@
5
5
  * вказуються через `mirror.gcr.io` (див. `utils/docker-mirror.mjs`).
6
6
  *
7
7
  * Також перевіряє, що Dockerfile/Containerfile має **multistage build** і що фінальний stage
8
- * використовує мінімальний runtime-образ:
9
- * - backend: `mirror.gcr.io/library/alpine:*`
8
+ * використовує дозволений runtime-образ (див. docker.mdc):
9
+ * - backend: `mirror.gcr.io/library/alpine:*`, `scratch`, `mirror.gcr.io/library/debian:` з тегом, що
10
+ * містить `slim` (не повний `debian:bookworm`), за винятком PHP/Python — `mirror.gcr.io/library/php:*` або
11
+ * `mirror.gcr.io/library/python:*`
10
12
  * - frontend: `mirror.gcr.io/library/nginx:*` або `mirror.gcr.io/openresty/openresty:*`
11
13
  *
12
14
  * Якщо в Dockerfile є крок `bun install` і це не frontend-образ (фінальний stage — alpine),
@@ -14,7 +16,7 @@
14
16
  * фінальному stage не повинно залишатися build tooling (Bun/Node).
15
17
  *
16
18
  * Мета — щоб у фінальному образі не було build tooling (Bun/Node та залежностей), а лише
17
- * runtime (alpine), nginx або openresty.
19
+ * дозволений runtime (alpine, scratch, debian slim, за потреби php/python, nginx або openresty).
18
20
  *
19
21
  * Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
20
22
  * тощо. Спочатку hadolint з PATH, інакше docker run з образом hadolint/hadolint.
@@ -84,10 +86,31 @@ export function parseFromStages(fileContent) {
84
86
 
85
87
  const RUNTIME_IMAGES = /** @type {const} */ ([
86
88
  'mirror.gcr.io/library/alpine',
89
+ 'mirror.gcr.io/library/php',
90
+ 'mirror.gcr.io/library/python',
87
91
  'mirror.gcr.io/library/nginx',
88
92
  'mirror.gcr.io/openresty/openresty'
89
93
  ])
90
94
 
95
+ /** @type {RegExp} */
96
+ const DEBIAN_VIA_MIRROR_RE = /^mirror\.gcr\.io\/library\/debian:(.+)$/i
97
+
98
+ /**
99
+ * Чи ref фінального `FROM` відповідає дозволеним у docker.mdc (multistage / runtime).
100
+ * @param {string} lastLower ref без digest, lower case
101
+ * @returns {boolean}
102
+ */
103
+ function isAllowedFinalRuntimeImage(lastLower) {
104
+ if (lastLower === 'scratch' || lastLower.startsWith('scratch:')) {
105
+ return true
106
+ }
107
+ const deb = lastLower.match(DEBIAN_VIA_MIRROR_RE)
108
+ if (deb) {
109
+ return deb[1].toLowerCase().includes('slim')
110
+ }
111
+ return RUNTIME_IMAGES.some(img => lastLower.startsWith(`${img}:`) || lastLower === img)
112
+ }
113
+
91
114
  /**
92
115
  * Розбиває Dockerfile на stages за `FROM` (порожній масив, якщо FROM немає).
93
116
  * @param {string} fileContent вміст Dockerfile/Containerfile
@@ -113,7 +136,7 @@ export function splitDockerfileStages(fileContent) {
113
136
  /**
114
137
  * Перевіряє базові вимоги до структури Dockerfile:
115
138
  * - multistage: мінімум 2 FROM
116
- * - фінальний FROM: alpine/nginx/openresty з mirror.gcr.io
139
+ * - фінальний FROM: дозволені образи в docker.mdc (alpine, scratch, debian slim, php, python, nginx, openresty, …)
117
140
  * @param {string} fileContent вміст Dockerfile/Containerfile
118
141
  * @returns {string | null} повідомлення помилки або null
119
142
  */
@@ -129,9 +152,8 @@ export function getMultistageAndRuntimeHint(fileContent) {
129
152
  const lastImage = (last?.image || '').split('@')[0] || ''
130
153
  const lastLower = lastImage.toLowerCase()
131
154
 
132
- const okRuntime = RUNTIME_IMAGES.some(img => lastLower.startsWith(`${img}:`) || lastLower === img)
133
- if (!okRuntime) {
134
- return `фінальний FROM має бути ${RUNTIME_IMAGES.join(' або ')} (runtime stage), зараз: ${last?.image} (рядок ${last?.line})`
155
+ if (!isAllowedFinalRuntimeImage(lastLower)) {
156
+ return `фінальний FROM має бути дозволеним runtime-образом (див. docker.mdc: multistage), зараз: ${last?.image} (рядок ${last?.line})`
135
157
  }
136
158
 
137
159
  return null
@@ -5,6 +5,7 @@
5
5
  * VSCode (formatOnSave, defaultFormatter для js/ts/json/vue/css/html),
6
6
  * відсутність Prettier у конфігах і залежностях.
7
7
  *
8
+ * cspell: `.cspell.json` з обовʼязковим набором `ignorePaths` (клон text.mdc: node_modules, vscode, git, report, svg, k8s yaml);
8
9
  * cspell, markdownlint через `bunx markdownlint-cli2` у `lint-text` (без оголошення пакета в package.json); у кореневих **`devDependencies`**
9
10
  * дозволені лише **`@nitra/*`** (як у bun.mdc), зокрема **`@nitra/cspell-dict` ^2.0.0+**; без імпорту **`@cspell/dict-*`** у `.cspell.json`, заборона
10
11
  * `markdownlint-cli2` у dependencies/devDependencies, v8r (`run-v8r.mjs` або чотири `bunx v8r`),
@@ -31,6 +32,17 @@ const UK_APOSTROPHE_HEADING = '**Український апостроф:**'
31
32
  /** Мінімальні glob-и в `ignorePatterns` у `.oxfmtrc.json` (text.mdc). */
32
33
  const OXFMT_REQUIRED_IGNORE_PATTERNS = ['**/hasura/metadata/**', '**/schema.graphql']
33
34
 
35
+ /** Канонічні записи `ignorePaths` у `.cspell.json` (text.mdc) — кожен має бути присутнім. */
36
+ const CSPELL_REQUIRED_IGNORE_PATHS = [
37
+ '**/node_modules/**',
38
+ '**/vscode-extension/**',
39
+ '**/.git/**',
40
+ '.vscode',
41
+ 'report',
42
+ '*.svg',
43
+ '**/k8s/**/*.yaml',
44
+ ]
45
+
34
46
  /**
35
47
  * Чи діапазон версії `@nitra/cspell-dict` у package.json означає лінію 2.0.0+ (з цієї версії словники входять у пакет).
36
48
  * @param {string|undefined} range наприклад "^2.0.0"
@@ -384,6 +396,14 @@ async function checkCspellConfig(pass, fail) {
384
396
  } else {
385
397
  fail('.cspell.json не містить ignorePaths')
386
398
  }
399
+ if (Array.isArray(cfg.ignorePaths)) {
400
+ const missing = CSPELL_REQUIRED_IGNORE_PATHS.filter(p => !cfg.ignorePaths.includes(p))
401
+ if (missing.length === 0) {
402
+ pass(`.cspell.json ignorePaths містить усі обовʼязкові glob-и з text.mdc`)
403
+ } else {
404
+ fail(`.cspell.json ignorePaths бракує за замовчанням: ${missing.join(', ')} (див. text.mdc)`)
405
+ }
406
+ }
387
407
  }
388
408
 
389
409
  /**