@nitra/cursor 1.13.34 → 1.13.38

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 (27) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/package.json +1 -1
  3. package/rules/changelog/fix/consistency/check.mjs +100 -85
  4. package/rules/ci4/ci4.mdc +7 -7
  5. package/rules/js-lint/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  6. package/rules/js-run/fix/runtime/check.mjs +3 -0
  7. package/rules/js-run/js-run.mdc +16 -1
  8. package/rules/js-run/policy/package_json/package_json.rego +17 -0
  9. package/rules/js-run/policy/package_json/template/package.json.deny.json +13 -1
  10. package/rules/k8s/fix/manifests/check.mjs +775 -139
  11. package/rules/k8s/k8s.mdc +52 -6
  12. package/rules/k8s/policy/base_kustomization/base_kustomization.rego +13 -6
  13. package/rules/k8s/policy/network_policy/network_policy.rego +158 -0
  14. package/rules/k8s/policy/network_policy/template/networkpolicy.snippet.yaml +32 -0
  15. package/rules/security/fix/trufflehog/check.mjs +3 -0
  16. package/rules/security/policy/package_json/template/package.json.snippet.json +5 -1
  17. package/rules/text/policy/cspell/cspell.rego +1 -1
  18. package/rules/text/policy/markdownlint/markdownlint.rego +1 -1
  19. package/rules/text/policy/oxfmtrc/template/.oxfmtrc.json.snippet.json +1 -5
  20. package/rules/text/policy/vscode_extensions/template/extensions.json.snippet.json +1 -5
  21. package/rules/vue/vue.mdc +1 -0
  22. package/scripts/sync-claude-config.mjs +2 -2
  23. package/scripts/utils/check-mdc-template-refs.mjs +15 -5
  24. package/scripts/utils/inline-template-links.mjs +15 -8
  25. package/scripts/utils/package-manifest.mjs +24 -19
  26. package/scripts/utils/run-conftest-batch.mjs +22 -15
  27. package/scripts/utils/template.mjs +89 -21
package/rules/k8s/k8s.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: K8s YAML — $schema (yaml-language-server); lint-k8s (kubeconform, kubescape); check-k8s
3
- version: '1.31'
3
+ version: '1.34'
4
4
  globs: "**/k8s/**/*.yaml"
5
5
  alwaysApply: false
6
6
  ---
@@ -118,7 +118,7 @@ resources:
118
118
  memory: '128Mi'
119
119
  ```
120
120
 
121
- **HPA і PDB у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` поруч із `Deployment`, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
121
+ **HPA, PDB і NetworkPolicy у base не тримаємо**: ні локальних `hpa.yaml` / `pdb.yaml` / `networkpolicy.yaml` поруч із workload-маніфестами, ні через `resources` / `components` / `bases`. Канон — sibling каталог **`components/`** (Kustomize Component) поруч з `base/` (див. розділ нижче).
122
122
 
123
123
  ### Поза base (оверлеї, окремі каталоги)
124
124
 
@@ -380,12 +380,15 @@ images:
380
380
 
381
381
  ## Deployment: `topologySpreadConstraints`, HPA / PDB через `components/`
382
382
 
383
- Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB також **не дозволені** через `resources` / `components` / `bases`.
383
+ Для **кожного** `kind: Deployment` у каталозі **`…/k8s/…/base/`** (у будь-якому файлі `.yaml`, наприклад **`deploy.yaml`**, **`deployment.yaml`**) сам Deployment має канонічні **`spec.template.spec.topologySpreadConstraints`**, а **HPA і PDB** живуть у **sibling каталозі** **`…/k8s/…/components/`** (Kustomize Component, фіксована назва каталогу — `components`). У `base/` локальні `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` **заборонені** (file-existence error). У дереві base-kustomize HPA / PDB / NetworkPolicy також **не дозволені** через `resources` / `components` / `bases`.
384
+
385
+ Для **кожного** з **`Deployment`**, **`StatefulSet`**, **`DaemonSet`**, **`Job`**, **`CronJob`** під `k8s` обов'язковий **NetworkPolicy**: у **`…/k8s/…/base/`** — у **`components/networkpolicy.yaml`** (multi-doc, якщо workload-ів кілька); у **не-base** — **`networkpolicy.yaml`** поруч із маніфестом workload у тому ж каталозі. `metadata.name` NetworkPolicy **= `metadata.name`** workload; `spec.podSelector.matchLabels.app` **= мітка `app`** з `spec.selector.matchLabels` (для **CronJob** — з `spec.jobTemplate.spec.selector.matchLabels`). Відсутні документи **`check k8s`** створює автоматично.
384
386
 
