@7n/rules 1.4.1 → 1.5.1

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 (105) hide show
  1. package/.claude-template/hooks/.gitignore.snippet +9 -0
  2. package/CHANGELOG.md +18 -0
  3. package/bin/n-rules.js +65 -17
  4. package/package.json +11 -1
  5. package/rules/abie/clean_merged_ignore_branches/concern.json +3 -0
  6. package/rules/docker/lint_docker_yml/concern.json +3 -0
  7. package/rules/js/lint_js_yml/concern.json +3 -0
  8. package/rules/k8s/lint_k8s_yml/concern.json +3 -0
  9. package/rules/npm-module/npm_publish_yml/concern.json +3 -0
  10. package/rules/php/lint_php_yml/concern.json +3 -0
  11. package/rules/python/lint_python_yml/concern.json +3 -0
  12. package/rules/rust/lint_rust_yml/concern.json +3 -0
  13. package/rules/rust/toolchain_cache/concern.json +10 -1
  14. package/rules/security/lint_security_yml/concern.json +3 -0
  15. package/rules/style/lint_style_yml/concern.json +3 -0
  16. package/rules/tauri/main.mdc +1 -1
  17. package/rules/tauri/release/release.mdc +15 -0
  18. package/rules/text/lint_text/concern.json +3 -0
  19. package/schemas/concern.json +86 -16
  20. package/schemas/n-rules.json +11 -1
  21. package/schemas/v8r-catalog.json +123 -27
  22. package/scripts/auto-rules.mjs +47 -6
  23. package/scripts/docs/auto-rules.md +3 -1
  24. package/scripts/docs/skills-cli.md +3 -2
  25. package/scripts/docs/sync-claude-config.md +1 -1
  26. package/scripts/docs/upgrade-n-rules-and-install.md +3 -1
  27. package/scripts/lib/acp-runner.mjs +174 -0
  28. package/scripts/lib/concern-meta.mjs +8 -1
  29. package/scripts/lib/docs/acp-runner.md +58 -0
  30. package/scripts/lib/docs/concern-meta.md +1 -1
  31. package/scripts/lib/docs/read-n-rules-config-lite.md +3 -1
  32. package/scripts/lib/docs/resolve-plugins.md +17 -0
  33. package/scripts/lib/docs/run-conftest-batch.md +3 -1
  34. package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +3 -1
  35. package/scripts/lib/lint-surface/docs/run-detectors.md +5 -1
  36. package/scripts/lib/lint-surface/policy-lint-adapter.mjs +2 -0
  37. package/scripts/lib/lint-surface/run-detectors.mjs +72 -5
  38. package/scripts/lib/read-n-rules-config-lite.mjs +5 -3
  39. package/scripts/lib/resolve-plugins.mjs +248 -0
  40. package/scripts/lib/run-conftest-batch.mjs +3 -1
  41. package/scripts/skills-cli.mjs +30 -52
  42. package/scripts/sync-claude-config.mjs +2 -2
  43. package/scripts/upgrade-n-rules-and-install.mjs +5 -4
  44. package/rules/ga/clean_ga_workflows/clean_ga_workflows.mdc +0 -20
  45. package/rules/ga/clean_ga_workflows/clean_ga_workflows.rego +0 -112
  46. package/rules/ga/clean_ga_workflows/concern.json +0 -10
  47. package/rules/ga/clean_ga_workflows/docs/fix-clean_ga_workflows.md +0 -33
  48. package/rules/ga/clean_ga_workflows/docs/index.md +0 -9
  49. package/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs +0 -8
  50. package/rules/ga/clean_ga_workflows/template/clean-ga-workflows.yml.snippet.yml +0 -26
  51. package/rules/ga/clean_merged_branch/clean_merged_branch.mdc +0 -24
  52. package/rules/ga/clean_merged_branch/clean_merged_branch.rego +0 -163
  53. package/rules/ga/clean_merged_branch/concern.json +0 -10
  54. package/rules/ga/clean_merged_branch/docs/fix-clean_merged_branch.md +0 -27
  55. package/rules/ga/clean_merged_branch/docs/index.md +0 -9
  56. package/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs +0 -8
  57. package/rules/ga/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +0 -37
  58. package/rules/ga/docs/index.md +0 -11
  59. package/rules/ga/git_ai/concern.json +0 -11
  60. package/rules/ga/git_ai/docs/fix-git_ai.md +0 -30
  61. package/rules/ga/git_ai/docs/index.md +0 -9
  62. package/rules/ga/git_ai/fix-git_ai.mjs +0 -5
  63. package/rules/ga/git_ai/git_ai.mdc +0 -19
  64. package/rules/ga/git_ai/git_ai.rego +0 -76
  65. package/rules/ga/git_ai/template/git-ai.yml.snippet.yml +0 -30
  66. package/rules/ga/lint_ga/concern.json +0 -10
  67. package/rules/ga/lint_ga/docs/fix-lint_ga.md +0 -32
  68. package/rules/ga/lint_ga/docs/index.md +0 -9
  69. package/rules/ga/lint_ga/fix-lint_ga.mjs +0 -5
  70. package/rules/ga/lint_ga/lint_ga.mdc +0 -21
  71. package/rules/ga/lint_ga/lint_ga.rego +0 -132
  72. package/rules/ga/lint_ga/template/lint-ga.yml.snippet.yml +0 -41
  73. package/rules/ga/main.json +0 -1
  74. package/rules/ga/main.mdc +0 -17
  75. package/rules/ga/vscode_extensions/concern.json +0 -11
  76. package/rules/ga/vscode_extensions/docs/fix-vscode_extensions.md +0 -25
  77. package/rules/ga/vscode_extensions/docs/index.md +0 -11
  78. package/rules/ga/vscode_extensions/fix-vscode_extensions.mjs +0 -1
  79. package/rules/ga/vscode_extensions/template/extensions.json.snippet.json +0 -1
  80. package/rules/ga/vscode_extensions/vscode_extensions.mdc +0 -9
  81. package/rules/ga/vscode_extensions/vscode_extensions.rego +0 -18
  82. package/rules/ga/vscode_settings/concern.json +0 -11
  83. package/rules/ga/vscode_settings/docs/fix-vscode_settings.md +0 -26
  84. package/rules/ga/vscode_settings/docs/index.md +0 -9
  85. package/rules/ga/vscode_settings/fix-vscode_settings.mjs +0 -5
  86. package/rules/ga/vscode_settings/template/settings.json.snippet.json +0 -1
  87. package/rules/ga/vscode_settings/vscode_settings.mdc +0 -9
  88. package/rules/ga/vscode_settings/vscode_settings.rego +0 -22
  89. package/rules/ga/workflow_common/concern.json +0 -9
  90. package/rules/ga/workflow_common/template/uses-min-versions.snippet.json +0 -4
  91. package/rules/ga/workflow_common/workflow_common.mdc +0 -18
  92. package/rules/ga/workflow_common/workflow_common.rego +0 -288
  93. package/rules/ga/workflows/concern.json +0 -8
  94. package/rules/ga/workflows/docs/fix-workflows.md +0 -37
  95. package/rules/ga/workflows/docs/main.md +0 -38
  96. package/rules/ga/workflows/fix-workflows.mjs +0 -212
  97. package/rules/ga/workflows/main.mjs +0 -433
  98. package/rules/ga/workflows/workflows.mdc +0 -33
  99. package/rules/ga/zizmor_yml/concern.json +0 -11
  100. package/rules/ga/zizmor_yml/docs/fix-zizmor_yml.md +0 -28
  101. package/rules/ga/zizmor_yml/docs/index.md +0 -9
  102. package/rules/ga/zizmor_yml/fix-zizmor_yml.mjs +0 -3
  103. package/rules/ga/zizmor_yml/template/zizmor.yml.snippet.yml +0 -5
  104. package/rules/ga/zizmor_yml/zizmor_yml.mdc +0 -9
  105. package/rules/ga/zizmor_yml/zizmor_yml.rego +0 -27
