@nitra/cursor 3.13.0 → 3.14.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.14.1] - 2026-06-02
4
+
5
+ ### Changed
6
+
7
+ - flow review: рецензент верифікує cross-file твердження читанням (Read) — промпт дозволяє/зобов'язує дочитувати referenced-файли/spec точково, репортувати лише те, що вносить diff (не преіснуючі баги сусідів), і не видавати нефальсифіковних findings «з diff не видно». Зменшує хибні findings.
8
+
9
+ ### Fixed
10
+
11
+ - coverage-gate: запускати Stryker із локально встановленого @stryker-mutator/core (резолв через package.json у node_modules пакета, bin запускається напряму через node-shebang), а не через npx/bunx — ті тягнуть core у власний кеш без vitest-runner, тож plugin-discovery падав 'Cannot find TestRunner plugin vitest' і flow verify coverage-gate червонів. Працює й з worktree без власного node_modules.
12
+
13
+ ## [3.14.0] - 2026-06-02
14
+
15
+ ### Changed
16
+
17
+ - k8s hasura_configmap: base/dev ConfigMap Hasura-Deployment має містити HASURA_GRAPHQL_ENABLED_APIS="metadata,graphql,pgdump" (точний рядок). Кожен не-base overlay (k8s/<env>/, env≠base/dev), що успадковує Hasura-base, має у kustomization.yaml перевизначати цей ключ до "metadata,graphql" (без pgdump) патчем JSON6902/Strategic Merge на ConfigMap — нова cross-file перевірка validateHasuraOverlayEnabledApisOverride
18
+
3
19
  ## [3.13.0] - 2026-06-02
4
20
 
5
21
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "3.13.0",
3
+ "version": "3.14.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -9,8 +9,9 @@
9
9
  import { spawnSync } from 'node:child_process'
10
10
  import { existsSync, readFileSync } from 'node:fs'
11
11
  import { mkdtemp, readFile, rm } from 'node:fs/promises'
12
+ import { createRequire } from 'node:module'
12
13
  import { tmpdir } from 'node:os'
13
- import { isAbsolute, join, relative } from 'node:path'
14
+ import { dirname, isAbsolute, join, relative } from 'node:path'
14
15
 
15
16
  import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
16
17
  import { addCoverage, addMutation } from '../../test/coverage/coverage.mjs'
