@nitra/cursor 1.8.206 → 1.8.208

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 (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/mdc/js-run.mdc +49 -2
  3. package/package.json +1 -1
  4. package/policy/abie/health_check_policy/health_check_policy.rego +73 -0
  5. package/policy/abie/http_route_base/http_route_base.rego +45 -0
  6. package/policy/adr/settings_json/settings_json.rego +31 -0
  7. package/policy/adr/settings_local_json/settings_local_json.rego +28 -0
  8. package/policy/bun/bunfig/bunfig.rego +33 -0
  9. package/policy/bun/package_json/package_json.rego +94 -0
  10. package/policy/capacitor/package_json/package_json.rego +45 -0
  11. package/policy/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -26
  12. package/policy/ga/clean_merged_branch/clean_merged_branch.rego +0 -25
  13. package/policy/ga/git_ai/git_ai.rego +0 -26
  14. package/policy/ga/lint_ga/lint_ga.rego +0 -26
  15. package/policy/ga/workflow_common/workflow_common.rego +161 -0
  16. package/policy/graphql/package_json/package_json.rego +35 -0
  17. package/policy/hasura/svc_hl/svc_hl.rego +27 -0
  18. package/policy/image_compress/package_json/package_json.rego +94 -0
  19. package/policy/js_bun_db/package_json/package_json.rego +28 -0
  20. package/policy/js_lint/lint_js_yml/lint_js_yml.rego +98 -0
  21. package/policy/js_lint/package_json/package_json.rego +137 -0
  22. package/policy/js_mssql/package_json/package_json.rego +57 -0
  23. package/policy/js_run/configmap/configmap.rego +45 -0
  24. package/policy/js_run/jsconfig/jsconfig.rego +66 -0
  25. package/policy/js_run/package_json/package_json.rego +31 -0
  26. package/policy/k8s/manifest/manifest.rego +130 -0
  27. package/policy/npm_module/emit_types_config/emit_types_config.rego +37 -0
  28. package/policy/npm_module/npm_package_json/npm_package_json.rego +55 -0
  29. package/policy/npm_module/npm_publish_yml/npm_publish_yml.rego +79 -0
  30. package/policy/npm_module/root_package_json/root_package_json.rego +28 -0
  31. package/policy/php/lint_php_yml/lint_php_yml.rego +32 -0
  32. package/policy/php/package_json/package_json.rego +19 -0
  33. package/policy/style_lint/lint_style_yml/lint_style_yml.rego +35 -0
  34. package/policy/style_lint/package_json/package_json.rego +49 -0
  35. package/policy/text/cspell/cspell.rego +91 -0
  36. package/policy/text/markdownlint/markdownlint.rego +21 -0
  37. package/policy/text/oxfmtrc/oxfmtrc.rego +90 -0
  38. package/policy/text/package_json/package_json.rego +88 -0
  39. package/policy/vue/package_json/package_json.rego +54 -0
  40. package/scripts/check-adr.mjs +3 -2
  41. package/scripts/check-bun.mjs +21 -117
  42. package/scripts/check-graphql.mjs +6 -45
  43. package/scripts/check-hasura.mjs +2 -3
  44. package/scripts/check-image-avif.mjs +3 -3
  45. package/scripts/check-image-compress.mjs +25 -132
  46. package/scripts/check-js-bun-db.mjs +3 -50
  47. package/scripts/check-js-run.mjs +84 -86
  48. package/scripts/check-k8s.mjs +4 -4
  49. package/scripts/check-npm-module.mjs +17 -8
  50. package/scripts/check-php.mjs +16 -51
  51. package/scripts/check-style-lint.mjs +28 -52
  52. package/scripts/check-text.mjs +47 -219
  53. package/scripts/check-vue.mjs +3 -16
  54. package/scripts/lint-conftest.mjs +351 -0
  55. package/scripts/lint-ga.mjs +39 -2
  56. package/scripts/run-shellcheck-text.mjs +2 -2
  57. package/scripts/utils/conn-file-rules.mjs +170 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,42 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.8.208] - 2026-05-08
