@nitra/cursor 1.27.3 → 1.27.6

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 (43) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +1 -1
  2. package/.claude-template/hooks/normalize-decisions.sh +1 -1
  3. package/.pi-template/extensions/n-cursor-adr/tsconfig.json +1 -0
  4. package/CHANGELOG.md +61 -32
  5. package/bin/n-cursor.js +2 -2
  6. package/package.json +1 -1
  7. package/rules/bun/bun.mdc +1 -1
  8. package/rules/bun/policy/package_json/package_json.rego +14 -4
  9. package/rules/changelog/js/consistency.mjs +1 -1
  10. package/rules/image-avif/js/avif_generation.mjs +2 -2
  11. package/rules/js-lint/js-lint.mdc +1 -1
  12. package/rules/js-mssql/js/deps.mjs +1 -1
  13. package/rules/k8s/js/manifests.mjs +19 -19
  14. package/rules/k8s/k8s.mdc +9 -9
  15. package/rules/k8s/policy/network_policy/network_policy.rego +5 -5
  16. package/rules/tauri/js/cargo_mutants_config.mjs +3 -3
  17. package/rules/tauri/tauri.mdc +2 -2
  18. package/rules/test/coverage/coverage.mjs +4 -4
  19. package/rules/test/js/cargo_mutants_config.mjs +2 -2
  20. package/rules/test/js/data/cargo_mutants_config/mutants.toml.baseline +1 -1
  21. package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +1 -1
  22. package/rules/test/js/stryker_config.mjs +3 -3
  23. package/rules/test/test.mdc +5 -5
  24. package/rules/text/js/forbidden-prettier.mjs +59 -0
  25. package/rules/text/js/formatting.mjs +1 -4
  26. package/rules/text/policy/package_json/package_json.rego +16 -0
  27. package/rules/text/text.mdc +1 -1
  28. package/rules/vue/vue.mdc +1 -1
  29. package/schemas/v8r-catalog.json +6 -0
  30. package/scripts/coverage-fix.mjs +12 -12
  31. package/scripts/lib/run-lint-cli.mjs +5 -5
  32. package/scripts/lib/run-lint-step.mjs +1 -1
  33. package/scripts/lib/run-rule.mjs +1 -1
  34. package/scripts/lib/timing-summary.mjs +3 -3
  35. package/scripts/post-tool-use-fix.mjs +4 -4
  36. package/scripts/sync-claude-config.mjs +2 -2
  37. package/scripts/utils/ensure-gitignore-entries.mjs +2 -2
  38. package/scripts/utils/resolve-cargo-manifest.mjs +1 -1
  39. package/scripts/utils/resolve-js-root.mjs +1 -1
  40. package/scripts/utils/walkDir.mjs +2 -2
  41. package/skills/coverage-fix/SKILL.md +15 -12
  42. package/skills/fix-tests/SKILL.md +13 -13
  43. /package/rules/k8s/policy/network_policy/template/{statefulset.snippet.yaml → stateful-set.snippet.yaml} +0 -0
@@ -46,7 +46,7 @@
46
46
  * (**`HTTPRoute`**, **`GRPCRoute`**, **`TCPRoute`**, **`TLSRoute`**, **`UDPRoute`**, група **`gateway.networking.k8s.io`**)
47
47
  * посилання **`backendRefs` / `backendRef`** на **Service** мають вказувати лише сервіси з суфіксом **`-hl`** у **`name`**.
48
48
  * Поле **`namespace`** у **`backendRef`**, що збігається з **`metadata.namespace`** самого маршруту, — надлишкове:
49
- * прибери його, бо за замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут (див. k8s.mdc).
49
+ * прибери його, бо за замовчуванням Gateway API визначає backend у тому ж namespace, що й маршрут (див. k8s.mdc).
50
50
  * **HealthCheckPolicy** (**`networking.gke.io/v1`**, GKE): **`spec.targetRef`** на **Service** — **`name`** з суфіксом **`-hl`** (див. k8s.mdc).
51
51
  * Якщо **`kustomization.yaml`** посилається на **`svc.yaml`** (**`resources`**, **`bases`**, **`components`**, **`crds`**,
52
52
  * **`patches[].path`**, **`patchesStrategicMerge`**), у **тому ж** файлі має бути посилання на відповідний **`svc-hl.yaml`**
@@ -73,7 +73,7 @@
73
73
  *
74
74
  * **Зайві `group` / `version` у `patches[].target` / `patchesJson6902[].target`:** якщо в інвентарі **`resources`** /
75
75
  * **`bases`** / **`components`** / **`crds`** (рекурсивно) за **`kind`** + **`name`** немає колізії між різними
76
- * API-групами/версіями, поля **`group`** і **`version`** у **`target`** треба прибрати — Kustomize резолвить ціль
76
+ * API-групами/версіями, поля **`group`** і **`version`** у **`target`** треба прибрати — Kustomize визначає ціль
77
77
  * за **GVK + name**, а зайві поля ламаються мовчки під час змін API (k8s.mdc «patches[].target: лише kind і name»).
78
78
  *
79
79
  * Явні винятки до загальної логіки yannh/datree — таблиця **`EXPLICIT_K8S_SCHEMAS`** (`Map`): ключ
