@nitra/cursor 1.13.8 → 1.13.12

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 (29) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/package.json +1 -1
  3. package/rules/ga/fix/workflows/check.mjs +12 -3
  4. package/rules/ga/ga.mdc +18 -152
  5. package/rules/ga/policy/clean_ga_workflows/clean_ga_workflows.rego +38 -49
  6. package/rules/ga/policy/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +26 -0
  7. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +55 -57
  8. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +37 -0
  9. package/rules/ga/policy/git_ai/git_ai.rego +28 -35
  10. package/rules/ga/policy/git_ai/template/git-ai.yml.snippet.yml +30 -0
  11. package/rules/ga/policy/lint_ga/lint_ga.rego +46 -55
  12. package/rules/ga/policy/lint_ga/template/lint-ga.yml.snippet.yml +35 -0
  13. package/rules/ga/policy/package_json/package_json.rego +9 -13
  14. package/rules/ga/policy/package_json/template/package.json.contains.json +1 -0
  15. package/rules/ga/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
  16. package/rules/ga/policy/vscode_extensions/vscode_extensions.rego +6 -4
  17. package/rules/ga/policy/vscode_settings/template/settings.json.snippet.json +1 -0
  18. package/rules/ga/policy/vscode_settings/vscode_settings.rego +11 -13
  19. package/rules/ga/policy/zizmor_yml/template/zizmor.yml.snippet.yml +5 -0
  20. package/rules/ga/policy/zizmor_yml/zizmor_yml.rego +16 -6
  21. package/rules/rego/lint/lint.mjs +5 -4
  22. package/rules/rego/policy/package_json/package_json.rego +8 -29
  23. package/rules/rego/policy/package_json/template/package.json.snippet.json +1 -0
  24. package/rules/rego/policy/vscode_extensions/template/extensions.json.snippet.json +1 -0
  25. package/rules/rego/policy/vscode_extensions/vscode_extensions.rego +7 -11
  26. package/rules/rego/policy/vscode_settings/template/settings.json.snippet.json +6 -0
  27. package/rules/rego/policy/vscode_settings/vscode_settings.rego +19 -27
  28. package/rules/rego/rego.mdc +10 -8
  29. package/rules/security/todo.MD +27 -0
@@ -1,44 +1,32 @@
1
- # Порт перевірки `validateCleanMergedBranch` з `npm/scripts/check-ga.mjs` (ga.mdc).
1
+ # Перевірка `.github/workflows/clean-merged-branch.yml` (ga.mdc).
2
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).
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/clean-merged-branch.yml.snippet.yml.
10
5
  package ga.clean_merged_branch
11
6
 
12
7
  import rego.v1
13
8
 
14
- # ── Очікувані значення ─────────────────────────────────────────────────────
15
- #
16
- # Шаблонні токени GitHub Actions (`${{ … }}`) збираємо з фрагментів через
17
- # `concat`, бо `{{` у Rego починає string interpolation.
18
-
19
- expected_github_token := concat("", ["$", "{{ github.token }}"])
9
+ # ── Аліаси ─────────────────────────────────────────────────────────────────
20
10
 
21
- expected_deleted_branches_expr := concat("", ["$", "{{ steps.delete_stuff.outputs.deleted_branches }}"])
11
+ gha_on := input["true"]
22
12
 
23
- expected_echo_substring := concat("", ["echo \"Deleted branches: $", "{DELETED_BRANCHES}\""])
13
+ steps := input.jobs.cleanup_old_branches.steps
24
14
 
25
- expected_name := "Clean abandoned branches"
15
+ step0 := steps[0]
26
16
 
27
- expected_cron := "0 1 15 * *"
17
+ step1 := steps[1]
28
18
 
29
- # ── Аліаси на input ────────────────────────────────────────────────────────
30
- #
31
- # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
19
+ expected_name := data.template.snippet.name
32
20
 
33
- gha_on := input["true"]
21
+ expected_cron := data.template.snippet.on.schedule[0].cron
34
22
 
