@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
@@ -4,16 +4,16 @@
4
4
  * Скіли читаються з `npm/skills/<id>/SKILL.md` установленого пакета (або кешу `npx`).
5
5
  * Промпт збирає інструкцію скілу + контекст поточного CWD (`package.json`, `tsconfig.json`,
6
6
  * `.n-rules.json`) — далі stdout або виконання через один з раннерів: вбудований
7
- * pi-агент, чи зовнішній CLI (`cursor-agent`, `codex exec`, або deprecated `claude`).
7
+ * pi-агент, чи зовнішній ACP-агент (`cursor-agent acp`, `codex-acp`, або deprecated `claude`).
8
8
  *
9
9
  * Підтримувані формати:
10
10
  * `npx \@7n/rules skill list`
11
11
  * `npx \@7n/rules skill taze`
12
12
  * `npx \@7n/rules skill pi taze` — виконати через вбудований pi-агент (рекомендовано)
13
13
  * `npx \@7n/rules skill pi taze "онови залежності"`
14
- * `npx \@7n/rules skill cursor taze` — зовнішній Cursor CLI (`cursor-agent -p`)
15
- * `npx \@7n/rules skill codex taze` — зовнішній Codex CLI (`codex exec -`)
16
- * `npx \@7n/rules skill claude taze` — deprecated: зовнішній Claude Code CLI
14
+ * `npx \@7n/rules skill cursor taze` — Cursor CLI через ACP (`cursor-agent acp`)
15
+ * `npx \@7n/rules skill codex taze` — Codex через ACP-адаптер (`@agentclientprotocol/codex-acp`)
16
+ * `npx \@7n/rules skill claude taze` — deprecated: Claude Code через ACP-адаптер
17
17
  */
18
18
 
19
19
  import { spawnSync } from 'node:child_process'
@@ -22,20 +22,17 @@ import { dirname, join } from 'node:path'
22
22
  import { cwd } from 'node:process'
23
23
  import { fileURLToPath } from 'node:url'
24
24
 
25
- import { readSkillMetaRaw, skillRequiresRoot, skillTier } from './lib/skill-meta.mjs'
25
+ import { runAcpRunner } from './lib/acp-runner.mjs'
26
+ import { readSkillMetaRaw, skillTier } from './lib/skill-meta.mjs'
26
27
 
27
- /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`codex`/`claude` — зовнішні CLI (`claude` — deprecated). */
28
+ /** Виконавці скіла. `pi` — вбудований (рекомендований); `cursor`/`codex`/`claude` — зовнішні ACP-агенти (`claude` — deprecated). */
28
29
  const RUNNERS = new Set(['pi', 'cursor', 'codex', 'claude'])
29
30
 