385
387
  **Канонічна структура `<pkg>/k8s/components/`** (sibling до `base/`):
386
388
 
387
- - **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, pdb.yaml]`.
389
+ - **`kustomization.yaml`** — `apiVersion: kustomize.config.k8s.io/v1alpha1`, `kind: Component`, `resources: [hpa.yaml, networkpolicy.yaml, pdb.yaml]` (відсортовано за алфавітом).
388
390
  - **`hpa.yaml`** — `autoscaling/v2`, `HorizontalPodAutoscaler`, **без** `metadata.namespace` (namespace задає kustomization-споживач), `spec.scaleTargetRef.name` **= `metadata.name`** Deployment з base, dev-like значення `minReplicas: 1`, `maxReplicas: 1`.
391
+ - **`networkpolicy.yaml`** — `networking.k8s.io/v1`, `NetworkPolicy`, **без** `metadata.namespace`; один або кілька документів (`---`) — по одному на кожен workload (**Deployment** / **StatefulSet** / **DaemonSet** / **Job** / **CronJob**) у sibling `base/`; `metadata.name` **= `metadata.name`** workload, `spec.podSelector.matchLabels.app` **= мітка `app`** workload. **Ingress:** `from.podSelector: {}` (інші Pod у namespace). **Egress (усі workload-и):** kube-dns (UDP/TCP 53); **TCP 80 і 443** на `0.0.0.0/0` (HTTP/HTTPS назовні, включно з metadata `169.254.169.254:80`); **інші порти** — лише in-cluster через `to.namespaceSelector: {}` (трафік на `*.svc` / Pod-и в кластері; Postgres лише `*.svc`, без Cloud SQL). Заборонено `egress: [{}]`. Канон: [networkpolicy.snippet.yaml](./policy/network_policy/template/networkpolicy.snippet.yaml). Якщо документа для workload немає — **`check k8s`** дописує його автоматично.
389
392
  - **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, **без** `metadata.namespace`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment, dev-like `minAvailable: 0`.
390
393
 
391
394
  Інші назви каталогу (`scale/`, `hpa-component/`, `pdb-component/`) — fail.
@@ -394,13 +397,14 @@ images:
394
397
 
395
398
  **`<pkg>/k8s/components/kustomization.yaml`** має `kind: Component` (не `kind: Kustomization`) — це **джерело** канонічних HPA/PDB для всіх overlays, а не overlay сам по собі. Прод-перезаписи (`/spec/minReplicas`, `/spec/maxReplicas`, `/spec/minAvailable`) живуть у `<env>/kustomization.yaml`, що підключає Component через `components:`. У самому Component patches не потрібні — він env-неутральний; **`check k8s`** не вимагає прод-патчів від `components/kustomization.yaml`.
396
399
 
397
- У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремий **`hpa.yaml`** / **`pdb.yaml`**, якщо такі потрібні для цього середовища. **`check k8s`** звіряє прив'язку за іменами:
400
+ У **не-base** оверлеях (без `components/`) поруч із `Deployment` лишається звична схема: окремі **`hpa.yaml`**, **`networkpolicy.yaml`** і **`pdb.yaml`**, якщо такі потрібні для цього середовища. Для **StatefulSet**, **DaemonSet**, **Job**, **CronJob** у не-base — обов'язковий лише **`networkpolicy.yaml`** (HPA/PDB лишаються прив'язаними до **Deployment**, якщо є). **`check k8s`** звіряє прив'язку за іменами (і **дописує** відсутні NetworkPolicy-документи автоматично):
398
401
 
399
402
  - **`hpa.yaml`** (поза **`…/base/`**) — `autoscaling/v2`, `HorizontalPodAutoscaler`, `spec.scaleTargetRef.name` **= `metadata.name`** Deployment.
403
+ - **`networkpolicy.yaml`** — той самий канон egress/ingress, що в `components/` (multi-doc, якщо у каталозі кілька workload-ів).
400
404
  - **`pdb.yaml`** — `policy/v1`, `PodDisruptionBudget`, `spec.selector.matchLabels.app` **= `spec.selector.matchLabels.app`** Deployment.
401
405
  - **`topologySpreadConstraints`** — запис з `maxSkew: 1`, `topologyKey: kubernetes.io/hostname`, `whenUnsatisfiable: ScheduleAnyway`, `labelSelector.matchLabels.app` рівне тій самій мітці `app`.
402
406
 
403
- **Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
407
+ **Перевірка структури `components/`** (для кожного Deployment у `base/`): наявність каталогу, валідний `kustomization.yaml` як Component, `hpa.yaml`, `networkpolicy.yaml` і `pdb.yaml` з відповідністю до Deployment-name / app-label. Алгоритм — функція `validateComponentsForBaseDeployment` у **`check-k8s.mjs`**.
404
408
 
405
409
  **Локальні шляхи в `kustomization.yaml`:** кожен запис без `://` (remote) з `resources` / `bases` / `components` / `crds`, `patchesStrategicMerge`, `patches[].path`, `patchesJson6902[].path`, `configurations[]`, `replacements[].path` має вказувати на **існуючий** у репозиторії файл (`.yaml` / `.yml`) або **каталог**; биті посилання — помилка **`check k8s`**.
