@nitra/cursor 1.9.23 → 1.11.0

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 (84) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +3 -3
  2. package/.claude-template/hooks/normalize-decisions.sh +370 -0
  3. package/CHANGELOG.md +52 -0
  4. package/bin/n-cursor.js +30 -29
  5. package/package.json +2 -1
  6. package/rules/abie/js/applies/check.mjs +24 -0
  7. package/rules/abie/js/env_dns/check.mjs +53 -0
  8. package/rules/abie/js/firebase_hosting/check.mjs +49 -0
  9. package/rules/abie/js/hc_pairing/check.mjs +58 -0
  10. package/rules/abie/js/ua_http_route/check.mjs +86 -0
  11. package/rules/abie/js/ua_node_selector/check.mjs +65 -0
  12. package/rules/abie/policy/base_deployment_preem/target.json +10 -0
  13. package/rules/abie/policy/clean_merged_ignore_branches/target.json +4 -0
  14. package/rules/abie/policy/health_check_policy/target.json +4 -0
  15. package/rules/abie/policy/http_route_base/target.json +4 -0
  16. package/rules/abie/utils/enabled.mjs +35 -0
  17. package/rules/abie/utils/env-dns.mjs +81 -0
  18. package/rules/abie/utils/hc-yaml.mjs +27 -0
  19. package/rules/abie/utils/http-route.mjs +93 -0
  20. package/rules/abie/utils/k8s-tree.mjs +102 -0
  21. package/rules/abie/utils/kustomization-patches.mjs +224 -0
  22. package/rules/abie/utils/overlay-paths.mjs +97 -0
  23. package/rules/abie/utils/yaml.mjs +72 -0
  24. package/rules/adr/adr.mdc +82 -18
  25. package/rules/adr/js/check.mjs +84 -40
  26. package/rules/adr/policy/settings_json/settings_json.rego +17 -11
  27. package/rules/adr/policy/settings_json/target.json +4 -0
  28. package/rules/adr/policy/settings_local_json/settings_local_json.rego +24 -12
  29. package/rules/adr/policy/settings_local_json/target.json +4 -0
  30. package/rules/bun/policy/bunfig/target.json +4 -0
  31. package/rules/bun/policy/package_json/target.json +4 -0
  32. package/rules/capacitor/policy/package_json/target.json +4 -0
  33. package/rules/docker/policy/lint_docker_yml/target.json +4 -0
  34. package/rules/docker/policy/package_json/target.json +4 -0
  35. package/rules/hasura/policy/svc_hl/target.json +4 -0
  36. package/rules/image-avif/policy/package_json/target.json +4 -0
  37. package/rules/image-compress/policy/package_json/target.json +4 -0
  38. package/rules/js-bun-db/policy/package_json/target.json +4 -0
  39. package/rules/js-bun-redis/policy/package_json/target.json +4 -0
  40. package/rules/js-lint/policy/lint_js_yml/target.json +4 -0
  41. package/rules/js-lint/policy/package_json/target.json +4 -0
  42. package/rules/js-mssql/policy/package_json/target.json +4 -0
  43. package/rules/js-run/policy/configmap/target.json +4 -0
  44. package/rules/js-run/policy/package_json/target.json +4 -0
  45. package/rules/k8s/policy/base_kustomization/target.json +4 -0
  46. package/rules/k8s/policy/base_manifest/target.json +10 -0
  47. package/rules/k8s/policy/gateway/target.json +4 -0
  48. package/rules/k8s/policy/hpa_pdb/target.json +4 -0
  49. package/rules/k8s/policy/kustomization/target.json +4 -0
  50. package/rules/k8s/policy/manifest/target.json +4 -0
  51. package/rules/k8s/policy/svc_hl_yaml/target.json +4 -0
  52. package/rules/k8s/policy/svc_yaml/target.json +4 -0
  53. package/rules/npm-module/policy/emit_types_config/target.json +4 -0
  54. package/rules/npm-module/policy/npm_package_json/target.json +4 -0
  55. package/rules/npm-module/policy/npm_publish_yml/target.json +4 -0
  56. package/rules/npm-module/policy/root_package_json/target.json +4 -0
  57. package/rules/php/policy/lint_php_yml/target.json +4 -0
  58. package/rules/php/policy/package_json/target.json +4 -0
  59. package/rules/rego/js/applies/check.mjs +54 -0
  60. package/rules/rego/policy/package_json/target.json +5 -0
  61. package/rules/rego/policy/vscode_extensions/target.json +5 -0
  62. package/rules/rego/policy/vscode_settings/target.json +5 -0
  63. package/rules/style-lint/policy/lint_style_yml/target.json +4 -0
  64. package/rules/style-lint/policy/package_json/target.json +4 -0
  65. package/rules/style-lint/policy/vscode_extensions/target.json +4 -0
  66. package/rules/style-lint/policy/vscode_settings/target.json +4 -0
  67. package/rules/text/policy/cspell/target.json +4 -0
  68. package/rules/text/policy/markdownlint/target.json +4 -0
  69. package/rules/text/policy/oxfmtrc/target.json +4 -0
  70. package/rules/text/policy/package_json/target.json +4 -0
  71. package/rules/text/policy/vscode_extensions/target.json +4 -0
  72. package/rules/text/policy/vscode_settings/target.json +4 -0
  73. package/rules/vue/policy/package_json/target.json +4 -0
  74. package/schemas/target.json +58 -0
  75. package/scripts/auto-skills.mjs +2 -0
  76. package/scripts/lint-conftest.mjs +65 -414
  77. package/scripts/sync-claude-config.mjs +70 -14
  78. package/scripts/utils/discover-checkable-rules.mjs +123 -0
  79. package/scripts/utils/resolve-target-files.mjs +109 -0
  80. package/scripts/utils/run-rule.mjs +131 -0
  81. package/skills/adr-normalize/SKILL.md +71 -0
  82. package/skills/adr-normalize/auto.md +1 -0
  83. package/rules/abie/js/check.mjs +0 -1152
  84. package/rules/rego/js/check.mjs +0 -106
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
+ "files": { "walkGlob": "**/package.json" }
4
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://unpkg.com/@nitra/cursor/schemas/target.json",
4
+ "title": "Rego policy target manifest",
5
+ "description": "Маніфест поряд із <name>.rego: декларує, які файли проєкту фідити в conftest для цієї полісі. Лежить у npm/rules/<id>/policy/<name>/target.json (per-policy, поруч із <name>.rego). CLI читає й передає у runConftestBatch — JS-оркестратор у js/<name>/check.mjs не зобовʼязаний дублювати виклик. Namespace полісі обчислюється з шляху: <id>.<name>, відповідно `package <id>.<name>` у .rego.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["files"],
9
+ "properties": {
10
+ "$schema": {
11
+ "type": "string",
12
+ "format": "uri",
13
+ "description": "Опційне посилання на JSON Schema для IDE-валідації; очікувано https://unpkg.com/@nitra/cursor/schemas/target.json"
14
+ },
15
+ "files": {
16
+ "oneOf": [
17
+ {
18
+ "type": "object",
19
+ "additionalProperties": false,
20
+ "required": ["single"],
21
+ "properties": {
22
+ "single": {
23
+ "type": "string",
24
+ "minLength": 1,
25
+ "description": "Відносний шлях від кореня репозиторію (posix). Заборонені сегменти '..' і абсолютні шляхи."
26
+ },
27
+ "required": {
28
+ "type": "boolean",
29
+ "description": "true → fail при відсутності файла з відповідним missingMessage; false/відсутнє → silent skip (полісі не запускається). За замовчуванням false."
30
+ }
31
+ }
32
+ },
33
+ {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": ["walkGlob"],
37
+ "properties": {
38
+ "walkGlob": {
39
+ "oneOf": [
40
+ { "type": "string", "minLength": 1 },
41
+ {
42
+ "type": "array",
43
+ "minItems": 1,
44
+ "items": { "type": "string", "minLength": 1 }
45
+ }
46
+ ],
47
+ "description": "Один або декілька picomatch-globів (matched проти відносного posix-шляху). walkDir від cwd із загальними skip-ами + .n-cursor.json:ignore."
48
+ }
49
+ }
50
+ }
51
+ ]
52
+ },
53
+ "missingMessage": {
54
+ "type": "string",
55
+ "description": "Override дефолтного fail-повідомлення при відсутності required:single файла. Без значення CLI використає шаблон '<path> не існує (<id>.<name>)'."
56
+ }
57
+ }
58
+ }
@@ -14,6 +14,7 @@
14
14
  /** Порядок автододавання skills відповідно до `skills/<skill>/auto.md`. */