@@ -761,7 +761,7 @@ export function kustomizePathRefsForExistenceCheck(obj) {
761
761
  * @param {string} kustDir каталог kustomization.yaml
762
762
  * @param {string} rootNorm нормалізований корінь
763
763
  * @param {(msg: string) => void} fail callback при помилці
764
- * @returns {Promise<void>} резолвиться по завершенню перевірки
764
+ * @returns {Promise<void>} визначається по завершенню перевірки
765
765
  */
766
766
  async function validateKustomizationRef(rel, r, kustDir, rootNorm, fail) {
767
767
  const target = resolve(kustDir, r.trim())
@@ -889,7 +889,7 @@ async function validateOneKustomizationSvcHlWithSvc(root, kustAbs, fail) {
889
889
  try {
890
890
  docs = parseAllDocuments(body)
891
891
  } catch {
892
- fail(`${rel}: не вдалося розпарсити YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
892
+ fail(`${rel}: не вдалося розібрати YAML для перевірки svc.yaml/svc-hl.yaml у kustomization (див. k8s.mdc)`)
893
893
  return
894
894
  }
895
895
  const first = docs[0]?.toJSON()
@@ -1703,7 +1703,7 @@ async function validatePatchTargetsOneKustomizationFile(root, kustAbs, rootNorm,
1703
1703
  try {
1704
1704
  docs = parseAllDocuments(body)
1705
1705
  } catch {
1706
- fail(`${rel}: не вдалося розпарсити YAML для перевірки patch target`)
1706
+ fail(`${rel}: не вдалося розібрати YAML для перевірки patch target`)
1707
1707
  return
1708
1708
  }
1709
1709
  const first = docs[0]?.toJSON()
@@ -1833,7 +1833,7 @@ function updateBackendConfigKindFlags(kind, acc) {
1833
1833
  /**
1834
1834
  * Чи всі нетривіальні документи у тілі — **`kind: BackendConfig`**, чи є змішування з іншими kind.
1835
1835
  * @param {string} body YAML без обов’язкового modeline (див. `k8sYamlBodyForDocumentParse`)
1836
- * @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розпарсити YAML
1836
+ * @returns {'none' | 'only' | 'mixed' | 'unparsed'} unparsed — не вдалося розібрати YAML
1837
1837
  */
1838
1838
  export function classifyBackendConfigManifestPresence(body) {
1839
1839
  /**
@@ -2928,7 +2928,7 @@ export function collectGatewayApiRouteBackendServiceNames(spec) {
2928
2928
  /**
2929
2929
  * Збирає **`backendRef`** до **Service** з полем **`namespace`**, що збігається з namespace маршруту.
2930
2930
  *
2931
- * Поле **`namespace`** у такому **`backendRef`** надлишкове: за замовчуванням Gateway API резолвить backend
2931
+ * Поле **`namespace`** у такому **`backendRef`** надлишкове: за замовчуванням Gateway API визначає backend
2932
2932
  * у тому ж namespace, що й сам маршрут (див. k8s.mdc). Зайві поля у YAML — джерело розсинхрону між середовищами.
2933
2933
  * @param {unknown} spec значення **`spec`** маршруту
2934
2934
  * @param {string} routeNs **`metadata.namespace`** маршруту (непорожній рядок)
@@ -4245,7 +4245,7 @@ export function pdbManifestViolations(manifest, expectedAppLabel, isDevLike) {
4245
4245
 
4246
4246
  const NETWORK_POLICY_SNIPPET_URLS = {
4247
4247
  deployment: new URL('../policy/network_policy/template/deployment.snippet.yaml', import.meta.url),
4248
- statefulset: new URL('../policy/network_policy/template/statefulset.snippet.yaml', import.meta.url)
4248
+ statefulSet: new URL('../policy/network_policy/template/stateful-set.snippet.yaml', import.meta.url)
4249
4249
  }
4250
4250
 
4251
4251
  /** @type {Record<string, Record<string, unknown>>} */
@@ -4255,7 +4255,7 @@ const _snippetCache = {}
4255
4255
  * Читає snippet-файл і повертає розпарсений spec. Результат кешується в пам'яті процесу.
4256
4256
  * Кожен snippet — повний самодостатній канон NetworkPolicy для своєї групи workload-типів
4257
4257
  * (без merge між snippets у runtime).
4258
- * @param {'deployment' | 'statefulset'} snippetName ім'я сніпету
4258
+ * @param {'deployment' | 'statefulSet'} snippetName ім'я snippet
4259
4259
  * @returns {{ podSelector?: Record<string, unknown>, policyTypes?: string[], ingress?: unknown[], egress?: unknown[] }} розпарсений spec
4260
4260
  */
4261
4261
  export function loadSnippetSpec(snippetName) {
@@ -4270,20 +4270,20 @@ export function loadSnippetSpec(snippetName) {
4270
4270
  /**
4271
4271
  * Mapping workload-kind → snippet name. Єдине джерело dispatch'а в JS;
4272
4272
  * rego використовує симетричний mapping через анотацію `nitra.dev/workload-kind`.
4273
- * @type {Record<string, 'deployment' | 'statefulset'>}
4273
+ * @type {Record<string, 'deployment' | 'statefulSet'>}
4274
4274
  */
4275
4275
  export const KIND_TO_SNIPPET = {
4276
4276
  Deployment: 'deployment',
4277
4277
  Job: 'deployment',
4278
4278
  CronJob: 'deployment',
4279
4279
  DaemonSet: 'deployment',
4280
- StatefulSet: 'statefulset'
4280
+ StatefulSet: 'statefulSet'
4281
4281
  }
4282
4282
 
4283
4283
  /**
4284
4284
  * Обирає snippet name для конкретного workload-kind; throws на невідомий.
4285
4285
  * @param {string} kind workload-kind
4286
- * @returns {'deployment' | 'statefulset'} snippet name
4286
+ * @returns {'deployment' | 'statefulSet'} snippet name
4287
4287
  */
4288
4288
  export function snippetNameForKind(kind) {
4289
4289
  const name = KIND_TO_SNIPPET[kind]
@@ -4325,7 +4325,7 @@ const NETWORK_POLICY_GCLB_INGRESS_FROM = Object.freeze([
4325
4325
  /**
4326
4326
  * Канонічний YAML **NetworkPolicy** для workload з іменем `deployName`, міткою `app` і типом `kind`.
4327
4327
  * Snippet обирається за `kind` через `KIND_TO_SNIPPET` (без merge — кожен snippet самодостатній).
4328
- * Анотація `nitra.dev/workload-kind` додається, щоб rego диспатчив на правильний канон.
4328
+ * Анотація `nitra.dev/workload-kind` додається, щоб rego обрав на правильний канон.
4329
4329
  *
4330
4330
  * Якщо `gclbPorts` непорожній — після canon ingress-правил додається одне ingress-правило
4331
4331
  * з фіксованими CIDR-ами (GCP HC global + Envoy data-plane) і відсортованими унікальними TCP-портами
@@ -4407,7 +4407,7 @@ export async function collectHttpRouteIngressForWorkload(dir, appLabel, fail) {
4407
4407
  docs = parseAllDocuments(body)
4408
4408
  } catch (error) {
4409
4409
  const msg = error instanceof Error ? error.message : String(error)
4410
- fail(`${abs}: не вдалося розпарсити YAML для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
4410
+ fail(`${abs}: не вдалося розібрати YAML для GCLB ingress (HTTPRoute → NetworkPolicy mapping; k8s.mdc): ${msg}`)
4411
4411
  continue
4412
4412
  }
4413
4413
  for (const doc of docs) {
@@ -4952,7 +4952,7 @@ async function verifyOverlayHpaPdbFileRefsRespectBaseDeployment(
4952
4952
  * @param {boolean} anyBaseHasDep чи є Deployment у base
4953
4953
  * @param {(msg: string) => void} fail callback при помилці
4954
4954
  * @param {(msg: string) => void} passFn callback при успіху
4955
- * @returns {Promise<void>} резолвиться по завершенню перевірки
4955
+ * @returns {Promise<void>} визначається по завершенню перевірки
4956
4956
  */
4957
4957
  async function checkOverlayRefHpaPdb(root, kustDir, rel, ref, baseDirs, anyBaseHasDep, fail, passFn) {
4958
4958
  const fAbs = resolve(kustDir, ref.trim())
@@ -5324,7 +5324,7 @@ async function validateComponentsKustomizationManifest(componentsDir, components
5324
5324
  }
5325
5325
  const obj = await readFirstYamlObject(kustAbs)
5326
5326
  if (obj === null) {
5327
- fail(`${componentsRel}/kustomization.yaml: не вдалося розпарсити перший YAML-документ (k8s.mdc)`)
5327
+ fail(`${componentsRel}/kustomization.yaml: не вдалося розібрати перший YAML-документ (k8s.mdc)`)
5328
5328
  return
5329
5329
  }
5330
5330
  if (obj.apiVersion !== KUSTOMIZE_COMPONENT_API_VERSION) {
@@ -6178,7 +6178,7 @@ export async function convertImagePatchesToImagesInKustomization(kustAbs, rootNo
6178
6178
  /**
6179
6179
  * Парсить kustomization.yaml як Document і повертає його разом зі списком кандидатів-патчів
6180
6180
  * (по одному кандидату на кожну image-replace op у `patches[i].patch` — патч може містити кілька).
6181
- * Повертає null, якщо документ не розпарсився, не є Kustomization або не має масиву `patches:`.
6181
+ * Повертає null, якщо документ не розібрався, не є Kustomization або не має масиву `patches:`.
6182
6182
  * @param {string} raw текст файлу
6183
6183
  * @returns {{
6184
6184
  * doc: ReturnType<typeof parseDocument>,
@@ -6389,7 +6389,7 @@ function appendConvertedImagesNode(doc, conversions) {
6389
6389
 
6390
6390
  /**
6391
6391
  * Видаляє ops за списком індексів з inline `patch:` (текст YAML-масиву JSON6902-ops)
6392
- * і повертає переписаний текст. Зберігає block-style. Повертає null, якщо не вдалося розпарсити
6392
+ * і повертає переписаний текст. Зберігає block-style. Повертає null, якщо не вдалося розібрати
6393
6393
  * або після видалення не лишилось ops.
6394
6394
  * @param {string} patchText текст YAML-масиву ops (literal block scalar)
6395
6395
  * @param {number[]} opIndices індекси ops, які треба видалити
@@ -6747,7 +6747,7 @@ function runAllK8sRego(root, yamlFiles, fail) {
6747
6747
  files: allYaml,
6748
6748
  templateData: {
6749
6749
  deployment_snippet: loadSnippetSpec('deployment'),
6750
- statefulset_snippet: loadSnippetSpec('statefulset')
6750
+ stateful_set_snippet: loadSnippetSpec('statefulSet')
6751
6751
  }
6752
6752
  },
6753
6753
  { ns: 'k8s.kustomization', dir: 'k8s/kustomization', files: kustYaml },
package/rules/k8s/k8s.mdc CHANGED
@@ -33,7 +33,7 @@ alwaysApply: false
33
33
 
34
34
  **Версія Kubernetes для kubeconform** має відповідати PIN yannh у цьому правилі та в **`rules/k8s/fix.mjs`** (зараз **`-kubernetes-version 1.33.9`** — semver без префікса `v`, еквівалент релізу **v1.33.9**; набір схем **`v1.33.9-standalone-strict`**). Для CRD додатково підключай реєстр [datreeio/CRDs-catalog](https://github.com/datreeio/CRDs-catalog) другим **`-schema-location`**, як у [прикладах kubeconform](https://github.com/yannh/kubeconform#readme). За потреби **`-ignore-missing-schemas`**, якщо частина CRD ще без публічної схеми.
35
35
 
36
- **kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir>`** і передає stdout у **`kubescape scan <tmp-file>`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Маніфест проходить через тимчасовий файл, бо **`kubescape scan` у v4.x не читає stdin** (`-` як шлях → `no resources found to scan`; прапорця `--input`/`--stdin` немає); тимчасова директорія створюється під `os.tmpdir()` і прибирається після скану. Збірка через kustomize нормалізує namespace на workload-маніфестах і **NetworkPolicy у `base/networkpolicy.yaml`** (через `base/kustomization.yaml` `namespace:`), що дає коректний матчинг `podSelector` у `C-0260` (`Missing network policy`) і дозволяє kubescape бачити дерево overlays / components зі справжніми ресурсами. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
36
+ **kubescape — вхід через зібраний kustomize-маніфест:** для кожного dir-у з `kustomization.yaml` (`kind: Kustomization`; **`kind: Component`** пропускається — він не білдиться окремо) `lint-k8s` виконує **`kubectl kustomize <dir>`** і передає stdout у **`kubescape scan <tmp-file>`** з порогом **`--severity-threshold high`** (вбудована в kubectl підкоманда `kustomize` — окремий бінарник `kustomize` не потрібен; рендеринг локальний і не потребує доступу до кластера). Маніфест проходить через тимчасовий файл, бо **`kubescape scan` у v4.x не читає stdin** (`-` як шлях → `no resources found to scan`; прапорця `--input`/`--stdin` немає); тимчасова директорія створюється під `os.tmpdir()` і прибирається після скану. Збірка через kustomize нормалізує namespace на workload-manifest-файлах і **NetworkPolicy у `base/networkpolicy.yaml`** (через `base/kustomization.yaml` `namespace:`), що дає коректний матчинг `podSelector` у `C-0260` (`Missing network policy`) і дозволяє kubescape бачити дерево overlays / components зі справжніми ресурсами. Якщо в дереві **`…/k8s`** немає жодного `kustomization.yaml` (проєкт без Kustomize) — fallback на старий dir-скан **`kubescape scan <каталог-k8s>`**. Перший запуск kubescape може завантажувати артефакти — у CI потрібна мережа або [offline](https://github.com/kubescape/kubescape#readme). На відміну від kubeconform, у **kubescape scan** немає прапорця **`-kubernetes-version`**: перевірка йде за **framework/control** (NSA, MITRE, CIS тощо), а не проти OpenAPI-схеми конкретного релізу Kubernetes. **Орієнтир** для репозиторію той самий, що й для kubeconform — кластер **v1.33.9** (див. **`-kubernetes-version 1.33.9`** вище); для CIS і подібних наближень обирай актуальний framework під політику команди (**`kubescape list frameworks`**, див. [CLI reference](https://github.com/kubescape/kubescape/blob/master/docs/cli-reference.md)).
37
37
 
38
38
  ### Винятки kubescape: `.kubescape-exceptions.json`
39
39
 
@@ -121,7 +121,7 @@ resources:
121
121
  memory: '128Mi'
122
122
  ```
123
123
 
124
- **HPA і PDB у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/`, який підключають лише прод-overlays (див. розділ нижче). **NetworkPolicy** — навпаки: **обов'язковий і у `base/`**, у вигляді `base/networkpolicy.yaml` поруч з workload-маніфестом, підключений через `base/kustomization.yaml` `resources:` — щоб обмеження діяли і на dev-середовищі.
124
+ **HPA і PDB у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` поруч із workload-manifest-файлами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/`, який підключають лише прод-overlays (див. розділ нижче). **NetworkPolicy** — навпаки: **обов'язковий і у `base/`**, у вигляді `base/networkpolicy.yaml` поруч з workload-маніфестом, підключений через `base/kustomization.yaml` `resources:` — щоб обмеження діяли і на dev-середовищі.
125
125
 
126
126
  ### Поза base (оверлеї, окремі каталоги)
127
127
 
@@ -258,7 +258,7 @@ spec:
258
258
 
259
259
  ### Gateway API: не дублюй namespace у `backendRef`
260
260
 
261
- У маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**) у `spec.rules[*].backendRefs[*]` **не** додавай поле **`namespace`**, якщо його значення збігається з **`metadata.namespace`** самого маршруту. За замовчуванням Gateway API резолвить backend у тому ж namespace, що й маршрут, тож такий рядок — мертвий: він плутає під час перенесень між середовищами і ламається мовчки, якщо overlay змінює namespace маршруту через Kustomize, а в backendRef залишився старий рядок. Прибери поле — поведінка не зміниться. **`check k8s`** падає на такому збігу.
261
+ У маршрутах Gateway API (**HTTPRoute**, **GRPCRoute**, **TCPRoute**, **TLSRoute**, **UDPRoute**) у `spec.rules[*].backendRefs[*]` **не** додавай поле **`namespace`**, якщо його значення збігається з **`metadata.namespace`** самого маршруту. За замовчуванням Gateway API визначає backend у тому ж namespace, що й маршрут, тож такий рядок — мертвий: він плутає під час перенесень між середовищами і ламається мовчки, якщо overlay змінює namespace маршруту через Kustomize, а в backendRef залишився старий рядок. Прибери поле — поведінка не зміниться. **`check k8s`** падає на такому збігу.
262
262
 
263
263
  ```yaml
264
264
  # ❌ погано — namespace дублює metadata.namespace маршруту
@@ -329,7 +329,7 @@ data:
329
329
 
330
330
  ### Рядки, що змінюються між середовищами
331
331
 
332
- - У маніфестах у **`base`** для полів (або значень), які **будуть відрізнятися** в інших середовищах, на **тому самому рядку** додай коментар:
332
+ - У manifest-файлах у **`base`** для полів (або значень), які **будуть відрізнятися** в інших середовищах, на **тому самому рядку** додай коментар:
333
333
 
334
334
  ```yaml
335
335
  image: my-app:dev-tag # буде замінено через kustomize
@@ -358,7 +358,7 @@ images:
358
358
 
359
359
  **`check k8s` автоматично** для кожного `kustomization.yaml`:
360
360
 
361
- 1. конвертує кожну JSON6902-операцію `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:` (резолвить оригінальний image у base через `resources:` / `bases` / `components` / `crds`). Якщо у `patches[i].patch` після конвертації не залишилось ops — патч прибирається повністю; інакше у `patches[i].patch` залишаються лише не-image ops у вихідному порядку;
361
+ 1. конвертує кожну JSON6902-операцію `op: replace` на `/spec/template/spec/containers/<N>/image` (target `kind: Deployment`) у запис `images:` (визначає оригінальний image у base через `resources:` / `bases` / `components` / `crds`). Якщо у `patches[i].patch` після конвертації не залишилось ops — патч прибирається повністю; інакше у `patches[i].patch` залишаються лише не-image ops у вихідному порядку;
362
362
  2. чистить існуючий блок `images:` — зрізає `:tag` з `name` і видаляє `newTag`, який збігається з відрізаним тегом.
363
363
 
364
364
  ## Ingress → Gateway API (GKE)
@@ -393,7 +393,7 @@ images:
393
393
  - **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, **без** `metadata.namespace` (namespace задає kustomization-споживач), `spec.scaleTargetRef.name` **= `metadata.name`** Deployment з base, dev-like значення `minReplicas: 1`, `maxReplicas: 1`.
394
394
  - **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
395
395
 
396
- **Канонічний `base/networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace` (namespace додає `base/kustomization.yaml`); один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у тому ж `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns через `kube-system` namespaceSelector (UDP/TCP 53); link-local DNS `169.254.0.0/16` (UDP/TCP 53, GKE NodeLocal DNSCache); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні); **in-cluster** — `to.namespaceSelector: {}` з **явним списком TCP-портів** (`80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`; трафік на `*.svc` / Pod-и в кластері). **StatefulSet** додатково має egress/ingress `to/from.podSelector: {}` для intra-replica реплікації. Заборонено: `egress: [{}]`; `to.namespaceSelector: {}` без `ports:` (catch-all). Додаткові правила можна дописати поряд — superset-підхід. Канон — два **повних** snippet-файли (без merge у runtime; JS-генератор/rego обирають один за `kind` workload-у через анотацію `metadata.annotations['nitra.dev/workload-kind']`): [deployment.snippet.yaml](./policy/network_policy/template/deployment.snippet.yaml) (для `Deployment`/`Job`/`CronJob`/`DaemonSet`) та [statefulset.snippet.yaml](./policy/network_policy/template/statefulset.snippet.yaml) (для `StatefulSet`; містить deployment-канон + intra-replica правила). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
396
+ **Канонічний `base/networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace` (namespace додає `base/kustomization.yaml`); один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у тому ж `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns через `kube-system` namespaceSelector (UDP/TCP 53); link-local DNS `169.254.0.0/16` (UDP/TCP 53, GKE NodeLocal DNSCache); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні); **in-cluster** — `to.namespaceSelector: {}` з **явним списком TCP-портів** (`80, 443, 5432, 3306, 1433, 6379, 8080, 4317, 4318`; трафік на `*.svc` / Pod-и в кластері). **StatefulSet** додатково має egress/ingress `to/from.podSelector: {}` для intra-replica реплікації. Заборонено: `egress: [{}]`; `to.namespaceSelector: {}` без `ports:` (catch-all). Додаткові правила можна дописати поряд — superset-підхід. Канон — два **повних** snippet-файли (без merge у runtime; JS-генератор/rego обирають один за `kind` workload-у через анотацію `metadata.annotations['nitra.dev/workload-kind']`): [deployment.snippet.yaml](./policy/network_policy/template/deployment.snippet.yaml) (для `Deployment`/`Job`/`CronJob`/`DaemonSet`) та [stateful-set.snippet.yaml](./policy/network_policy/template/stateful-set.snippet.yaml) (для `StatefulSet`; містить deployment-канон + intra-replica правила). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
397
397
 
398
398
  Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
399
399
 
@@ -440,7 +440,7 @@ ingress:
440
440
  - { protocol: TCP, port: 8080 }
441
441
  ```
442
442
 
443
- Якщо workload не прив'язаний до жодного HTTPRoute — правило **не** додається; NP лишається baseline (intra-namespace + canon egress). Не-HTTP routes (`GRPCRoute`, `TCPRoute`, `TLSRoute`, `UDPRoute`) поки не покриті — додається лише за HTTPRoute. Алгоритм: функція `collectHttpRouteIngressForWorkload` у **`rules/k8s/js/manifests.mjs`** індексує `HTTPRoute.backendRefs` і `Service` у каталозі, резолвить через `selector.app`, дедуп TCP-портів. Виклики — з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile`.
443
+ Якщо workload не прив'язаний до жодного HTTPRoute — правило **не** додається; NP лишається baseline (intra-namespace + canon egress). Не-HTTP routes (`GRPCRoute`, `TCPRoute`, `TLSRoute`, `UDPRoute`) поки не покриті — додається лише за HTTPRoute. Алгоритм: функція `collectHttpRouteIngressForWorkload` у **`rules/k8s/js/manifests.mjs`** індексує `HTTPRoute.backendRefs` і `Service` у каталозі, визначає через `selector.app`, дедуп TCP-портів. Виклики — з `appendNetworkPolicyDocuments` і `regenerateLegacyNetworkPolicyDocsInFile`.
444
444
 
445
445
  ### Env-залежні межі (за сегментом після `/k8s/`)
446
446
 
@@ -687,7 +687,7 @@ spec:
687
687
 
688
688
  ## HorizontalPodAutoscaler: `autoscaling/v2`
689
689
 
690
- У маніфестах під **`k8s`** заборонено **`apiVersion: autoscaling/v1`** (legacy HPA з єдиною метрикою CPU). Мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**: поле **`spec.metrics`** (замість **`spec.targetCPUUtilizationPercentage`**) з **`type: Resource`** і **`target.type: Utilization`** / **`AverageUtilization`** — підтримує декілька метрик і зовнішні метрики. `check k8s` падає на будь-якому документі з **`apiVersion: autoscaling/v1`**.
690
+ У manifest-файлах під **`k8s`** заборонено **`apiVersion: autoscaling/v1`** (legacy HPA з єдиною метрикою CPU). Мігруй **HorizontalPodAutoscaler** на **`autoscaling/v2`**: поле **`spec.metrics`** (замість **`spec.targetCPUUtilizationPercentage`**) з **`type: Resource`** і **`target.type: Utilization`** / **`AverageUtilization`** — підтримує декілька метрик і зовнішні метрики. `check k8s` падає на будь-якому документі з **`apiVersion: autoscaling/v1`**.
691
691
 
692
692
  Ресурси **batch** (наприклад **CronJob**, **Job**): застаріле **`apiVersion: batch/v1beta1`** у файлах під **`k8s` під час `check k8s` переписується** на **`apiVersion: batch/v1`**.
693
693
 
@@ -729,7 +729,7 @@ patches:
729
729
 
730
730
  ### `patches[].target`: лише `kind` і `name`
731
731
 
732
- У `patches[].target` залишай **тільки** **`kind`** і **`name`** — поля **`group`** і **`version`** прибирай. Kustomize резолвить ціль за GVK+name; `group`/`version` — звужувальні фільтри, потрібні лише за реальної колізії `kind+name` між різними API-групами або версіями. У межах одного namespace apiserver зберігає об'єкт у єдиному storage-GVK, тож для звичайних маніфестів така колізія неможлива, і `group`/`version` у `target` — мертвий шум, який ламається мовчки під час змін API (наприклад, перехід `v1beta1` → `v1`).
732
+ У `patches[].target` залишай **тільки** **`kind`** і **`name`** — поля **`group`** і **`version`** прибирай. Kustomize визначає ціль за GVK+name; `group`/`version` — звужувальні фільтри, потрібні лише за реальної колізії `kind+name` між різними API-групами або версіями. У межах одного namespace apiserver зберігає об'єкт у єдиному storage-GVK, тож для звичайних маніфестів така колізія неможлива, і `group`/`version` у `target` — мертвий шум, який ламається мовчки під час змін API (наприклад, перехід `v1beta1` → `v1`).
733
733
 
734
734
  ```yaml
735
735
  # ❌ зайві group / version
@@ -4,7 +4,7 @@
4
4
  # Superset-перевірка egress/ingress: кожне правило з обраного canon-snippet'у
5
5
  # має бути присутнє в input (extra-правила дозволені). Канон обирається за
6
6
  # анотацією `nitra.dev/workload-kind`:
7
- # StatefulSet → data.template.statefulset_snippet (повний канон з intra-replica)
7
+ # StatefulSet → data.template.stateful_set_snippet (повний канон з intra-replica)
8
8
  # решта → data.template.deployment_snippet (повний канон, default fallback)
9
9
  #
10
10
  # Обидва snippets — самодостатні (без merge на runtime).
@@ -15,7 +15,7 @@
15
15
  # conftest test path/to/networkpolicy.yaml -p npm/rules/k8s/policy/network_policy \
16
16
  # --namespace k8s.network_policy \
17
17
  # --data npm/rules/k8s/policy/network_policy/template/deployment.snippet.yaml \
18
- # --data npm/rules/k8s/policy/network_policy/template/statefulset.snippet.yaml
18
+ # --data npm/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml
19
19
  package k8s.network_policy
20
20
 
21
21
  import rego.v1
@@ -107,14 +107,14 @@ deny contains "spec.egress: заборонено allow-all {} — додавай
107
107
  }
108
108
 
109
109
  # Dispatch на повний canon-snippet за анотацією nitra.dev/workload-kind.
110
- # StatefulSet → statefulset_snippet (з intra-replica), решта → deployment_snippet.
111
- canon_for_kind("StatefulSet") := data.template.statefulset_snippet
110
+ # StatefulSet → stateful_set_snippet (з intra-replica), решта → deployment_snippet.
111
+ canon_for_kind("StatefulSet") := data.template.stateful_set_snippet
112
112
 
113
113
  canon_for_kind(kind) := data.template.deployment_snippet if {
114
114
  kind != "StatefulSet"
115
115
  }
116
116
 
117
- snippet_name_for_kind("StatefulSet") := "statefulset"
117
+ snippet_name_for_kind("StatefulSet") := "stateful-set"
118
118
 
119
119
  snippet_name_for_kind(kind) := "deployment" if {
120
120
  kind != "StatefulSet"
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Концерн `cargo_mutants_config` правила tauri (tauri.mdc): для кожного
3
- * `<workspace>/src-tauri/Cargo.toml` ідемпотентно гарантує наявність
3
+ * `<workspace>/src-tauri/Cargo.toml` без дублювання гарантує наявність
4
4
  * Tauri-specific cargo-mutants налаштувань у `<workspace>/src-tauri/.cargo/mutants.toml`.
5
5
  *
6
6
  * Семантика (фіксована між Tauri-проєктами):
@@ -28,7 +28,7 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
28
28
  import { getMonorepoPackageRootDirs } from '../../../scripts/lib/workspaces.mjs'
29
29
 
30
30
  const TAURI_BASELINE_HEADER = `# .cargo/mutants.toml — Tauri canonical cargo-mutants config (tauri.mdc).
31
- # Виключаємо --bins і --doc щоб бінарник Tauri та doc-tests не перезбиралися
31
+ # Виключаємо --bins і --doc щоб бінарник Tauri та doc-tests не збиралися повторно
32
32
  # з нуля під кожного мутанта (секунди → хвилини).
33
33
  `
34
34
 
@@ -103,7 +103,7 @@ function buildBaseline() {
103
103
  }
104
104
 
105
105
  /**
106
- * Обробляє один `src-tauri/` каталог: створює або ідемпотентно доповнює `.cargo/mutants.toml`.
106
+ * Обробляє один `src-tauri/` каталог: створює або без дублювання доповнює `.cargo/mutants.toml`.
107
107
  * @param {string} srcTauriDir абсолютний шлях до `src-tauri/`
108
108
  * @param {string} cwd корінь проєкту (для relative-шляхів у репортах)
109
109
  * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter репортер концерну
@@ -29,7 +29,7 @@ version: '1.3'
29
29
 
30
30
  ## Mutation-testing: семантика app shell та platform bridge
31
31
 
32
- `tauri` rule володіє Tauri-specific семантикою mutation-testing для каталога `src-tauri/`. Концерн `js/cargo_mutants_config.mjs` ідемпотентно додає у `<ws>/src-tauri/.cargo/mutants.toml` такі канонічні ключі:
32
+ `tauri` rule володіє Tauri-specific семантикою mutation-testing для каталога `src-tauri/`. Концерн `js/cargo_mutants_config.mjs` без дублювання додає у `<ws>/src-tauri/.cargo/mutants.toml` такі канонічні ключі:
33
33
 
34
34
  ```toml title="<ws>/src-tauri/.cargo/mutants.toml"
35
35
  additional_cargo_test_args = ["--lib", "--tests"]
@@ -58,7 +58,7 @@ exclude_globs = [
58
58
 
59
59
  ### Ідемпотентність і взаємодія з `test`-rule
60
60
 
61
- - `test` rule створює універсальний нейтральний `.cargo/mutants.toml` (порожній з коментом) для кожного Cargo.toml-manifesta — без framework-specific exclude'ів. Це наш baseline.
61
+ - `test` rule створює універсальний нейтральний `.cargo/mutants.toml` (порожній з коментарем) для кожного Cargo.toml-manifest-файла — без framework-specific exclude'ів. Це наш baseline.
62
62
  - `tauri` rule додає Tauri-канонічні ключі **поверх** того, що вже є у `<ws>/src-tauri/.cargo/mutants.toml`:
63
63
  - якщо файла немає — створює з повного Tauri-baseline;
64
64
  - якщо обидва канонічні ключі (`additional_cargo_test_args`, `exclude_globs`) вже присутні — `manual cargo-mutants config preserved`, нічого не зміниться;
@@ -71,7 +71,7 @@ export function formatScore({ caught, total }) {
71
71
  /**
72
72
  * Рендерить таблицю покриття + мутаційного тестування як Markdown.
73
73
  * Якщо будь-який рядок містить непустий `survived`, додає секцію
74
- * `## Вижилі мутанти` з JSON-блоком для `/n-fix-tests`.
74
+ * `## Вцілілі мутанти` з JSON-блоком для `/n-fix-tests`.
75
75
  * Без timestamp, щоб git diff рухався лише при зміні метрик.
76
76
  * @param {Array<{area:string, coverage:{lines:{covered:number,total:number},functions:{covered:number,total:number}}, mutation:{caught:number,total:number}, survived?: Array<{file:string,line:number,col:number,mutantType:string,original:string,replacement:string}>}>} rows рядки провайдерів
77
77
  * @returns {string} Markdown з заголовком `# Coverage`
@@ -92,8 +92,8 @@ export function renderMarkdown(rows) {
92
92
 
93
93
  const allSurvived = rows.flatMap(r => r.survived ?? [])
94
94
  if (allSurvived.length > 0) {
95
- lines.push('', '## Вижилі мутанти', '', '```json', JSON.stringify(allSurvived, null, 2), '```')
96
- // Людиночитабельна таблиця
95
+ lines.push('', '## Вцілілі мутанти', '', '```json', JSON.stringify(allSurvived, null, 2), '```')
96
+ // Зрозуміла для людини таблиця
97
97
  for (const group of allSurvived) {
98
98
  lines.push('', `### ${group.file}`, '', '| Рядок | Оригінал | Заміна | Тип |', '| --- | --- | --- | --- |')
99
99
  for (const m of group.mutants) {
@@ -156,7 +156,7 @@ function buildTotalsRow(rows) {
156
156
  * Виконує coverage-pipeline: discovery провайдерів за `.n-cursor.json#rules`,
157
157
  * detect+collect для кожного, агрегація, запис COVERAGE.md.
158
158
  * При `opts.fix === true` після запису COVERAGE.md запускає агента (coverage-fix.mjs)
159
- * для написання тестів по вижилих мутантах.
159
+ * для написання тестів по вцілілих мутантах.
160
160
  * @param {{cwd?:string, rulesDir?:string, fix?:boolean}} [opts] ін'єкція cwd/rulesDir для тестів; fix — --fix режим
161
161
  * @returns {Promise<number>} exit code (0 OK, 1 коли жоден провайдер не дав даних)
162
162
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Концерн `cargo_mutants_config` правила test (test.mdc): якщо `rust` присутнє
3
- * в `.n-cursor.json#rules` і не у `disable-rules` — резолвить ВСІ Cargo.toml
3
+ * в `.n-cursor.json#rules` і не у `disable-rules` — визначає ВСІ Cargo.toml
4
4
  * (cwd і всі workspaces, з підтримкою Tauri-патерну) і копіює canonical
5
5
  * baseline `.cargo/mutants.toml` у каталог кожного manifest'а, якщо файлу немає.
6
6
  *
@@ -8,7 +8,7 @@
8
8
  * Якщо `rust` enabled, але жодного Cargo.toml не знайдено — теж silently skip
9
9
  * (manifest може з'явитися пізніше; це не помилка).
10
10
  *
11
- * Baseline — порожній файл з коментом; cargo-mutants має робочі defaults.
11
+ * Baseline — порожній файл з коментарем; cargo-mutants має робочі defaults.
12
12
  */
13
13
  import { existsSync } from 'node:fs'
14
14
  import { copyFile, mkdir } from 'node:fs/promises'
@@ -2,6 +2,6 @@
2
2
  # Цей baseline нейтральний: він не робить припущень про framework/app shell,
3
3
  # не виключає platform glue, generated wrappers або binary entrypoints.
4
4
  # Framework-specific tuning (Tauri, Capacitor тощо) належить відповідним
5
- # правилам — вони ідемпотентно доповнюють цей файл, не перетирають його.
5
+ # правилам — вони без дублювання доповнюють цей файл, не перетирають його.
6
6
  # cargo-mutants має робочі defaults; цей файл — стартова точка для customization.
7
7
  # Документація: https://mutants.rs/
@@ -6,7 +6,7 @@ export default {
6
6
  // швидкості проти command runner (де треба було б ганяти ввесь test-suite на кожен мутант).
7
7
  coverageAnalysis: 'perTest',
8
8
  // concurrency: за замовч. Stryker обирає os.cpus().length - 1.
9
- // inPlace більше не потрібен — vitest-runner ізолює мутантів у пам'яті через AST-патчінг,
9
+ // inPlace більше не потрібен — vitest-runner ізолює мутантів у пам'яті через AST-patching,
10
10
  // без копіювання node_modules у sandbox (стара проблема command runner у Bun monorepo).
11
11
  tempDirName: 'reports/stryker/.tmp',
12
12
  reporters: ['json', 'clear-text'],
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Концерн `stryker_config` правила test (test.mdc): якщо `js-lint` присутнє в
3
- * `.n-cursor.json#rules` і не у `disable-rules` — резолвить ВСІ JS-roots
3
+ * `.n-cursor.json#rules` і не у `disable-rules` — визначає ВСІ JS-roots
4
4
  * (всі workspaces з package.json, або cwd у single-package) і копіює canonical
5
5
  * baseline `stryker.config.mjs` + `vitest.config.js` у кожен root, де файлу немає.
6
6
  *
@@ -36,7 +36,7 @@ const STRYKER_GITIGNORE_ENTRIES = ['**/reports/stryker/']
36
36
  * @param {string} cwd корінь проєкту (для relative-шляхів у логах)
37
37
  * @param {string} baselinePath абсолютний шлях до canonical baseline
38
38
  * @param {string} target абсолютний шлях, куди копіювати
39
- * @param {string} label людиночитна мітка ("stryker.config.mjs" / "vitest.config.js")
39
+ * @param {string} label зрозуміла для людини мітка ("stryker.config.mjs" / "vitest.config.js")
40
40
  * @returns {Promise<void>}
41
41
  */
42
42
  async function ensureBaselineFile(reporter, cwd, baselinePath, target, label) {
@@ -85,7 +85,7 @@ export async function check() {
85
85
  await ensureBaselineFile(reporter, cwd, VITEST_BASELINE_PATH, join(jsRoot, 'vitest.config.js'), 'vitest.config.js')
86
86
  }
87
87
 
88
- // Гарантуємо що Stryker temp/output ніколи не комітяться. Patterns
88
+ // Гарантуємо що Stryker temp/output ніколи не потрапляють у commit. Patterns
89
89
  // покривають усі workspaces через `**/`-префікс (єдиний root .gitignore).
90
90
  const { added } = await ensureGitignoreEntries(cwd, STRYKER_GITIGNORE_ENTRIES, 'Stryker mutation testing (test.mdc)')
91
91
  if (added.length > 0) {
@@ -78,7 +78,7 @@ Recursive globs ловлять файли всередині `tests/` так с
78
78
 
79
79
  ### Vitest baseline та `package.json#scripts`
80
80
 
81
- Поряд зі Stryker концерн `stryker_config` ідемпотентно копіює `vitest.config.js` (теж тільки якщо файлу немає). Canonical: [vitest.config.baseline.js](./js/data/vitest_config/vitest.config.baseline.js) — `environment: 'node'`, `coverage.provider: 'v8'` з lcov+text-summary репортами, `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']` (підхоплює обидві розкладки — тести у `tests/`-піддиректоріях і top-level integration suites у `<root>/tests/`).
81
+ Поряд зі Stryker концерн `stryker_config` без дублювання копіює `vitest.config.js` (теж тільки якщо файлу немає). Canonical: [vitest.config.baseline.js](./js/data/vitest_config/vitest.config.baseline.js) — `environment: 'node'`, `coverage.provider: 'v8'` з lcov+text-summary репортами, `include: ['**/*.test.{js,mjs}', 'tests/**/*.test.{js,mjs}']` (підхоплює обидві розкладки — тести у `tests/`-піддиректоріях і top-level integration suites у `<root>/tests/`).
82
82
 
83
83
  У `package.json#scripts` має бути `"test": "vitest run"` (canonical contains-substring `vitest` — допустимо `vitest run` та інші локальні розширення); опційно — `"test:watch": "vitest"`.
84
84
 
@@ -86,7 +86,7 @@ Recursive globs ловлять файли всередині `tests/` так с
86
86
 
87
87
  ### Frontend-варіант (Vue/Vite + happy-dom)
88
88
 
89
- Для проєктів зі своїм `vite.config.js` `vitest.config.js` має реюзати vite-плагіни та aliases і перемкнути `environment` на `'happy-dom'` (або `'jsdom'`):
89
+ Для проєктів зі своїм `vite.config.js` `vitest.config.js` має повторно використовувати vite-плагіни та aliases і перемкнути `environment` на `'happy-dom'` (або `'jsdom'`):
90
90
 
91
91
  ```js
92
92
  import { defineConfig, mergeConfig } from 'vitest/config'
@@ -113,11 +113,11 @@ Customization (mutate patterns, exclude rules, timeout) — відповідал
113
113
 
114
114
  Правило `test` відповідає лише за **універсальний baseline** mutation-testing:
115
115
 
116
- - створює `.cargo/mutants.toml` для Rust crates (порожній, з коментом);
116
+ - створює `.cargo/mutants.toml` для Rust crates (порожній, з коментарем);
117
117
  - не робить припущень про framework/app shell;
118
118
  - не виключає platform glue, generated wrappers або binary entrypoints;
119
119
  - framework-specific tuning (`--lib`/`--tests`, `exclude_globs` для app-shell і platform bridge файлів) належить відповідним правилам (`tauri`, `capacitor` тощо).
120
120
 
121
- Якщо інше правило спеціалізує mutation-behavior — воно зобов'язане **доповнювати** існуючий `.cargo/mutants.toml` ідемпотентно (додавати лише відсутні ключі) і **не перетирати** ручні налаштування. Послідовний запуск `npx @nitra/cursor fix test` після `fix tauri` не має скидати tauri-tuning, і навпаки — повторний `fix tauri` не дублює секції.
121
+ Якщо інше правило спеціалізує mutation-behavior — воно зобов'язане **доповнювати** існуючий `.cargo/mutants.toml` без дублювання (додавати лише відсутні ключі) і **не перетирати** ручні налаштування. Послідовний запуск `npx @nitra/cursor fix test` після `fix tauri` не має скидати tauri-tuning, і навпаки — повторний `fix tauri` не дублює секції.
122
122
 
123
- Додатково: коли `js-lint` enabled, концерн `stryker_config` ідемпотентно додає у кореневий `.gitignore` патерн `**/reports/stryker/` — увесь каталог Stryker-output-у (backup'и `tempDirName`, `mutation.json`, HTML/dashboard-репорти якщо додасте інші reporter-и). Це запобігає випадковому коміту build-артефактів.
123
+ Додатково: коли `js-lint` enabled, концерн `stryker_config` без дублювання додає у кореневий `.gitignore` патерн `**/reports/stryker/` — увесь каталог Stryker-output-у (backup'и `tempDirName`, `mutation.json`, HTML/dashboard-репорти якщо додасте інші reporter-и). Це запобігає випадковому коміту build-артефактів.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Suspect FS-перевірка: жоден Prettier-артефакт у корені проєкту не дозволений.
3
+ *
4
+ * `text.mdc` забороняє `prettier`, `@nitra/prettier-config` і всі прояви Prettier-конфігів.
5
+ * Rego-полісі `text.package_json` ловить scripts/dependencies/devDependencies; цей concern
6
+ * ловить FS-сторону — конфіги й ignore-файли, які runner Prettier зчитує автоматично.
7
+ *
8
+ * Список синхронізовано з конфіг-форматами Prettier 3.x
9
+ * (https://prettier.io/docs/configuration). Якщо Prettier додасть новий формат — додай рядок.
10
+ */
11
+ import { existsSync } from 'node:fs'
12
+
13
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
14
+
15
+ /** Файли, які Prettier шукає у корені; всі заборонені (text.mdc). */
16
+ const FORBIDDEN_PRETTIER_FILES = [
17
+ '.prettierignore',
18
+ '.prettierrc',
19
+ '.prettierrc.json',
20
+ '.prettierrc.jsonc',
21
+ '.prettierrc.json5',
22
+ '.prettierrc.yaml',
23
+ '.prettierrc.yml',
24
+ '.prettierrc.toml',
25
+ '.prettierrc.js',
26
+ '.prettierrc.cjs',
27
+ '.prettierrc.mjs',
28
+ '.prettierrc.ts',
29
+ '.prettierrc.cts',
30
+ '.prettierrc.mts',
31
+ 'prettier.config.js',
32
+ 'prettier.config.cjs',
33
+ 'prettier.config.mjs',
34
+ 'prettier.config.ts',
35
+ 'prettier.config.cts',
36
+ 'prettier.config.mts'
37
+ ]
38
+
39
+ /**
40
+ * Перевіряє, що жоден Prettier-конфіг чи ignore-файл не лежить у корені проєкту.
41
+ * @returns {number} 0 — все OK, 1 — знайдено заборонений файл
42
+ */
43
+ export function check() {
44
+ const reporter = createCheckReporter()
45
+ const { pass, fail } = reporter
46
+
47
+ let anyFound = false
48
+ for (const file of FORBIDDEN_PRETTIER_FILES) {
49
+ if (existsSync(file)) {
50
+ fail(`${file} заборонено — Prettier не використовуємо, перейди на oxfmt (text.mdc)`)
51
+ anyFound = true
52
+ }
53
+ }
54
+ if (!anyFound) {
55
+ pass('Prettier-конфігів і .prettierignore немає в корені')
56
+ }
57
+
58
+ return reporter.getExitCode()
59
+ }
@@ -7,7 +7,6 @@
7
7
  * `.vscode/settings.json` (`editor.formatOnSave`, `[lang].editor.defaultFormatter`);
8
8
  * - наявність FS-файлів `.oxfmtrc.json`, `.cspell.json`, `.markdownlint-cli2.jsonc`,
9
9
  * `package.json` (саме *існування* — структуру вже валідує Rego);
10
- * - конфіги Prettier у корені (заборонено — FS);
11
10
  * - абзац про український апостроф у `.cursor/rules/n-text.mdc` /
12
11
  * `npm/mdc/text.mdc` (markdown-текст, не JSON/YAML);
13
12
  * - складна валідація скрипта `lint-text` (cspell, markdownlint, v8r у трьох
@@ -176,9 +175,7 @@ export async function check() {
176
175
  await checkV8rIgnore(pass, fail)
177
176
  await checkTextConfigsExistence(pass, fail)
178
177
 
179
- for (const f of ['.prettierrc', '.prettierrc.json', '.prettierrc.js', 'prettier.config.js', '.prettierrc.yml']) {
180
- if (existsSync(f)) fail(`Знайдено конфіг prettier: ${f} — видали його`)
181
- }
178
+ // Prettier-конфіги/ignore окремий concern `text.forbidden-prettier` (rules/text/js/forbidden-prettier.mjs).
182
179
 
183
180
  const textRulePaths = ['.cursor/rules/n-text.mdc', 'npm/mdc/text.mdc'].filter(p => existsSync(p))
184
181
  if (textRulePaths.length === 0) {
@@ -42,9 +42,25 @@ deny contains msg if {
42
42
  msg := sprintf("@nitra/cspell-dict має бути ^2.0.0 або новіший (зараз %q) (text.mdc)", [range])
43
43
  }
44
44
 
45
+ # Будь-який scripts.* з токеном `prettier` (наприклад `bunx prettier`, `npx prettier`,
46
+ # `prettier --write`) — заборонено: каноном форматування є oxfmt (text.mdc).
47
+ deny contains msg if {
48
+ some name, cmd in object.get(input, "scripts", {})
49
+ is_string(cmd)
50
+ script_invokes_prettier(cmd)
51
+ msg := sprintf("package.json: scripts.%s містить prettier — використовуй oxfmt (text.mdc)", [name])
52
+ }
53
+
45
54
  cspell_dict_major_at_least_2(range) if {
46
55
  match := regex.find_n(`^[\^~>=<]*\s*(\d+)`, range, 1)
47
56
  count(match) > 0
48
57
  major := to_number(regex.replace(match[0], `^[\^~>=<]*\s*`, ""))
49
58
  major >= 2
50
59
  }
60
+
61
+ # Token-based, щоб уникнути false-positive на словах типу `not-prettier` чи
62
+ # `prettier-ignore` всередині інших ідентифікаторів. Виконавчий runner: команда
63
+ # або шлях, що закінчується на `prettier`, або `prettier` як аргумент CLI.
64
+ script_invokes_prettier(cmd) if {
65
+ regex.match(`(^|[\s/"'])prettier($|[\s'"@])`, cmd)
66
+ }
@@ -262,4 +262,4 @@ kebab-case
262
262
 
263
263
  ## Перевірка
264
264
 
265
- `npx @nitra/cursor fix text` (охоплює oxfmt, cspell, shellcheck у `lint-text`, markdownlint, v8r, CI для `lint-text`)
265
+ `npx @nitra/cursor fix text` (охоплює oxfmt, cspell, shellcheck у `lint-text`, markdownlint, v8r, CI для `lint-text`). Зокрема падає, якщо у корені є `.prettierignore`, `.prettierrc*`, `prettier.config.*` або у `package.json#scripts` зустрічається команда з `prettier` — Prettier повністю заборонено, форматування лише через **oxfmt**.
package/rules/vue/vue.mdc CHANGED
@@ -104,7 +104,7 @@ const additionalInstructions = `
104
104
 
105
105
  ### Тестування
106
106
 
107
- - **Unit + Component / DOM:** **Vitest** (`vitest`) + **Vue Test Utils** з **happy-dom** як DOM-середовищем. Це канон, узгоджений з `test.mdc` (Stryker з vitest-runner + `perTest`-аналіз покриття). `vitest.config.js` реюзає `vite.config.js` через `mergeConfig` і перемикає `environment` на `'happy-dom'`:
107
+ - **Unit + Component / DOM:** **Vitest** (`vitest`) + **Vue Test Utils** з **happy-dom** як DOM-середовищем. Це канон, узгоджений з `test.mdc` (Stryker з vitest-runner + `perTest`-аналіз покриття). `vitest.config.js` повторно використовує `vite.config.js` через `mergeConfig` і перемикає `environment` на `'happy-dom'`:
108
108
 
109
109
  ```js title="vitest.config.js"
110
110
  import { defineConfig, mergeConfig } from 'vitest/config'