@nitra/cursor 1.8.203 → 1.8.204

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,18 @@
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.204] - 2026-05-07
8
+
9
+ ### Changed
10
+
11
+ - Реструктурував `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).
12
+ - `scripts/lint-ga.mjs`: `CONFTEST_TARGETS` тепер містить `clean-ga-workflows.yml` і `clean-merged-branch.yml`, conftest викликаємо з `--namespace ga.<name>` для ізоляції правил між workflow.
13
+ - `scripts/check-ga.mjs`: видалено `validateCleanGaWorkflows*` і `validateCleanMergedBranch*` — їх повністю покриває conftest у `lint-ga`. `checkCanonicalWorkflowsMatchRule` тепер валідує лише `lint-ga.yml` і `git-ai.yml` (наступні кандидати на міграцію).
14
+
15
+ ### Added
16
+
17
+ - `.regal/config.yaml` у корені — вимикає `idiomatic.no-defined-entrypoint` (для conftest-полісі `deny`-правила є де-факто entrypoint-ами, формальна анотація не несе семантики).
18
+
7
19
  ## [1.8.203] - 2026-05-07
8
20
 
9
21
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.8.203",
3
+ "version": "1.8.204",
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
+ }
@@ -170,220 +170,6 @@ function isExactString(v, expected) {
170
170
  return typeof v === 'string' && v === expected
171
171
  }
172
172
 
173
- /**
174
- * Перевіряє крок dmvict/clean-workflow-runs@v1 у `clean-ga-workflows.yml`.
175
- * @param {unknown} step0 перший крок workflow
176
- * @param {(msg: string) => void} passFn pass
177
- * @param {(msg: string) => void} failFn fail
178
- */
179
- function validateCleanGaWorkflowsStep0(step0, passFn, failFn) {
180
- if (!isExactString(getObjKey(step0, 'name'), 'Delete workflow runs')) {
181
- failFn('clean-ga-workflows.yml: перший крок має мати name: Delete workflow runs (ga.mdc)')
182
- }
183
- if (!isExactString(getObjKey(step0, 'uses'), 'dmvict/clean-workflow-runs@v1')) {
184
- failFn('clean-ga-workflows.yml: перший крок має uses: dmvict/clean-workflow-runs@v1 (ga.mdc)')
185
- }
186
- const withObj = getObjKey(step0, 'with')
187
- const githubToken = ['$', '{{ github.token }}'].join('')
188
- if (
189
- getObjKey(withObj, 'token') === githubToken &&
190
- getObjKey(withObj, 'save_period') === 31 &&
191
- getObjKey(withObj, 'save_min_runs_number') === 0
192
- ) {
193
- passFn('clean-ga-workflows.yml: jobs/steps OK')
194
- } else {
195
- failFn('clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc')
196
- }
197
- }
198
-
199
- /**
200
- * Перевіряє структуру workflow `clean-ga-workflows.yml` (ga.mdc).
201
- * @param {Record<string, unknown> | null} root parsed YAML
202
- * @param {(msg: string) => void} passFn pass
203
- * @param {(msg: string) => void} failFn fail
204
- */
205
- function validateCleanGaWorkflows(root, passFn, failFn) {
206
- if (!root) {
207
- failFn('clean-ga-workflows.yml: YAML не вдалося розібрати (ga.mdc)')
208
- return
209
- }
210
-
211
- if (isExactString(root.name, 'Clean action for removing completed workflow runs')) {
212
- passFn('clean-ga-workflows.yml: name OK')
213
- } else {
214
- failFn('clean-ga-workflows.yml: name має бути "Clean action for removing completed workflow runs" (ga.mdc)')
215
- }
216
-
217
- const on = root.on
218
- const schedule = getObjKey(on, 'schedule')
219
- const wfDispatch = getObjKey(on, 'workflow_dispatch')
220
-
221
- const hasCron =
222
- Array.isArray(schedule) &&
223
- schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 16 * *')
224
-
225
- if (hasCron) {
226
- passFn('clean-ga-workflows.yml: cron OK')
227
- } else {
228
- failFn("clean-ga-workflows.yml: on.schedule має містити cron: '0 1 16 * *' (ga.mdc)")
229
- }
230
-
231
- if (!wfDispatch || typeof wfDispatch !== 'object') {
232
- failFn('clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)')
233
- } else {
234
- passFn('clean-ga-workflows.yml: workflow_dispatch OK')
235
- }
236
-
237
- validateConcurrencyOnRoot('clean-ga-workflows.yml', root, passFn, failFn)
238
-
239
- const jobs = getObjKey(root, 'jobs')
240
- const job = getObjKey(jobs, 'cleanup_old_workflows')
241
- if (!job) {
242
- failFn('clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)')
243
- return
244
- }
245
-
246
- if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
247
- failFn('clean-ga-workflows.yml: runs-on має бути ubuntu-latest (ga.mdc)')
248
- }
249
-
250
- const perm = getObjKey(job, 'permissions')
251
- if (!(getObjKey(perm, 'actions') === 'write' && getObjKey(perm, 'contents') === 'read')) {
252
- failFn('clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)')
253
- }
254
-
255
- const steps = getObjKey(job, 'steps')
256
- const step0 = Array.isArray(steps) ? steps[0] : null
257
- if (!step0 || typeof step0 !== 'object') {
258
- failFn('clean-ga-workflows.yml: steps має містити крок з dmvict/clean-workflow-runs@v1 (ga.mdc)')
259
- return
260
- }
261
-
262
- validateCleanGaWorkflowsStep0(step0, passFn, failFn)
263
- }
264
-
265
- /**
266
- * Перевіряє крок `phpdocker-io/github-actions-delete-abandoned-branches` у `clean-merged-branch.yml`.
267
- * @param {unknown} step0 перший крок workflow
268
- * @param {(msg: string) => void} failFn fail
269
- */
270
- function validateCleanMergedBranchStep0(step0, failFn) {
271
- if (!isExactString(getObjKey(step0, 'id'), 'delete_stuff')) {
272
- failFn('clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)')
273
- }
274
- if (!isExactString(getObjKey(step0, 'uses'), 'phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3')) {
275
- failFn('clean-merged-branch.yml: перший крок має uses як у ga.mdc')
276
- }
277
- const withObj = getObjKey(step0, 'with')
278
- const ghToken = ['$', '{{ github.token }}'].join('')
279
- if (getObjKey(withObj, 'github_token') !== ghToken) {
280
- failFn(['clean-merged-branch.yml: with.github_token має бути $', '{{ github.token }} (ga.mdc)'].join(''))
281
- }
282
- if (getObjKey(withObj, 'last_commit_age_days') !== 90) {
283
- failFn('clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)')
284
- }
285
- const ignoreBranches = String(getObjKey(withObj, 'ignore_branches') ?? '')
286
- if (!(ignoreBranches.includes('main') && ignoreBranches.includes('dev'))) {
287
- failFn('clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)')
288
- }
289
- if (getObjKey(withObj, 'dry_run') !== 'no') {
290
- failFn('clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)')
291
- }
292
- }
293
-
294
- /**
295
- * Перевіряє крок виводу в `clean-merged-branch.yml`.
296
- * @param {unknown} step1 другий крок workflow
297
- * @param {(msg: string) => void} passFn pass
298
- * @param {(msg: string) => void} failFn fail
299
- */
300
- function validateCleanMergedBranchStep1(step1, passFn, failFn) {
301
- if (!isExactString(getObjKey(step1, 'name'), 'Get output')) {
302
- failFn('clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)')
303
- }
304
- const env = getObjKey(step1, 'env')
305
- const deletedBranchesExpr = ['$', '{{ steps.delete_stuff.outputs.deleted_branches }}'].join('')
306
- if (getObjKey(env, 'DELETED_BRANCHES') !== deletedBranchesExpr) {
307
- failFn('clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc')
308
- }
309
- const echoDeletedBranches = ['echo "Deleted branches: $', '{DELETED_BRANCHES}"'].join('')
310
- if (String(getObjKey(step1, 'run') ?? '').includes(echoDeletedBranches)) {
311
- passFn('clean-merged-branch.yml: jobs/steps OK')
312
- } else {
313
- failFn('clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc')
314
- }
315
- }
316
-
317
- /**
318
- * Перевіряє структуру workflow `clean-merged-branch.yml` (ga.mdc).
319
- * @param {Record<string, unknown> | null} root parsed YAML
320
- * @param {(msg: string) => void} passFn pass
321
- * @param {(msg: string) => void} failFn fail
322
- */
323
- function validateCleanMergedBranch(root, passFn, failFn) {
324
- if (!root) {
325
- failFn('clean-merged-branch.yml: YAML не вдалося розібрати (ga.mdc)')
326
- return
327
- }
328
-
329
- if (isExactString(root.name, 'Clean abandoned branches')) {
330
- passFn('clean-merged-branch.yml: name OK')
331
- } else {
332
- failFn('clean-merged-branch.yml: name має бути "Clean abandoned branches" (ga.mdc)')
333
- }
334
-
335
- const on = root.on
336
- const schedule = getObjKey(on, 'schedule')
337
- const wfDispatch = getObjKey(on, 'workflow_dispatch')
338
- const hasCron =
339
- Array.isArray(schedule) &&
340
- schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 15 * *')
341
-
342
- if (hasCron) {
343
- passFn('clean-merged-branch.yml: cron OK')
344
- } else {
345
- failFn("clean-merged-branch.yml: on.schedule має містити cron: '0 1 15 * *' (ga.mdc)")
346
- }
347
-
348
- if (!wfDispatch || typeof wfDispatch !== 'object') {
349
- failFn('clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)')
350
- }
351
-
352
- validateConcurrencyOnRoot('clean-merged-branch.yml', root, passFn, failFn)
353
-
354
- const jobs = getObjKey(root, 'jobs')
355
- const job = getObjKey(jobs, 'cleanup_old_branches')
356
- if (!job) {
357
- failFn('clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)')
358
- return
359
- }
360
-
361
- const perm = getObjKey(job, 'permissions')
362
- if (getObjKey(perm, 'contents') !== 'write') {
363
- failFn('clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)')
364
- }
365
-
366
- const steps = getObjKey(job, 'steps')
367
- if (!Array.isArray(steps) || steps.length < 2) {
368
- failFn('clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc')
369
- return
370
- }
371
-
372
- const step0 = steps[0]
373
- if (!step0 || typeof step0 !== 'object') {
374
- failFn('clean-merged-branch.yml: перший крок невалідний (ga.mdc)')
375
- return
376
- }
377
- validateCleanMergedBranchStep0(step0, failFn)
378
-
379
- const step1 = steps[1]
380
- if (!step1 || typeof step1 !== 'object') {
381
- failFn('clean-merged-branch.yml: другий крок невалідний (ga.mdc)')
382
- return
383
- }
384
- validateCleanMergedBranchStep1(step1, passFn, failFn)
385
- }
386
-
387
173
  /**
388
174
  * Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
389
175
  * @param {unknown} on корінь `on:` з YAML
@@ -973,26 +759,20 @@ async function checkGitAiWorkflow(wfDir, passFn, failFn) {
973
759
 
974
760
  /**
975
761
  * Перевіряє, що “канонічні” workflows відповідають ga.mdc (структура і значення).
762
+ *
763
+ * Структурні валідатори `clean-ga-workflows.yml` і `clean-merged-branch.yml` мігровано в Rego-полісі
764
+ * під `npm/policy/ga/clean_ga_workflows/` та `npm/policy/ga/clean_merged_branch/` (виконує conftest з
765
+ * `bun run lint-ga`). Тут лишаються `lint-ga.yml` і `git-ai.yml` — їх перенесення в наступних ітераціях.
976
766
  * @param {string} wfDir директорія workflows
977
767
  * @param {(msg: string) => void} passFn pass
978
768
  * @param {(msg: string) => void} failFn fail
979
769
  */
980
770
  async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
981
771
  const paths = {
982
- cleanGa: join(wfDir, 'clean-ga-workflows.yml'),
983
- cleanMerged: join(wfDir, 'clean-merged-branch.yml'),
984
772
  lintGa: join(wfDir, 'lint-ga.yml'),
985
773
  gitAi: join(wfDir, 'git-ai.yml')
986
774
  }
987
775
 
988
- if (existsSync(paths.cleanGa)) {
989
- const c = await readFile(paths.cleanGa, 'utf8')
990
- validateCleanGaWorkflows(parseWorkflowYaml(c), passFn, failFn)
991
- }
992
- if (existsSync(paths.cleanMerged)) {
993
- const c = await readFile(paths.cleanMerged, 'utf8')
994
- validateCleanMergedBranch(parseWorkflowYaml(c), passFn, failFn)
995
- }
996
776
  if (existsSync(paths.lintGa)) {
997
777
  const c = await readFile(paths.lintGa, 'utf8')
998
778
  validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
@@ -27,16 +27,26 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
27
27
  /** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиту директорію policy/. */
28
28
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
29
29
 
30
- /** Шлях до Rego-полісі (PoC: лише clean-ga-workflows). У npm-tarball публікується через `files` у package.json. */
30
+ /** Шлях до кореня Rego-полісі для GA. У npm-tarball публікується через `files: ["policy"]` у package.json. */
31
31
  const GA_POLICY_DIR = join(PACKAGE_ROOT, 'policy', 'ga')
32
32
 
33
33
  /**
34
- * Workflow-файли, для яких маємо відповідну Rego-полісі. PoC: один файл; інші підтягуватимемо в міру міграції
35
- * перевірок із `npm/scripts/check-ga.mjs`.
36
- * @type {Array<{ workflow: string, label: string }>}
34
+ * Workflow-файли, для яких маємо відповідну Rego-полісі. Кожен таргет посилається на під-пакет
35
+ * `ga.<name>` у `policy/ga/<name>/<name>.rego`; conftest викликаємо з `--namespace`, щоб правила
36
+ * іншого workflow не застосовувалися до чужого файлу.
37
+ * @type {Array<{ workflow: string, namespace: string, label: string }>}
37
38
  */
38
39
  const CONFTEST_TARGETS = [
39
- { workflow: '.github/workflows/clean-ga-workflows.yml', label: 'clean-ga-workflows.yml structure' }
40
+ {
41
+ workflow: '.github/workflows/clean-ga-workflows.yml',
42
+ namespace: 'ga.clean_ga_workflows',
43
+ label: 'clean-ga-workflows.yml structure'
44
+ },
45
+ {
46
+ workflow: '.github/workflows/clean-merged-branch.yml',
47
+ namespace: 'ga.clean_merged_branch',
48
+ label: 'clean-merged-branch.yml structure'
49
+ }
40
50
  ]
41
51
 
42
52
  /**
@@ -218,6 +228,8 @@ function runConftestStep() {
218
228
  target.workflow,
219
229
  '-p',
220
230
  GA_POLICY_DIR,
231
+ '--namespace',
232
+ target.namespace,
221
233
  '--no-color'
222
234
  ])
223
235
  if (code !== 0) return code