406
410
 
@@ -462,9 +466,51 @@ apiVersion: kustomize.config.k8s.io/v1alpha1
462
466
  kind: Component
463
467
  resources:
464
468
  - hpa.yaml
469
+ - networkpolicy.yaml
465
470
  - pdb.yaml
466
471
  ```
467
472
 
473
+ ```yaml title="k8s/components/networkpolicy.yaml"
474
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/networkpolicy-networking-k8s-io-v1.json
475
+ apiVersion: networking.k8s.io/v1
476
+ kind: NetworkPolicy
477
+ metadata:
478
+ name: backend-api
479
+ spec:
480
+ podSelector:
481
+ matchLabels:
482
+ app: backend-api
483
+ policyTypes:
484
+ - Ingress
485
+ - Egress
486
+ ingress:
487
+ - from:
488
+ - podSelector: {}
489
+ egress:
490
+ - to:
491
+ - namespaceSelector:
492
+ matchLabels:
493
+ kubernetes.io/metadata.name: kube-system
494
+ podSelector:
495
+ matchLabels:
496
+ k8s-app: kube-dns
497
+ ports:
498
+ - protocol: UDP
499
+ port: 53
500
+ - protocol: TCP
501
+ port: 53
502
+ - to:
503
+ - ipBlock:
504
+ cidr: 0.0.0.0/0
505
+ ports:
506
+ - protocol: TCP
507
+ port: 80
508
+ - protocol: TCP
509
+ port: 443
510
+ - to:
511
+ - namespaceSelector: {}
512
+ ```
513
+
468
514
  ```yaml title="k8s/components/hpa.yaml"
469
515
  # yaml-language-server: $schema=https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/v1.33.9-standalone-strict/horizontalpodautoscaler-autoscaling-v2.json
470
516
  apiVersion: autoscaling/v2