35
- steps := input.jobs.cleanup_old_branches.steps
23
+ expected_step0 := data.template.snippet.jobs.cleanup_old_branches.steps[0]
36
24
 
37
- step0 := steps[0]
25
+ expected_step1 := data.template.snippet.jobs.cleanup_old_branches.steps[1]
38
26
 
39
- step1 := steps[1]
27
+ expected_perms := data.template.snippet.jobs.cleanup_old_branches.permissions
40
28
 
41
- # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
29
+ # ── deny rules ─────────────────────────────────────────────────────────────
42
30
 
43
31
  deny contains msg if {
44
32
  input.name != expected_name
@@ -51,7 +39,7 @@ deny contains msg if {
51
39
  }
52
40
 
53
41
  deny contains msg if {
54
- not has_workflow_dispatch
42
+ not is_object(object.get(gha_on, "workflow_dispatch", null))
55
43
  msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
56
44
  }
57
45
 
@@ -61,65 +49,73 @@ deny contains msg if {
61
49
  }
62
50
 
63
51
  deny contains msg if {
64
- input.jobs.cleanup_old_branches.permissions.contents != "write"
65
- msg := "clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)"
52
+ input.jobs.cleanup_old_branches.permissions.contents != expected_perms.contents
53
+ msg := sprintf("clean-merged-branch.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
66
54
  }
67
55
 
68
56
  deny contains msg if {
69
57
  count(steps) < 2
70
- msg := "clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc"
58
+ msg := "clean-merged-branch.yml: steps має містити 2 кроки як у template (ga.mdc)"
71
59
  }
72
60
 
73
61
  # ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
74
62
 
75
63
  deny contains msg if {
76
- step0.id != "delete_stuff"
77
- msg := "clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)"
64
+ step0.id != expected_step0.id
65
+ msg := sprintf("clean-merged-branch.yml: перший крок має id: %s (ga.mdc)", [expected_step0.id])
78
66
  }
79
67
 
80
68
  deny contains msg if {
81
- step0.uses != "phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3"
82
- msg := "clean-merged-branch.yml: перший крок має uses як у ga.mdc"
69
+ step0.uses != expected_step0.uses
70
+ msg := sprintf("clean-merged-branch.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
83
71
  }
84
72
 
85
73
  deny contains msg if {
86
- step0.with.github_token != expected_github_token
87
- msg := sprintf("clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)", [expected_github_token])
74
+ step0.with.github_token != expected_step0.with.github_token
75
+ msg := sprintf(
76
+ "clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)",
77
+ [expected_step0.with.github_token],
78
+ )
88
79
  }
89
80
 
90
81
  deny contains msg if {
91
- step0.with.last_commit_age_days != 90
92
- msg := "clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)"
82
+ step0.with.last_commit_age_days != expected_step0.with.last_commit_age_days
83
+ msg := sprintf(
84
+ "clean-merged-branch.yml: with.last_commit_age_days має бути %d (ga.mdc)",
85
+ [expected_step0.with.last_commit_age_days],
86
+ )
93
87
  }
94
88
 
95
89
  deny contains msg if {
96
- not ignore_branches_has_main_and_dev
97
- msg := "clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)"
90
+ not ignore_branches_subset
91
+ msg := sprintf(
92
+ "clean-merged-branch.yml: with.ignore_branches має містити %s (ga.mdc)",
93
+ [expected_step0.with.ignore_branches],
94
+ )
98
95
  }
99
96
 
100
- # `dry_run: no` у YAML парситься як boolean `false`. JS-перевірка порівнює зі
101
- # рядком "no", але в нас input уже Go-yaml-парсений тому очікуємо `false`.
102
- # (Якщо комусь схочеться явного `"no"` — треба буде брати in quotes у YAML.)
97
+ # YAML 1.1 quirk: `dry_run: no` парситься як boolean false у Go-yaml (conftest).
98
+ # Template (від `yaml` npm) теж нормалізовано до false у фікстурі.
103
99
  deny contains msg if {
104
- step0.with.dry_run != false # noqa: rules-style-no-equality-with-false
100
+ step0.with.dry_run != expected_step0.with.dry_run # noqa: rules-style-no-equality-with-false
105
101
  msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
106
102
  }
107
103
 
108
104
  # ── Step 1 (Get output) ────────────────────────────────────────────────────
109
105
 
110
106
  deny contains msg if {
111
- step1.name != "Get output"
112
- msg := "clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)"
107
+ step1.name != expected_step1.name
108
+ msg := sprintf("clean-merged-branch.yml: другий крок має name: %s (ga.mdc)", [expected_step1.name])
113
109
  }
114
110
 
115
111
  deny contains msg if {
116
- step1.env.DELETED_BRANCHES != expected_deleted_branches_expr
117
- msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc"
112
+ step1.env.DELETED_BRANCHES != expected_step1.env.DELETED_BRANCHES
113
+ msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у template (ga.mdc)"
118
114
  }
119
115
 
120
116
  deny contains msg if {
121
117
  not echo_deleted_branches
122
- msg := "clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc"
118
+ msg := "clean-merged-branch.yml: run має echo Deleted branches як у template (ga.mdc)"
123
119
  }
124
120
 
125
121
  # ── helpers ────────────────────────────────────────────────────────────────
@@ -128,15 +124,17 @@ has_expected_cron if {
128
124
  gha_on.schedule[_].cron == expected_cron
129
125
  }
130
126
 
131
- has_workflow_dispatch if {
132
- is_object(gha_on.workflow_dispatch)
133
- }
134
-
135
- ignore_branches_has_main_and_dev if {
136
- contains(step0.with.ignore_branches, "main")
137
- contains(step0.with.ignore_branches, "dev")
127
+ # Кожна гілка з template-літералу (через кому) має бути присутня у actual.
128
+ ignore_branches_subset if {
129
+ required_branches := split(expected_step0.with.ignore_branches, ",")
130
+ actual := step0.with.ignore_branches
131
+ every b in required_branches {
132
+ contains(actual, trim_space(b))
133
+ }
138
134
  }
139
135
 
140
136
  echo_deleted_branches if {
141
- contains(step1.run, expected_echo_substring)
137
+ # Звіряємо substring "echo "Deleted branches: …${DELETED_BRANCHES}…"" — формується з template run.
138
+ contains(step1.run, "Deleted branches:")
139
+ contains(step1.run, "${DELETED_BRANCHES}")
142
140
  }
@@ -0,0 +1,37 @@
1
+ name: Clean abandoned branches
2
+
3
+ on:
4
+ # Run daily at midnight
5
+ schedule:
6
+ - cron: '0 1 15 * *'
7
+
8
+ # Allow workflow to be manually run from the GitHub UI
9
+ workflow_dispatch: {}
10
+
11
+ concurrency:
12
+ group: ${{ github.ref }}-${{ github.workflow }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ cleanup_old_branches:
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: write
20
+ steps:
21
+ - id: delete_stuff
22
+ name: Delete those pesky dead branches
23
+ uses: phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3
24
+ with:
25
+ github_token: ${{ github.token }}
26
+ last_commit_age_days: 90
27
+ ignore_branches: main,dev
28
+ # `no` у workflow → Go-yaml парсить як bool false; тут пишемо явно false,
29
+ # бо template читає `yaml` (YAML 1.2), де `no` лишається рядком — а нам
30
+ # потрібна семантична відповідність runtime-парсингу conftest.
31
+ dry_run: false
32
+
33
+ - name: Get output
34
+ env:
35
+ DELETED_BRANCHES: ${{ steps.delete_stuff.outputs.deleted_branches }}
36
+ run: |
37
+ echo "Deleted branches: ${DELETED_BRANCHES}"
@@ -1,42 +1,38 @@
1
- # Порт перевірки `validateGitAiWorkflowStructure` з `npm/scripts/check-ga.mjs` (ga.mdc).
1
+ # Перевірка `.github/workflows/git-ai.yml` (ga.mdc).
2
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).
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/git-ai.yml.snippet.yml.
5
+ # Substring-перевірки (`if:`, `run:` блоки) — на основі ключових фраз з template
6
+ # steps, бо повний multi-line `run:` дуже крихкий для exact-match.
10
7
  package ga.git_ai
11
8
 
12
9
  import rego.v1
13
10
 
14
- # ── Очікувані значення ─────────────────────────────────────────────────────
15
-
16
- expected_name := "Git AI"
17
-
18
- expected_if_substring := "github.event.pull_request.merged == true"
19
-
20
- expected_install_substring := "curl -fsSL https://usegitai.com/install.sh | bash"
21
-
22
- expected_run_substring := "git-ai ci github run"
23
-
24
- # ── Аліаси на input ────────────────────────────────────────────────────────
25
- #
26
- # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
11
+ # ── Аліаси ─────────────────────────────────────────────────────────────────
27
12
 
28
13
  gha_on := input["true"]
29
14
 
30
- # Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
31
- # — щоб уникнути regal-правила `rule-name-repeats-package`.
32
15
  job := input.jobs["git-ai"]
33
16
 
34
- # Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
35
17
  job_run_blob := concat("\n", [run |
36
18
  run := job.steps[_].run
37
19
  ])
38
20
 
39
- # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
21
+ expected_name := data.template.snippet.name
22
+
23
+ expected_types := {t | some t in data.template.snippet.on.pull_request.types}
24
+
25
+ expected_if := data.template.snippet.jobs["git-ai"].if
26
+
27
+ expected_perms := data.template.snippet.jobs["git-ai"].permissions
28
+
29
+ # Substring-маркери з template `run:` блоків — ключові команди, наявність яких
30
+ # гарантує що workflow робить очікувані дії. Конкретний multi-line — не порівнюємо.
31
+ install_substring := "https://usegitai.com/install.sh"
32
+
33
+ run_substring := "git-ai ci github run"
34
+
35
+ # ── deny rules ─────────────────────────────────────────────────────────────
40
36
 
41
37
  deny contains msg if {
42
38
  input.name != expected_name
@@ -54,30 +50,27 @@ deny contains msg if {
54
50
  }
55
51
 
56
52
  deny contains msg if {
57
- not contains(job_if_str, expected_if_substring)
58
- msg := "git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)"
53
+ not contains(job_if_str, expected_if)
54
+ msg := sprintf("git-ai.yml: job має містити if: %s (ga.mdc)", [expected_if])
59
55
  }
60
56
 
61
57
  deny contains msg if {
62
- job.permissions.contents != "write"
63
- msg := "git-ai.yml: permissions мають бути contents: write (ga.mdc)"
58
+ job.permissions.contents != expected_perms.contents
59
+ msg := sprintf("git-ai.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
64
60
  }
65
61
 
66
62
  deny contains msg if {
67
- not contains(job_run_blob, expected_install_substring)
63
+ not contains(job_run_blob, install_substring)
68
64
  msg := "git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)"
69
65
  }
70
66
 
71
67
  deny contains msg if {
72
- not contains(job_run_blob, expected_run_substring)
73
- msg := "git-ai.yml: має виконувати git-ai ci github run (ga.mdc)"
68
+ not contains(job_run_blob, run_substring)
69
+ msg := sprintf("git-ai.yml: має виконувати %s (ga.mdc)", [run_substring])
74
70
  }
75
71
 
76
72
  # ── helpers ────────────────────────────────────────────────────────────────
77
73
 
78
- # `if` поле job-а може бути відсутнім — тоді `sprintf` дає невизначене значення
79
- # і спрацьовує `default`, повертаючи порожній рядок; `contains(…)` нижче дасть
80
- # false і відповідне `deny`-правило спрацює зі зрозумілим повідомленням.
81
74
  default job_if_str := ""
82
75
 
83
76
  job_if_str := sprintf("%v", [job.if])
@@ -0,0 +1,30 @@
1
+ name: Git AI
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ concurrency:
8
+ group: ${{ github.ref }}-${{ github.workflow }}
9
+ cancel-in-progress: true
10
+
11
+ jobs:
12
+ git-ai:
13
+ if: github.event.pull_request.merged == true
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: write
17
+
18
+ steps:
19
+ - name: Install git-ai
20
+ run: |
21
+ curl -fsSL https://usegitai.com/install.sh | bash
22
+ echo "$HOME/.git-ai/bin" >> $GITHUB_PATH
23
+ - name: Run git-ai
24
+ id: run-git-ai
25
+ env:
26
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27
+ run: |
28
+ git config --global user.name "github-actions[bot]"
29
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
30
+ git-ai ci github run
@@ -1,44 +1,50 @@
1
- # Порт перевірок `validateLintGaWorkflowStructure` + `validateLintGaOnTriggers` з
2
- # `npm/scripts/check-ga.mjs` (ga.mdc).
1
+ # Перевірка `.github/workflows/lint-ga.yml` (ga.mdc).
3
2
  #
4
- # Запуск (локально):
5
- # conftest test .github/workflows/lint-ga.yml \
6
- # -p npm/policy/ga --namespace ga.lint_ga
7
- #
8
- # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
9
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
10
- # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/lint-ga.yml.snippet.yml.
11
5
  package ga.lint_ga
12
6
 
13
7
  import rego.v1
14
8
 
15
- # ── Очікувані значення ─────────────────────────────────────────────────────
16
-
17
- expected_name := "Lint GA"
18
-
19
- expected_branches := {"dev", "main"}
20
-
21
- expected_push_paths := {".github/actions/**", ".github/workflows/**"}
22
-
23
- # ── Аліаси на input ────────────────────────────────────────────────────────
24
- #
25
- # YAML 1.1 quirk: `on:` → boolean true → у конфтесті ключ "true".
9
+ # ── Аліаси ─────────────────────────────────────────────────────────────────
26
10
 
27
11
  gha_on := input["true"]
28
12
 
29
- # Job-id містить дефіс — звертаємося через `[…]`. Імʼя `job` (без префіксу пакету)
30
- # — щоб уникнути regal-правила `rule-name-repeats-package`.
31
13
  job := input.jobs["lint-ga"]
32
14
 
33
- # Усі `uses:` зі steps цього job-а — для перевірки членства.
34
15
  job_uses_set contains job.steps[_].uses
35
16
 
36
- # Усі `run:` зі steps цього job-а, склеєні в один blob — для substring-перевірки.
37
17
  job_run_blob := concat("\n", [run |
38
18
  run := job.steps[_].run
39
19
  ])
40
20
 
41
- # ── deny rules (контигно — regal: messy-rule) ──────────────────────────────
21
+ expected_name := data.template.snippet.name
22
+
23
+ expected_push_branches := {b | some b in data.template.snippet.on.push.branches}
24
+
25
+ expected_pr_branches := {b | some b in data.template.snippet.on.pull_request.branches}
26
+
27
+ expected_push_paths := {p | some p in data.template.snippet.on.push.paths}
28
+
29
+ expected_runs_on := data.template.snippet.jobs["lint-ga"]["runs-on"]
30
+
31
+ expected_perms := data.template.snippet.jobs["lint-ga"].permissions
32
+
33
+ # Required `uses:` зі template — фільтруємо тільки кроки що мають `uses`.
34
+ expected_uses_set contains u if {
35
+ some step in data.template.snippet.jobs["lint-ga"].steps
36
+ u := object.get(step, "uses", "")
37
+ u != ""
38
+ }
39
+
40
+ # Required `run:` substrings — collected from steps with `run`.
41
+ expected_run_blob := concat("\n", [r |
42
+ some step in data.template.snippet.jobs["lint-ga"].steps
43
+ r := object.get(step, "run", "")
44
+ r != ""
45
+ ])
46
+
47
+ # ── deny rules ─────────────────────────────────────────────────────────────
42
48
 
43
49
  deny contains msg if {
44
50
  input.name != expected_name
@@ -46,17 +52,17 @@ deny contains msg if {
46
52
  }
47
53
 
48
54
  deny contains msg if {
49
- not push_branches_have_dev_and_main
55
+ not branches_superset_of(gha_on.push.branches, expected_push_branches)
50
56
  msg := "lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)"
51
57
  }
52
58
 
53
59
  deny contains msg if {
54
- not pr_branches_have_dev_and_main
60
+ not branches_superset_of(gha_on.pull_request.branches, expected_pr_branches)
55
61
  msg := "lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)"
56
62
  }
57
63
 
58
64
  deny contains msg if {
59
- not push_paths_have_required
65
+ not paths_superset_of(gha_on.push.paths, expected_push_paths)
60
66
  msg := "lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)"
61
67
  }
62
68
 
@@ -66,13 +72,13 @@ deny contains msg if {
66
72
  }
67
73
 
68
74
  deny contains msg if {
69
- job["runs-on"] != "ubuntu-latest"
70
- msg := "lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)"
75
+ job["runs-on"] != expected_runs_on
76
+ msg := sprintf("lint-ga.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
71
77
  }
72
78
 
73
79
  deny contains msg if {
74
- job.permissions.contents != "read"
75
- msg := "lint-ga.yml: permissions мають бути contents: read (ga.mdc)"
80
+ job.permissions.contents != expected_perms.contents
81
+ msg := sprintf("lint-ga.yml: permissions.contents має бути %s (ga.mdc)", [expected_perms.contents])
76
82
  }
77
83
 
78
84
  deny contains msg if {
@@ -81,38 +87,23 @@ deny contains msg if {
81
87
  }
82
88
 
83
89
  deny contains msg if {
84
- not "actions/checkout@v6" in job_uses_set
85
- msg := "lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)"
86
- }
87
-
88
- deny contains msg if {
89
- not "./.github/actions/setup-bun-deps" in job_uses_set
90
- msg := "lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)"
91
- }
92
-
93
- deny contains msg if {
94
- not "astral-sh/setup-uv@v8.0.0" in job_uses_set
95
- msg := "lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)"
90
+ some required_use in expected_uses_set
91
+ not required_use in job_uses_set
92
+ msg := sprintf("lint-ga.yml: має бути uses: %s (ga.mdc)", [required_use])
96
93
  }
97
94
 
98
95
  deny contains msg if {
96
+ expected_run_blob != ""
99
97
  not contains(job_run_blob, "bun run lint-ga")
100
98
  msg := "lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)"
101
99
  }
102
100
 
103
101
  # ── helpers ────────────────────────────────────────────────────────────────
104
102
 
105
- push_branches_have_dev_and_main if {
106
- branches := gha_on.push.branches
107
- expected_branches & {b | some b in branches} == expected_branches
108
- }
109
-
110
- pr_branches_have_dev_and_main if {
111
- branches := gha_on.pull_request.branches
112
- expected_branches & {b | some b in branches} == expected_branches
103
+ branches_superset_of(actual, expected) if {
104
+ expected & {b | some b in actual} == expected
113
105
  }
114
106
 
115
- push_paths_have_required if {
116
- paths := gha_on.push.paths
117
- expected_push_paths & {p | some p in paths} == expected_push_paths
107
+ paths_superset_of(actual, expected) if {
108
+ expected & {p | some p in actual} == expected
118
109
  }
@@ -0,0 +1,35 @@
1
+ name: Lint GA
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - dev
7
+ - main
8
+ paths:
9
+ - '.github/actions/**'
10
+ - '.github/workflows/**'
11
+ pull_request:
12
+ branches:
13
+ - dev
14
+ - main
15
+
16
+ concurrency:
17
+ group: ${{ github.ref }}-${{ github.workflow }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ lint-ga:
22
+ runs-on: ubuntu-latest
23
+ permissions:
24
+ contents: read
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ with:
28
+ persist-credentials: false
29
+
30
+ - uses: ./.github/actions/setup-bun-deps
31
+
32
+ - uses: astral-sh/setup-uv@v8.0.0
33
+
34
+ - name: Lint GA
35
+ run: bun run lint-ga
@@ -1,24 +1,20 @@
1
1
  # Перевірка кореневого `package.json` для GitHub Actions tooling (ga.mdc).
2
2
  #
3
- # Структурні workflow-перевірки живуть у `ga.workflow_common` і per-workflow
4
- # policy-пакетах. JS лишається для PATH-preflight (`shellcheck`) і git-залежної
5
- # перевірки `on.*.paths` через `git ls-files`.
3
+ # Канон надходить через --data: { "template": { "contains": ... } }
4
+ # Структура --data сформована з template/package.json.contains.json.
6
5
  #
7
- # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
8
6
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
9
7
  # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
10
8
  package ga.package_json
11
9
 
12
10
  import rego.v1
13
11
 
12
+ # Кожне рядкове поле з contains має містити кожен substring.
13
+ # Відсутність ключа → `""` → contains() = false → deny.
14
14
  deny contains msg if {
15
- not is_string(object.get(object.get(input, "scripts", {}), "lint-ga", null))
16
- msg := "package.json: додай скрипт \"lint-ga\" (ga.mdc)"
17
- }
18
-
19
- deny contains msg if {
20
- lint_ga := object.get(object.get(input, "scripts", {}), "lint-ga", "")
21
- is_string(lint_ga)
22
- not regex.match(`\bn-cursor\s+lint-ga\b`, lint_ga)
23
- msg := "lint-ga має делегувати CLI `n-cursor lint-ga` (ga.mdc)"
15
+ some script_name, needles in data.template.contains.scripts
16
+ actual := object.get(object.get(input, "scripts", {}), script_name, "")
17
+ some needle in needles
18
+ not contains(actual, needle)
19
+ msg := sprintf("package.json: scripts.%s має містити %q (ga.mdc)", [script_name, needle])
24
20
  }
@@ -0,0 +1 @@
1
+ { "scripts": { "lint-ga": ["n-cursor lint-ga"] } }
@@ -0,0 +1 @@
1
+ { "recommendations": ["github.vscode-github-actions"] }
@@ -1,9 +1,10 @@
1
1
  # Перевірка `.vscode/extensions.json` для GitHub Actions (ga.mdc).
2
2
  #
3
- # Canonical: у `recommendations` має бути `github.vscode-github-actions`.
3
+ # Канон надходить через --data: { "template": { "snippet": ... } }
4
+ # Структура --data сформована з template/extensions.json.snippet.json.
5
+ # `recommendations` — subset-of: кожна рекомендація з template має бути у input.
4
6
  # Додаткові рекомендації від інших правил дозволені.
5
7
  #
6
- # Структура каталогу збігається зі шляхом пакету (regal: directory-package-mismatch).
7
8
  # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
8
9
  # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
9
10
  package ga.vscode_extensions
@@ -11,6 +12,7 @@ package ga.vscode_extensions
11
12
  import rego.v1
12
13
 
13
14
  deny contains msg if {
14
- not "github.vscode-github-actions" in {r | some r in object.get(input, "recommendations", [])}
15
- msg := ".vscode/extensions.json: recommendations має містити \"github.vscode-github-actions\" (ga.mdc)"
15
+ some rec in data.template.snippet.recommendations
16
+ not rec in {r | some r in object.get(input, "recommendations", [])}
17
+ msg := sprintf(".vscode/extensions.json: recommendations має містити %q (ga.mdc)", [rec])
16
18
  }
@@ -0,0 +1 @@
1
+ { "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } }