@nitra/cursor 1.8.203 → 1.8.206

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@
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.206] - 2026-05-08
8
+
9
+ ### Added
10
+
11
+ - `mdc/rego.mdc` (нова версія 1.1, з 1.0): VS Code-секція з рекомендованим розширенням `tsandall.opa` (LSP від автора OPA: підсвічування, hover, go-to-definition, format-on-save через `opa fmt`), `.vscode/extensions.json` і `.vscode/settings.json` сніпети для `[rego]` (`editor.defaultFormatter: tsandall.opa`, `formatOnSave: true`); опис кроків `lint-rego` (preflight `opa`+`regal`, далі `opa check --strict` і `regal lint`); `package.json`-сніпет зі скриптом `lint-rego`; install-команди (`brew install opa regal` + universal лінки); приклад `.regal/config.yaml`. Раніше файл містив лише placeholder `npx @nitra/cursor check rego`.
12
+
13
+ ### Changed
14
+
15
+ - `scripts/lint-rego.mjs`: додано preflight на `opa` (поряд з `regal`) з install-hint `brew install opa` і покликом до VS Code-розширення `tsandall.opa`; до `regal lint` додано попередній крок `opa check --strict <targets>` (типи + строгий режим: мертвий код, неоднозначні правила, незадекларовані змінні) — `opa check` ловить compile-помилки, які `regal` навмисно лишає поза скоупом. Якщо хоч один з `opa`/`regal` відсутній у `PATH` — exit 1 ще до запуску, з підказкою встановлення для обох.
16
+
17
+ ## [1.8.205] - 2026-05-08
18
+
19
+ ### Added
20
+
21
+ - `npm/policy/ga/lint_ga/lint_ga.rego` — порт `validateLintGaWorkflowStructure` + `validateLintGaOnTriggers`: `name` / `on.push.branches∋{dev,main}` / `on.pull_request.branches∋{dev,main}` / `on.push.paths∋{.github/actions/**,.github/workflows/**}` / `concurrency` / `jobs.lint-ga.runs-on` / `jobs.lint-ga.permissions.contents=read` / `steps` non-empty / `uses` set містить `actions/checkout@v6`, `./.github/actions/setup-bun-deps`, `astral-sh/setup-uv@v8.0.0` / `run` blob містить `bun run lint-ga`.
22
+ - `npm/policy/ga/git_ai/git_ai.rego` — порт `validateGitAiWorkflowStructure`: `name` / `on.pull_request.types∋closed` / `concurrency` / `jobs.git-ai.if` містить `merged == true` / `permissions.contents=write` / `run` blob містить `curl … usegitai.com … bash` і `git-ai ci github run`.
23
+
24
+ ### Changed
25
+
26
+ - `scripts/lint-ga.mjs`: `CONFTEST_TARGETS` тепер охоплює всі 4 канонічні GA-workflow — `clean-ga-workflows.yml`, `clean-merged-branch.yml`, `lint-ga.yml`, `git-ai.yml` — кожен зі своїм `--namespace ga.<name>`.
27
+ - `scripts/check-ga.mjs`: видалено `validateLintGaWorkflowStructure`, `validateLintGaOnTriggers`, `validateGitAiWorkflowStructure`, `validateGitAiParsedYaml`, `hasPullRequestClosedTrigger`, `hasJobMergedCondition`, `checkLintGaWorkflow`, `checkGitAiWorkflow`, `checkCanonicalWorkflowsMatchRule`, локальний `isExactString` і відповідні імпорти `anyRunStepIncludes`/`flattenWorkflowSteps`/`getStepRun`/`getStepUses`. Файл скоротився з 1074 → 570 рядків (≈47%) — структурні перевірки канонічних GA-workflow повністю мігрували в conftest. У JS лишилися: file-existence (zizmor.yml, .vscode/settings.json, setup-bun-deps), `package.json` script `lint-ga`, MegaLinter-зачистка, `verifyConcurrencyBlock` для всіх workflow без винятків (включно з не-канонічними), `verifyNoDirectBunOrCache`, `verifyCheckoutBeforeLocalSetupBunDeps`, paths-globs через `git ls-files`, preflight `shellcheck`.
28
+
29
+ ## [1.8.204] - 2026-05-07
30
+
31
+ ### Changed
32
+
33
+ - Реструктурував `npm/policy/ga/` під namespaced sub-packages, які проходять regal: `ga/clean_ga_workflows/clean_ga_workflows.rego` та новий `ga/clean_merged_branch/clean_merged_branch.rego` (порт `validateCleanMergedBranch` з check-ga.mjs — `name` / `cron 0 1 15 * *` / `workflow_dispatch` / `concurrency` / `jobs.cleanup_old_branches` / step0 `phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3` з token / age=90 / ignore_branches main,dev / `dry_run: false` (YAML 1.1) / step1 `Get output` + `DELETED_BRANCHES` env + echo).
34
+ - `scripts/lint-ga.mjs`: `CONFTEST_TARGETS` тепер містить `clean-ga-workflows.yml` і `clean-merged-branch.yml`, conftest викликаємо з `--namespace ga.<name>` для ізоляції правил між workflow.
35
+ - `scripts/check-ga.mjs`: видалено `validateCleanGaWorkflows*` і `validateCleanMergedBranch*` — їх повністю покриває conftest у `lint-ga`. `checkCanonicalWorkflowsMatchRule` тепер валідує лише `lint-ga.yml` і `git-ai.yml` (наступні кандидати на міграцію).
36
+
37
+ ### Added
38
+
39
+ - `.regal/config.yaml` у корені — вимикає `idiomatic.no-defined-entrypoint` (для conftest-полісі `deny`-правила є де-факто entrypoint-ами, формальна анотація не несе семантики).
40
+
7
41
  ## [1.8.203] - 2026-05-07