8
+
9
+ ### Added
10
+
11
+ - `mdc/js-run.mdc` (1.6, з 1.5): новий розділ «Нейминг файлів у `src/conn/`» — префікси `ql-` (GraphQL endpoint), `pg-`/`mysql-` з обовʼязковим `read`/`write` режимом і опційним ідентифікатором підключення для multi-БД (`pg-read-smart.js`, `pg-write-contract.js`); якщо режим не очевидний з імені env — визначати за наявністю операцій зміни даних. Також правило про експорти в `src/conn/`: заборонено `export default`, лише іменований експорт у camelCase від назви файла (`ql-smart.js` → `export const qlSmart`, `pg-write-contract.js` → `export const pgWriteContract`).
12
+ - `scripts/utils/conn-file-rules.mjs` + інтеграція в `scripts/check-js-run.mjs` — для кожного файла всередині `#conn/` каталогу пакета перевіряє: (а) basename відповідає канону `ql-<id>` / `(pg|mysql)-(read|write)[-<id>]` (kebab-case `[a-z0-9-]`); (б) відсутній `export default`; (в) серед іменованих експортів є рівно `<camelCase(basename)>` (`pg-write-contract.js` → `pgWriteContract`). `index.*` пропускається як reexport-барель. Розпізнає `export const/let/var`, `export function`, `export class` і `export { x as Y }` через AST на oxc-parser.
13
+
14
+ ## [1.8.207] - 2026-05-08
15
+
16
+ ### Added
17
+
18
+ - `npm/policy/ga/workflow_common/workflow_common.rego` — універсальні Rego-перевірки для **кожного** `.github/workflows/*.yml`: блок `concurrency` (group / cancel-in-progress), заборонені `oven-sh/setup-bun` / `actions/cache` / `bun install` у `uses`/`run` будь-якого кроку, заборонене shell-продовження `\` перед NL у `run:`, обовʼязковий `actions/checkout@…` перед локальним composite-action `setup-bun-deps`. Підключено в `lint-ga.mjs` як один прогін `conftest test <…all yml…> --namespace ga.workflow_common`.
19
+ - `npm/policy/bun/{bunfig,package_json}/*.rego` — порт `check-bun.mjs` (TOML і JSON-частина): `[install].linker == "hoisted"` у `bunfig.toml`; у кореневому `package.json` без `packageManager`, без `dependencies`, у `devDependencies` лише `@nitra/*`; агрегований `lint`-скрипт покриває всі `lint-*` через `bun run` і завершується `&& oxfmt .`.
20
+ - `npm/policy/text/{oxfmtrc,cspell,markdownlint,package_json}/*.rego` — порт `check-text.mjs`: `.oxfmtrc.json` обовʼязкові ключі і канонічні значення; `.cspell.json` `version "0.2"`, `language`, імпорт `@nitra/cspell-dict`, заборона `@cspell/dict-*`, обовʼязкові `ignorePaths`; `.markdownlint-cli2.jsonc` `gitignore: true`; `package.json` без Prettier, `@nitra/cspell-dict ^2.0.0+`, без `markdownlint-cli2` у залежностях.
21
+ - `npm/policy/style_lint/{package_json,lint_style_yml}/*.rego` — порт `check-style-lint.mjs`: скрипт `lint-style` через `npx stylelint`, `@nitra/stylelint-config` у `devDependencies`, `stylelint.extends == "@nitra/stylelint-config"`; у `lint-style.yml` хоча б один `run` з `npx stylelint`.
22
+ - `npm/policy/php/{package_json,lint_php_yml}/*.rego` — порт `check-php.mjs`: скрипт `lint-php` у `package.json`; у `lint-php.yml` хоча б один `run` з `bun run lint-php`.
23
+ - `npm/policy/npm_module/{root_package_json,npm_package_json,emit_types_config,npm_publish_yml}/*.rego` — порт `check-npm-module.mjs`: `workspaces ∋ "npm"` у кореневому `package.json`; у `npm/package.json` `types` відповідає одному з канонічних патернів і `files ∋ "types"`; `npm/tsconfig.emit-types.json` має канонічні `compilerOptions`; `.github/workflows/npm-publish.yml` має `on.push.paths ∋ "npm/**"`, `branches ∋ "main"`, `permissions.id-token: write` і крок `JS-DevTools/npm-publish` з `with.package: npm/package.json`.
24
+ - `npm/policy/k8s/manifest/manifest.rego` — порт пер-документних структурних правил `check-k8s.mjs`: `kind: Ingress` заборонено (Gateway API), `apiVersion: autoscaling/v1` заборонено (HPA → v2), у `kind: Service` заборонені анотації `cloud.google.com/neg` / `cloud.google.com/backend-config`, у `kind: Deployment` кожен контейнер `containers`+`initContainers` має непорожнє `resources.requests.cpu`. Cross-file Kustomize-логіка (svc/svc-hl, HPA/PDB, namespace base, kustomization patches) лишається в JS.
25
+ - `npm/policy/js_lint/{package_json,lint_js_yml}/*.rego` — порт `check-js-lint.mjs`: канонічний `lint-js`, `@nitra/eslint-config ≥ 3.9.2`, `engines.node ≥ 24`, `engines.bun ≥ 1.3`, `type: "module"`; у `lint-js.yml` `actions/checkout@v6` з `persist-credentials: false`, `setup-bun-deps`, `bunx oxlint/eslint/jscpd .`, без `--fix` у CI.
26
+ - `npm/policy/js_mssql/package_json/package_json.rego` — порт `check-js-mssql.mjs`: `dependencies.mssql ≥ 12.5.0` (підтримує `^12.5.0`, `>=12.5.0`, `workspace:*`).
27
+ - `npm/policy/js_bun_db/package_json/package_json.rego` — порт `check-js-bun-db.mjs`: у `dependencies` заборонені `pg`, `pg-format`, `mysql2`.
28
+ - `npm/policy/js_run/{package_json,jsconfig,configmap}/*.rego` — порт `check-js-run.mjs`: заборона `bunyan` / `@nitra/bunyan` у залежностях; `jsconfig.json` має канонічні `compilerOptions` і `include`; у k8s ConfigMap `OTEL_RESOURCE_ATTRIBUTES` містить `service.name=` і `service.namespace=`.
29
+ - `npm/policy/vue/package_json/package_json.rego` — порт `check-vue.mjs`: якщо `dependencies.vue` присутній, у `devDependencies` має бути `vite` мажорної версії ≥ 8.
30
+ - `npm/policy/graphql/package_json/package_json.rego` — порт `check-graphql.mjs`: `scripts.dump-schema` точно відповідає канонічному.
31
+ - `npm/policy/image_compress/package_json/package_json.rego` — порт `check-image-compress.mjs`: `lint-image` викликає `npx @nitra/minify-image --src=. --write` без `--avif`; агрегований `lint` містить `bun run lint-image`; `@nitra/minify-image` НЕ у `dependencies`/`devDependencies`.
32
+ - `npm/policy/hasura/svc_hl/svc_hl.rego` — порт `check-hasura.mjs` (мінімум): у `hasura/k8s/base/svc-hl.yaml` Service з `metadata.name` має закінчуватись на `-h`.
33
+ - `npm/policy/adr/{settings_json,settings_local_json}/*.rego` — порт `check-adr.mjs`: `.claude/settings.json` має містити Stop-hook з командою `.claude/hooks/capture-decisions.sh`; `.claude/settings.local.json` (якщо існує) — НЕ повинен мати дубля цього хука.
34
+ - `npm/policy/capacitor/package_json/package_json.rego` — порт `check-capacitor.mjs`: `dependencies['@capacitor/core']` мажорна ≥ 8 (підтримує `workspace:*`).
35
+ - `npm/policy/abie/{health_check_policy,http_route_base}/*.rego` — порт `check-abie.mjs`: `HealthCheckPolicy` (`networking.gke.io/v1`) має непорожній `requestPath` зі слешем, `port: 8080`, `targetRef.name` закінчується на `-hl`; `HTTPRoute` у `…/base/…` приймає лише hostnames у домені `aiml.live`.
36
+ - `npm/scripts/lint-conftest.mjs` (+ `bun run lint-conftest` у `package.json`) — єдиний раннер conftest по всіх нових polysi: для кожного namespace — single-file або walk-предикат, з gating-ом по `.n-cursor.json:rules`, як у `check-*.mjs`. Викликається в кореневому `lint` після `lint-rego`.
37
+
38
+ ### Changed
39
+
40
+ - `npm/policy/ga/{lint_ga,clean_ga_workflows,clean_merged_branch,git_ai}/*.rego`: прибрано дублікати правил `concurrency` (group / cancel-in-progress / missing) — їх покриває `ga.workflow_common`. Заодно усунено мовчазний баг `not is_object(input.concurrency)` (коли поля немає, повертає `undefined`, не `true`); у `workflow_common` через `object.get(input, "concurrency", false)` дає визначене значення. Канонічна тригер-група `expected_concurrency_group` теж видалена з кожної per-workflow polysi.
41
+ - `npm/scripts/lint-ga.mjs`: до існуючих per-workflow conftest-таргетів додано фінальний прогін `ga.workflow_common` одним викликом `conftest test <усі .yml> --namespace ga.workflow_common`. Імпорт `readdirSync` з `node:fs` для перерахунку workflow-файлів.
42
+
7
43
  ## [1.8.206] - 2026-05-08
8
44
 
9
45
  ### Added
package/mdc/js-run.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Це правила для backend проектів на JavaScript/Node.js, сюди входять і job і WEB сервери.
3
3
  alwaysApply: true
4
- version: '1.5'
4
+ version: '1.6'
5
5
  ---
6
6
 
7
7
  ## Область застосування
@@ -121,6 +121,47 @@ import { pool } from '#conn/pg.js'
121
121
  import { gql, graphQLClient } from '@nitra/graphql-request'
122
122
  ```
123
123
 
124
+ ### Нейминг файлів у `src/conn/`
125
+
126
+ Назва файла в `src/conn/` має одразу повідомляти, **до чого** підключаємось і **в якому режимі**:
127
+
128
+ - **GraphQL** — префікс `ql-`, далі ідентифікатор endpoint:
129
+ - `src/conn/ql-contract.js`
130
+ - `src/conn/ql-smart.js`
131
+ - **PostgreSQL** — префікс `pg-`, далі тип підключення (репліка vs мастер): `read` або `write`:
132
+ - `src/conn/pg-read.js`
133
+ - `src/conn/pg-write.js`
134
+ - **PostgreSQL до кількох БД** — додатково ідентифікатор підключення після типу:
135
+ - `src/conn/pg-read-smart.js`
136
+ - `src/conn/pg-write-contract.js`
137
+ - **MySQL / MSSQL** — префікс `mysql-` за тією ж схемою (`mysql-read.js`, `mysql-write-<id>.js` тощо).
138
+
139
+ Підключення до БД **обов'язково** має бути ідентифіковано як `read` (репліка) або `write` (мастер). Якщо з імені змінної оточення (наприклад, `env.PG_CONN`) це не очевидно — визнач режим за операціями в коді: якщо немає операцій зміни даних (`INSERT`/`UPDATE`/`DELETE`/DDL) — це `pg-read.js`, інакше `pg-write.js`.
140
+
141
+ ### Експорти у файлах `src/conn/`
142
+
143
+ У файлах підключень **заборонений** `export default`. Експорт має бути **іменований** і збігатися з назвою файла в camelCase.
144
+
145
+ Приклад — `src/conn/ql-smart.js`:
146
+
147
+ ```javascript title="❌ Так не можна"
148
+ export default new GraphQLClient(env.SMART_QL, {
149
+ headers: {
150
+ 'X-Hasura-Admin-Secret': env.SMART_X_HASURA_ADMIN_SECRET
151
+ }
152
+ })
153
+ ```
154
+
155
+ ```javascript title="✅ Канон: іменований експорт за іменем файла"
156
+ export const qlSmart = new GraphQLClient(env.SMART_QL, {
157
+ headers: {
158
+ 'X-Hasura-Admin-Secret': env.SMART_X_HASURA_ADMIN_SECRET
159
+ }
160
+ })
161
+ ```
162
+
163
+ Відповідно: `pg-read.js` → `export const pgRead = …`, `pg-write-contract.js` → `export const pgWriteContract = …`, `ql-contract.js` → `export const qlContract = …`.
164
+
124
165
  ## CheckEnv
125
166
 
126
167
  Усі змінні оточення, які використовуються в коді, повинні бути перевірені за допомогою `checkEnv` з пакету `@nitra/check-env`. Це гарантує, що всі необхідні змінні оточення встановлені перед запуском програми.
@@ -189,4 +230,10 @@ on:
189
230
 
190
231
  ## Перевірка
191
232
 
192
- `npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище.
233
+ `npx @nitra/cursor check js-run` — зокрема для кожного backend workspace-пакета з каталогом **`src/`** перевіряє наявність **`jsconfig.json`** і збіг вмісту з каноном вище. Додатково для файлів у каталозі `#conn/` (за замовчуванням `src/conn/`) перевіряється:
234
+
235
+ - **basename файла** відповідає канону: `ql-<id>` (GraphQL) / `(pg|mysql)-(read|write)[-<id>]` (БД), kebab-case `[a-z0-9-]`;
236
+ - **відсутній `export default`** — лише іменований експорт;
237
+ - **імʼя експорту** дорівнює camelCase від basename (`pg-write-contract.js` → `export const pgWriteContract`).
238
+
239
+ Файли `index.*` у conn-каталозі пропускаються як можливий reexport-барель.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.206",
3
+ "version": "1.8.208",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -0,0 +1,73 @@
1
+ # Порт мінімальної структурної перевірки `HealthCheckPolicy` з
2
+ # `npm/scripts/check-abie.mjs` (abie.mdc).
3
+ #
4
+ # Запуск (локально):
5
+ # conftest test path/to/k8s/.../hc.yaml -p npm/policy/abie \
6
+ # --namespace abie.health_check_policy
7
+ #
8
+ # Перевіряє, для документів з `kind: HealthCheckPolicy` (apiVersion
9
+ # `networking.gke.io/v1`):
10
+ # - `spec.config.httpHealthCheck.requestPath` — непорожній шлях, що починається з `/`;
11
+ # - `spec.config.httpHealthCheck.port` (або `spec.targetRef.name` суфікс) — `8080`;
12
+ # - `spec.targetRef.name` має закінчуватись на `-hl` (headless backend).
13
+ #
14
+ # Cross-file gating (`abie` правило в `.n-cursor.json`, парність з Deployment-каталогу,
15
+ # узгодження з `metadata.name` Deployment) — у JS (`check-abie.mjs`).
16
+ #
17
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
18
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
19
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
20
+ package abie.health_check_policy
21
+
22
+ import rego.v1
23
+
24
+ req_path_starts_with_slash_template := concat(" ", [
25
+ "HealthCheckPolicy: requestPath має починатись з `/`",
26
+ "(зараз %q) (abie.mdc)",
27
+ ])
28
+
29
+ # ── deny: requestPath ──────────────────────────────────────────────────────
30
+
31
+ deny contains msg if {
32
+ is_health_check_policy
33
+ req_path == ""
34
+ msg := "HealthCheckPolicy: spec.config.httpHealthCheck.requestPath має бути непорожнім (abie.mdc)"
35
+ }
36
+
37
+ deny contains msg if {
38
+ is_health_check_policy
39
+ req_path != ""
40
+ not startswith(req_path, "/")
41
+ msg := sprintf(req_path_starts_with_slash_template, [req_path])
42
+ }
43
+
44
+ # ── deny: port == 8080 ────────────────────────────────────────────────────
45
+
46
+ deny contains msg if {
47
+ is_health_check_policy
48
+ port := object.get(http_health_check, "port", null)
49
+ port != null
50
+ port != 8080
51
+ msg := sprintf("HealthCheckPolicy: port має бути 8080 (зараз %v) (abie.mdc)", [port])
52
+ }
53
+
54
+ # ── deny: targetRef.name закінчується на `-hl` ────────────────────────────
55
+
56
+ deny contains msg if {
57
+ is_health_check_policy
58
+ name := object.get(object.get(input.spec, "targetRef", {}), "name", "")
59
+ name != ""
60
+ not endswith(name, "-hl")
61
+ msg := sprintf("HealthCheckPolicy: targetRef.name має закінчуватись на `-hl` (зараз %q) (abie.mdc)", [name])
62
+ }
63
+
64
+ # ── helpers ────────────────────────────────────────────────────────────────
65
+
66
+ is_health_check_policy if {
67
+ input.kind == "HealthCheckPolicy"
68
+ startswith(object.get(input, "apiVersion", ""), "networking.gke.io/")
69
+ }
70
+
71
+ http_health_check := object.get(object.get(object.get(input, "spec", {}), "config", {}), "httpHealthCheck", {})
72
+
73
+ req_path := object.get(http_health_check, "requestPath", "")
@@ -0,0 +1,45 @@
1
+ # Порт перевірки `HTTPRoute` у шарі `…/k8s/.../base/...` з
2
+ # `npm/scripts/check-abie.mjs` (abie.mdc): дозволені лише hostnames з домену
3
+ # `aiml.live` (включно з піддоменами та `*.aiml.live`).
4
+ #
5
+ # Запуск (локально):
6
+ # conftest test path/to/k8s/base/hr.yaml -p npm/policy/abie \
7
+ # --namespace abie.http_route_base
8
+ #
9
+ # Cross-file gating (саме шлях `…/base/…` визначає, чи застосовувати правило)
10
+ # — у JS: conftest викликаємо лише на YAML-ах з base/. Тут — лише валідація вмісту
11
+ # `spec.hostnames`.
12
+ #
13
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
14
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
15
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
16
+ package abie.http_route_base
17
+
18
+ import rego.v1
19
+
20
+ allowed_apex := "aiml.live"
21
+
22
+ deny contains msg if {
23
+ input.kind == "HTTPRoute"
24
+ some host in object.get(object.get(input, "spec", {}), "hostnames", [])
25
+ is_string(host)
26
+ not host_matches_aiml_live(host)
27
+ msg := sprintf("HTTPRoute (base): %q має бути в домені aiml.live (abie.mdc)", [host])
28
+ }
29
+
30
+ # Чи hostname належить до aiml.live (точна відповідність, піддомен `*.aiml.live`
31
+ # або довільний субдомен `*.aiml.live`).
32
+ host_matches_aiml_live(host) if {
33
+ host_lower := lower(host)
34
+ host_lower == allowed_apex
35
+ }
36
+
37
+ host_matches_aiml_live(host) if {
38
+ host_lower := lower(host)
39
+ endswith(host_lower, sprintf(".%s", [allowed_apex]))
40
+ }
41
+
42
+ host_matches_aiml_live(host) if {
43
+ host_lower := lower(host)
44
+ host_lower == sprintf("*.%s", [allowed_apex])
45
+ }
@@ -0,0 +1,31 @@
1
+ # Порт перевірки `.claude/settings.json` з `npm/scripts/check-adr.mjs` (adr.mdc):
2
+ # `hooks.Stop[*]` має містити групу, де хоча б один елемент `hooks[]` має `command`
3
+ # зі substring `.claude/hooks/capture-decisions.sh`.
4
+ #
5
+ # Запуск (локально):
6
+ # conftest test .claude/settings.json -p npm/policy/adr \
7
+ # --namespace adr.settings_json
8
+ #
9
+ # Hash-порівняння bash-скрипта з канонічним bundled-варіантом і `.gitignore`-перевірки
10
+ # — у JS (`check-adr.mjs`).
11
+ #
12
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
13
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
14
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
15
+ package adr.settings_json
16
+
17
+ import rego.v1
18
+
19
+ hook_command_marker := ".claude/hooks/capture-decisions.sh"
20
+
21
+ deny contains msg if {
22
+ not has_adr_stop_hook
23
+ msg := ".claude/settings.json: відсутній Stop-hook для `capture-decisions.sh` у hooks.Stop (adr.mdc)"
24
+ }
25
+
26
+ # Чи є в `hooks.Stop[*].hooks[*].command` рядок з маркером скрипта.
27
+ has_adr_stop_hook if {
28
+ some group in object.get(object.get(input, "hooks", {}), "Stop", [])
29
+ some hook in object.get(group, "hooks", [])
30
+ contains(object.get(hook, "command", ""), hook_command_marker)
31
+ }
@@ -0,0 +1,28 @@
1
+ # Порт перевірки `.claude/settings.local.json` з `npm/scripts/check-adr.mjs`
2
+ # (adr.mdc): після переходу на project-shared `settings.json` цей файл (якщо є)
3
+ # НЕ повинен мати дубля Stop-хука з маркером `.claude/hooks/capture-decisions.sh`,
4
+ # інакше один і той самий скрипт виконається двічі на одну подію.
5
+ #
6
+ # Запуск (локально):
7
+ # conftest test .claude/settings.local.json -p npm/policy/adr \
8
+ # --namespace adr.settings_local_json
9
+ #
10
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
11
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
12
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
13
+ package adr.settings_local_json
14
+
15
+ import rego.v1
16
+
17
+ hook_command_marker := ".claude/hooks/capture-decisions.sh"
18
+
19
+ duplicate_template := concat(" ", [
20
+ ".claude/settings.local.json: видали дубль Stop-хука для",
21
+ "`capture-decisions.sh` — він уже у project-shared settings.json (adr.mdc)",
22
+ ])
23
+
24
+ deny contains duplicate_template if {
25
+ some group in object.get(object.get(input, "hooks", {}), "Stop", [])
26
+ some hook in object.get(group, "hooks", [])
27
+ contains(object.get(hook, "command", ""), hook_command_marker)
28
+ }
@@ -0,0 +1,33 @@
1
+ # Порт перевірки `checkBunfigHoisted` з `npm/scripts/check-bun.mjs` (bun.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test bunfig.toml -p npm/policy/bun --namespace bun.bunfig
5
+ #
6
+ # Conftest парсить `.toml` нативно: секція `[install]` стає обʼєктом `input.install`.
7
+ # FS-перевірки (наявність самого `bunfig.toml`, `bun.lock`, заборонені lockfile-и
8
+ # `package-lock.json` тощо, директорія `.yarn/`) живуть у `check-bun.mjs` — Rego
9
+ # працює лише з вже завантаженим input.
10
+ #
11
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
12
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
13
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
14
+ package bun.bunfig
15
+
16
+ import rego.v1
17
+
18
+ deny contains msg if {
19
+ # `object.get(…, false)` дає визначене значення, коли поля немає, інакше
20
+ # `not is_object(input.install)` повернув би `undefined`, і правило мовчки
21
+ # не спрацювало б (той самий патерн, що й у `ga.workflow_common`).
22
+ not is_object(object.get(input, "install", false))
23
+ msg := "bunfig.toml: відсутня секція [install] (bun.mdc)"
24
+ }
25
+
26
+ deny contains msg if {
27
+ is_object(object.get(input, "install", false))
28
+
29
+ # `object.get(…, null)` робить значення визначеним, інакше при відсутньому
30
+ # `linker` порівняння `!= "hoisted"` дало б `undefined`, не `true`.
31
+ object.get(input.install, "linker", null) != "hoisted"
32
+ msg := "bunfig.toml: у секції [install] має бути linker = \"hoisted\" (bun.mdc)"
33
+ }
@@ -0,0 +1,94 @@
1
+ # Порт структурних перевірок `package.json` з `npm/scripts/check-bun.mjs` (bun.mdc).
2
+ #
3
+ # Запуск (локально, КОРЕНЕВИЙ package.json):
4
+ # conftest test package.json -p npm/policy/bun --namespace bun.package_json
5
+ #
6
+ # Перевіряє: відсутність `packageManager`, відсутність кореневих `dependencies`,
7
+ # у `devDependencies` лише `@nitra/*`, агрегований `lint`-скрипт (якщо є `lint-*`
8
+ # скрипти): покриває всі lint-* через `bun run`, закінчується на `&& oxfmt .`.
9
+ #
10
+ # Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
11
+ # - `lint-docker` / `lint-k8s` коли `.n-cursor.json:rules` містить відповідне
12
+ # правило (потрібен другий файл-вхід — у Rego без `--combine` не зробити).
13
+ #
14
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
15
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
16
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
17
+ package bun.package_json
18
+
19
+ import rego.v1
20
+
21
+ # ── Шаблони повідомлень ────────────────────────────────────────────────────
22
+
23
+ # Через `concat` — дотримуємося regal style/line-length.
24
+ lint_aggregate_missing_template := concat(" ", [
25
+ "У package.json є скрипти %v, але немає агрегованого `lint`.",
26
+ "Додай скрипт, який запускає їх через `bun run` (bun.mdc)",
27
+ ])
28
+
29
+ # ── deny: заборонені поля ──────────────────────────────────────────────────
30
+
31
+ deny contains msg if {
32
+ pm := object.get(input, "packageManager", "")
33
+ pm != ""
34
+ msg := sprintf("package.json містить поле packageManager: %q — видали його (bun.mdc)", [pm])
35
+ }
36
+
37
+ # `dependencies` не повинно бути взагалі — навіть пусте `{}`. Сентинельний рядок
38
+ # дозволяє відрізнити «поле відсутнє» від «поле є з будь-яким значенням».
39
+ deny contains msg if {
40
+ object.get(input, "dependencies", "__bun_missing__") != "__bun_missing__"
41
+ msg := "Кореневий package.json не повинен містити поле dependencies — додай залежності в workspace-пакети (bun.mdc)"
42
+ }
43
+
44
+ # ── deny: devDependencies — лише `@nitra/*` ───────────────────────────────
45
+
46
+ deny contains msg if {
47
+ is_object(input.devDependencies)
48
+ some name, _ in input.devDependencies
49
+ not startswith(name, "@nitra/")
50
+ msg := sprintf("Кореневі devDependencies: дозволені лише @nitra/* — прибери або перенеси: %s (bun.mdc)", [name])
51
+ }
52
+
53
+ # ── deny: агрегований lint-скрипт ─────────────────────────────────────────
54
+ #
55
+ # Якщо в `scripts` є хоч один `lint-*`, має бути скрипт `lint`, у якому
56
+ # через `bun run <ім'я>` викликається кожен такий скрипт; рядок завершується
57
+ # на `&& oxfmt .`.
58
+
59
+ deny contains msg if {
60
+ count(lint_prefixed_scripts) > 0
61
+ lint_script == ""
62
+ msg := sprintf(lint_aggregate_missing_template, [lint_prefixed_scripts])
63
+ }
64
+
65
+ deny contains msg if {
66
+ count(lint_prefixed_scripts) > 0
67
+ lint_script != ""
68
+ some script in lint_prefixed_scripts
69
+ not contains(lint_script, sprintf("bun run %s", [script]))
70
+ msg := sprintf("Скрипт `lint` має викликати `%s` через `bun run` (bun.mdc)", [script])
71
+ }
72
+
73
+ deny contains msg if {
74
+ count(lint_prefixed_scripts) > 0
75
+ lint_script != ""
76
+
77
+ # Перевіряємо, що рядок завершується `&& oxfmt .` (з можливими пробілами/табами).
78
+ # Trim не потрібен — пробіли/таби в кінці допускаємо в самому regex (`[ \t]*$`).
79
+ not regex.match(`&&[ \t]+oxfmt[ \t]+\.[ \t]*$`, lint_script)
80
+ msg := "Скрипт `lint` має закінчуватися на `&& oxfmt .` (bun.mdc)"
81
+ }
82
+
83
+ # ── helpers ────────────────────────────────────────────────────────────────
84
+
85
+ # Ключі скриптів, що починаються з `lint-` (наприклад `lint-js`, `lint-ga`).
86
+ lint_prefixed_scripts := [name |
87
+ some name, _ in object.get(input, "scripts", {})
88
+ startswith(name, "lint-")
89
+ ]
90
+
91
+ # Значення `scripts.lint` як рядок (порожній, якщо поля немає або тип не string).
92
+ default lint_script := ""
93
+
94
+ lint_script := input.scripts.lint if is_string(input.scripts.lint)
@@ -0,0 +1,45 @@
1
+ # Порт перевірки версії `@capacitor/core` з `npm/scripts/check-capacitor.mjs`
2
+ # (capacitor.mdc) — мінімальна мажорна версія = 8.
3
+ #
4
+ # Запуск (локально, у пакеті з Capacitor):
5
+ # conftest test path/to/package.json -p npm/policy/capacitor \
6
+ # --namespace capacitor.package_json
7
+ #
8
+ # Перевіряє: якщо в `dependencies['@capacitor/core']` присутній (gating: пакет
9
+ # реально використовує Capacitor), то перша мажорна цифра в діапазоні має бути ≥ 8.
10
+ # Підтримує `^8.0.0`, `>=8`, `8.x`, `workspace:*` тощо.
11
+ #
12
+ # Цей порт спрощує JS-логіку — повна семантика OR-діапазонів (`a || b`) і нижня
13
+ # межа діапазону лишається в JS (`check-capacitor.mjs`: `capacitorVersionRangeMinMajor`).
14
+ # JS-перевірка лишилась authoritative й бігає через `npx @nitra/cursor check capacitor`;
15
+ # ця Rego — швидкий gate для одиничного `package.json` (наприклад через IDE).
16
+ #
17
+ # FS-сканування пакетів через workspaces, iOS-специфічна логіка (Podfile), вибір
18
+ # каталогу пакета з Capacitor — у JS.
19
+ #
20
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
21
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
22
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
23
+ package capacitor.package_json
24
+
25
+ import rego.v1
26
+
27
+ deny contains msg if {
28
+ range := object.get(object.get(input, "dependencies", {}), "@capacitor/core", "")
29
+ range != ""
30
+ not capacitor_major_at_least_8(range)
31
+ msg := sprintf("@capacitor/core має бути >= 8 (зараз %q) (capacitor.mdc)", [range])
32
+ }
33
+
34
+ # `workspace:*` / `*` / `x` / `latest` — пропускаємо (як у JS).
35
+ capacitor_major_at_least_8(range) if startswith(trim_space(range), "workspace:")
36
+
37
+ capacitor_major_at_least_8(range) if {
38
+ first_major(range) >= 8
39
+ }
40
+
41
+ first_major(range) := major if {
42
+ match := regex.find_n(`\d+`, range, 1)
43
+ count(match) > 0
44
+ major := to_number(match[0])
45
+ }
@@ -20,21 +20,12 @@ import rego.v1
20
20
  # interpolation. Збираємо очікувані рядки з фрагментів через `concat`, як це
21
21
  # зроблено в check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
22
22
 
23
- expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
24
-
25
23
  expected_github_token := concat("", ["$", "{{ github.token }}"])
26
24
 
27
25
  expected_name := "Clean action for removing completed workflow runs"
28
26
 
29
27
  expected_cron := "0 1 16 * *"
30
28
 
31
- # Шаблон повідомлення про відсутню `concurrency`-секцію — винесено через `concat`,
32
- # щоб дотриматися regal style/line-length.
33
- concurrency_missing_template := concat(" ", [
34
- "clean-ga-workflows.yml: відсутня секція concurrency —",
35
- "додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
36
- ])
37
-
38
29
  # ── Аліаси на input ────────────────────────────────────────────────────────
39
30
  #
40
31
  # GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, конфтест серіалізує його
@@ -62,23 +53,6 @@ deny contains msg if {
62
53
  msg := "clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)"
63
54
  }
64
55
 
65
- deny contains msg if {
66
- not is_object(input.concurrency)
67
- msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
68
- }
69
-
70
- deny contains msg if {
71
- is_object(input.concurrency)
72
- input.concurrency.group != expected_concurrency_group
73
- msg := sprintf("clean-ga-workflows.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
74
- }
75
-
76
- deny contains msg if {
77
- is_object(input.concurrency)
78
- input.concurrency["cancel-in-progress"] != true
79
- msg := "clean-ga-workflows.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
80
- }
81
-
82
56
  deny contains msg if {
83
57
  not input.jobs.cleanup_old_workflows
84
58
  msg := "clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)"
@@ -16,8 +16,6 @@ import rego.v1
16
16
  # Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
17
17
  # `concat`, бо `{{` у Rego починає string interpolation.
18
18
 
19
- expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
20
-
21
19
  expected_github_token := concat("", ["$", "{{ github.token }}"])
22
20
 
23
21
  expected_deleted_branches_expr := concat("", ["$", "{{ steps.delete_stuff.outputs.deleted_branches }}"])
@@ -28,12 +26,6 @@ expected_name := "Clean abandoned branches"
28
26
 
29
27
  expected_cron := "0 1 15 * *"
30
28
 
31
- # Шаблони повідомлень — через `concat` для regal style/line-length.
32
- concurrency_missing_template := concat(" ", [
33
- "clean-merged-branch.yml: відсутня секція concurrency —",
34
- "додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
35
- ])
36
-
37
29
  # ── Аліаси на input ────────────────────────────────────────────────────────
38
30
  #
39
31
  # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
@@ -63,23 +55,6 @@ deny contains msg if {
63
55
  msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
64
56
  }
65
57
 
66
- deny contains msg if {
67
- not is_object(input.concurrency)
68
- msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
69
- }
70
-
71
- deny contains msg if {
72
- is_object(input.concurrency)
73
- input.concurrency.group != expected_concurrency_group
74
- msg := sprintf("clean-merged-branch.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
75
- }
76
-
77
- deny contains msg if {
78
- is_object(input.concurrency)
79
- input.concurrency["cancel-in-progress"] != true
80
- msg := "clean-merged-branch.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
81
- }
82
-
83
58
  deny contains msg if {
84
59
  not input.jobs.cleanup_old_branches
85
60
  msg := "clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)"
@@ -13,8 +13,6 @@ import rego.v1
13
13
 
14
14
  # ── Очікувані значення ─────────────────────────────────────────────────────
15
15
 
16
- expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
17
-
18
16
  expected_name := "Git AI"
19
17
 
20
18
  expected_if_substring := "github.event.pull_request.merged == true"
@@ -23,13 +21,6 @@ expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash
23
21
 
24
22
  expected_run_substring := "git-ai ci github run"
25
23
 
26
- # Шаблон повідомлення про відсутню `concurrency`-секцію — через `concat` для
27
- # regal style/line-length.
28
- concurrency_missing_template := concat(" ", [
29
- "git-ai.yml: відсутня секція concurrency —",
30
- "додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
31
- ])
32
-
33
24
  # ── Аліаси на input ────────────────────────────────────────────────────────
34
25
  #
35
26
  # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
@@ -57,23 +48,6 @@ deny contains msg if {
57
48
  msg := "git-ai.yml: on.pull_request.types має містити closed (ga.mdc)"
58
49
  }
59
50
 
60
- deny contains msg if {
61
- not is_object(input.concurrency)
62
- msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
63
- }
64
-
65
- deny contains msg if {
66
- is_object(input.concurrency)
67
- input.concurrency.group != expected_concurrency_group
68
- msg := sprintf("git-ai.yml: concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
69
- }
70
-
71
- deny contains msg if {
72
- is_object(input.concurrency)
73
- input.concurrency["cancel-in-progress"] != true
74
- msg := "git-ai.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
75
- }
76
-
77
51
  deny contains msg if {
78
52
  not job
79
53
  msg := "git-ai.yml: jobs.git-ai відсутній (ga.mdc)"