30
- /**
31
- * Табличний опис зовнішніх CLI-раннерів: бінарник, аргументи для non-interactive
32
- * режиму зі stdin-промптом, і чи раннер deprecated (друкує попередження перед запуском).
33
- * @type {Record<'claude' | 'cursor' | 'codex', { bin: string, args: string[], deprecated: boolean }>}
34
- */
35
- const EXTERNAL_RUNNERS = {
36
- claude: { bin: 'claude', args: ['-p'], deprecated: true },
37
- cursor: { bin: 'cursor-agent', args: ['-p'], deprecated: false },
38
- codex: { bin: 'codex', args: ['exec', '-'], deprecated: false }
31
+ /** Раннери, що йдуть через зовнішнього ACP-агента (`runAcpRunner`), і чи deprecated (друкує попередження перед запуском). */
32
+ const ACP_RUNNERS = {
33
+ claude: { deprecated: true },
34
+ cursor: { deprecated: false },
35
+ codex: { deprecated: false }
39
36
  }
40
37
 
41
38
  const USAGE_LINES = [
@@ -43,8 +40,8 @@ const USAGE_LINES = [
43
40
  ' npx @7n/rules skill list',
44
41
  ' npx @7n/rules skill <skill-id> ["task"]',
45
42
  ' npx @7n/rules skill pi <skill-id> ["task"] # вбудований pi-агент (рекомендовано)',
46
- ' npx @7n/rules skill cursor <skill-id> ["task"] # зовнішній Cursor CLI',
47
- ' npx @7n/rules skill codex <skill-id> ["task"] # зовнішній Codex CLI',
43
+ ' npx @7n/rules skill cursor <skill-id> ["task"] # Cursor CLI через ACP',
44
+ ' npx @7n/rules skill codex <skill-id> ["task"] # Codex через ACP-адаптер',
48
45
  ' npx @7n/rules skill claude <skill-id> ["task"] # deprecated',
49
46
  '',
50
47
  'Skill id: каталог у пакеті (lint, taze, …) або з префіксом n- (n-lint → lint).'
@@ -173,44 +170,27 @@ async function runPiRunner(prompt, rawSkillName, skillsRoot, projectDir, logErro
173
170
  }
174
171
 
175
172
  /**
176
- * Делегує виконання скіла у зовнішній non-interactive CLI (`cursor-agent -p`, `codex exec -`,
177
- * або deprecated `claude -p`) — промпт передається через stdin. `claude` лишається як
178
- * deprecated fallback для тих, у кого pi-модель ще не налаштована; буде прибрано
179
- * (мігруй на `skill pi`). `cursor`/`codex` повноцінні альтернативні раннери.
180
- *
181
- * `cursor-agent` у non-interactive режимі блокується власним `approvalMode: "allowlist"`
182
- * користувача (git/bun/npx/rm тощо потребують інтерактивного підтвердження, якого в headless
183
- * виклику немає). Для `worktree:true`/`requireRoot:true` скілів (блок-радіус вже ізольований)
184
- * додаємо `--force`, замість того щоб просити користувача розширювати глобальний allowlist
185
- * у `~/.cursor/cli-config.json`.
186
- * @param {'claude' | 'cursor' | 'codex'} kind який LLM CLI запускати
187
- * @param {string} prompt промпт для передачі у stdin
188
- * @param {string} projectDir робочий каталог дочірнього процесу
173
+ * Делегує виконання скіла зовнішньому ACP-агенту (`cursor-agent acp`, `codex-acp`,
174
+ * або deprecated `claude`-адаптер) через `runAcpRunner` JSON-RPC поверх stdio,
175
+ * дозволи на tool-calls автоапрувляться (без інтерактивних питань, паритет із
176
+ * колишнім non-interactive `-p`). `claude` лишається як deprecated fallback для
177
+ * тих, у кого pi-модель ще не налаштована; буде прибрано (мігруй на `skill pi`).
178
+ * @param {'claude' | 'cursor' | 'codex'} kind якого ACP-агента запускати
179
+ * @param {string} prompt промпт скіла
180
+ * @param {string} projectDir робочий каталог агента
189
181
  * @param {(line: string) => void} logError вивід попередження/помилок
190
- * @param {boolean} isolated чи скіл ізольований (worktree/requireRoot) дозволяє force-approve для `cursor`
191
- * @returns {number} exit code дочірнього процесу
182
+ * @param {{ runAcpRunner?: typeof runAcpRunner }} [deps] інжект для тестів
183
+ * @returns {Promise<number>} exit code (0 — `end_turn`, 1 — інакше)
192
184
  */
193
- function runLlmCli(kind, prompt, projectDir, logError, isolated) {
194
- const runner = EXTERNAL_RUNNERS[kind]
185
+ function runLlmCli(kind, prompt, projectDir, logError, deps = {}) {
186
+ const runner = ACP_RUNNERS[kind]
187
+ const runAcp = deps.runAcpRunner ?? runAcpRunner
195
188
 
196
189
  if (runner.deprecated) {
197
- logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній CLI буде прибрано`)
190
+ logError(`[deprecated] skill ${kind} → use 'skill pi'; зовнішній ACP-агент буде прибрано`)
198
191
  }
199
192
 
200
- if (!isBinaryInPath(runner.bin)) {
201
- throw new Error(`\`${runner.bin}\` not found in PATH. Install ${kind} CLI or use \`skill pi\`.`)
202
- }
203
-
204
- const args = kind === 'cursor' && isolated ? [...runner.args, '--force'] : runner.args
205
-
206
- const result = spawnSync(runner.bin, args, {
207
- input: prompt,
208
- cwd: projectDir,
209
- stdio: ['pipe', 'inherit', 'inherit'],
210
- encoding: 'utf8',
211
- env: process.env
212
- })
213
- return result.status ?? 1
193
+ return runAcp(kind, prompt, projectDir, logError, { isBinaryInPath })
214
194
  }
215
195
 
216
196
  /**
@@ -261,9 +241,7 @@ export async function runSkillsCli(argv, options = {}) {
261
241
  if (first === 'pi') {
262
242
  return await runPiRunner(prompt, second, skillsRoot, projectDir, logError, deps)
263
243
  }
264
- const skillMeta = readSkillMetaRaw(join(skillsRoot, normalizeSkillId(second)))
265
- const isolated = skillRequiresRoot(skillMeta)
266
- return runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError, isolated)
244
+ return await runLlmCli(/** @type {'claude' | 'cursor' | 'codex'} */ (first), prompt, projectDir, logError, deps)
267
245
  }
268
246
 
269
247
  if (skillIds.includes(normalizeSkillId(first))) {
@@ -26,7 +26,7 @@
26
26
  * `.pi/extensions/rtk.ts`. Без установленого rtk hooks мовчки пропускають команди,
27
27
  * тож конфіг безпечно потрапляє в git — установка зводиться до `brew install rtk-ai/tap/rtk`.
28
28
  * - `.gitignore` — **merge** (лише з `adr`): дописує відсутні рядки з канонічного
29
- * фрагмента `rules/adr/js/hooks/template/.gitignore.snippet` (`node_modules/`, `dist/`,
29
+ * фрагмента `.claude-template/hooks/.gitignore.snippet` (`node_modules/`, `dist/`,
30
30
  * `*.secret`, логи capture/normalize, `.normalize-state`, `.normalize.lock`,
31
31
  * `.claude/scheduled_tasks.lock`); існуючі рядки не перезаписуються.
32
32
  *
@@ -101,7 +101,7 @@ export const PI_EXTENSION_NAME = 'n-rules-adr'
101
101
  /** @deprecated — ім'я extension-теки до перейменування пакету; лишається для cleanup при ресинку. */
102
102
  export const LEGACY_PI_EXTENSION_NAME = 'n-cursor-adr'
103
103
  /** Відносний шлях до канонічного фрагмента `.gitignore` для ADR Stop-hook'ів у tarball пакета. */
104
- export const ADR_GITIGNORE_SNIPPET_REL = 'rules/adr/js/templates/hooks/.gitignore.snippet'
104
+ export const ADR_GITIGNORE_SNIPPET_REL = '.claude-template/hooks/.gitignore.snippet'
105
105
  const GITIGNORE_FILE = '.gitignore'
106
106
  const EOL_RE = /\r?\n/u
107
107
 
@@ -101,11 +101,12 @@ async function readPackageVersionSafe(packageRoot) {
101
101
  /**
102
102
  * Шлях до встановленого пакета в `node_modules` або fallback.
103
103
  * @param {string} projectRoot корінь репозиторію
104
- * @param {string} fallbackPackageRoot корінь пакету з поточного процесу
105
- * @returns {string} абсолютний шлях до каталогу з `mdc/`, `scripts/` тощо
104
+ * @param {string} fallbackPackageRoot корінь пакету з поточного процесу (`null` для плагінів — fallback-кореня нема)
105
+ * @param {string} [packageName] npm-ім'я пакета (за замовчуванням ядро `@7n/rules`; плагіни передають своє)
106
+ * @returns {string | null} абсолютний шлях до каталогу з `rules/`, `scripts/` тощо, або `fallbackPackageRoot`
106
107
  */
107
- export function resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot) {
108
- const installed = join(projectRoot, 'node_modules', PACKAGE_NAME)
108
+ export function resolveInstalledPackageRoot(projectRoot, fallbackPackageRoot, packageName = PACKAGE_NAME) {
109
+ const installed = join(projectRoot, 'node_modules', packageName)
109
110
  if (existsSync(join(installed, 'package.json'))) {
110
111
  return installed
111
112
  }
@@ -1,20 +0,0 @@
1
- ## Структура `clean-ga-workflows.yml` — видалення завершених workflow runs
2
-
3
- Rego-пакет: `ga.clean_ga_workflows`
4
-
5
- **Цільовий файл:** `.github/workflows/clean-ga-workflows.yml`
6
-
7
- ### Що перевіряється
8
-
9
- - `name` — точна відповідність значенню з template
10
- - `on.schedule[].cron` — має містити cron із template (`0 1 16 * *`)
11
- - `on.workflow_dispatch` — має бути об'єктом `{}`
12
- - `jobs.cleanup_old_workflows` — job має існувати
13
- - `jobs.cleanup_old_workflows.runs-on` — відповідно до template (`ubuntu-latest`)
14
- - `jobs.cleanup_old_workflows.permissions` — `actions: write`, `contents: read`
15
- - `steps[0].name` і `steps[0].uses` — відповідно до template (`dmvict/clean-workflow-runs@v1`)
16
- - `steps[0].with` — `token`, `save_period`, `save_min_runs_number` як у template
17
-
18
- **Pin-aware `uses`:** SHA-пін того самого action (`owner/action@<40-hex SHA>`, з тег-коментарем `# vX` чи без) задовольняє канонічний тег із template. Якщо в репо вже SHA-пін (zizmor-політика ref-pin) — НЕ замінюй його тегом.
19
-
20
- Канон: [clean-ga-workflows.yml.snippet.yml](./template/clean-ga-workflows.yml.snippet.yml)
@@ -1,112 +0,0 @@
1
- # Перевірка `.github/workflows/clean-ga-workflows.yml` (ga.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/clean-ga-workflows.yml.snippet.yml
5
- # (повний YAML канон). Path-and-value перевірки — у цьому rego (per-concern
6
- # field-by-field з .data.template.snippet.<path>); жодних inline literals.
7
- package ga.clean_ga_workflows
8
-
9
- import rego.v1
10
-
11
- # ── Аліаси на input ────────────────────────────────────────────────────────
12
- # GHA YAML quirk: ключ `on:` — YAML 1.1 boolean `true`, у conftest серіалізується
13
- # як рядковий ключ "true". Template (через --data JSON) має ключ "on".
14
-
15
- gha_on := input["true"]
16
-
17
- step0 := input.jobs.cleanup_old_workflows.steps[0]
18
-
19
- # Експектації з template.
20
- expected_name := data.template.snippet.name
21
-
22
- expected_cron := data.template.snippet.on.schedule[0].cron
23
-
24
- expected_step0 := data.template.snippet.jobs.cleanup_old_workflows.steps[0]
25
-
26
- expected_perms := data.template.snippet.jobs.cleanup_old_workflows.permissions
27
-
28
- expected_runs_on := data.template.snippet.jobs.cleanup_old_workflows["runs-on"]
29
-
30
- # ── deny rules ─────────────────────────────────────────────────────────────
31
-
32
- deny contains msg if {
33
- input.name != expected_name
34
- msg := sprintf("clean-ga-workflows.yml: name має бути %q (ga.mdc)", [expected_name])
35
- }
36
-
37
- deny contains msg if {
38
- not has_expected_cron
39
- msg := sprintf("clean-ga-workflows.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
40
- }
41
-
42
- deny contains msg if {
43
- not is_object(object.get(gha_on, "workflow_dispatch", null))
44
- msg := "clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)"
45
- }
46
-
47
- deny contains msg if {
48
- not input.jobs.cleanup_old_workflows
49
- msg := "clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)"
50
- }
51
-
52
- deny contains msg if {
53
- job := input.jobs.cleanup_old_workflows
54
- job["runs-on"] != expected_runs_on
55
- msg := sprintf("clean-ga-workflows.yml: runs-on має бути %s (ga.mdc)", [expected_runs_on])
56
- }
57
-
58
- deny contains msg if {
59
- perms := input.jobs.cleanup_old_workflows.permissions
60
- not perms_match(perms, expected_perms)
61
- msg := "clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)"
62
- }
63
-
64
- deny contains msg if {
65
- step0.name != expected_step0.name
66
- msg := sprintf("clean-ga-workflows.yml: перший крок має мати name: %s (ga.mdc)", [expected_step0.name])
67
- }
68
-
69
- deny contains msg if {
70
- actual := step0.uses
71
- not uses_satisfies(actual, expected_step0.uses)
72
- msg := sprintf("clean-ga-workflows.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
73
- }
74
-
75
- deny contains msg if {
76
- not step0_with_canonical
77
- msg := "clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у template (ga.mdc)"
78
- }
79
-
80
- deny contains msg if {
81
- step0.with.save_period != expected_step0.with.save_period
82
- msg := sprintf("clean-ga-workflows.yml: with.save_period має бути %d (ga.mdc)", [expected_step0.with.save_period])
83
- }
84
-
85
- # ── helpers ────────────────────────────────────────────────────────────────
86
-
87
- # `uses:` з input задовольняє канонічний `owner/action@tag`: точний збіг…
88
- uses_satisfies(actual, expected) if actual == expected
89
-
90
- # …або той самий action-slug і ref — повний 40-hex commit SHA (zizmor ref-pin).
91
- # SHA-пін відповідної версії задовольняє вимогу тега — не даунгрейдимо до тега.
92
- uses_satisfies(actual, expected) if {
93
- slug := split(expected, "@")[0]
94
- startswith(actual, concat("", [slug, "@"]))
95
- parts := split(actual, "@")
96
- regex.match(`^[0-9a-fA-F]{40}$`, parts[count(parts) - 1])
97
- }
98
-
99
- has_expected_cron if {
100
- gha_on.schedule[_].cron == expected_cron
101
- }
102
-
103
- perms_match(actual, expected) if {
104
- actual.actions == expected.actions
105
- actual.contents == expected.contents
106
- }
107
-
108
- step0_with_canonical if {
109
- step0.with.token == expected_step0.with.token
110
- step0.with.save_period == expected_step0.with.save_period
111
- step0.with.save_min_runs_number == expected_step0.with.save_min_runs_number
112
- }
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "policy": {
4
- "files": {
5
- "single": ".github/workflows/clean-ga-workflows.yml",
6
- "required": true
7
- },
8
- "missingMessage": ".github/workflows/clean-ga-workflows.yml не існує — створи за каноном ga.mdc"
9
- }
10
- }
@@ -1,33 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-clean_ga_workflows.mjs
4
- resource: npm/rules/ga/clean_ga_workflows/fix-clean_ga_workflows.mjs
5
- docgen:
6
- crc: e09e62cc
7
- model: openai-codex/gpt-5.5
8
- tier: cloud-avg
9
- score: 100
10
- issues: judge:inaccurate:0.99
11
- judgeModel: openai-codex/gpt-5.4-mini
12
- ---
13
-
14
- ## Огляд
15
-
16
- `patterns` визначає, які шляхи правило свідомо не розглядає як цільові для перевірки: `.github` і `.git` пропускаються, щоб службові каталоги репозиторію не змішувалися з робочим кодом.
17
-
18
- Правило працює read-only: не записує у файлову систему чи бази даних.
19
-
20
- ## Поведінка
21
-
22
- 1. `patterns` визначає правило автоматичного виправлення для workflow очищення GitHub Actions.
23
-
24
- 2. Правило гарантує наявність очікуваного файлу `.github/workflows/clean-ga-workflows.yml` на основі затвердженого шаблону.
25
-
26
- 3. Виправлення працює як read-only опис дії: саме по собі не записує зміни у файлову систему чи зовнішні сховища.
27
-
28
- 4. Під час застосування загальних перевірок свідомо не розглядаються службові шляхи `.github` і `.git`, щоб не аналізувати інфраструктурні каталоги як звичайний код.
29
-
30
- ## Гарантії поведінки
31
-
32
- - Read-only: не виконує операцій запису (ФС/БД).
33
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,9 +0,0 @@
1
- ---
2
- type: Directory Index
3
- title: npm/rules/ga/clean_ga_workflows
4
- resource: npm/rules/ga/clean_ga_workflows/
5
- ---
6
-
7
- | Файл | Тип |
8
- | ------------------------------------------------------- | --------- |
9
- | [fix-clean_ga_workflows.mjs](fix-clean_ga_workflows.md) | JS Module |
@@ -1,8 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [
4
- createTemplateFixPattern({
5
- id: 'ga-clean_ga_workflows-template',
6
- targetPath: '.github/workflows/clean-ga-workflows.yml'
7
- })
8
- ]
@@ -1,26 +0,0 @@
1
- name: Clean action for removing completed workflow runs
2
-
3
- on:
4
- schedule:
5
- - cron: '0 1 16 * *'
6
-
7
- # Allow workflow to be manually run from the GitHub UI
8
- workflow_dispatch: {}
9
-
10
- concurrency:
11
- group: ${{ github.ref }}-${{ github.workflow }}
12
- cancel-in-progress: true
13
-
14
- jobs:
15
- cleanup_old_workflows:
16
- runs-on: ubuntu-latest
17
- permissions:
18
- actions: write
19
- contents: read
20
- steps:
21
- - name: Delete workflow runs
22
- uses: dmvict/clean-workflow-runs@v1
23
- with:
24
- token: ${{ github.token }}
25
- save_period: 31
26
- save_min_runs_number: 0
@@ -1,24 +0,0 @@
1
- ## Структура `clean-merged-branch.yml` — видалення злитих гілок
2
-
3
- Rego-пакет: `ga.clean_merged_branch`
4
-
5
- **Цільовий файл:** `.github/workflows/clean-merged-branch.yml`
6
-
7
- ### Що перевіряється
8
-
9
- - `name` — точна відповідність значенню з template
10
- - `on.schedule[].cron` — має містити cron із template (`0 1 15 * *`)
11
- - `on.workflow_dispatch` — має бути об'єктом `{}`
12
- - `jobs.cleanup_old_branches` — job має існувати
13
- - `jobs.cleanup_old_branches.permissions` — кожне поле із template (`contents: write`, `pull-requests: read`)
14
- - `steps` — має бути рівно 2 кроки
15
- - `steps[0].id` — `delete_stuff`; `steps[0].uses` — `phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3`
16
- - `steps[0].with.github_token`, `last_commit_age_days` — за template
17
- - `steps[0].with.ignore_branches` — має містити всі гілки з template (`main,dev`)
18
- - `steps[0].with.dry_run` — `no` (нормалізується: YAML 1.1 `no` → boolean `false` у conftest)
19
- - `steps[1].name` — за template; `steps[1].env.DELETED_BRANCHES` — за template
20
- - `steps[1].run` — містить `Deleted branches:` та `${DELETED_BRANCHES}`
21
-
22
- **Pin-aware `uses`:** SHA-пін того самого action (`owner/action@<40-hex SHA>`, з тег-коментарем `# vX` чи без) задовольняє канонічний тег із template. Якщо в репо вже SHA-пін (zizmor-політика ref-pin) — НЕ замінюй його тегом.
23
-
24
- Канон: [clean-merged-branch.yml.snippet.yml](./template/clean-merged-branch.yml.snippet.yml)
@@ -1,163 +0,0 @@
1
- # Перевірка `.github/workflows/clean-merged-branch.yml` (ga.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Структура --data сформована з template/clean-merged-branch.yml.snippet.yml.
5
- package ga.clean_merged_branch
6
-
7
- import rego.v1
8
-
9
- # ── Аліаси ─────────────────────────────────────────────────────────────────
10
-
11
- gha_on := input["true"]
12
-
13
- steps := input.jobs.cleanup_old_branches.steps
14
-
15
- step0 := steps[0]
16
-
17
- step1 := steps[1]
18
-
19
- expected_name := data.template.snippet.name
20
-
21
- expected_cron := data.template.snippet.on.schedule[0].cron
22
-
23
- expected_step0 := data.template.snippet.jobs.cleanup_old_branches.steps[0]
24
-
25
- expected_step1 := data.template.snippet.jobs.cleanup_old_branches.steps[1]
26
-
27
- expected_perms := data.template.snippet.jobs.cleanup_old_branches.permissions
28
-
29
- # ── deny rules ─────────────────────────────────────────────────────────────
30
-
31
- deny contains msg if {
32
- input.name != expected_name
33
- msg := sprintf("clean-merged-branch.yml: name має бути %q (ga.mdc)", [expected_name])
34
- }
35
-
36
- deny contains msg if {
37
- not has_expected_cron
38
- msg := sprintf("clean-merged-branch.yml: on.schedule має містити cron: '%s' (ga.mdc)", [expected_cron])
39
- }
40
-
41
- deny contains msg if {
42
- not is_object(object.get(gha_on, "workflow_dispatch", null))
43
- msg := "clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)"
44
- }
45
-
46
- deny contains msg if {
47
- not input.jobs.cleanup_old_branches
48
- msg := "clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)"
49
- }
50
-
51
- deny contains msg if {
52
- some permission, expected in expected_perms
53
- object.get(input.jobs.cleanup_old_branches.permissions, permission, null) != expected
54
- msg := sprintf("clean-merged-branch.yml: permissions.%s має бути %s (ga.mdc)", [permission, expected])
55
- }
56
-
57
- deny contains msg if {
58
- count(steps) < 2
59
- msg := "clean-merged-branch.yml: steps має містити 2 кроки як у template (ga.mdc)"
60
- }
61
-
62
- # ── Step 0 (delete_stuff) ──────────────────────────────────────────────────
63
-
64
- deny contains msg if {
65
- step0.id != expected_step0.id
66
- msg := sprintf("clean-merged-branch.yml: перший крок має id: %s (ga.mdc)", [expected_step0.id])
67
- }
68
-
69
- deny contains msg if {
70
- actual := step0.uses
71
- not uses_satisfies(actual, expected_step0.uses)
72
- msg := sprintf("clean-merged-branch.yml: перший крок має uses: %s (ga.mdc)", [expected_step0.uses])
73
- }
74
-
75
- deny contains msg if {
76
- step0.with.github_token != expected_step0.with.github_token
77
- msg := sprintf(
78
- "clean-merged-branch.yml: with.github_token має бути %s (ga.mdc)",
79
- [expected_step0.with.github_token],
80
- )
81
- }
82
-
83
- deny contains msg if {
84
- step0.with.last_commit_age_days != expected_step0.with.last_commit_age_days
85
- msg := sprintf(
86
- "clean-merged-branch.yml: with.last_commit_age_days має бути %d (ga.mdc)",
87
- [expected_step0.with.last_commit_age_days],
88
- )
89
- }
90
-
91
- deny contains msg if {
92
- not ignore_branches_subset
93
- msg := sprintf(
94
- "clean-merged-branch.yml: with.ignore_branches має містити %s (ga.mdc)",
95
- [expected_step0.with.ignore_branches],
96
- )
97
- }
98
-
99
- # YAML 1.1 quirk: `dry_run: no` парситься як boolean false у Go-yaml (conftest).
100
- # Template (від `yaml` npm) читає `no` як рядок, тому нормалізуємо обидві форми.
101
- deny contains msg if {
102
- normalize_dry_run(step0.with.dry_run) != normalize_dry_run(expected_step0.with.dry_run) # noqa: rules-style-no-equality-with-false
103
- msg := "clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)"
104
- }
105
-
106
- # ── Step 1 (Get output) ────────────────────────────────────────────────────
107
-
108
- deny contains msg if {
109
- step1.name != expected_step1.name
110
- msg := sprintf("clean-merged-branch.yml: другий крок має name: %s (ga.mdc)", [expected_step1.name])
111
- }
112
-
113
- deny contains msg if {
114
- step1.env.DELETED_BRANCHES != expected_step1.env.DELETED_BRANCHES
115
- msg := "clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у template (ga.mdc)"
116
- }
117
-
118
- deny contains msg if {
119
- not echo_deleted_branches
120
- msg := "clean-merged-branch.yml: run має echo Deleted branches як у template (ga.mdc)"
121
- }
122
-
123
- # ── helpers ────────────────────────────────────────────────────────────────
124
-
125
- # `uses:` з input задовольняє канонічний `owner/action@tag`: точний збіг…
126
- uses_satisfies(actual, expected) if actual == expected
127
-
128
- # …або той самий action-slug і ref — повний 40-hex commit SHA (zizmor ref-pin).
129
- # SHA-пін відповідної версії задовольняє вимогу тега — не даунгрейдимо до тега.
130
- uses_satisfies(actual, expected) if {
131
- slug := split(expected, "@")[0]
132
- startswith(actual, concat("", [slug, "@"]))
133
- parts := split(actual, "@")
134
- regex.match(`^[0-9a-fA-F]{40}$`, parts[count(parts) - 1])
135
- }
136
-
137
- has_expected_cron if {
138
- gha_on.schedule[_].cron == expected_cron
139
- }
140
-
141
- # Кожна гілка з template-літералу (через кому) має бути присутня у actual.
142
- ignore_branches_subset if {
143
- required_branches := split(expected_step0.with.ignore_branches, ",")
144
- actual := step0.with.ignore_branches
145
- every b in required_branches {
146
- contains(actual, trim_space(b))
147
- }
148
- }
149
-
150
- echo_deleted_branches if {
151
- # Звіряємо substring "echo "Deleted branches: …${DELETED_BRANCHES}…"" — формується з template run.
152
- contains(step1.run, "Deleted branches:")
153
- contains(step1.run, "${DELETED_BRANCHES}")
154
- }
155
-
156
- normalize_dry_run(false) := false
157
-
158
- normalize_dry_run(value) := false if lower(sprintf("%v", [value])) == "no"
159
-
160
- normalize_dry_run(value) := value if {
161
- value != false # noqa: equals-pattern-matching
162
- lower(sprintf("%v", [value])) != "no"
163
- }
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@7n/rules/schemas/concern.json",
3
- "policy": {
4
- "files": {
5
- "single": ".github/workflows/clean-merged-branch.yml",
6
- "required": true
7
- },
8
- "missingMessage": ".github/workflows/clean-merged-branch.yml не існує — створи за каноном ga.mdc"
9
- }
10
- }
@@ -1,27 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: fix-clean_merged_branch.mjs
4
- resource: npm/rules/ga/clean_merged_branch/fix-clean_merged_branch.mjs
5
- docgen:
6
- crc: c38d9d28
7
- model: openai-codex/gpt-5.4-mini
8
- score: 100
9
- issues: judge:inaccurate:0.98
10
- judgeModel: openai-codex/gpt-5.4-mini
11
- ---
12
-
13
- ## Огляд
14
-
15
- `patterns` звіряє шаблонну конфігурацію з відповідними файлами репозиторію, щоб підтримувати узгодженість між шаблоном і робочим станом. Працює лише на читання, не торкається `.github` і `.git`, і не змінює файлову систему чи базу даних.
16
-
17
- ## Поведінка
18
-
19
- 1. `patterns` формує набір правил для узгодження шаблонної конфігурації з цільовим workflow-файлом `.github/workflows/clean-merged-branch.yml`.
20
- 2. `patterns` використовує це правило як частину автоматизованого вирівнювання між шаблоном і робочим репозиторієм, щоб підтримувати однакову поведінку в потрібному workflow.
21
- 3. `patterns` свідомо не охоплює шляхи `.github` і `.git`, щоб не зачіпати службові та внутрішні дані репозиторію.
22
- 4. `patterns` працює read-only: лише описує, що треба зіставити, і не записує зміни у файлову систему чи базу даних.
23
-
24
- ## Гарантії поведінки
25
-
26
- - Read-only: не виконує операцій запису (ФС/БД).
27
- - Свідомо пропускає шляхи: `.github`, `.git`.
@@ -1,9 +0,0 @@
1
- ---
2
- type: Directory Index
3
- title: npm/rules/ga/clean_merged_branch
4
- resource: npm/rules/ga/clean_merged_branch/
5
- ---
6
-
7
- | Файл | Тип |
8
- | --------------------------------------------------------- | --------- |
9
- | [fix-clean_merged_branch.mjs](fix-clean_merged_branch.md) | JS Module |
@@ -1,8 +0,0 @@
1
- import { createTemplateFixPattern } from '../../../scripts/lib/fix/template-deep-merge.mjs'
2
-
3
- export const patterns = [
4
- createTemplateFixPattern({
5
- id: 'ga-clean_merged_branch-template',
6
- targetPath: '.github/workflows/clean-merged-branch.yml'
7
- })
8
- ]