8
42
 
9
43
  ### Changed
@@ -108,7 +142,7 @@
108
142
  ### Added
109
143
 
110
144
  - `run-shellcheck-text.mjs`: для `lint-text` — перевірка наявності `shellcheck`/`patch`, авто-виправлення через `shellcheck -f diff` + `patch -p1`, фінальний прогін по tracked `*.sh` (git) або `**/*.sh` без `node_modules`.
111
- - `text` (mdc v1.25 → v1.26): **shellcheck** у ланцюжку `lint-text`, рекомендація **`timonwong.shellcheck`**, тригер workflow **`**/*.sh`**; тести `run-shellcheck-text.test.mjs`.
145
+ - `text` (mdc v1.25 → v1.26): **shellcheck** у ланцюжку `lint-text`, рекомендація **`timonwong.shellcheck`**, тригер workflow **`**/\*.sh`**; тести `run-shellcheck-text.test.mjs`.
112
146
 
113
147
  ### Changed
114
148
 
package/bin/auto-rules.md CHANGED
@@ -42,6 +42,8 @@ npm-module - якщо в корені присутня директорія npm
42
42
 
43
43
  php - якщо в корені є composer.json
44
44
 
45
+ rego - якщо в проекті є хоч один rego
46
+
45
47
  style-lint - якщо присутній хоч один vue або css файл
46
48
 
47
49
  text - завжди