@@ -41,16 +41,16 @@ deny contains base_namespace_required_msg if {
41
41
  # (із зануренням у вкладені kustomization.yaml) — JS-оркестратор
42
42
  # `verifyK8sBaseKustomizeHasNoHpaPdb` у `check-k8s.mjs` (потребує fs-доступу). Цей
43
43
  # rego-deny — defense-in-depth: спрацює навіть якщо JS-крок упаде з винятку раніше.
44
- deny contains hpa_pdb_in_base_resources_msg(r) if {
44
+ deny contains hpa_pdb_np_in_base_resources_msg(r) if {
45
45
  is_kustomization
46
46
  some r in object.get(input, "resources", [])
47
47
  is_string(r)
48
- is_hpa_or_pdb_filename(r)
48
+ is_hpa_pdb_or_np_filename(r)
49
49
  }
50
50
 
51
- hpa_pdb_in_base_resources_msg(file) := sprintf(
51
+ hpa_pdb_np_in_base_resources_msg(file) := sprintf(
52
52
  concat("", [
53
- "у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB заборонені у base, ",
53
+ "у base/kustomization.yaml `resources:` містить '%v' — HPA/PDB/NetworkPolicy заборонені у base, ",
54
54
  "перенесіть у sibling каталог components/ і підключайте з overlay (k8s.mdc)",
55
55
  ]),
56
56
  [file],
@@ -61,8 +61,15 @@ is_kustomization if {
61
61
  startswith(object.get(input, "apiVersion", ""), "kustomize.config.k8s.io/")
62
62
  }
63
63
 
64
- is_hpa_or_pdb_filename(p) if {
65
- basename(p) in {"hpa.yaml", "pdb.yaml", "hpa.yml", "pdb.yml"}
64
+ is_hpa_pdb_or_np_filename(p) if {
65
+ basename(p) in {
66
+ "hpa.yaml",
67
+ "pdb.yaml",
68
+ "networkpolicy.yaml",
69
+ "hpa.yml",
70
+ "pdb.yml",
71
+ "networkpolicy.yml",
72
+ }
66
73
  }
67
74
 
68
75
  basename(p) := parts[count(parts) - 1] if {
@@ -0,0 +1,158 @@
1
+ # Пер-документна структурна перевірка NetworkPolicy (k8s.mdc).
2
+ # Cross-file (metadata.name = workload, podSelector.app = мітка app) — JS
3
+ # (`networkPolicyManifestViolations`, `validateNetworkPolicyForWorkload`).
4
+ #
5
+ # Канон egress: kube-dns; TCP 80/443 на 0.0.0.0/0; інші порти — namespaceSelector: {}
6
+ # (in-cluster, зокрема *.svc). Заборонено egress: [{}] (allow-all).
7
+ #
8
+ # Запуск:
9
+ # conftest test path/to/networkpolicy.yaml -p npm/policy/k8s/network_policy \
10
+ # --namespace k8s.network_policy
11
+ package k8s.network_policy
12
+
13
+ import rego.v1
14
+
15
+ np_kind_template := "kind має бути NetworkPolicy (зараз: %v) (k8s.mdc)"
16
+
17
+ np_api_template := "apiVersion має бути networking.k8s.io/v1 (зараз: %v) (k8s.mdc)"
18
+
19
+ deny contains msg if {
20
+ is_np_doc
21
+ input.kind != "NetworkPolicy"
22
+ msg := sprintf(np_kind_template, [input.kind])
23
+ }
24
+
25
+ deny contains msg if {
26
+ is_np_doc
27
+ input.apiVersion != "networking.k8s.io/v1"
28
+ msg := sprintf(np_api_template, [input.apiVersion])
29
+ }
30
+
31
+ deny contains "spec відсутній або некоректний (NetworkPolicy; k8s.mdc)" if {
32
+ is_np_doc
33
+ not is_object(object.get(input, "spec", null))
34
+ }
35
+
36
+ deny contains "spec.podSelector.matchLabels відсутній (NetworkPolicy; k8s.mdc)" if {
37
+ is_np_doc
38
+ spec := object.get(input, "spec", null)
39
+ is_object(spec)
40
+ selector := object.get(spec, "podSelector", null)
41
+ is_object(selector)
42
+ not is_object(object.get(selector, "matchLabels", null))
43
+ }
44
+
45
+ deny contains "spec.policyTypes має містити Ingress і Egress (NetworkPolicy; k8s.mdc)" if {
46
+ is_np_doc
47
+ spec := object.get(input, "spec", null)
48
+ is_object(spec)
49
+ types := object.get(spec, "policyTypes", [])
50
+ not policy_types_has_ingress_and_egress(types)
51
+ }
52
+
53
+ deny contains "spec.ingress має містити from.podSelector (NetworkPolicy; k8s.mdc)" if {
54
+ is_np_doc
55
+ spec := object.get(input, "spec", null)
56
+ is_object(spec)
57
+ not ingress_has_pod_selector_rule(spec)
58
+ }
59
+
60
+ deny contains "spec.egress має бути непорожнім масивом (NetworkPolicy; k8s.mdc)" if {
61
+ is_np_doc
62
+ spec := object.get(input, "spec", null)
63
+ is_object(spec)
64
+ not is_non_empty_array(object.get(spec, "egress", null))
65
+ }
66
+
67
+ deny contains "spec.egress: заборонено allow-all {} — канон k8s.mdc (80/443 назовні, інше — in-cluster)" if {
68
+ is_np_doc
69
+ spec := object.get(input, "spec", null)
70
+ is_object(spec)
71
+ egress_allows_all(object.get(spec, "egress", null))
72
+ }
73
+
74
+ deny contains "spec.egress: потрібен ipBlock 0.0.0.0/0 з ports 80 і 443 (HTTP/HTTPS назовні; k8s.mdc)" if {
75
+ is_np_doc
76
+ spec := object.get(input, "spec", null)
77
+ is_object(spec)
78
+ not egress_has_internet_http_https(spec)
79
+ }
80
+
81
+ deny contains "spec.egress: потрібен to.namespaceSelector: {} (інші порти лише in-cluster / *.svc; k8s.mdc)" if {
82
+ is_np_doc
83
+ spec := object.get(input, "spec", null)
84
+ is_object(spec)
85
+ not egress_has_cluster_namespace_selector(spec)
86
+ }
87
+
88
+ is_np_doc if input.kind == "NetworkPolicy"
89
+
90
+ is_np_doc if startswith(object.get(input, "apiVersion", ""), "networking.k8s.io/")
91
+
92
+ policy_types_has_ingress_and_egress(types) if {
93
+ is_array(types)
94
+ "Ingress" in types
95
+ "Egress" in types
96
+ }
97
+
98
+ ingress_has_pod_selector_rule(spec) if {
99
+ ingress := object.get(spec, "ingress", null)
100
+ is_array(ingress)
101
+ some rule in ingress
102
+ is_object(rule)
103
+ from_list := object.get(rule, "from", null)
104
+ is_array(from_list)
105
+ some peer in from_list
106
+ is_object(peer)
107
+ object.get(peer, "podSelector", null) != null
108
+ }
109
+
110
+ is_non_empty_array(x) if {
111
+ is_array(x)
112
+ count(x) > 0
113
+ }
114
+
115
+ egress_allows_all(egress) if {
116
+ is_array(egress)
117
+ some rule in egress
118
+ is_object(rule)
119
+ count(object.keys(rule)) == 0
120
+ }
121
+
122
+ egress_has_internet_http_https(spec) if {
123
+ egress := object.get(spec, "egress", null)
124
+ is_array(egress)
125
+ some rule in egress
126
+ is_object(rule)
127
+ to_list := object.get(rule, "to", null)
128
+ is_array(to_list)
129
+ some peer in to_list
130
+ is_object(peer)
131
+ ipb := object.get(peer, "ipBlock", null)
132
+ is_object(ipb)
133
+ object.get(ipb, "cidr", "") == "0.0.0.0/0"
134
+ ports := object.get(rule, "ports", null)
135
+ is_array(ports)
136
+ egress_ports_include(ports, 80)
137
+ egress_ports_include(ports, 443)
138
+ }
139
+
140
+ egress_ports_include(ports, want) if {
141
+ some p in ports
142
+ is_object(p)
143
+ object.get(p, "port", null) == want
144
+ }
145
+
146
+ egress_has_cluster_namespace_selector(spec) if {
147
+ egress := object.get(spec, "egress", null)
148
+ is_array(egress)
149
+ some rule in egress
150
+ is_object(rule)
151
+ to_list := object.get(rule, "to", null)
152
+ is_array(to_list)
153
+ some peer in to_list
154
+ is_object(peer)
155
+ ns := object.get(peer, "namespaceSelector", null)
156
+ is_object(ns)
157
+ count(ns) == 0
158
+ }
@@ -0,0 +1,32 @@
1
+ spec:
2
+ podSelector:
3
+ matchLabels: {}
4
+ policyTypes:
5
+ - Ingress
6
+ - Egress
7
+ ingress:
8
+ - from:
9
+ - podSelector: {}
10
+ egress:
11
+ - to:
12
+ - namespaceSelector:
13
+ matchLabels:
14
+ kubernetes.io/metadata.name: kube-system
15
+ podSelector:
16
+ matchLabels:
17
+ k8s-app: kube-dns
18
+ ports:
19
+ - protocol: UDP
20
+ port: 53
21
+ - protocol: TCP
22
+ port: 53
23
+ - to:
24
+ - ipBlock:
25
+ cidr: 0.0.0.0/0
26
+ ports:
27
+ - protocol: TCP
28
+ port: 80
29
+ - protocol: TCP
30
+ port: 443
31
+ - to:
32
+ - namespaceSelector: {}
@@ -17,6 +17,9 @@ import { checkTextSubset } from '../../../../scripts/utils/template.mjs'
17
17
  const HERE = dirname(fileURLToPath(import.meta.url))
18
18
  const SNIPPET_PATH = join(HERE, 'template', '.trufflehog-exclude.snippet.txt')
19
19
 
20
+ /**
21
+ * @returns {Promise<number>} exit-код перевірки
22
+ */
20
23
  export async function check() {
21
24
  const reporter = createCheckReporter()
22
25
  const { pass, fail } = reporter
@@ -1 +1,5 @@
1
- { "scripts": { "lint-security": "trufflehog filesystem . --no-update --exclude-paths .trufflehog-exclude --results=verified,unknown --fail" } }
1
+ {
2
+ "scripts": {
3
+ "lint-security": "trufflehog filesystem . --no-update --exclude-paths .trufflehog-exclude --results=verified,unknown --fail"
4
+ }
5
+ }
@@ -53,9 +53,9 @@ deny contains msg if {
53
53
  # ── deny: import substrings forbidden ────────────────────────────────────
54
54
 
55
55
  deny contains msg if {
56
- some forbidden, reason in data.template.deny["import-substrings"]
57
56
  imports := object.get(input, "import", [])
58
57
  is_array(imports)
58
+ some forbidden, reason in data.template.deny["import-substrings"]
59
59
  some imp in imports
60
60
  is_string(imp)
61
61
  contains(imp, forbidden)
@@ -47,7 +47,7 @@ deny contains msg if {
47
47
  msg := sprintf(".markdownlint-cli2.jsonc: %s.%s.%s має бути %v (text.mdc)", [section, inner_key, leaf, expected])
48
48
  }
49
49
 
50
- # ── deny: vкладеного обʼєкта взагалі немає ──────────────────────────────
50
+ # ── deny: вкладеного обʼєкта взагалі немає ──────────────────────────────
51
51
 
52
52
  deny contains msg if {
53
53
  some section, expected_inner in data.template.snippet
@@ -4,9 +4,5 @@
4
4
  "tabWidth": 2,
5
5
  "useTabs": false,
6
6
  "printWidth": 120,
7
- "ignorePatterns": [
8
- "**/hasura/metadata/**",
9
- "**/schema.graphql",
10
- "**/auto-imports.d.ts"
11
- ]
7
+ "ignorePatterns": ["**/hasura/metadata/**", "**/schema.graphql", "**/auto-imports.d.ts"]
12
8
  }
@@ -1,7 +1,3 @@
1
1
  {
2
- "recommendations": [
3
- "DavidAnson.vscode-markdownlint",
4
- "oxc.oxc-vscode",
5
- "timonwong.shellcheck"
6
- ]
2
+ "recommendations": ["DavidAnson.vscode-markdownlint", "oxc.oxc-vscode", "timonwong.shellcheck"]
7
3
  }
package/rules/vue/vue.mdc CHANGED
@@ -118,6 +118,7 @@ GlobalRegistrator.register()
118
118
  ```
119
119
 
120
120
  `jsdom` не використовуй — happy-dom швидший і достатній для типових Vue-компонентних тестів.
121
+
121
122
  - **E2E:** **Playwright** змістовні сценарії користувацьких потоків.
122
123
 
123
124
  ### Інструменти (узгоджено з Vite і цим правилом)
@@ -84,7 +84,7 @@ const ADR_NORMALIZE_STOP_HOOK_GROUP = Object.freeze({
84
84
  /** Канонічний Cursor stop-hook для ADR capture. Cursor передає payload через stdin JSON. */
85
85
  const CURSOR_ADR_STOP_HOOK = Object.freeze({
86
86
  command: [
87
- "bash -lc 'root=\"$PWD\";",
87
+ 'bash -lc \'root="$PWD";',
88
88
  `if [ ! -f "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
89
89
  `bash "$root/${CURSOR_ADR_HOOK_COMMAND_MARKER}"'`
90
90
  ].join(' '),
@@ -94,7 +94,7 @@ const CURSOR_ADR_STOP_HOOK = Object.freeze({
94
94
  /** Канонічний Cursor stop-hook для ADR normalize. */
95
95
  const CURSOR_ADR_NORMALIZE_STOP_HOOK = Object.freeze({
96
96
  command: [
97
- "bash -lc 'root=\"$PWD\";",
97
+ 'bash -lc \'root="$PWD";',
98
98
  `if [ ! -f "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ] && [ -f "$root/../${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}" ]; then root="$root/.."; fi;`,
99
99
  `bash "$root/${CURSOR_ADR_NORMALIZE_HOOK_COMMAND_MARKER}"'`
100
100
  ].join(' '),
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Returns list of template/ files that are NOT referenced in <id>.mdc as
3
3
  * markdown link targets. Paths returned are relative to ruleDir.
4
- *
5
- * @param {string} ruleDir absolute path to npm/rules/<id>/
6
- * @param {string} ruleId basename (e.g. "security")
7
- * @returns {Promise<string[]>}
8
4
  */
9
5
  import { existsSync } from 'node:fs'
10
6
  import { readdir, readFile, stat } from 'node:fs/promises'
11
7
  import { join, relative } from 'node:path'
12
8
 
9
+ /**
10
+ * @param {string} ruleDir абсолютний шлях до каталогу правила
11
+ * @returns {Promise<string[]>} абсолютні шляхи всіх файлів у template/
12
+ */
13
13
  async function walkTemplateDirs(ruleDir) {
14
14
  const out = []
15
15
  for (const kind of ['fix', 'policy']) {
@@ -18,13 +18,18 @@ async function walkTemplateDirs(ruleDir) {
18
18
  for (const concern of await readdir(kindDir)) {
19
19
  const tpl = join(kindDir, concern, 'template')
20
20
  if (!existsSync(tpl)) continue
21
- if (!(await stat(tpl)).isDirectory()) continue
21
+ const tplStat = await stat(tpl)
22
+ if (!tplStat.isDirectory()) continue
22
23
  out.push(...(await collectFiles(tpl)))
23
24
  }
24
25
  }
25
26
  return out.map(p => relative(ruleDir, p))
26
27
  }
27
28
 
29
+ /**
30
+ * @param {string} dir каталог для обходу
31
+ * @returns {Promise<string[]>} абсолютні шляхи знайдених файлів
32
+ */
28
33
  async function collectFiles(dir) {
29
34
  const out = []
30
35
  for (const entry of await readdir(dir, { withFileTypes: true })) {
@@ -35,6 +40,11 @@ async function collectFiles(dir) {
35
40
  return out
36
41
  }
37
42
 
43
+ /**
44
+ * @param {string} ruleDir абсолютний шлях до npm/rules/<id>/
45
+ * @param {string} ruleId basename правила (напр. "security")
46
+ * @returns {Promise<string[]>} відносні шляхи template-файлів без посилань у .mdc
47
+ */
38
48
  export async function findMissingMdcRefs(ruleDir, ruleId) {
39
49
  const mdcPath = join(ruleDir, `${ruleId}.mdc`)
40
50
  if (!existsSync(mdcPath)) return []
@@ -2,10 +2,14 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { basename, extname, join } from 'node:path'
4
4
 
5
- const TEMPLATE_LINK_RE = /\[([^\]]+)\]\((\.\/[^)]*\/template\/[^)]+)\)/g
5
+ const MD_LINK_RE = /\[([^\]]{1,200})\]\((\.\/[^)]{1,500})\)/g
6
+ const TEMPLATE_SEGMENT_RE = /\/template\//
6
7
  const SLOTS = ['snippet', 'deny', 'contains']
7
8
 
8
- /** @param {string} filePath */
9
+ /**
10
+ * @param {string} filePath шлях до файлу
11
+ * @returns {string} назва мови для fenced-блока
12
+ */
9
13
  function langFromExt(filePath) {
10
14
  const ext = extname(filePath)
11
15
  if (ext === '.json') return 'json'
@@ -16,10 +20,13 @@ function langFromExt(filePath) {
16
20
 
17
21
  // Strip `.<slot>.<ext>` suffix (slot ∈ snippet/deny/contains) to recover the
18
22
  // real target file name (e.g. `package.json.snippet.json` → `package.json`).
19
- /** @param {string} fileBasename */
23
+ /**
24
+ * @param {string} fileBasename базове ім'я template-файлу
25
+ * @returns {string} ім'я реального target-файлу
26
+ */
20
27
  function normalizeTargetName(fileBasename) {
21
28
  for (const slot of SLOTS) {
22
- const m = fileBasename.match(new RegExp(`^(.+)\\.${slot}\\.[^.]+$`))
29
+ const m = fileBasename.match(new RegExp(String.raw`^(.+)\.${slot}\.[^.]+$`))
23
30
  if (m) return m[1]
24
31
  }
25
32
  return fileBasename
@@ -29,19 +36,18 @@ function normalizeTargetName(fileBasename) {
29
36
  * Finds markdown links whose path contains /template/ and replaces them with
30
37
  * inline fenced blocks. Reads file from join(ruleDir, rel-path).
31
38
  * Throws Error if a matched link target doesn't exist (fail loud — user must know).
32
- *
33
39
  * @param {string} text .mdc file contents
34
40
  * @param {string} ruleDir absolute path to the rule directory (e.g. .../npm/rules/security/)
35
41
  * @returns {Promise<string>} transformed text
36
42
  */
37
43
  export async function inlineTemplateLinks(text, ruleDir) {
38
- const matches = [...text.matchAll(TEMPLATE_LINK_RE)]
44
+ const matches = [...text.matchAll(MD_LINK_RE)].filter(m => TEMPLATE_SEGMENT_RE.test(m[2]))
39
45
  if (matches.length === 0) return text
40
46
 
41
47
  let result = text
42
48
  for (const match of matches) {
43
49
  const [fullMatch, , href] = match
44
- // href starts with ./ and contains /template/ already guaranteed by regex
50
+ // href starts with ./ (regex) and contains /template/ (filter above)
45
51
  const relPath = href.slice(2) // strip leading ./
46
52
  const absPath = join(ruleDir, relPath)
47
53
 
@@ -49,7 +55,8 @@ export async function inlineTemplateLinks(text, ruleDir) {
49
55
  throw new Error(`inlineTemplateLinks: file not found: ${absPath} (referenced from .mdc)`)
50
56
  }
51
57
 
52
- const contents = (await readFile(absPath, 'utf8')).trim()
58
+ const raw = await readFile(absPath, 'utf8')
59
+ const contents = raw.trim()
53
60
  const lang = langFromExt(absPath)
54
61
  const targetName = normalizeTargetName(basename(absPath))
55
62
  const replacement = `\`${targetName}\`:\n\n\`\`\`${lang}\n${contents}\n\`\`\``