@@ -1,5 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [
4
- createTemplateFixPattern({ id: 'ga-vscode_settings-template', targetPath: '.vscode/settings.json' })
5
- ]
@@ -1 +0,0 @@
1
- { "[github-actions-workflow]": { "editor.defaultFormatter": "oxc.oxc-vscode" } }
@@ -1,9 +0,0 @@
1
- ## `.vscode/settings.json` — налаштування форматування GitHub Actions workflow
2
-
3
- Rego-пакет: `ga.vscode_settings`
4
-
5
- **Цільовий файл:** `.vscode/settings.json`
6
-
7
- Перевіряє 2-рівневу структуру: для кожного `<block-key>` з template кожен `<leaf-key>` у відповідному блоці input має точно відповідати очікуваному значенню. Ключі можуть містити дужки та крапки (VS Code-конвенція, наприклад `[github-actions-workflow]`).
8
-
9
- Канон: [settings.json.snippet.json](./template/settings.json.snippet.json)
@@ -1,22 +0,0 @@
1
- # Перевірка `.vscode/settings.json` для GitHub Actions workflow (ga.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/settings.json.snippet.json.
5
- # Snippet — 2-рівнева мапа: <language-block-key>.<setting-key> = <expected>
6
- # (VS Code-конвенція: ключі типу `[github-actions-workflow]` і `editor.defaultFormatter`
7
- # — це літеральні string-keys із дужками/крапкою, не вкладені обʼєкти).
8
- #
9
- # Конвенція проєкту — `import rego.v1` + multi-value `deny contains msg if { … }`
10
- # (.cursor/rules/conftest.mdc). Лінт — `bun run lint-rego` (regal).
11
- package ga.vscode_settings
12
-
13
- import rego.v1
14
-
15
- deny contains msg if {
16
- some block_key, expected_inner in data.template.snippet
17
- inner := object.get(input, block_key, {})
18
- some leaf_key, expected_value in expected_inner
19
- actual := object.get(inner, leaf_key, null)
20
- actual != expected_value
21
- msg := sprintf(".vscode/settings.json: %s.%s має бути %q (ga.mdc)", [block_key, leaf_key, expected_value])
22
- }
@@ -1,9 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "fixability": "config",
4
- "policy": {
5
- "files": {
6
- "walkGlob": ".github/workflows/*.yml"
7
- }
8
- }
9
- }
@@ -1,4 +0,0 @@
1
- {
2
- "actions/checkout": "6",
3
- "Infisical/secrets-action": "1.0.16"
4
- }
@@ -1,18 +0,0 @@
1
- ## Універсальні Rego-перевірки для всіх workflow-файлів
2
-
3
- Rego-пакет: `ga.workflow_common`
4
-
5
- **Цільові файли:** `.github/workflows/*.yml` (кожен файл окремо)
6
-
7
- ### Що перевіряється
8
-
9
- - **concurrency** — обов'язкова наявність блоку; `group` = `${{ github.ref }}-${{ github.workflow }}`; `cancel-in-progress: true`
10
- - **Заборонені кроки** — `oven-sh/setup-bun`, `actions/cache`, `bun install` у будь-якому `uses:`/`run:` (мають бути інкапсульовані у composite `setup-bun-deps`)
11
- - **Порядок кроків** — якщо є `setup-bun-deps`, перед ним обов'язковий `actions/checkout@`
12
- - **Shell-продовження** — `\` перед переносом рядка у `run:` заборонено; треба `run: >-`
13
- - **Заборонені команди** — `depcheck` у `run:` (мігровано на `knip`)
14
- - **Мінімальні версії** — `uses:` з marketplace-actions перевіряються за канонічним JSON; SHA-пін (`owner/action@<40-hex SHA>`) задовольняє вимогу мінімальної версії — НЕ замінюй наявний SHA-пін тегом
15
-
16
- Канон мінімальних версій: [uses-min-versions.snippet.json](./template/uses-min-versions.snippet.json)
17
-
18
- Детальна документація поведінки — у [ga-workflow_common](../../js/workflow_common.mdc).
@@ -1,288 +0,0 @@
1
- # Універсальні перевірки для будь-якого `.github/workflows/*.yml` (ga.mdc).
2
- #
3
- # Порт `verifyNoDirectBunOrCache`, `verifyNoRunShellLineContinuationBackslash`,
4
- # `verifyCheckoutBeforeLocalSetupBunDeps` та `validateConcurrencyOnRoot` з
5
- # `npm/scripts/rules/ga/fix.mjs`. На відміну від `lint_ga`/`clean_ga_workflows`/
6
- # `clean_merged_branch`/`git_ai`, цей пакет не привʼязаний до конкретного
7
- # workflow — `conftest test` запускається на кожному файлі окремо з
8
- # `--namespace ga.workflow_common`, і `input` — це окремий розпарсений YAML.
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 ga.workflow_common
14
-
15
- import rego.v1
16
-
17
- # ── Очікувані значення ─────────────────────────────────────────────────────
18
-
19
- expected_concurrency_group := concat("", ["$", "{{ github.ref }}-$", "{{ github.workflow }}"])
20
-
21
- # Локальні composite setup-bun-deps (ga.mdc) — два варіанти шляху:
22
- # `.github/actions/...` (cursor-репо) і `npm/github-actions/...` (npm-пакет dev-локально).
23
- local_setup_bun_markers := {
24
- "./.github/actions/setup-bun-deps",
25
- "./npm/github-actions/setup-bun-deps",
26
- }
27
-
28
- # Заборонені підрядки в кроках `uses` та `run` (ga.mdc): дублюючі setup/cache/install,
29
- # які мають бути всередині composite-action `setup-bun-deps`.
30
- forbidden_step_substrings := {
31
- "oven-sh/setup-bun": "використовуй .github/actions/setup-bun-deps замість oven-sh/setup-bun",
32
- "actions/cache": "використовуй .github/actions/setup-bun-deps замість actions/cache",
33
- "bun install": "використовуй .github/actions/setup-bun-deps замість bun install",
34
- }
35
-
36
- # Заборонені бінарки у `run:` кроках (ga.mdc). `depcheck` мігровано на `knip`
37
- # у `js.mdc` — окремий крок у workflow не потрібен. Регексп ловить виклики
38
- # через `npx`, `bunx`, `npm exec`, або як standalone-команду на початку рядка.
39
- forbidden_run_command_patterns := {"depcheck": `(?:^|[\s;&|])(?:npx|bunx|npm exec|pnpm exec)?[ \t]*depcheck\b`}
40
-
41
- # Шаблони довгих повідомлень — через `concat`, щоб дотримуватися regal style/line-length.
42
-
43
- concurrency_missing_template := concat(" ", [
44
- "відсутня секція concurrency —",
45
- "додай concurrency.group: %s і cancel-in-progress: true (ga.mdc)",
46
- ])
47
-
48
- shell_continuation_template := concat(" ", [
49
- "jobs.%s.steps[%d]: у run заборонено продовження рядків через зворотний сліш;",
50
- "оформи як folded block (run: >-) (ga.mdc)",
51
- ])
52
-
53
- setup_bun_no_checkout_template := concat(" ", [
54
- "jobs.%s: перед локальним setup-bun-deps потрібен крок actions/checkout@v6 —",
55
- "інакше runner не знайде action.yml (ga.mdc)",
56
- ])
57
-
58
- min_uses_version_template := concat(" ", [
59
- "jobs.%s.steps[%d]: %s має бути >= v%s (зараз %q) —",
60
- "онови ref у uses: (ga.mdc)",
61
- ])
62
-
63
- forbidden_run_command_template := concat(" ", [
64
- "jobs.%s.steps[%d]: `%s` заборонено у workflow —",
65
- "мігровано на knip (js.mdc, ga.mdc)",
66
- ])
67
-
68
- # ── Аліаси на input ────────────────────────────────────────────────────────
69
-
70
- # Усі jobs (з гарантією, що це обʼєкт) — щоб не падати на нетипових YAML.
71
- jobs := input.jobs
72
-
73
- # Плоский список усіх кроків з усіх jobs з метаданими — для перевірок, де job-id
74
- # і позиція кроку нам потрібні в повідомленні (shell line continuation).
75
- all_flat_steps contains entry if {
76
- some job_id, step_index
77
- step := jobs[job_id].steps[step_index]
78
- entry := {"job_id": job_id, "step_index": step_index, "step": step}
79
- }
80
-
81
- # ── deny: заборонені setup-bun/cache/install у будь-якому кроці ────────────
82
-
83
- deny contains msg if {
84
- some entry in all_flat_steps
85
- some pattern, hint in forbidden_step_substrings
86
- step_uses_or_run_blob(entry.step) != ""
87
- contains(step_uses_or_run_blob(entry.step), pattern)
88
- msg := sprintf("jobs.%s.steps[%d]: %s (ga.mdc)", [entry.job_id, entry.step_index, hint])
89
- }
90
-
91
- # ── deny: depcheck у будь-якому `run:` ────────────────────────────────────
92
- #
93
- # `depcheck` мігровано на `knip` (js.mdc); `knip` вже запускається у lint-js
94
- # CI як частина `bunx knip` у скрипті, тож окремий depcheck-крок зайвий і має
95
- # бути видалений з workflow-файлів.
96
-
97
- deny contains msg if {
98
- some entry in all_flat_steps
99
- some name, pattern in forbidden_run_command_patterns
100
- regex.match(pattern, step_run_text(entry.step))
101
- msg := sprintf(forbidden_run_command_template, [entry.job_id, entry.step_index, name])
102
- }
103
-
104
- # ── deny: shell-продовження `\` перед переносом рядка у `run:` ─────────────
105
- #
106
- # `\` + `\n` — bash line-continuation; у workflow замінюй на folded block `>-`
107
- # без зворотних слішів (ga.mdc).
108
-
109
- deny contains msg if {
110
- some entry in all_flat_steps
111
- run_text := step_run_text(entry.step)
112
- regex.match(`\\\r?\n`, run_text)
113
- msg := sprintf(shell_continuation_template, [entry.job_id, entry.step_index])
114
- }
115
-
116
- # ── deny: setup-bun-deps без попереднього checkout у тому ж job ────────────
117
- #
118
- # Без `actions/checkout` локальний composite-action недоступний — runner не
119
- # знайде `action.yml` у дереві. Перевіряємо порядок індексів кроків у кожному
120
- # job: setup-bun-deps має бути після принаймні одного `actions/checkout@`.
121
-
122
- deny contains msg if {
123
- some job_id, job in jobs
124
- first_setup := first_local_setup_bun_index(job)
125
- first_setup >= 0
126
- not has_checkout_before(job, first_setup)
127
- msg := sprintf(setup_bun_no_checkout_template, [job_id])
128
- }
129
-
130
- # ── deny: actions/checkout без persist-credentials: false ─────────────────
131
- #
132
- # `persist-credentials: false` запобігає кешуванню git-токена після checkout,
133
- # що є обов'язковою вимогою безпеки (ga.mdc).
134
-
135
- deny contains msg if {
136
- some _job_id, job in jobs
137
- some step in object.get(job, "steps", [])
138
- uses := object.get(step, "uses", "")
139
- startswith(uses, "actions/checkout@")
140
- creds := object.get(object.get(step, "with", {}), "persist-credentials", true)
141
- creds != false
142
- msg := sprintf("jobs.%s: actions/checkout@v6 потребує `with: persist-credentials: false` (ga.mdc)", [_job_id])
143
- }
144
-
145
- # ── deny: concurrency блок ─────────────────────────────────────────────────
146
- #
147
- # Дублює окремі per-workflow перевірки для clean-ga-workflows / clean-merged-branch /
148
- # lint-ga / git-ai, але вкриває й решту workflow-файлів (apply-k8s, lint-js, …),
149
- # для яких поки немає виділеної polysi.
150
-
151
- deny contains msg if {
152
- # `object.get(…, default)` повертає `false` коли ключа немає — інакше `not is_object(…)`
153
- # над відсутнім полем дає `undefined`, не `true`, і правило мовчки не спрацьовує.
154
- not is_object(object.get(input, "concurrency", false))
155
- msg := sprintf(concurrency_missing_template, [expected_concurrency_group])
156
- }
157
-
158
- deny contains msg if {
159
- is_object(object.get(input, "concurrency", false))
160
- input.concurrency.group != expected_concurrency_group
161
- msg := sprintf("concurrency.group має бути %s (ga.mdc)", [expected_concurrency_group])
162
- }
163
-
164
- deny contains msg if {
165
- is_object(object.get(input, "concurrency", false))
166
- input.concurrency["cancel-in-progress"] != true
167
- msg := "concurrency.cancel-in-progress має бути true (ga.mdc)"
168
- }
169
-
170
- # ── deny: мінімальні версії marketplace actions у `uses:` ─────────────────
171
- #
172
- # Канон — `template/uses-min-versions.snippet.json` (через --data).
173
- # Перевіряє semver-подібні теги `vX.Y.Z` / `vN`; SHA-pin (40 hex) пропускаємо.
174
-
175
- deny contains msg if {
176
- some entry in all_flat_steps
177
- uses := object.get(entry.step, "uses", "")
178
- uses != ""
179
- some action_slug, min_ver in data.template.snippet
180
- action_uses_matches(uses, action_slug)
181
- ref := action_uses_ref(uses)
182
- not action_ref_meets_min(ref, min_ver)
183
- msg := sprintf(min_uses_version_template, [
184
- entry.job_id,
185
- entry.step_index,
186
- action_slug,
187
- min_ver,
188
- ref,
189
- ])
190
- }
191
-
192
- # ── helpers ────────────────────────────────────────────────────────────────
193
-
194
- # Об'єднаний рядок `uses` + `run` для одного кроку — для substring-пошуку
195
- # заборонених патернів. `run` може бути рядком або масивом рядків (YAML).
196
- step_uses_or_run_blob(step) := blob if {
197
- uses := object.get(step, "uses", "")
198
- run_text := step_run_text(step)
199
- blob := concat("\n", [uses, run_text])
200
- }
201
-
202
- # Текст `run:` як один рядок: підтримує рядкові та масивові форми (YAML).
203
- step_run_text(step) := step.run if is_string(step.run)
204
-
205
- else := concat("\n", [s | some s in step.run]) if is_array(step.run)
206
-
207
- else := ""
208
-
209
- # Індекс першого кроку з локальним setup-bun-deps; -1 якщо такого немає.
210
- first_local_setup_bun_index(job) := min(indices) if {
211
- indices := [i |
212
- some i, step in job.steps
213
- uses := object.get(step, "uses", "")
214
- some marker in local_setup_bun_markers
215
- contains(uses, marker)
216
- ]
217
- count(indices) > 0
218
- } else := -1
219
-
220
- # Чи є в `job.steps[0..before]` крок `actions/checkout@…`.
221
- has_checkout_before(job, before) if {
222
- some i, step in job.steps
223
- i < before
224
- uses := object.get(step, "uses", "")
225
- contains(uses, "actions/checkout@")
226
- }
227
-
228
- # `uses:` починається з `owner/repo@` для заданого slug.
229
- action_uses_matches(uses, slug) if {
230
- startswith(uses, concat("", [slug, "@"]))
231
- }
232
-
233
- # Ref після останнього `@` у `uses:` (owner/repo@ref).
234
- action_uses_ref(uses) := ref if {
235
- parts := split(uses, "@")
236
- count(parts) >= 2
237
- ref := parts[count(parts) - 1]
238
- }
239
-
240
- # SHA-pin — semver-політика не застосовується.
241
- action_ref_is_sha_pin(ref) if {
242
- regex.match(`^[0-9a-fA-F]{40}$`, ref)
243
- }
244
-
245
- # Semver ref >= min (обидва як X.Y.Z після optional `v`).
246
- action_ref_meets_min(ref, _) if {
247
- action_ref_is_sha_pin(ref)
248
- }
249
-
250
- action_ref_meets_min(ref, min_ver) if {
251
- not action_ref_is_sha_pin(ref)
252
- version_triple_gte(version_triple(ref), version_triple(min_ver))
253
- }
254
-
255
- version_triple(raw) := [major, minor, patch] if {
256
- stripped := trim_prefix(trim_prefix(raw, "v"), "V")
257
- parts := split_to_numbers(stripped)
258
- major := version_part(parts, 0)
259
- minor := version_part(parts, 1)
260
- patch := version_part(parts, 2)
261
- }
262
-
263
- version_part(parts, idx) := parts[idx] if {
264
- count(parts) > idx
265
- }
266
-
267
- else := 0
268
-
269
- version_triple_gte(a, b) if {
270
- a[0] > b[0]
271
- }
272
-
273
- version_triple_gte(a, b) if {
274
- a[0] == b[0]
275
- a[1] > b[1]
276
- }
277
-
278
- version_triple_gte(a, b) if {
279
- a[0] == b[0]
280
- a[1] == b[1]
281
- a[2] >= b[2]
282
- }
283
-
284
- split_to_numbers(spec) := nums if {
285
- tokens := regex.split(`\D+`, spec)
286
- non_empty := [t | some t in tokens; t != ""]
287
- nums := [n | some t in non_empty; n := to_number(t)]
288
- }
@@ -1,8 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "fixability": "config",
4
- "lint": {
5
- "scope": "full",
6
- "glob": [".github/workflows/**"]
7
- }
8
- }
@@ -1,37 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-workflows.mjs
4
- resource: npm/rules/ga/workflows/fix-workflows.mjs
5
- docgen:
6
- crc: 5f7fc736
7
- ---
8
-
9
- ## Огляд
10
-
11
- T0-autofix для `ga/workflows`: детерміновані текстові правки GitHub Actions workflow-файлів без
12
- участі LLM, керовані structured fix-hint детектора (поле `data.kind` у violation). Виконується у
13
- fix-фазі перед LLM-ладдером; правки незворотні (поза rollback), а коректність підтверджує
14
- повторний прогін детектора.
15
-
16
- ## Поведінка
17
-
18
- - **checkout-persist-credentials** — у кожен крок `actions/checkout`, де бракує
19
- `persist-credentials: false`, дописує цей ключ: створює блок `with:` (відступ за колонкою
20
- `uses:`) або додає ключ у наявний `with:`. Кілька checkout-кроків у файлі обробляються всі.
21
- - **unmatched-paths-glob** — прибирає з `on.<event>.paths` (push / pull_request) list-елементи,
22
- що відповідають застарілим glob-ам із fix-hint; видалення обмежене блоком `paths:`, тож
23
- однойменні значення деінде не зачіпаються.
24
-
25
- Правки текстові — коментарі, форматування й порожні рядки зберігаються, diff мінімальний.
26
-
27
- ## Публічний API
28
-
29
- - `patterns` — масив T0-патернів (`id` / `test` / `apply`), що його споживає центральний fix-pipeline.
30
- - `addPersistCredentials`, `removePathsGlobs` — чисті трансформери вмісту файлу: повертають новий
31
- текст або `null`, якщо змін немає.
32
-
33
- ## Гарантії поведінки
34
-
35
- - Жодного запису без фактичної зміни вмісту; перед записом реєструється pre-image через `recordWrite`.
36
- - Регулярні вирази лінійні (без ReDoS); парсинг list-елементів — рядковими операціями.
37
- - Не-checkout кроки та значення поза блоком `paths:` лишаються недоторканими.
@@ -1,38 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: main.mjs
4
- resource: npm/rules/ga/workflows/main.mjs
5
- docgen:
6
- crc: ac064a5f
7
- ---
8
-
9
- ## Огляд
10
-
11
- Please provide the code you want me to document.
12
-
13
- ## Поведінка
14
-
15
- Поведінка:
16
- checkShellcheckInstalled перевіряє локальну наявність інструменту `shellcheck` у системному PATH, щоб забезпечити узгодженість логіки CI та локального запуску.
17
- main виконує комплексну перевірку відповідності репозиторію правилам `ga.mdc`, включаючи валідацію структури GitHub Actions workflow, аналіз посилань на файли за допомогою `git ls-files` та пошук залишків MegaLinter. При цьому він ігнорує шляхи, що стосуються `.github` та `.git`.
18
- lint запускає стандартний лінтинг, який включає перевірку `actionlint` та `zizmor` для забезпечення якості YAML-файлів та робочого середовища, а також ініціює повну перевірку `main`.
19
- runLintGaCli запускає процес лінтингу `ga` через командний інтерфейс.
20
-
21
- ## Публічний API
22
-
23
- Підтверджую. Я дію як технічний письменник.
24
-
25
- Ось переписаний список у вигляді стислих маркерів відповідно до ваших вимог:
26
-
27
- - checkShellcheckInstalled — Визначає, чи є інструмент `shellcheck` у системному шляху, щоб уникнути ложного позитивного результату локального линтування, коли CI-оркестр вимагає його присутності.
28
- - main — Валідує відповідність усієї проєктної конфігурації вимогам, визначеним у `ga.mdc`.
29
- - Plan B-патерн — Виконує первинну валідацію структури CI-workflow за допомогою Rego-полісі у `npm/policy/ga/`; додатково проводить крос-файлові перевірки та аналіз конфігурацій (наприклад, для VS Code), ініційовавши цей процес за допомогою `bun run lint-ga`.
30
- - lint — Виконує повноцінний аналіз конфігурацій CI/CD з використанням `actionlint` та `zizmor`.
31
- - runLintGaCli — Запускає повний набір перевірок, що включають Rego-валідацію, аналіз `actionlint` та зовнішні інструменти для забезпечення консистентності конфігурацій проєкту.
32
-
33
- ## Гарантії поведінки
34
-
35
- - Read-only: не виконує операцій запису (ФС/БД).
36
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
37
- - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
38
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,212 +0,0 @@
1
- /** @see ./docs/fix-workflows.md */
2
-
3
- /**
4
- * T0-autofix для `ga/workflows` — детерміновані правки, керовані structured `data`
5
- * детектора (#3 fix-hints), БЕЗ LLM. Покриває дві механічні родини порушень:
6
- * - `checkout-persist-credentials` — дописати `with: persist-credentials: false`
7
- * у кожен `actions/checkout` крок, де його бракує;
8
- * - `unmatched-paths-glob` — прибрати застарілий glob із `on.<event>.paths`.
9
- *
10
- * Трансформації **текстові** (зберігають коментарі/формат/blank-lines, мінімальний diff);
11
- * семантичну коректність гарантує canonical re-detect runner-а (T0 permanent, поза rollback).
12
- * Чисті трансформери експортуються для unit-тестів.
13
- */
14
- import { applyToFiles } from '../../../scripts/utils/apply-to-files.mjs'
15
-
16
- const CHECKOUT_USES_RE = /uses:\s*actions\/checkout@/u
17
- const WITH_LINE_RE = /^\s*with:\s*$/u
18
- const PERSIST_KEY_RE = /^\s*persist-credentials\s*:/u
19
- const QUOTE_EDGE_RE = /^['"]|['"]$/gu
20
- const PATHS_KEY_RE = /^\s*paths:\s*$/u
21
-
22
- /**
23
- * Індекс першого непорожнього рядка з `from` (включно).
24
- * @param {string[]} lines усі рядки
25
- * @param {number} from стартовий індекс
26
- * @returns {number} індекс непорожнього рядка (або `lines.length`)
27
- */
28
- function nextNonEmpty(lines, from) {
29
- let j = from
30
- while (j < lines.length && lines[j].trim() === '') j++
31
- return j
32
- }
33
-
34
- /**
35
- * Чи блок `with:` (на рядку `withLine`, колонка ключа `col`) уже містить
36
- * `persist-credentials`. Скан до dedent.
37
- * @param {string[]} lines усі рядки
38
- * @param {number} withLine індекс рядка `with:`
39
- * @param {number} col колонка ключа `with:`
40
- * @returns {boolean} true, якщо ключ уже присутній
41
- */
42
- function withBlockHasPersist(lines, withLine, col) {
43
- for (let k = withLine + 1; k < lines.length; k++) {
44
- if (lines[k].trim() === '') continue
45
- const c = lines[k].length - lines[k].trimStart().length
46
- if (c <= col) return false // dedent → блок завершився
47
- if (PERSIST_KEY_RE.test(lines[k])) return true
48
- }
49
- return false
50
- }
51
-
52
- /**
53
- * Insert-план для одного `actions/checkout` кроку на рядку `i` (колонка `uses:` = `col`).
54
- * @param {string[]} lines усі рядки
55
- * @param {number} i індекс рядка `uses:`
56
- * @param {number} col колонка `uses:`
57
- * @returns {{ at: number, text: string[] } | null} вставка або null, якщо ключ уже є
58
- */
59
- function persistInsertFor(lines, i, col) {
60
- const ind = ' '.repeat(col)
61
- const j = nextNonEmpty(lines, i + 1)
62
- const hasWithBlock = j < lines.length && WITH_LINE_RE.test(lines[j]) && lines[j].indexOf('with:') === col
63
- if (hasWithBlock) {
64
- return withBlockHasPersist(lines, j, col) ? null : { at: j + 1, text: [`${ind} persist-credentials: false`] }
65
- }
66
- return { at: i + 1, text: [`${ind}with:`, `${ind} persist-credentials: false`] }
67
- }
68
-
69
- /**
70
- * Дописує `with: persist-credentials: false` у кожен `actions/checkout` крок,
71
- * де його бракує. Дві форми: крок без `with:` (створити блок) і з наявним `with:`
72
- * (вставити ключ). Відступ `with:` = колонка `uses:`; ключа = +2.
73
- * @param {string} content вміст workflow-файла
74
- * @returns {string|null} новий вміст або null, якщо нічого не змінилось
75
- */
76
- export function addPersistCredentials(content) {
77
- const lines = content.split('\n')
78
- /** @type {Array<{ at: number, text: string[] }>} */
79
- const inserts = []
80
- for (let i = 0; i < lines.length; i++) {
81
- const col = lines[i].indexOf('uses:')
82
- if (col === -1 || !CHECKOUT_USES_RE.test(lines[i])) continue
83
- const ins = persistInsertFor(lines, i, col)
84
- if (ins) inserts.push(ins)
85
- }
86
- if (inserts.length === 0) return null
87
- // Згори вниз — індекси не зсуваються під час splice.
88
- inserts.sort((a, b) => b.at - a.at)
89
- for (const ins of inserts) lines.splice(ins.at, 0, ...ins.text)
90
- return lines.join('\n')
91
- }
92
-
93
- /**
94
- * Прибирає list-елементи із заданими значеннями всередині блоків `paths:`
95
- * (`on.push.paths` / `on.pull_request.paths`). Scoped до paths-блоку, щоб не
96
- * зачепити однойменні значення деінде.
97
- * @param {string} content вміст workflow-файла
98
- * @param {Set<string>} globs значення glob-ів на видалення (без лапок)
99
- * @returns {string|null} новий вміст або null, якщо нічого не змінилось
100
- */
101
- export function removePathsGlobs(content, globs) {
102
- const lines = content.split('\n')
103
- /** @type {string[]} */
104
- const out = []
105
- let pathsCol = -1 // колонка ключа `paths:` поточного блоку; -1 = поза блоком
106
- let changed = false
107
- for (const line of lines) {
108
- if (PATHS_KEY_RE.test(line)) {
109
- pathsCol = line.indexOf('paths:')
110
- out.push(line)
111
- continue
112
- }
113
- if (pathsCol >= 0 && line.trim() !== '') {
114
- const col = line.length - line.trimStart().length
115
- if (col > pathsCol) {
116
- // Лінійний парс list-елемента `- <value>` (без regex → без ReDoS).
117
- const trimmed = line.trimStart()
118
- if (trimmed.startsWith('- ')) {
119
- const val = trimmed.slice(2).trim().replace(QUOTE_EDGE_RE, '')
120
- if (globs.has(val)) {
121
- changed = true
122
- continue // викидаємо рядок
123
- }
124
- }
125
- out.push(line)
126
- continue
127
- }
128
- pathsCol = -1 // dedent → блок `paths:` завершився
129
- }
130
- out.push(line)
131
- }
132
- return changed ? out.join('\n') : null
133
- }
134
-
135
- const WRAPPED_NCURSOR_RE = /\b(?:bunx|npx)\s+n-(?:cursor|rules)/u
136
- const RUN_INLINE_NCURSOR_MATCH = /^(\s*(?:-\s*)?run:\s*)n-(?:cursor|rules)(\s.*)$/u
137
- const BARE_LINE_NCURSOR_MATCH = /^(\s+)n-(?:cursor|rules)(\s.*)$/u
138
-
139
- /**
140
- * Префіксує bare `n-rules …` (чи legacy `n-rules …`) у `run`-кроках через `bunx n-rules` (у CI
141
- * бінарник не на PATH). Пропускає рядки, де вже є `bunx`/`npx`. Покриває inline `run:` і рядок у run-блоці.
142
- * @param {string} content вміст workflow-файла
143
- * @returns {string|null} новий вміст або null, якщо нічого не змінилось
144
- */
145
- export function prefixBunxNCursor(content) {
146
- let changed = false
147
- const out = content.split('\n').map(line => {
148
- if (WRAPPED_NCURSOR_RE.test(line)) return line
149
- const inline = RUN_INLINE_NCURSOR_MATCH.exec(line)
150
- if (inline) {
151
- changed = true
152
- return `${inline[1]}bunx n-rules${inline[2]}`
153
- }
154
- const bare = BARE_LINE_NCURSOR_MATCH.exec(line)
155
- if (bare) {
156
- changed = true
157
- return `${bare[1]}bunx n-rules${bare[2]}`
158
- }
159
- return line
160
- })
161
- return changed ? out.join('\n') : null
162
- }
163
-
164
- /** @type {import('../../../scripts/lib/lint-surface/types.mjs').T0Pattern[]} */
165
- export const patterns = [
166
- {
167
- id: 'ga-workflows-checkout-persist-credentials',
168
- test: violations => violations.some(v => v.data?.kind === 'checkout-persist-credentials' && v.file),
169
- apply: (violations, ctx) => {
170
- const targets = violations.filter(v => v.data?.kind === 'checkout-persist-credentials' && v.file)
171
- const touchedFiles = applyToFiles(targets, ctx, () => addPersistCredentials)
172
- return touchedFiles.length > 0
173
- ? { touchedFiles, message: `persist-credentials: false → ${touchedFiles.length} workflow(s)` }
174
- : { touchedFiles: [] }
175
- }
176
- },
177
- {
178
- id: 'ga-workflows-unmatched-paths-glob',
179
- test: violations => violations.some(v => v.data?.kind === 'unmatched-paths-glob' && v.file),
180
- apply: (violations, ctx) => {
181
- const targets = violations.filter(v => v.data?.kind === 'unmatched-paths-glob' && v.file)
182
- // Глоби на видалення згруповані per-file.
183
- /** @type {Map<string, Set<string>>} */
184
- const byFile = new Map()
185
- for (const v of targets) {
186
- const glob = typeof v.data?.glob === 'string' ? v.data.glob : null
187
- if (!glob) continue
188
- if (!byFile.has(v.file)) byFile.set(v.file, new Set())
189
- byFile.get(v.file).add(glob)
190
- }
191
- const touchedFiles = applyToFiles(
192
- targets,
193
- ctx,
194
- rel => content => removePathsGlobs(content, byFile.get(rel) ?? new Set())
195
- )
196
- return touchedFiles.length > 0
197
- ? { touchedFiles, message: `прибрано не-матчені paths-glob у ${touchedFiles.length} файл(ах)` }
198
- : { touchedFiles: [] }
199
- }
200
- },
201
- {
202
- id: 'ga-workflows-bare-n-rules',
203
- test: violations => violations.some(v => v.data?.kind === 'bare-n-rules' && v.file),
204
- apply: (violations, ctx) => {
205
- const targets = violations.filter(v => v.data?.kind === 'bare-n-rules' && v.file)
206
- const touchedFiles = applyToFiles(targets, ctx, () => prefixBunxNCursor)
207
- return touchedFiles.length > 0
208
- ? { touchedFiles, message: `bunx-префікс n-rules → ${touchedFiles.length} workflow(s)` }
209
- : { touchedFiles: [] }
210
- }
211
- }
212
- ]