package/mdc/rego.mdc ADDED
@@ -0,0 +1,77 @@
1
+ ---
2
+ description: Opa, Rego — інструментарій (VS Code, lint-rego)
3
+ version: '1.1'
4
+ globs: "**/*.rego"
5
+ alwaysApply: false
6
+ ---
7
+
8
+ # Rego (opa)
9
+
10
+ Синтаксичні правила (`rego.v1`, `import rego.v1`, заборона legacy v0) — у `conftest.mdc` (alwaysApply). Цей файл — про **інструментарій**: VS Code, лінтери, форматування.
11
+
12
+ ## VS Code
13
+
14
+ Розширення `tsandall.opa` (від автора OPA): підсвічування, hover, go-to-definition, оцінка виразів і `format-on-save` через `opa fmt`. Працює лише за наявності `opa` у `PATH` — встановити нижче.
15
+
16
+ ```json title=".vscode/extensions.json"
17
+ {
18
+ "recommendations": ["tsandall.opa"]
19
+ }
20
+ ```
21
+
22
+ ```json title=".vscode/settings.json"
23
+ {
24
+ "[rego]": {
25
+ "editor.defaultFormatter": "tsandall.opa",
26
+ "editor.formatOnSave": true
27
+ }
28
+ }
29
+ ```
30
+
31
+ `opa.checkOnSave` за замовчуванням увімкнено в розширенні — діагностика від `opa check` показується в редакторі, тож синтаксичні/типові помилки видно одразу, без запуску `lint-rego`.
32
+
33
+ ## Перевірка
34
+
35
+ ```bash
36
+ bun run lint-rego
37
+ ```
38
+
39
+ Скрипт делегує до `bun ./npm/scripts/lint-rego.mjs`. Послідовність:
40
+
41
+ 1. **preflight** — наявність `opa` і `regal` у `PATH`; якщо хоча б одного нема — exit 1 з підказкою встановлення;
42
+ 2. `opa check --strict <targets>` — компіляція з типами та `--strict` (мертвий код, неоднозначні правила, незадекларовані змінні);
43
+ 3. `regal lint <targets>` — статичний лінтер Rego ([Styra Regal](https://docs.styra.com/regal)): ловить v0-синтаксис, неявні set-rules і відхилення від `rego.v1`, плюс bugs/idiomatic/style-правила.
44
+
45
+ Цілі — `npm/policy/` (де живуть Rego-полісі пакета). Інші *.rego поза деревом додай у `LINT_TARGETS` у `lint-rego.mjs`.
46
+
47
+ ### Встановлення інструментів
48
+
49
+ - macOS: `brew install opa regal`
50
+ - Linux/Windows:
51
+ - opa — <https://www.openpolicyagent.org/docs/latest/#1-download-opa>
52
+ - regal — <https://docs.styra.com/regal#installation>
53
+
54
+ Обидва — лише в `PATH`, **не** додавай у `dependencies` / `devDependencies` (як `shellcheck` у `text.mdc`).
55
+
56
+ ### `package.json`
57
+
58
+ ```json title="package.json"
59
+ {
60
+ "scripts": {
61
+ "lint-rego": "bun ./npm/scripts/lint-rego.mjs"
62
+ }
63
+ }
64
+ ```
65
+
66
+ У кореневому `lint` (з `text.mdc` і дотичних) включай `bun run lint-rego` — щоб локальний прогін співпадав з CI.
67
+
68
+ ## Конфіг regal
69
+
70
+ У корені — `.regal/config.yaml`. Дозволено вимикати окремі правила під специфіку репо (наприклад, conftest-полісі — `deny`-правила як де-факто entrypoint-и):
71
+
72
+ ```yaml title=".regal/config.yaml"
73
+ rules:
74
+ idiomatic:
75
+ no-defined-entrypoint:
76
+ level: ignore
77
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.203",
3
+ "version": "1.8.206",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,26 +1,25 @@
1
- # PoC-порт перевірки `validateCleanGaWorkflows` з `npm/scripts/check-ga.mjs`.
1
+ # Порт перевірки `validateCleanGaWorkflows` з `npm/scripts/check-ga.mjs` (ga.mdc).
2
2
  #
3
3
  # Запуск (локально):
4
- # conftest test .github/workflows/clean-ga-workflows.yml -p npm/policy/ga
4
+ # conftest test .github/workflows/clean-ga-workflows.yml \
5
+ # -p npm/policy/ga --namespace ga.clean_ga_workflows
5
6
  #
6
- # Conftest читає YAML і дає його в `input`. Кожне правило `deny contains msg if { … }`,
7
- # що матчиться, друкується як порушення; пустий список exit 0.
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
8
10
  #
9
- # Rego v1 синтаксис (OPA 1.x за замовчуванням; `import rego.v1` робить файл портованим
10
- # і на старі OPA 0.x): `contains` для partial set rules, `if` перед тілом правила.
11
- package main
11
+ # Усі `deny`-правила йдуть контигно (regal: messy-rule); helpers і константи
12
+ # секціями вище та нижче.
13
+ package ga.clean_ga_workflows
12
14
 
13
15
  import rego.v1
14
16
 
15
- # GHA YAML quirk: ключ `on:` парситься як YAML 1.1 boolean `true`, після чого conftest
16
- # серіалізує його в Rego-input як рядок `"true"`. Тому `input.on` / `input["on"]` /
17
- # `input[true]` всі недоступні; реальний шлях `input["true"]`. Виносимо в alias, щоб
18
- # решта правил читалася як `gha_on.schedule` без бойлерплейту.
19
- gha_on := input["true"]
17
+ # ── Очікувані значення ─────────────────────────────────────────────────────
18
+ #
19
+ # `${{ … }}` шаблонний синтаксис GitHub Actions; `{{` у Rego починає string
20
+ # interpolation. Збираємо очікувані рядки з фрагментів через `concat`, як це
21
+ # зроблено в check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
20
22
 
21
- # `${{ … }}` — це шаблонний синтаксис GitHub Actions, але `{{` у Rego починає
22
- # string interpolation. Збираємо очікувані рядки з фрагментів, як це зроблено в
23
- # check-ga.mjs, щоб і Rego-парсер, і людина-читач не плуталися.
24
23
  expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
25
24
 
26
25
  expected_github_token := concat("", ["$", "{{ github.token }}"])
@@ -29,43 +28,43 @@ expected_name := "Clean action for removing completed workflow runs"
29
28
 
30
29
  expected_cron := "0 1 16 * *"
31
30
 
32
- # --- name --------------------------------------------------------------------
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
+ # ── Аліаси на input ────────────────────────────────────────────────────────
39
+ #
40
+ # GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, конфтест серіалізує його
41
+ # як рядковий ключ "true". Ані `input.on`, ані `input["on"]`, ані `input[true]`
42
+ # не працюють — лише `input["true"]`.
43
+
44
+ gha_on := input["true"]
45
+
46
+ step0 := input.jobs.cleanup_old_workflows.steps[0]
47
+
48
+ # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
33
49
 
34
50
  deny contains msg if {
35
51
  input.name != expected_name
36
52
  msg := sprintf("clean-ga-workflows.yml: name має бути %q (ga.mdc)", [expected_name])
37
53
  }
38
54
 
39
- # --- on.schedule.cron --------------------------------------------------------
40
-
41
55
  deny contains msg if {
42
56
  not has_expected_cron
43
57
  msg := sprintf("clean-ga-workflows.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
44
58
  }
45
59
 
46
- has_expected_cron if {
47
- gha_on.schedule[_].cron == expected_cron
48
- }
49
-
50
- # --- on.workflow_dispatch ----------------------------------------------------
51
-
52
60
  deny contains msg if {
53
61
  not has_workflow_dispatch
54
62
  msg := "clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)"
55
63
  }
56
64
 
57
- has_workflow_dispatch if {
58
- is_object(gha_on.workflow_dispatch)
59
- }
60
-
61
- # --- concurrency -------------------------------------------------------------
62
-
63
65
  deny contains msg if {
64
66
  not is_object(input.concurrency)
65
- msg := sprintf(
66
- "clean-ga-workflows.yml: відсутня секція concurrency — додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
67
- [expected_concurrency_group],
68
- )
67
+ msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
69
68
  }
70
69
 
71
70
  deny contains msg if {
@@ -80,8 +79,6 @@ deny contains msg if {
80
79
  msg := "clean-ga-workflows.yml: concurrency.cancel-in-progress має бути true (ga.mdc)"
81
80
  }
82
81
 
83
- # --- jobs.cleanup_old_workflows ---------------------------------------------
84
-
85
82
  deny contains msg if {
86
83
  not input.jobs.cleanup_old_workflows
87
84
  msg := "clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)"
@@ -99,15 +96,6 @@ deny contains msg if {
99
96
  msg := "clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)"
100
97
  }
101
98
 
102
- actions_write_contents_read(perms) if {
103
- perms.actions == "write"
104
- perms.contents == "read"
105
- }
106
-
107
- # --- jobs.cleanup_old_workflows.steps[0] ------------------------------------
108
-
109
- step0 := input.jobs.cleanup_old_workflows.steps[0]
110
-
111
99
  deny contains msg if {
112
100
  step0.name != "Delete workflow runs"
113
101
  msg := "clean-ga-workflows.yml: перший крок має мати name: Delete workflow runs (ga.mdc)"
@@ -120,12 +108,27 @@ deny contains msg if {
120
108
 
121
109
  # Триплет полів `with`: token (gh-токен), save_period=31, save_min_runs_number=0.
122
110
  # В JS-перевірці помилка спільна для всіх трьох — лишаємо такий самий формат, щоб
123
- # повідомлення збігалися. Окремі правила нижче роблять діагноз точнішим.
111
+ # повідомлення збігалися.
124
112
  deny contains msg if {
125
113
  not step0_with_canonical
126
114
  msg := "clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc"
127
115
  }
128
116
 
117
+ # ── helpers ────────────────────────────────────────────────────────────────
118
+
119
+ has_expected_cron if {
120
+ gha_on.schedule[_].cron == expected_cron
121
+ }
122
+
123
+ has_workflow_dispatch if {
124
+ is_object(gha_on.workflow_dispatch)
125
+ }
126
+
127
+ actions_write_contents_read(perms) if {
128
+ perms.actions == "write"
129
+ perms.contents == "read"
130
+ }
131
+
129
132
  step0_with_canonical if {
130
133
  step0.with.token == expected_github_token
131
134
  step0.with.save_period == 31
@@ -0,0 +1,167 @@
1
+ # Порт перевірки `validateCleanMergedBranch` з `npm/scripts/check-ga.mjs` (ga.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test .github/workflows/clean-merged-branch.yml \
5
+ # -p npm/policy/ga --namespace ga.clean_merged_branch
6
+ #
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
+ package ga.clean_merged_branch
11
+
12
+ import rego.v1
13
+
14
+ # ── Очікувані значення ─────────────────────────────────────────────────────
15
+ #
16
+ # Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
17
+ # `concat`, бо `{{` у Rego починає string interpolation.
18
+
19
+ expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
20
+
21
+ expected_github_token := concat("", ["$", "{{ github.token }}"])
22
+
23
+ expected_deleted_branches_expr := concat("", ["$", "{{ steps.delete_stuff.outputs.deleted_branches }}"])
24
+
25
+ expected_echo_substring := concat("", ["echo \"Deleted branches: $", "{DELETED_BRANCHES}\""])
26
+
27
+ expected_name := "Clean abandoned branches"
28
+
29
+ expected_cron := "0 1 15 * *"
30
+
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
+ # ── Аліаси на input ────────────────────────────────────────────────────────
38
+ #
39
+ # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
40
+
41
+ gha_on := input["true"]
42
+
43
+ steps := input.jobs.cleanup_old_branches.steps
44
+
45
+ step0 := steps[0]
46
+
47
+ step1 := steps[1]
48
+
49
+ # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
50
+
51
+ deny contains msg if {
52
+ input.name != expected_name
53
+ msg := sprintf("clean-merged-branch.yml: name має бути %q (ga.mdc)", [expected_name])
54
+ }
55
+
56
+ deny contains msg if {
57
+ not has_expected_cron
58
+ msg := sprintf("clean-merged-branch.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
59
+ }
60
+
61
+ deny contains msg if {
62
+ not has_workflow_dispatch
63
+ msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
64
+ }
65
+
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
+ deny contains msg if {
84
+ not input.jobs.cleanup_old_branches
85
+ msg := "clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)"
86
+ }
87
+
88
+ deny contains msg if {
89
+ input.jobs.cleanup_old_branches.permissions.contents != "write"
90
+ msg := "clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)"
91
+ }
92
+
93
+ deny contains msg if {
94
+ count(steps) < 2
95
+ msg := "clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc"
96
+ }
97
+
98
+ # ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
99
+
100
+ deny contains msg if {
101
+ step0.id != "delete_stuff"
102
+ msg := "clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)"
103
+ }
104
+
105
+ deny contains msg if {
106
+ step0.uses != "phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3"
107
+ msg := "clean-merged-branch.yml: перший крок має uses як у ga.mdc"
108
+ }
109
+
110
+ deny contains msg if {
111
+ step0.with.github_token != expected_github_token
112
+ msg := sprintf("clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)", [expected_github_token])
113
+ }
114
+
115
+ deny contains msg if {
116
+ step0.with.last_commit_age_days != 90
117
+ msg := "clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)"
118
+ }
119
+
120
+ deny contains msg if {
121
+ not ignore_branches_has_main_and_dev
122
+ msg := "clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)"
123
+ }
124
+
125
+ # `dry_run: no` у YAML парситься як boolean `false`. JS-перевірка порівнює зі
126
+ # рядком "no", але в нас input уже Go-yaml-парсений — тому очікуємо `false`.
127
+ # (Якщо комусь схочеться явного `"no"` — треба буде брати in quotes у YAML.)
128
+ deny contains msg if {
129
+ step0.with.dry_run != false # noqa: rules-style-no-equality-with-false
130
+ msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
131
+ }
132
+
133
+ # ── Step 1 (Get output) ────────────────────────────────────────────────────
134
+
135
+ deny contains msg if {
136
+ step1.name != "Get output"
137
+ msg := "clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)"
138
+ }
139
+
140
+ deny contains msg if {
141
+ step1.env.DELETED_BRANCHES != expected_deleted_branches_expr
142
+ msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc"
143
+ }
144
+
145
+ deny contains msg if {
146
+ not echo_deleted_branches
147
+ msg := "clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc"
148
+ }
149
+
150
+ # ── helpers ────────────────────────────────────────────────────────────────
151
+
152
+ has_expected_cron if {
153
+ gha_on.schedule[_].cron == expected_cron
154
+ }
155
+
156
+ has_workflow_dispatch if {
157
+ is_object(gha_on.workflow_dispatch)
158
+ }
159
+
160
+ ignore_branches_has_main_and_dev if {
161
+ contains(step0.with.ignore_branches, "main")
162
+ contains(step0.with.ignore_branches, "dev")
163
+ }
164
+
165
+ echo_deleted_branches if {
166
+ contains(step1.run, expected_echo_substring)
167
+ }
@@ -0,0 +1,109 @@
1
+ # Порт перевірки `validateGitAiWorkflowStructure` з `npm/scripts/check-ga.mjs` (ga.mdc).
2
+ #
3
+ # Запуск (локально):
4
+ # conftest test .github/workflows/git-ai.yml \
5
+ # -p npm/policy/ga --namespace ga.git_ai
6
+ #
7
+ # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
+ # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
+ # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
+ package ga.git_ai
11
+
12
+ import rego.v1
13
+
14
+ # ── Очікувані значення ─────────────────────────────────────────────────────
15
+
16
+ expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
17
+
18
+ expected_name := "Git AI"
19
+
20
+ expected_if_substring := "github.event.pull_request.merged == true"
21
+
22
+ expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash"
23
+
24
+ expected_run_substring := "git-ai ci github run"
25
+
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
+ # ── Аліаси на input ────────────────────────────────────────────────────────
34
+ #
35
+ # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
36
+
37
+ gha_on := input["true"]
38
+
39
+ # Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
40
+ # — щоб уникнути regal-правила `rule-name-repeats-package`.
41
+ job := input.jobs["git-ai"]
42
+
43
+ # Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
44
+ job_run_blob := concat("\n", [run |
45
+ run := job.steps[_].run
46
+ ])
47
+
48
+ # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
49
+
50
+ deny contains msg if {
51
+ input.name != expected_name
52
+ msg := sprintf("git-ai.yml: name має бути %q (ga.mdc)", [expected_name])
53
+ }
54
+
55
+ deny contains msg if {
56
+ not "closed" in {t | some t in gha_on.pull_request.types}
57
+ msg := "git-ai.yml: on.pull_request.types має містити closed (ga.mdc)"
58
+ }
59
+
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
+ deny contains msg if {
78
+ not job
79
+ msg := "git-ai.yml: jobs.git-ai відсутній (ga.mdc)"
80
+ }
81
+
82
+ deny contains msg if {
83
+ not contains(job_if_str, expected_if_substring)
84
+ msg := "git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)"
85
+ }
86
+
87
+ deny contains msg if {
88
+ job.permissions.contents != "write"
89
+ msg := "git-ai.yml: permissions мають бути contents: write (ga.mdc)"
90
+ }
91
+
92
+ deny contains msg if {
93
+ not contains(job_run_blob, expected_install_substring)
94
+ msg := "git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)"
95
+ }
96
+
97
+ deny contains msg if {
98
+ not contains(job_run_blob, expected_run_substring)
99
+ msg := "git-ai.yml: має виконувати git-ai ci github run (ga.mdc)"
100
+ }
101
+
102
+ # ── helpers ────────────────────────────────────────────────────────────────
103
+
104
+ # `if` поле job-а може бути відсутнім — тоді `sprintf` дає невизначене значення
105
+ # і спрацьовує `default`, повертаючи порожній рядок; `contains(…)` нижче дасть
106
+ # false і відповідне `deny`-правило спрацює зі зрозумілим повідомленням.
107
+ default job_if_str := ""
108
+
109
+ job_if_str := sprintf("%v", [job.if])