@@ -239,6 +240,32 @@ export function parseStrykerReport(report, jsRoot) {
239
240
  * (типовий патерн monorepo, де тести зосереджені в одному пакеті); пустий lcov
240
241
  * у такому випадку сигналізує "no tests" → collectOneRoot пропускає workspace.
241
242
  */
243
+ /**
244
+ * Шлях до локально встановленого Stryker core-bin (поряд із плагінами на кшталт
245
+ * `@stryker-mutator/vitest-runner`). Запуск саме його через `node` — не `npx`/`bunx` —
246
+ * дає Stryker побачити локальні плагіни при plugin-discovery.
247
+ * @returns {string | null} абсолютний шлях `bin/stryker.js` або `null`, якщо не встановлено
248
+ */
249
+ /** Мемо: `undefined` — ще не обчислено; `string`/`null` — результат. */
250
+ let strykerBinCache
251
+
252
+ function resolveLocalStrykerBin() {
253
+ if (strykerBinCache !== undefined) return strykerBinCache
254
+ try {
255
+ // `exports` у core НЕ відкриває `./bin/stryker.js`, тож резолвимо package.json
256
+ // (доступний) і беремо шлях bin звідти. Ключ bin зазвичай `stryker`; як запас —
257
+ // перше значення map'и.
258
+ const require = createRequire(import.meta.url)
259
+ const pkgJsonPath = require.resolve('@stryker-mutator/core/package.json')
260
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'))
261
+ const binRel = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin?.stryker ?? Object.values(pkg.bin ?? {})[0])
262
+ strykerBinCache = binRel ? join(dirname(pkgJsonPath), binRel) : null
263
+ } catch {
264
+ strykerBinCache = null
265
+ }
266
+ return strykerBinCache
267
+ }
268
+
242
269
  const defaultRunner = {
243
270
  runJsCoverage({ cwd, lcovDir, base }) {
244
271
  // base !== undefined ⇔ --changed-режим: vitest сам рахує зачеплені змінами тести
@@ -261,20 +288,21 @@ const defaultRunner = {
261
288
  return r.status ?? 1
262
289
  },
263
290
  runStryker({ cwd, mutate }) {
264
- // `npx`, не `bunx`: bunx завжди ставить пакет у `T/bunx-<uid>-<pkg>@latest` і запускає
265
- // Stryker звідти. Плагін-discovery у Stryker (`@stryker-mutator/*`) globится відносно
266
- // CORE-install-каталогу (`core/dist/src/di/plugin-loader.js` `../../../../../@stryker-mutator/*`),
267
- // тож у bunx-temp бачить лише `core/api/instrumenter/util` (усі в IGNORED_PACKAGES) — а локально
268
- // встановлений `@stryker-mutator/vitest-runner` залишається невидимим, і workers падають з
269
- // `Cannot find TestRunner plugin "vitest"`. `npx` ходить угору по `node_modules/.bin/` і
270
- // запускає Stryker з локального hoisted-install, де поряд лежить vitest-runner.
291
+ // Plugin-discovery Stryker (`@stryker-mutator/*`) globиться відносно CORE-install-каталогу
292
+ // (`core/dist/src/di/plugin-loader.js` `../../../../../@stryker-mutator/*`). Тож core
293
+ // МАЄ вантажитись із проєктного `node_modules`, де поряд лежить `@stryker-mutator/vitest-runner`.
294
+ // `npx`/`bunx` тягнуть core у власний кеш (`_npx/<hash>`, `bunx-temp`) БЕЗ плагінів воркери
295
+ // падають `Cannot find TestRunner plugin "vitest"`. Тому резолвимо локальний core-bin через
296
+ // `import.meta.url` (модуль у `npm/` кореневий `node_modules` пакета; працює й з worktree без
297
+ // власного node_modules) і запускаємо його через `node`. Fallback на `npx`, якщо не встановлено.
271
298
  // mutate (непорожній) ⇔ --changed-режим: мутуємо лише змінені production-файли цього root.
272
299
  const mutateArgs = mutate && mutate.length > 0 ? ['--mutate', mutate.join(',')] : []
273
- const r = spawnSync('npx', ['@stryker-mutator/core', 'run', ...mutateArgs], {
274
- cwd,
275
- stdio: 'inherit',
276
- env: process.env
277
- })
300
+ const strykerBin = resolveLocalStrykerBin()
301
+ // Запускаємо bin НАПРЯМУ (його shebang `#!/usr/bin/env node` → завжди node, навіть якщо
302
+ // coverage.mjs стартував під bun, де `process.execPath` вказував би на bun). Fallback на npx.
303
+ const r = strykerBin
304
+ ? spawnSync(strykerBin, ['run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
305
+ : spawnSync('npx', ['@stryker-mutator/core', 'run', ...mutateArgs], { cwd, stdio: 'inherit', env: process.env })
278
306
  return r.status ?? 1
279
307
  }
280
308
  }
@@ -2358,6 +2358,7 @@ export const HASURA_REQUIRED_ENV_KEYS = [
2358
2358
  'HASURA_GRAPHQL_ENABLE_RELAY',
2359
2359
  'HASURA_GRAPHQL_ENABLE_TELEMETRY',
2360
2360
  'HASURA_GRAPHQL_ENABLED_LOG_TYPES',
2361
+ 'HASURA_GRAPHQL_ENABLED_APIS',
2361
2362
  'HASURA_GRAPHQL_DISABLE_EVENTING'
2362
2363
  ]
2363
2364
 
@@ -4929,6 +4930,130 @@ async function validateProdKustomizationOverrides(root, yamlFilesAbs, fail, pass
4929
4930
  }
4930
4931
  }
4931
4932
 
4933
+ /** Очікуване `HASURA_GRAPHQL_ENABLED_APIS` у non-base/dev overlay (без `pgdump` — він лише для base/dev). */
4934
+ const HASURA_OVERLAY_ENABLED_APIS = 'metadata,graphql'
4935
+
4936
+ /** JSON-Pointer ключа `HASURA_GRAPHQL_ENABLED_APIS` у `data` ConfigMap (для JSON6902-патчів). */
4937
+ const HASURA_ENABLED_APIS_DATA_POINTER = '/data/HASURA_GRAPHQL_ENABLED_APIS'
4938
+
4939
+ /**
4940
+ * Чи дерево kustomization (`resources` / `bases` / `components` / `crds`, рекурсивно) містить
4941
+ * **Hasura-Deployment** у шарі base (образ `hasura/graphql-engine`). Маркер того, що overlay успадковує
4942
+ * Hasura-ConfigMap з pgdump-значенням `ENABLED_APIS` і має його перевизначити.
4943
+ * @param {string} kustAbs kustomization.yaml
4944
+ * @param {string} rootNorm нормалізований корінь репо
4945
+ * @returns {Promise<boolean>} true, якщо в успадкованому base є Hasura-Deployment
4946
+ */
4947
+ export async function kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm) {
4948
+ const visited = new Set()
4949
+ const paths = await collectYamlAbsPathsFromKustomizationTree(kustAbs, rootNorm, visited)
4950
+ const rootResolved = resolve(rootNorm)
4951
+ for (const abs of paths) {
4952
+ const rel = (relative(rootResolved, abs) || '').replaceAll('\\', '/')
4953
+ if (!isK8sYamlUnderBaseDirectory(rel)) continue
4954
+ const roots = await readK8sYamlDocumentRootsForInventory(abs)
4955
+ if (roots.some(o => isHasuraDeploymentManifest(o))) return true
4956
+ }
4957
+ return false
4958
+ }
4959
+
4960
+ /**
4961
+ * Значення, яке inline-`patch` присвоює `data.HASURA_GRAPHQL_ENABLED_APIS`. Підтримка двох форматів:
4962
+ * **JSON6902** (`op` add/replace на `/data/HASURA_GRAPHQL_ENABLED_APIS`) і **Strategic Merge**
4963
+ * (`data.HASURA_GRAPHQL_ENABLED_APIS`). Зовнішні patch-файли (`patches[].path`) не охоплені — Plan B trade-off.
4964
+ * @param {string} patchText вміст поля `patch`
4965
+ * @returns {string | null} присвоєне значення (рядок) або null, якщо patch не чіпає цей ключ
4966
+ */
4967
+ export function enabledApisValueFromPatchText(patchText) {
4968
+ const t = typeof patchText === 'string' ? patchText.trim() : ''
4969
+ if (t === '') return null
4970
+ let parsed
4971
+ try {
4972
+ for (const d of parseAllDocuments(t)) {
4973
+ if (d.errors.length === 0) {
4974
+ parsed = d.toJSON()
4975
+ break
4976
+ }
4977
+ }
4978
+ } catch {
4979
+ return null
4980
+ }
4981
+ if (Array.isArray(parsed)) {
4982
+ for (const item of parsed) {
4983
+ if (item === null || typeof item !== 'object' || Array.isArray(item)) continue
4984
+ const rec = /** @type {Record<string, unknown>} */ (item)
4985
+ const op = typeof rec.op === 'string' ? rec.op.trim().toLowerCase() : ''
4986
+ const path = typeof rec.path === 'string' ? normalizeJsonPatchPath(rec.path) : ''
4987
+ if ((op === 'add' || op === 'replace') && path === HASURA_ENABLED_APIS_DATA_POINTER) {
4988
+ return typeof rec.value === 'string' ? rec.value : JSON.stringify(rec.value)
4989
+ }
4990
+ }
4991
+ return null
4992
+ }
4993
+ if (parsed === null || typeof parsed !== 'object') return null
4994
+ const data = /** @type {Record<string, unknown>} */ (parsed).data
4995
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) return null
4996
+ const d = /** @type {Record<string, unknown>} */ (data)
4997
+ if (!Object.hasOwn(d, 'HASURA_GRAPHQL_ENABLED_APIS')) return null
4998
+ const v = d.HASURA_GRAPHQL_ENABLED_APIS
4999
+ return typeof v === 'string' ? v : JSON.stringify(v)
5000
+ }
5001
+
5002
+ /**
5003
+ * Значення, яке `patches[]` kustomization присвоюють `data.HASURA_GRAPHQL_ENABLED_APIS` на цілі **ConfigMap**.
5004
+ * Повертає значення першого patch-а, що чіпає цей ключ, або null, якщо такого немає.
5005
+ * @param {Record<string, unknown>} kust об'єкт kustomization.yaml
5006
+ * @returns {string | null} присвоєне значення або null
5007
+ */
5008
+ export function hasuraEnabledApisOverrideValue(kust) {
5009
+ const patches = kust.patches
5010
+ if (!Array.isArray(patches)) return null
5011
+ for (const p of patches) {
5012
+ if (p === null || typeof p !== 'object' || Array.isArray(p)) continue
5013
+ const pr = /** @type {Record<string, unknown>} */ (p)
5014
+ if (typeof pr.patch !== 'string') continue
5015
+ if (resolvePatchTargetKind(pr) !== 'ConfigMap') continue
5016
+ const v = enabledApisValueFromPatchText(pr.patch)
5017
+ if (v !== null) return v
5018
+ }
5019
+ return null
5020
+ }
5021
+
5022
+ /**
5023
+ * Для кожного **non-base/dev** overlay `kustomization.yaml`, що успадковує Hasura-base (Deployment з
5024
+ * `hasura/graphql-engine`), вимагає у `patches[]` перевизначення **`data.HASURA_GRAPHQL_ENABLED_APIS`**
5025
+ * до **`"metadata,graphql"`** (pgdump лишається строго для base/dev) (k8s.mdc). `kind: Component`
5026
+ * пропускається (env-неутральне джерело, не overlay).
5027
+ * @param {string} root корінь репозиторію
5028
+ * @param {string[]} yamlFilesAbs yaml під k8s
5029
+ * @param {(msg: string) => void} fail callback при помилці
5030
+ * @param {(msg: string) => void} passFn callback при успіху
5031
+ */
5032
+ async function validateHasuraOverlayEnabledApisOverride(root, yamlFilesAbs, fail, passFn) {
5033
+ const rootNorm = resolve(root)
5034
+ const kustFiles = yamlFilesAbs.filter(abs => basename(abs) === 'kustomization.yaml')
5035
+ for (const kustAbs of kustFiles) {
5036
+ const rel = (relative(rootNorm, kustAbs) || kustAbs).replaceAll('\\', '/')
5037
+ const segment = k8sEnvSegmentFromRelPath(rel)
5038
+ if (segment === null || segment === 'base' || segment === 'dev') continue
5039
+ const kust = await readFirstYamlObject(kustAbs)
5040
+ if (kust === null || kust.kind === 'Component') continue
5041
+ if (!(await kustomizationTreeHasHasuraDeployment(kustAbs, rootNorm))) continue
5042
+ const value = hasuraEnabledApisOverrideValue(kust)
5043
+ if (value === HASURA_OVERLAY_ENABLED_APIS) {
5044
+ passFn(`${rel}: overlay '${segment}' перевизначає HASURA_GRAPHQL_ENABLED_APIS="${HASURA_OVERLAY_ENABLED_APIS}" (k8s.mdc)`)
5045
+ } else if (value === null) {
5046
+ fail(
5047
+ `${rel}: overlay '${segment}' має у patches[] перевизначати data.HASURA_GRAPHQL_ENABLED_APIS до "${HASURA_OVERLAY_ENABLED_APIS}" (pgdump лише для base/dev) (k8s.mdc)`
5048
+ )
5049
+ } else {
5050
+ fail(
5051
+ `${rel}: overlay '${segment}' patch data.HASURA_GRAPHQL_ENABLED_APIS має бути "${HASURA_OVERLAY_ENABLED_APIS}" (зараз: ${JSON.stringify(value)}) (k8s.mdc)`
5052
+ )
5053
+ }
5054
+ }
5055
+ }
5056
+
4932
5057
  /**
4933
5058
  * Шукає HPA за `scaleTargetRef.name` серед документів.
4934
5059
  * @param {Record<string, unknown>[]} hpaDocs масив HPA-документів
@@ -6626,5 +6751,7 @@ export async function check(cwd = process.cwd()) {
6626
6751
 
6627
6752
  await validateProdKustomizationOverrides(root, yamlFiles, fail, pass)
6628
6753
 
6754
+ await validateHasuraOverlayEnabledApisOverride(root, yamlFiles, fail, pass)
6755
+
6629
6756
  return reporter.getExitCode()
6630
6757
  }
package/rules/k8s/k8s.mdc CHANGED
@@ -303,6 +303,7 @@ spec:
303
303
  - **`HASURA_GRAPHQL_ENABLE_RELAY`** зі значенням **`"false"`**;
304
304
  - **`HASURA_GRAPHQL_ENABLE_TELEMETRY`** зі значенням **`"false"`**;
305
305
  - **`HASURA_GRAPHQL_ENABLED_LOG_TYPES`** зі значенням **`"startup,http-log"`** (точний рядок);
306
+ - **`HASURA_GRAPHQL_ENABLED_APIS`** зі значенням **`"metadata,graphql,pgdump"`** (точний рядок) — **значення для base/dev**;
306
307
  - **`HASURA_GRAPHQL_DISABLE_EVENTING`** — ключ обов'язковий, значення довільне (за замовчуванням **`"true"`**).
307
308
 
308
309
  Точні умови перевірки — rego-пакет **`k8s.hasura_configmap`** (cross-file прив'язка ConfigMap↔Deployment — у `rules/k8s/js/manifests.mjs`).
@@ -313,9 +314,25 @@ data:
313
314
  HASURA_GRAPHQL_ENABLE_RELAY: 'false'
314
315
  HASURA_GRAPHQL_ENABLE_TELEMETRY: 'false'
315
316
  HASURA_GRAPHQL_ENABLED_LOG_TYPES: 'startup,http-log'
317
+ HASURA_GRAPHQL_ENABLED_APIS: 'metadata,graphql,pgdump'
316
318
  HASURA_GRAPHQL_DISABLE_EVENTING: 'true'
317
319
  ```
318
320
 
321
+ ### `HASURA_GRAPHQL_ENABLED_APIS` поза base/dev
322
+
323
+ `pgdump` дозволено **лише** для **base**/**dev**. Кожен **не-base** overlay (`k8s/<env>/`, де `<env>` ≠ `base`/`dev`), що успадковує Hasura-base, **зобов'язаний** у своєму **`kustomization.yaml`** перевизначити `HASURA_GRAPHQL_ENABLED_APIS` до **`"metadata,graphql"`** (без `pgdump`) — патчем JSON6902 або Strategic Merge на ціль **ConfigMap**. Перевірка — cross-file у `rules/k8s/js/manifests.mjs` (`validateHasuraOverlayEnabledApisOverride`); `kind: Component` пропускається.
324
+
325
+ ```yaml title="k8s/prod/kustomization.yaml"
326
+ patches:
327
+ - target:
328
+ kind: ConfigMap
329
+ name: db-h
330
+ patch: |
331
+ - op: replace
332
+ path: /data/HASURA_GRAPHQL_ENABLED_APIS
333
+ value: metadata,graphql
334
+ ```
335
+
319
336
  ## Kustomize: структура каталогів (`base` / overlays)
320
337
 
321
338
  Трансформуй дерева **`**/k8s`**, щоб **винести спільне** через [Kustomize](https://kustomize.io/): один канонічний **`base`** і тонкі **overlays** для інших середовищ.
@@ -6,6 +6,9 @@
6
6
  # "HASURA_GRAPHQL_ENABLE_RELAY" → "false"
7
7
  # "HASURA_GRAPHQL_ENABLE_TELEMETRY" → "false"
8
8
  # "HASURA_GRAPHQL_ENABLED_LOG_TYPES" → "startup,http-log" (точний рядок)
9
+ # "HASURA_GRAPHQL_ENABLED_APIS" → "metadata,graphql,pgdump" (точний рядок;
10
+ # це значення для base/dev. Не-base overlay-и мають зводити його до "metadata,graphql"
11
+ # патчем у kustomization.yaml — перевіряє JS `validateHasuraOverlayEnabledApisOverride`)
9
12
  # "HASURA_GRAPHQL_DISABLE_EVENTING" → null (ключ обов'язковий,
10
13
  # значення довільне; за замовчуванням рекомендовано "true")
11
14
  #
@@ -39,6 +42,7 @@ required_env := {
39
42
  "HASURA_GRAPHQL_ENABLE_RELAY": "false",
40
43
  "HASURA_GRAPHQL_ENABLE_TELEMETRY": "false",
41
44
  "HASURA_GRAPHQL_ENABLED_LOG_TYPES": "startup,http-log",
45
+ "HASURA_GRAPHQL_ENABLED_APIS": "metadata,graphql,pgdump",
42
46
  "HASURA_GRAPHQL_DISABLE_EVENTING": null,
43
47
  }
44
48
 
@@ -33,8 +33,9 @@ export function diffFromBase(base, run, cwd) {
33
33
  }
34
34
 
35
35
  /**
36
- * Промпт adversarial-рецензента (читає ЛИШЕ diff). Для high-risk додає
37
- * безпекову лінзу.
36
+ * Промпт adversarial-рецензента. Фокус diff, але рецензент працює у робочій теці
37
+ * репо й має інструмент `Read`, тож cross-file твердження мусить верифікувати читанням.
38
+ * Для high-risk додає безпекову лінзу.
38
39
  * @param {string} diff текст diff
39
40
  * @param {string} [risk] low|med|high — фокус перевірки
40
41
  * @returns {string} промпт
@@ -45,11 +46,19 @@ export function reviewerPrompt(diff, risk) {
45
46
  ? 'ОСОБЛИВА УВАГА БЕЗПЕЦІ: auth/доступи, секрети/токени, ін\'єкції, валідація входу, незворотні операції.'
46
47
  : ''
47
48
  return [
48
- 'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells ЛИШЕ в цьому diff.',
49
+ 'Ти — прискіпливий adversarial-рецензент. Знайди баги, ризики й smells, які ВНОСИТЬ або зачіпає цей diff.',
50
+ 'Якщо тобі доступний інструмент Read — ти в робочій теці репо: читай ТОЧКОВО потрібні referenced-файли' +
51
+ ' (викликану функцію, інший модуль, spec/plan, конфіг), щоб ПЕРЕВІРИТИ cross-file твердження перед репортом.' +
52
+ ' Якщо Read недоступний — рецензуй лише diff.',
53
+ 'Сусідні файли читай ДЛЯ КОНТЕКСТУ й верифікації, а не щоб шукати в них окремі преіснуючі баги:' +
54
+ ' репортуй лише те, що вносить/ламає цей diff.',
55
+ 'НЕ видавай нефальсифіковних findings виду «з diff не видно / не показано / можливо» —' +
56
+ ' або підтверди читанням файлу, або відкинь. Кожен finding має бути перевірним фактом.',
49
57
  lens,
50
58
  'Поверни ЛИШЕ JSON-масив: [{ "severity": "high|med|low", "file": "...", "issue": "...", "suggestion": "..." }].',
51
59
  'Якщо проблем нема — поверни [].',
52
60
  '',
61
+ 'DIFF (фокус рецензування):',
53
62
  diff.slice(0, DIFF_LIMIT)
54
63
  ]
55
64
  .filter(Boolean)