15
15
  export const AUTO_SKILL_ORDER = Object.freeze([
16
16
  'abie-kustomize',
17
+ 'adr-normalize',
17
18
  'fix',
18
19
  'lint',
19
20
  'llm-patch',
@@ -28,6 +29,7 @@ export const AUTO_SKILL_ORDER = Object.freeze([
28
29
  export const AUTO_SKILL_RULE_DEPENDENCIES = Object.freeze(
29
30
  /** @type {Record<string, readonly string[]>} */ ({
30
31
  'abie-kustomize': Object.freeze(['abie']),
32
+ 'adr-normalize': Object.freeze(['adr']),
31
33
  taze: Object.freeze(['bun'])
32
34
  })
33
35
  )
@@ -1,59 +1,44 @@
1
1
  /**
2
- * Прогоняє `conftest test` по всіх Rego-полісі з `npm/rules/<rule>/policy/` (окрім `ga/*`,
3
- * які вже виконуються через `npm/rules/ga/js/lint.mjs`).
2
+ * Прогоняє `conftest test` по всіх Rego-полісі з `npm/rules/<rule>/policy/<concern>/`.
4
3
  *
5
- * Кожна полісі має свій namespace, обов'язковий `rule` (id у `.n-cursor.json:rules`,
6
- * інакше таргет пропускається як гейтинг у `check.mjs`), і список цільових
7
- * файлів — single-file або walk-предикат для дерева. Якщо цільових файлів немає
8
- * або правило не активне таргет мовчки пропускається.
4
+ * Джерело правди `target.json` поруч із кожним `<concern>.rego`. Маніфест декларує,
5
+ * які файли проєкту фідити в conftest (`files.single` або `files.walkGlob`). Resolver
6
+ * і walk-кеш спільні з CLI `check` (`scripts/utils/resolve-target-files.mjs`),
7
+ * discovery`scripts/utils/discover-checkable-rules.mjs`.
8
+ *
9
+ * Фільтрація за `.n-cursor.json:rules` — не перевіряємо полісі правил, які проєкт
10
+ * не активує (як було у попередній hardcoded TARGETS-таблиці).
9
11
  *
10
12
  * Поведінка fallback:
11
- * - якщо `conftest` не в `PATH`друкуємо `ℹ` повідомлення з підказкою
12
- * встановлення і повертаємо 0 (структурні JS-перевірки в `check.mjs`
13
- * лишаються паралельно). Те саме рішенняу `rules/ga/js/lint.mjs`.
14
- * - якщо `npm/rules/` не існує (нетипова інсталяція) — також `ℹ` skip.
13
+ * - якщо `conftest` не в PATH — `ℹ` install-hint, повертаємо 0 (структурні JS-перевірки
14
+ * в `check-*.mjs` лишаються паралельно). Те саме рішення — у `rules/ga/js/lint.mjs`.
15
+ * - якщо `rules/` каталог відсутній (нетипова інсталяція) також `ℹ` skip.
15
16
  *
16
- * Перший ненульовий exit-код conftest — повертаємо як результат, але всі
17
- * наступні таргети все одно виконуємо, щоб одразу побачити повний список
18
- * порушень (а не виправляти по одному).
17
+ * Перший ненульовий exit-код conftest — повертаємо як результат, але всі наступні цілі
18
+ * все одно виконуємо, щоб одразу побачити повний список порушень.
19
19
  *
20
- * Експортовано окремо `runLintConftestCli` — використовується з
21
- * `bin/n-cursor.js` як підкоманда `lint-conftest`.
20
+ * Експортовано `runLintConftestCli` — використовується з `bin/n-cursor.js` як підкоманда
21
+ * `lint-conftest`, а також виконується напряму через `bun ./npm/scripts/lint-conftest.mjs`.
22
22
  */
23
- import { existsSync, readdirSync, readFileSync } from 'node:fs'
23
+ import { existsSync, readFileSync } from 'node:fs'
24
+ import { readFile } from 'node:fs/promises'
24
25
  import { spawnSync } from 'node:child_process'
25
- import { dirname, join, sep } from 'node:path'
26
+ import { dirname, join } from 'node:path'
26
27
  import { fileURLToPath } from 'node:url'
27
28
 
29
+ import { discoverCheckableRules } from './utils/discover-checkable-rules.mjs'
28
30
  import { resolveCmd } from './utils/resolve-cmd.mjs'
31
+ import { resolveTargetFiles } from './utils/resolve-target-files.mjs'
29
32
 
30
33
  /** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиті директорії правил. */
31
34
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
32
35
 
33
- /** Шлях до кореня правил. У npm-tarball публікується через `files: ["rules"]`. Кожне правило: `rules/<id>/policy/`. */
36
+ /** Шлях до кореня правил. У npm-tarball публікується через `files: ["rules"]`. */
34
37
  const RULES_DIR = join(PACKAGE_ROOT, 'rules')
35
38
 
36
- /**
37
- * Опис одного таргета: namespace + спосіб розвʼязати цільові файли.
38
- *
39
- * `single` — конкретний файл відносно cwd, перевіряється `existsSync`-ом.
40
- * `walk` — рекурсивний обхід від cwd із простим суфікс-предикатом
41
- * (наприклад `name === 'package.json'`). Глибокі ігнори — як у `walkDir`
42
- * в інших скриптах: `node_modules`, `.git`, `dist`, `coverage`, `build`,
43
- * `.turbo`, `.next`. Не використовуємо bun Glob, щоб не плодити залежності
44
- * за межами `node:fs`.
45
- * @typedef {{
46
- * namespace: string,
47
- * policyDir: string,
48
- * rule?: string,
49
- * single?: string,
50
- * walk?: { match: (relPosix: string) => boolean }
51
- * }} ConftestTarget
52
- */
53
-
54
39
  /**
55
40
  * Зчитує `rules` з `.n-cursor.json` у cwd. Повертає множину рядків — або `null`,
56
- * якщо файлу немає чи поле некоректне (тоді гейтинг вимикаємо — як у `check-bun.mjs`).
41
+ * якщо файлу немає чи поле некоректне (тоді гейтинг вимикаємо — як було в попередній версії).
57
42
  * @param {string} cwd корінь репо
58
43
  * @returns {Set<string> | null} множина активних правил або null
59
44
  */
@@ -69,381 +54,37 @@ function loadActiveCursorRules(cwd) {
69
54
  }
70
55
  }
71
56
 
72
- const SKIP_DIR_NAMES = new Set(['node_modules', '.git', 'dist', 'coverage', 'build', '.turbo', '.next'])
73
-
74
- /** `…/k8s/<env>/configmap.yaml` (configmap безпосередньо у directory `k8s/<…>`). */
75
- const K8S_CONFIGMAP_PATH_RE = /(^|\/)k8s\/[^/]+\/configmap\.yaml$/u
76
- /** Будь-який шлях під сегментом `k8s/`. */
77
- const K8S_DIR_PATH_RE = /(^|\/)k8s\//u
78
- /** `…/k8s/<…>/hc.yaml` (HealthCheckPolicy будь-де під k8s). */
79
- const K8S_HC_YAML_PATH_RE = /(^|\/)k8s\/.+\/hc\.yaml$/u
80
- /** `…/k8s/…/base/…/hr.yaml` (HTTPRoute у base-шарі). */
81
- const K8S_BASE_HR_YAML_PATH_RE = /(^|\/)k8s\/.*base\/.*hr\.yaml$/u
82
- /** Будь-який ресурсний YAML під `…/k8s/.../base/...` (для abie.base_deployment_preem). */
83
- const K8S_BASE_RESOURCE_PATH_RE = /(^|\/)k8s\/.*base\//u
84
- /** `kustomization.yaml` будь-де під сегментом `k8s/`. */
85
- const K8S_KUSTOMIZATION_PATH_RE = /(^|\/)k8s\/.*\/kustomization\.yaml$/u
86
- /** `…/k8s/.../base/.../kustomization.yaml`. */
87
- const K8S_BASE_KUSTOMIZATION_PATH_RE = /(^|\/)k8s\/.*base\/(?:.*\/)?kustomization\.yaml$/u
88
- /** Будь-який ресурсний `*.yaml` під сегментом `…/k8s/.../base/...`, окрім `kustomization.yaml`. */
89
- const K8S_BASE_MANIFEST_PATH_RE = /(^|\/)k8s\/.*base\//u
90
- /** `…/k8s/.../svc.yaml` (cluster-IP Service). */
91
- const K8S_SVC_YAML_PATH_RE = /(^|\/)k8s\/.+\/svc\.yaml$/u
92
- /** `…/k8s/.../svc-hl.yaml` (headless Service). */
93
- const K8S_SVC_HL_YAML_PATH_RE = /(^|\/)k8s\/.+\/svc-hl\.yaml$/u
94
-
95
- /** @type {ConftestTarget[]} */
96
- const TARGETS = [
97
- // ── bun ─────────────────────────────────────────────────────────────────
98
- { namespace: 'bun.bunfig', policyDir: 'bun', rule: 'bun', single: 'bunfig.toml' },
99
- { namespace: 'bun.package_json', policyDir: 'bun', rule: 'bun', single: 'package.json' },
100
-
101
- // ── text ────────────────────────────────────────────────────────────────
102
- { namespace: 'text.oxfmtrc', policyDir: 'text', rule: 'text', single: '.oxfmtrc.json' },
103
- { namespace: 'text.cspell', policyDir: 'text', rule: 'text', single: '.cspell.json' },
104
- { namespace: 'text.markdownlint', policyDir: 'text', rule: 'text', single: '.markdownlint-cli2.jsonc' },
105
- { namespace: 'text.package_json', policyDir: 'text', rule: 'text', single: 'package.json' },
106
- { namespace: 'text.vscode_extensions', policyDir: 'text', rule: 'text', single: '.vscode/extensions.json' },
107
- { namespace: 'text.vscode_settings', policyDir: 'text', rule: 'text', single: '.vscode/settings.json' },
108
-
109
- // ── style-lint ──────────────────────────────────────────────────────────
110
- { namespace: 'style_lint.package_json', policyDir: 'style_lint', rule: 'style-lint', single: 'package.json' },
111
- {
112
- namespace: 'style_lint.lint_style_yml',
113
- policyDir: 'style_lint',
114
- rule: 'style-lint',
115
- single: '.github/workflows/lint-style.yml'
116
- },
117
- {
118
- namespace: 'style_lint.vscode_extensions',
119
- policyDir: 'style_lint',
120
- rule: 'style-lint',
121
- single: '.vscode/extensions.json'
122
- },
123
- {
124
- namespace: 'style_lint.vscode_settings',
125
- policyDir: 'style_lint',
126
- rule: 'style-lint',
127
- single: '.vscode/settings.json'
128
- },
129
-
130
- // ── php ─────────────────────────────────────────────────────────────────
131
- { namespace: 'php.package_json', policyDir: 'php', rule: 'php', single: 'package.json' },
132
- {
133
- namespace: 'php.lint_php_yml',
134
- policyDir: 'php',
135
- rule: 'php',
136
- single: '.github/workflows/lint-php.yml'
137
- },
138
-
139
- // ── docker ──────────────────────────────────────────────────────────────
140
- { namespace: 'docker.package_json', policyDir: 'docker', rule: 'docker', single: 'package.json' },
141
- {
142
- namespace: 'docker.lint_docker_yml',
143
- policyDir: 'docker',
144
- rule: 'docker',
145
- single: '.github/workflows/lint-docker.yml'
146
- },
147
-
148
- // ── npm-module ──────────────────────────────────────────────────────────
149
- {
150
- namespace: 'npm_module.root_package_json',
151
- policyDir: 'npm_module',
152
- rule: 'npm-module',
153
- single: 'package.json'
154
- },
155
- {
156
- namespace: 'npm_module.npm_package_json',
157
- policyDir: 'npm_module',
158
- rule: 'npm-module',
159
- single: 'npm/package.json'
160
- },
161
- {
162
- namespace: 'npm_module.emit_types_config',
163
- policyDir: 'npm_module',
164
- rule: 'npm-module',
165
- single: 'npm/tsconfig.emit-types.json'
166
- },
167
- {
168
- namespace: 'npm_module.npm_publish_yml',
169
- policyDir: 'npm_module',
170
- rule: 'npm-module',
171
- single: '.github/workflows/npm-publish.yml'
172
- },
173
-
174
- // ── js-lint ─────────────────────────────────────────────────────────────
175
- { namespace: 'js_lint.package_json', policyDir: 'js_lint', rule: 'js-lint', single: 'package.json' },
176
- {
177
- namespace: 'js_lint.lint_js_yml',
178
- policyDir: 'js_lint',
179
- rule: 'js-lint',
180
- single: '.github/workflows/lint-js.yml'
181
- },
182
-
183
- // ── image-compress / image-avif / capacitor ─────────────────────────────
184
- {
185
- namespace: 'image_compress.package_json',
186
- policyDir: 'image_compress',
187
- rule: 'image-compress',
188
- single: 'package.json'
189
- },
190
- {
191
- namespace: 'image_avif.package_json',
192
- policyDir: 'image_avif',
193
- rule: 'image-avif',
194
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
195
- },
196
- {
197
- namespace: 'capacitor.package_json',
198
- policyDir: 'capacitor',
199
- rule: 'capacitor',
200
- single: 'package.json'
201
- },
202
-
203
- // ── hasura ──────────────────────────────────────────────────────────────
204
- {
205
- namespace: 'hasura.svc_hl',
206
- policyDir: 'hasura',
207
- rule: 'hasura',
208
- single: 'hasura/k8s/base/svc-hl.yaml'
209
- },
210
-
211
- // ── adr ─────────────────────────────────────────────────────────────────
212
- { namespace: 'adr.settings_json', policyDir: 'adr', rule: 'adr', single: '.claude/settings.json' },
213
- {
214
- namespace: 'adr.settings_local_json',
215
- policyDir: 'adr',
216
- rule: 'adr',
217
- single: '.claude/settings.local.json'
218
- },
219
-
220
- // ── multi-file (walk) ───────────────────────────────────────────────────
221
- // Усі `package.json` у дереві (включно з workspace-пакетами).
222
- {
223
- namespace: 'js_mssql.package_json',
224
- policyDir: 'js_mssql',
225
- rule: 'js-mssql',
226
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
227
- },
228
- {
229
- namespace: 'js_bun_db.package_json',
230
- policyDir: 'js_bun_db',
231
- rule: 'js-bun-db',
232
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
233
- },
234
- {
235
- namespace: 'js_bun_redis.package_json',
236
- policyDir: 'js_bun_redis',
237
- rule: 'js-bun-redis',
238
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
239
- },
240
- {
241
- namespace: 'js_run.package_json',
242
- policyDir: 'js_run',
243
- rule: 'js-run',
244
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
245
- },
246
- // `js_run.jsconfig` НЕ реєструємо тут — `jsconfig.json` має канонічну структуру
247
- // лише для backend-пакетів (без `vite` у `devDependencies`) з каталогом `src/`,
248
- // а lint-conftest фільтрує лише по `activeRules` на рівні репозиторію — не
249
- // вміє пропустити окремий workspace-пакет за наявністю `vite`. Тому валідація
250
- // структури делегується з `check-js-run.mjs` через `runConftestBatch` після
251
- // того, як JS визначить, що пакет — backend з `src/`.
252
- {
253
- namespace: 'vue.package_json',
254
- policyDir: 'vue',
255
- rule: 'vue',
256
- walk: { match: rel => rel.endsWith('/package.json') || rel === 'package.json' }
257
- },
258
-
259
- // ConfigMap у `…/k8s/base/configmap.yaml` будь-де у дереві.
260
- {
261
- namespace: 'js_run.configmap',
262
- policyDir: 'js_run',
263
- rule: 'js-run',
264
- walk: { match: rel => K8S_CONFIGMAP_PATH_RE.test(rel) }
265
- },
266
-
267
- // Усі YAML у дереві з сегментом `k8s` — пер-документні структурні правила.
268
- {
269
- namespace: 'k8s.manifest',
270
- policyDir: 'k8s/manifest',
271
- rule: 'k8s',
272
- walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
273
- },
274
-
275
- // Gateway API + HealthCheckPolicy — застосовується до будь-якого YAML під k8s
276
- // (правила перевіряють лише відповідні kind / apiVersion).
277
- {
278
- namespace: 'k8s.gateway',
279
- policyDir: 'k8s/gateway',
280
- rule: 'k8s',
281
- walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
282
- },
283
-
284
- // Структурні перевірки HPA / PDB (apiVersion / behavior / metrics / selector).
285
- {
286
- namespace: 'k8s.hpa_pdb',
287
- policyDir: 'k8s/hpa_pdb',
288
- rule: 'k8s',
289
- walk: { match: rel => K8S_DIR_PATH_RE.test(rel) && (rel.endsWith('.yaml') || rel.endsWith('.yml')) }
290
- },
291
-
292
- // Kustomization-файли: resources sort, patches sort, JSON6902 conflicts.
293
- {
294
- namespace: 'k8s.kustomization',
295
- policyDir: 'k8s/kustomization',
296
- rule: 'k8s',
297
- walk: { match: rel => K8S_KUSTOMIZATION_PATH_RE.test(rel) }
298
- },
299
-
300
- // svc.yaml — cluster-IP Service.
301
- {
302
- namespace: 'k8s.svc_yaml',
303
- policyDir: 'k8s/svc_yaml',
304
- rule: 'k8s',
305
- walk: { match: rel => K8S_SVC_YAML_PATH_RE.test(rel) }
306
- },
307
-
308
- // svc-hl.yaml — headless Service з суфіксом `-hl`.
309
- {
310
- namespace: 'k8s.svc_hl_yaml',
311
- policyDir: 'k8s/svc_hl_yaml',
312
- rule: 'k8s',
313
- walk: { match: rel => K8S_SVC_HL_YAML_PATH_RE.test(rel) }
314
- },
315
-
316
- // base/kustomization.yaml — обов'язкове непорожнє поле `namespace:`.
317
- {
318
- namespace: 'k8s.base_kustomization',
319
- policyDir: 'k8s/base_kustomization',
320
- rule: 'k8s',
321
- walk: { match: rel => K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) }
322
- },
323
-
324
- // Ресурсні маніфести під `…/k8s/.../base/...` (окрім kustomization.yaml).
325
- {
326
- namespace: 'k8s.base_manifest',
327
- policyDir: 'k8s/base_manifest',
328
- rule: 'k8s',
329
- walk: {
330
- match: rel =>
331
- K8S_BASE_MANIFEST_PATH_RE.test(rel) &&
332
- !K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) &&
333
- (rel.endsWith('.yaml') || rel.endsWith('.yml'))
334
- }
335
- },
336
-
337
- // abie HealthCheckPolicy: `hc.yaml` у дереві k8s.
338
- {
339
- namespace: 'abie.health_check_policy',
340
- policyDir: 'abie/health_check_policy',
341
- rule: 'abie',
342
- walk: { match: rel => K8S_HC_YAML_PATH_RE.test(rel) }
343
- },
344
-
345
- // abie HTTPRoute у `base/`.
346
- {
347
- namespace: 'abie.http_route_base',
348
- policyDir: 'abie/http_route_base',
349
- rule: 'abie',
350
- walk: { match: rel => K8S_BASE_HR_YAML_PATH_RE.test(rel) }
351
- },
352
-
353
- // abie Deployment у `…/k8s/.../base/...` має preem nodeSelector.
354
- {
355
- namespace: 'abie.base_deployment_preem',
356
- policyDir: 'abie/base_deployment_preem',
357
- rule: 'abie',
358
- walk: {
359
- match: rel =>
360
- K8S_BASE_RESOURCE_PATH_RE.test(rel) &&
361
- !K8S_BASE_KUSTOMIZATION_PATH_RE.test(rel) &&
362
- (rel.endsWith('.yaml') || rel.endsWith('.yml'))
363
- }
364
- },
365
-
366
- // abie clean-merged-branch.yml: with.ignore_branches має містити dev/ua.
367
- {
368
- namespace: 'abie.clean_merged_ignore_branches',
369
- policyDir: 'abie/clean_merged_ignore_branches',
370
- rule: 'abie',
371
- single: '.github/workflows/clean-merged-branch.yml'
372
- }
373
- ]
374
-
375
- /**
376
- * Рекурсивно збирає відносні (posix) шляхи від cwd, які матчаться предикатом.
377
- * Глибокі ігнори — `SKIP_DIR_NAMES`. Не йде у симлінки, помилки stat — мовчки skip.
378
- * @param {string} root абсолютний корінь обходу
379
- * @param {(relPosix: string) => boolean} match предикат на відносний posix-шлях
380
- * @returns {string[]} список відносних posix-шляхів
381
- */
382
- function collectFiles(root, match) {
383
- /** @type {string[]} */
384
- const out = []
385
- /** @param {string} dirAbs абсолютний шлях каталогу для рекурсивного обходу */
386
- function visit(dirAbs) {
387
- /** @type {import('node:fs').Dirent[]} */
388
- let entries
389
- try {
390
- entries = readdirSync(dirAbs, { withFileTypes: true })
391
- } catch {
392
- return
393
- }
394
- for (const e of entries) {
395
- if (e.isSymbolicLink()) continue
396
- const abs = join(dirAbs, e.name)
397
- if (e.isDirectory()) {
398
- if (SKIP_DIR_NAMES.has(e.name)) continue
399
- visit(abs)
400
- continue
401
- }
402
- if (!e.isFile()) continue
403
- const rel = abs
404
- .slice(root.length + 1)
405
- .split(sep)
406
- .join('/')
407
- if (match(rel)) out.push(rel)
408
- }
409
- }
410
- visit(root)
411
- return out
412
- }
413
-
414
57
  /**
415
- * Розвʼязує файлові цілі для одного таргета щодо cwd.
416
- * @param {ConftestTarget} target опис таргета
417
- * @param {string} cwd корінь репозиторію
418
- * @returns {string[]} список абсолютних / відносних шляхів
58
+ * Обчислює namespace rego-полісі за id правила і ім'ям концерну.
59
+ * Rego не дозволяє '-' в імені пакета, тож kebab-id у `.n-cursor.json:rules`
60
+ * мапиться на snake у namespace; ім'я концерну йде як є (вже snake у `policy/<concern>/`).
61
+ * @param {string} ruleId id правила (kebab)
62
+ * @param {string} concernName ім'я concern (підкаталог у `policy/`)
63
+ * @returns {string} namespace для `conftest --namespace`
419
64
  */
420
- function resolveTargetFiles(target, cwd) {
421
- if (target.single) {
422
- return existsSync(join(cwd, target.single)) ? [target.single] : []
423
- }
424
- if (target.walk) {
425
- return collectFiles(cwd, target.walk.match)
426
- }
427
- return []
65
+ function computeNamespace(ruleId, concernName) {
66
+ return `${ruleId.replaceAll('-', '_')}.${concernName}`
428
67
  }
429
68
 
430
69
  /**
431
- * Запускає conftest на одному таргеті. Повертає exit-код (0 — OK, 1+ — помилки).
70
+ * Запускає conftest на одному policy-концерні. Повертає exit-код (0 — OK, 1+ — порушення).
432
71
  *
433
- * При відсутніх цільових файлах мовчки повертає 0 (правило неактуальне для repo).
434
- * Логує заголовок з namespace і кількістю файлів, як `lint-ga.mjs`.
72
+ * stdio: 'inherit' щоб користувач бачив рідну форматовану табличку conftest у виводі
73
+ * `bun run lint` (відрізняється від структурованого JSON-варіанта в `check`-команді).
435
74
  * @param {string} conftestBin абсолютний шлях до бінарника conftest
436
- * @param {ConftestTarget} target опис таргета
437
- * @param {string[]} files список файлів для перевірки (відносні до cwd)
75
+ * @param {string} ruleId id правила
76
+ * @param {string} concernName ім'я concern
77
+ * @param {string} namespace rego-пакет
78
+ * @param {string[]} files список файлів (відносні/абсолютні шляхи)
438
79
  * @returns {number} exit-код
439
80
  */
440
- function runConftestForTarget(conftestBin, target, files) {
441
- const policyAbs = join(RULES_DIR, target.rule, 'policy')
81
+ function runConftestForConcern(conftestBin, ruleId, concernName, namespace, files) {
82
+ const policyAbs = join(RULES_DIR, ruleId, 'policy', concernName)
442
83
  if (!existsSync(policyAbs)) {
443
84
  return 0
444
85
  }
445
- console.log(`\n▶ conftest (${target.namespace} — ${files.length} файл(ів))`)
446
- const r = spawnSync(conftestBin, ['test', ...files, '-p', policyAbs, '--namespace', target.namespace, '--no-color'], {
86
+ console.log(`\n▶ conftest (${namespace} — ${files.length} файл(ів))`)
87
+ const r = spawnSync(conftestBin, ['test', ...files, '-p', policyAbs, '--namespace', namespace, '--no-color'], {
447
88
  stdio: 'inherit',
448
89
  env: process.env
449
90
  })
@@ -455,14 +96,14 @@ function runConftestForTarget(conftestBin, target, files) {
455
96
  }
456
97
 
457
98
  /**
458
- * Запускає `conftest test` по всіх таргетах із `TARGETS`. Перший ненульовий exit-код
459
- * запамʼятовується, але цикл йде до кінця, щоб користувач побачив усі порушення.
99
+ * Запускає `conftest test` по всіх policy-концернах із `target.json`-маніфестів.
100
+ * Фільтрація за `activeRules` (поле `rules` у `.n-cursor.json`). Перший ненульовий
101
+ * exit-код запамʼятовується, але цикл йде до кінця.
460
102
  *
461
- * Якщо `conftest` не знайдено в PATH — друкує `ℹ` повідомлення і повертає 0
462
- * (структурні перевірки в `check-*.mjs` лишаються паралельно).
463
- * @returns {number} 0 — все OK або skip; інакше — перший ненульовий exit-код
103
+ * Якщо `conftest` не знайдено в PATH — друкує `ℹ` повідомлення і повертає 0.
104
+ * @returns {Promise<number>} 0 все OK або skip; інакше — перший ненульовий exit-код
464
105
  */
465
- export function runLintConftestCli() {
106
+ export async function runLintConftestCli() {
466
107
  const conftestBin = resolveCmd('conftest')
467
108
  if (!conftestBin) {
468
109
  console.log(
@@ -478,19 +119,29 @@ export function runLintConftestCli() {
478
119
 
479
120
  const cwd = process.cwd()
480
121
  const activeRules = loadActiveCursorRules(cwd)
122
+ const rules = await discoverCheckableRules(RULES_DIR)
123
+ /** @type {Map<string, Promise<string[]>>} */
124
+ const walkCache = new Map()
481
125
  let firstFailureCode = 0
482
- for (const target of TARGETS) {
483
- if (target.rule && activeRules && !activeRules.has(target.rule)) continue
484
- const files = resolveTargetFiles(target, cwd)
485
- if (files.length === 0) continue
486
- const code = runConftestForTarget(conftestBin, target, files)
487
- if (code !== 0 && firstFailureCode === 0) {
488
- firstFailureCode = code
126
+
127
+ for (const rule of rules) {
128
+ if (activeRules && !activeRules.has(rule.id)) continue
129
+ for (const concern of rule.policyConcerns) {
130
+ const targetPath = join(RULES_DIR, rule.id, 'policy', concern.name, 'target.json')
131
+ /** @type {{ files: { single?: string, walkGlob?: string|string[], required?: boolean }, missingMessage?: string }} */
132
+ const target = JSON.parse(await readFile(targetPath, 'utf8'))
133
+ const files = await resolveTargetFiles(target.files, cwd, walkCache)
134
+ if (files.length === 0) continue
135
+ const namespace = computeNamespace(rule.id, concern.name)
136
+ const code = runConftestForConcern(conftestBin, rule.id, concern.name, namespace, files)
137
+ if (code !== 0 && firstFailureCode === 0) {
138
+ firstFailureCode = code
139
+ }
489
140
  }
490
141
  }
491
142
  return firstFailureCode
492
143
  }
493
144
 
494
145
  if (import.meta.url === `file://${process.argv[1]}`) {
495
- process.exitCode = runLintConftestCli()
146
+ process.exitCode = (await runLintConftestCli()) ?? 0
496
147
  }