@nitra/cursor 12.3.3 → 12.5.0

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 (52) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/package.json +1 -1
  3. package/rules/bun/bun.mdc +2 -2
  4. package/rules/bun/js/layout.mjs +1 -139
  5. package/rules/bun/policy/package_json/package_json.rego +1 -12
  6. package/rules/docker/docker.mdc +3 -7
  7. package/rules/docker/lint/docs/lint.md +3 -3
  8. package/rules/docker/meta.json +1 -1
  9. package/rules/docker/policy/lint_docker_yml/lint_docker_yml.rego +1 -1
  10. package/rules/docker/policy/lint_docker_yml/template/lint-docker.yml.snippet.yml +1 -1
  11. package/rules/docker/policy/package_json/package_json.rego +3 -3
  12. package/rules/docker/policy/package_json/template/package.json.snippet.json +1 -1
  13. package/rules/image-avif/image-avif.mdc +1 -1
  14. package/rules/image-avif/js/avif_generation.mjs +1 -1
  15. package/rules/image-avif/js/docs/avif_generation.md +1 -1
  16. package/rules/image-compress/image-compress.mdc +4 -5
  17. package/rules/image-compress/js/docs/index.md +1 -0
  18. package/rules/image-compress/js/docs/lint.md +24 -0
  19. package/rules/image-compress/js/docs/package_setup.md +1 -1
  20. package/rules/image-compress/js/lint.mjs +78 -0
  21. package/rules/image-compress/meta.json +1 -1
  22. package/rules/image-compress/policy/package_json/package_json.rego +2 -34
  23. package/rules/k8s/js/lint.mjs +14 -0
  24. package/rules/k8s/k8s.mdc +2 -12
  25. package/rules/k8s/lint/docs/lint.md +2 -4
  26. package/rules/k8s/meta.json +1 -1
  27. package/rules/lint/js/docs/orchestrate.md +3 -3
  28. package/rules/lint/js/orchestrate.mjs +22 -4
  29. package/rules/php/js/docs/index.md +1 -0
  30. package/rules/php/js/docs/lint.md +20 -0
  31. package/rules/php/js/lint.mjs +15 -0
  32. package/rules/php/lint/lint.mjs +3 -2
  33. package/rules/php/meta.json +1 -1
  34. package/rules/php/php.mdc +1 -3
  35. package/rules/php/policy/lint_php_yml/template/lint-php.yml.snippet.yml +1 -1
  36. package/rules/python/policy/lint_python_yml/template/lint-python.yml.snippet.yml +1 -1
  37. package/rules/python/python.mdc +1 -3
  38. package/rules/rust/js/docs/index.md +1 -0
  39. package/rules/rust/js/docs/lint.md +21 -0
  40. package/rules/rust/js/lint.mjs +79 -0
  41. package/rules/rust/meta.json +1 -1
  42. package/rules/rust/rust.mdc +2 -4
  43. package/rules/image-compress/policy/package_json/template/package.json.contains.json +0 -5
  44. package/rules/php/policy/package_json/package_json.rego +0 -16
  45. package/rules/php/policy/package_json/target.json +0 -4
  46. package/rules/php/policy/package_json/template/package.json.contains.json +0 -5
  47. package/rules/python/policy/package_json/package_json.rego +0 -16
  48. package/rules/python/policy/package_json/target.json +0 -4
  49. package/rules/python/policy/package_json/template/package.json.contains.json +0 -5
  50. package/rules/rust/policy/package_json/package_json.rego +0 -18
  51. package/rules/rust/policy/package_json/target.json +0 -5
  52. package/rules/rust/policy/package_json/template/package.json.contains.json +0 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.5.0] - 2026-06-21
4
+
5
+ ### Changed
6
+
7
+ - Уніфіковано image-compress, docker і k8s lint через n-cursor lint <rule> без package.json wrappers; image-compress read-only використовує @nitra/minify-image --json.
8
+
9
+ ## [12.4.0] - 2026-06-21
10
+
11
+ ### Changed
12
+
13
+ - Уніфіковано selection linter-фази: unscoped n-cursor lint бере правила з .n-cursor.json, а meta.json#lint лишається тільки scope-класифікацією.
14
+
3
15
  ## [12.3.3] - 2026-06-20
4
16
 
5
17
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.3.3",
3
+ "version": "12.5.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
package/rules/bun/bun.mdc CHANGED
@@ -68,8 +68,8 @@ FROM oven/bun:alpine AS build-env
68
68
 
69
69
  Лінт запускається через CLI **`n-cursor`**, **не** через `package.json`-скрипти:
70
70
 
71
- - **`n-cursor lint --full`** — весь репо: усі правила (per-file лінтери + конформність) + `oxfmt` у кінці (fix-режим);
72
- - **`n-cursor lint`** — дельта vs origin (per-file лінтери лише змінених файлів);
71
+ - **`n-cursor lint --full`** — весь репо: активні у `.n-cursor.json` правила (per-file + full лінтери за `meta.json#lint` scope, конформність) + `oxfmt` у кінці (fix-режим);
72
+ - **`n-cursor lint`** — дельта vs origin (активні у `.n-cursor.json` per-file лінтери лише змінених файлів);
73
73
  - **`n-cursor lint <rule…>`** — конкретні правила (лінтер + конформність), напр. **`n-cursor lint ga`**.
74
74
 
75
75
  У кореневому `package.json` **не повинно бути** `lint`/`lint-*` скриптів — єдина точка лінту — CLI `n-cursor`. У CI кожен workflow викликає **`n-cursor lint <rule> --read-only`** напряму (без обгорток).
@@ -1,151 +1,19 @@
1
1
  /** @see ./docs/layout.md */
2
2
  import { existsSync } from 'node:fs'
3
- import { readFile } from 'node:fs/promises'
4
3
  import { join } from 'node:path'
5
4
 
6
5
  import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
7
6
 
8
- /** Розділювач токенів у `scripts.lint` (послідовність пробільних символів). */
9
- const WHITESPACE_RE = /\s+/u
10
-
11
7
  // Перевірка `devDependencies` кореневого `package.json` (дозволено лише `@nitra/*`)
12
8
  // — у rego (`npm/policy/bun/package_json/`). JS-копії `isAllowedRootDevDependency`
13
9
  // видалено, щоб не було двох джерел істини.
14
10
 
15
- /**
16
- * Зчитує `rules` та `disable-rules` з `.n-cursor.json`.
17
- * @param {string} cwd корінь репозиторію
18
- * @returns {Promise<{ rules: Set<string>, disabled: Set<string> }>} активні правила і явно вимкнені
19
- */
20
- async function loadNCursorRules(cwd) {
21
- const empty = { rules: new Set(), disabled: new Set() }
22
- const cfgPath = join(cwd, '.n-cursor.json')
23
- if (!existsSync(cfgPath)) return empty
24
- try {
25
- const raw = JSON.parse(await readFile(cfgPath, 'utf8'))
26
- const list = Array.isArray(raw?.rules) ? raw.rules.map(String) : []
27
- const disabled = Array.isArray(raw?.['disable-rules']) ? raw['disable-rules'].map(String) : []
28
- return { rules: new Set(list), disabled: new Set(disabled) }
29
- } catch {
30
- return empty
31
- }
32
- }
33
-
34
- /**
35
- * Чи містить `scripts.lint` виклик `bun run <script>` у chain'і. Шукаємо саме `bun run <script>`
36
- * як окремий токен (між пробілами/`&&`), щоб уникнути false-positive на префіксах
37
- * (`bun run lint-k8s-foo` не матчиться як `bun run lint-k8s`).
38
- * @param {string} lintScript значення `scripts.lint` (порожній рядок — якщо нема)
39
- * @param {string} target ім'я скрипта (без префіксів)
40
- * @returns {boolean} true, якщо chain згадує `bun run <target>`
41
- */
42
- function lintChainHasScript(lintScript, target) {
43
- if (!lintScript) return false
44
- const tokens = lintScript.split(WHITESPACE_RE)
45
- return tokens.some((tok, i) => tok === 'bun' && tokens[i + 1] === 'run' && tokens[i + 2] === target)
46
- }
47
-
48
- /**
49
- * Описує `lint-<id>`-обгортку та правила, що нею володіють. Один скрипт може мати кілька
50
- * власників (`lint-image` — обслуговує і `image-avif`, і `image-compress`); скрипт вважається
51
- * «потрібним», якщо **хоч одне** з власних правил активне у `.n-cursor.json:rules`.
52
- * @typedef {object} RuleScript
53
- * @property {string[]} rules id правил-власників (>=1); скрипт зобов'язаний існувати, поки активне хоч одне з них
54
- * @property {string} script ім'я скрипта в `package.json:scripts`
55
- * @property {string} doc `.mdc`-файл (або кома-список), на який посилається повідомлення check-у
56
- */
57
-
58
- /** @type {RuleScript[]} */
59
- const RULE_SCRIPTS = [
60
- { rules: ['docker'], script: 'lint-docker', doc: 'docker.mdc' },
61
- { rules: ['k8s'], script: 'lint-k8s', doc: 'k8s.mdc' },
62
- { rules: ['image-avif', 'image-compress'], script: 'lint-image', doc: 'image-avif.mdc / image-compress.mdc' }
63
- ]
64
-
65
- /**
66
- * Загортає кожен ідентифікатор у backticks та зʼєднує через роздільник. Винесено
67
- * окремою функцією, щоб не нестити template literals у `pass`/`fail`-повідомленнях.
68
- * @param {string[]} items ідентифікатори правил
69
- * @param {string} sep роздільник (наприклад `, ` або `/`)
70
- * @returns {string} рядок виду "`a`, `b`"
71
- */
72
- function backtickJoin(items, sep) {
73
- return items.map(r => '`' + r + '`').join(sep)
74
- }
75
-
76
- /**
77
- * Описує стан правил-власників скрипта для повідомлень про reason. Повертає або список увімкнених
78
- * правил (для passing-кейсу «правило є»), або компактний опис, чому всі вимкнені (для inverse-fail).
79
- * @param {string[]} owners id правил-власників (>=1)
80
- * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
81
- * @returns {{ enabled: string[], reason: string }} `enabled` — список з `cursorRules.rules`; `reason` — текст для лога
82
- */
83
- function ownerStatus(owners, cursorRules) {
84
- const enabled = owners.filter(r => cursorRules.rules.has(r))
85
- if (enabled.length > 0) {
86
- return { enabled, reason: `правил${enabled.length === 1 ? 'о' : 'а'} ${backtickJoin(enabled, ', ')}` }
87
- }
88
- if (owners.length === 1) {
89
- const [only] = owners
90
- const where = cursorRules.disabled.has(only) ? 'в disable-rules' : 'відсутнє в rules'
91
- return { enabled, reason: `правило \`${only}\` ${where}` }
92
- }
93
- const disabledCount = owners.filter(r => cursorRules.disabled.has(r)).length
94
- const note = disabledCount === owners.length ? 'усі власники в disable-rules' : 'жоден власник не активний у rules'
95
- return { enabled, reason: `${backtickJoin(owners, '/')} — ${note}` }
96
- }
97
-
98
- /**
99
- * Перевіряє двосторонній зв'язок `rules` ↔ `scripts.lint-<id>` для правил із `lint-<id>`-обгорткою
100
- * (див. `RULE_SCRIPTS`). Якщо активне хоч одне правило-власник — скрипт мусить існувати; якщо
101
- * жодне з власників не активне (відсутнє у `rules` або є в `disable-rules`), скрипту і згадки
102
- * `bun run <script>` у `scripts.lint` бути **не може**. Інакше `bun run lint` падатиме на
103
- * вимкненому правилі: `n-cursor lint-<id>` ігнорує `.n-cursor.json` і обходить дерево
104
- * незалежно від конфігу (як було в cursor-репо: `disable-rules: ["k8s"]` + залишений `lint-k8s`
105
- * ламав chain на template-сорцях власного правила).
106
- * @param {{ pass: (msg: string) => void, fail: (msg: string) => void }} reporter callback-и `pass`/`fail` для звіту
107
- * @param {Record<string, string>} scripts scripts з package.json
108
- * @param {{ rules: Set<string>, disabled: Set<string> }} cursorRules `rules` та `disable-rules`
109
- */
110
- function checkCursorRuleScripts(reporter, scripts, cursorRules) {
111
- const { pass, fail } = reporter
112
- const lintScript = typeof scripts.lint === 'string' ? scripts.lint : ''
113
- for (const { rules: owners, script, doc } of RULE_SCRIPTS) {
114
- const status = ownerStatus(owners, cursorRules)
115
- const present = Boolean(scripts[script])
116
- const inChain = lintChainHasScript(lintScript, script)
117
- if (status.enabled.length > 0) {
118
- if (present) {
119
- pass(`package.json: є \`${script}\` (${status.reason} у .n-cursor.json)`)
120
- } else {
121
- fail(
122
- `У .n-cursor.json увімкнено ${status.reason} — додай скрипт \`${script}\` у кореневий package.json (див. ${doc})`
123
- )
124
- }
125
- continue
126
- }
127
- if (present) {
128
- fail(
129
- `У .n-cursor.json немає активних власників ${backtickJoin(owners, '/')} — прибери скрипт \`${script}\` з кореневого package.json (див. ${doc})`
130
- )
131
- }
132
- if (inChain) {
133
- fail(
134
- `У \`scripts.lint\` є \`bun run ${script}\`, але серед \`${owners.join('/')}\` жоден не активний у .n-cursor.json — прибери з ланцюжка lint (див. ${doc})`
135
- )
136
- }
137
- if (!present && !inChain) {
138
- pass(`package.json: \`${script}\` відсутній (${status.reason})`)
139
- }
140
- }
141
- }
142
-
143
11
  /**
144
12
  * Перевіряє відповідність проєкту правилам bun.mdc
145
13
  * @param {string} [cwd] корінь репозиторію
146
14
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
147
15
  */
148
- export async function check(cwd = process.cwd()) {
16
+ export function check(cwd = process.cwd()) {
149
17
  const reporter = createCheckReporter()
150
18
  const { pass, fail } = reporter
151
19
 
@@ -174,17 +42,11 @@ export async function check(cwd = process.cwd()) {
174
42
  fail('Відсутній bunfig.toml — створи з [install] linker = "hoisted" (bun.mdc)')
175
43
  }
176
44
 
177
- const cursorRules = await loadNCursorRules(cwd)
178
-
179
45
  const pkgPath = join(cwd, 'package.json')
180
46
  if (!existsSync(pkgPath)) {
181
47
  fail('Відсутній package.json у корені')
182
48
  return reporter.getExitCode()
183
49
  }
184
50
 
185
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
186
- const scripts = pkg.scripts && typeof pkg.scripts === 'object' ? pkg.scripts : {}
187
- checkCursorRuleScripts(reporter, scripts, cursorRules)
188
-
189
51
  return reporter.getExitCode()
190
52
  }
@@ -9,9 +9,7 @@
9
9
  # (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
10
10
  # `devDependencies` за `npm-module.mdc`)
11
11
  #
12
- # Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
13
- # - `lint-docker` / `lint-k8s` коли `.n-cursor.json:rules` містить відповідне
14
- # правило (потрібен другий файл-вхід — у Rego без `--combine` не зробити).
12
+ # Перевірки, які потребують FS / cross-file контексту, лишаються у JS.
15
13
  package bun.package_json
16
14
 
17
15
  import rego.v1
@@ -47,12 +45,3 @@ allowed_root_dev_dependency(name) if {
47
45
  # не мають `devDependencies` (`npm-module.mdc`).
48
46
  name in allowed_root_test_deps
49
47
  }
50
-
51
- lint_prefixed_scripts := [name |
52
- some name, _ in object.get(input, "scripts", {})
53
- startswith(name, "lint-")
54
- ]
55
-
56
- default lint_script := ""
57
-
58
- lint_script := input.scripts.lint if is_string(input.scripts.lint)
@@ -182,15 +182,13 @@ RUN find ./ -type f -name "*.js" -exec gzip -k {} \;
182
182
 
183
183
  ## lint-docker
184
184
 
185
- CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить CLI **`n-cursor lint-docker`** (реалізація — **`npm/rules/docker/js/run.mjs`**).
185
+ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE...]` у **`hadolint --help`**); обхід репозиторію робить правило **`n-cursor lint docker`** (реалізація — **`npm/rules/docker/js/lint.mjs`**).
186
186
 
187
187
  **Область lint-docker (вужча, ніж `check docker`):** лише файли з іменем **`Dockerfile`** та **`*.Dockerfile`** (суфікс **`.dockerfile`** без урахування регістру, наприклад **`api.Dockerfile`**). Файли **`Dockerfile.prod`**, **`Containerfile`** тощо **не** входять у **`lint-docker`**; їх ловить **`check docker`** (`rules/docker/fix.mjs`).
188
188
 
189
189
  Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`rules/docker/fix.mjs`**. Виклик **`hadolint`** як **нативного бінарника** через **`ensureTool`** (PATH → кеш → авто-install brew/scoop/GitHub Release; **без** `docker run`) — спільна логіка **`npm/rules/docker/lib/docker-hadolint.mjs`**.
190
190
 
191
- - Канон `package.json#scripts.lint-docker`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
192
-
193
- Якщо правило **`docker`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **обов'язково** мають бути скрипт **`lint-docker`** і виклик **`bun run lint-docker`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor fix bun`**.
191
+ Окремий `package.json`-скрипт `lint-docker` не потрібен і не перевіряється — єдина точка входу для правила: **`n-cursor lint docker`**.
194
192
 
195
193
  Додай workflow **`.github/workflows/lint-docker.yml`** (гілки **`dev`** і **`main`**, лише **`.yml`**, узгоджено з **`ga.mdc`**):
196
194
 
@@ -198,11 +196,9 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
198
196
 
199
197
  Локально hadolint авто-встановлюється через **`ensureTool`** (latest, без піну версії). У CI встанови його кроком із workflow-сніпета (curl-download бінарника — без `docker run`).
200
198
 
201
- Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) **обов'язково** містить **`bun run lint-docker`**, коли в проєкті підключено правило **`docker`**.
202
-
203
199
  ## Запуск
204
200
 
205
- 1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
201
+ 1. **`n-cursor lint docker`** — **`js/lint.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI використовуй **`n-cursor lint docker --read-only`** і встанови hadolint (приклад у workflow).
206
202
  2. **`npx @nitra/cursor fix docker`** — **`rules/docker/fix.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (нативний бінарник через **`ensureTool`**; **без** `docker run`).
207
203
  3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
208
204
 
@@ -148,9 +148,9 @@ if (isRunAsCli(import.meta.url)) {
148
148
 
149
149
  ## Потік виконання / Використання
150
150
 
151
- ### Послідовність дій при `n-cursor lint-docker`
151
+ ### Послідовність дій при `n-cursor lint docker`
152
152
 
153
- 1. `bin/n-cursor.js` диспатчить підкоманду `lint-docker` на `runLintDocker` із цього модуля.
153
+ 1. `bin/n-cursor.js` диспатчить правило `docker` на `npm/rules/docker/js/lint.mjs`, яке використовує `runLintDocker` із цього модуля.
154
154
  2. `runLintDocker` → `runStandardLint(import.meta.dirname, runLintDockerSteps)`:
155
155
  - бере серіалізаційний лок на ім’я `lint-docker`;
156
156
  - перевіряє стан git-дерева; якщо стан збігається з попереднім успішним прогоном — крок може бути пропущено (дедуп);
@@ -167,7 +167,7 @@ if (isRunAsCli(import.meta.url)) {
167
167
 
168
168
  ### Як це використовується ззовні
169
169
 
170
- - **CLI:** `bun run n-cursor lint-docker` (або відповідний bin-скрипт) — основний сценарій.
170
+ - **CLI:** `n-cursor lint docker` — основний сценарій.
171
171
  - **Програмно з інших скриптів:**
172
172
 
173
173
  ```js
@@ -1 +1 @@
1
- { "auto": { "glob": ["**/Dockerfile", "**/Dockerfile.*"] } }
1
+ { "auto": { "glob": ["**/Dockerfile", "**/Dockerfile.*"] }, "lint": "full" }
@@ -3,7 +3,7 @@
3
3
  # Канон надходить через --data: { "template": { "snippet": ... } }
4
4
  # Структура --data сформована з template/lint-docker.yml.snippet.yml.
5
5
  # Path-маркери, hadolint версія (substring "v2.12.0" у run), setup-bun-deps
6
- # composite, `bun run lint-docker` substring — все читається з template's snippet.
6
+ # composite, `n-cursor lint docker --read-only` substring — все читається з template's snippet.
7
7
  # Універсальні workflow-перевірки — у `ga.workflow_common`.
8
8
  package docker.lint_docker_yml
9
9
 
@@ -38,4 +38,4 @@ jobs:
38
38
  - uses: ./.github/actions/setup-bun-deps
39
39
 
40
40
  - name: Lint Docker
41
- run: bun run lint-docker
41
+ run: n-cursor lint docker --read-only
@@ -1,9 +1,9 @@
1
1
  # Перевірка `package.json` (docker.mdc).
2
2
  #
3
3
  # Канон надходить через --data: { "template": { "snippet": ... } }
4
- # Перевіряє ЛИШЕ зміст значення `scripts.lint-docker`, якщо ключ присутній.
5
- # Умовну обовʼязковість (правило `docker` у `.n-cursor.json` `scripts.lint-docker`
6
- # зобовʼязаний існувати) перевіряє `rules/bun/fix.mjs` через cross-file логіку.
4
+ # Backward-compatible: перевіряє ЛИШЕ зміст значення `scripts.lint-docker`, якщо ключ
5
+ # присутній у старому проєкті. Нові проєкти використовують `n-cursor lint docker`
6
+ # напряму, без package.json wrapper.
7
7
  package docker.package_json
8
8
 
9
9
  import rego.v1
@@ -1 +1 @@
1
- { "scripts": { "lint-docker": "n-cursor lint-docker" } }
1
+ { "scripts": { "lint-docker": "n-cursor lint docker" } }
@@ -5,7 +5,7 @@ globs: "**/*.{png,jpg,jpeg,gif,avif,vue,html}"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
- AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor fix image-avif` у `lint-image` прапорець `--avif` заборонений (це валідує правило `image-compress`). Перевірка робить чотири кроки в порядку:
8
+ AVIF-двійники (`<name>.<ext>.avif`) генерує **виключно** `npx @nitra/cursor fix image-avif`. Правило `image-compress` відповідає лише за стиснення початкових raster/SVG-файлів через `@nitra/minify-image`, а AVIF лишається окремим правилом. Перевірка робить чотири кроки в порядку:
9
9
 
10
10
  1. **Pre-scan**: шукає у `.vue`/`.html` хоча б одне raster-посилання, яке потенційно треба переписати на AVIF-двійник (`import x from '...png'` або `<img src="...png" />`). Пакети з opt-out `disable-avif: true` пропускаються. **Якщо жодного raster-посилання не знайдено — `check image-avif` завершується успіхом одразу: ні `npx --avif`, ні rewrite, ні cleanup-сиріт не виконуються** (нічого було б змінювати). Так уникаємо дорогого `npx @nitra/minify-image` у проєктах, де AVIF не вживається.
11
11
  2. Запускає `npx @nitra/minify-image --src=. --write --avif` (≥ **3.3.1**) — генерує `<name>.<ext>.avif` поряд з кожним PNG/JPEG/GIF. CLI порівнює sha1 кожного raster-сорсу зі збереженим у `.n-minify-image.tsv` і перезаписує `<source>.avif` при зміні оригіналу.
@@ -389,7 +389,7 @@ async function cleanupOrphanAvifs(usedAvifAbs, optedOutAbs, ignorePaths, cwd) {
389
389
 
390
390
  /**
391
391
  * Виконує AVIF-етап: запуск AVIF-генерації, авто-заміна raster-посилань у `.vue`/`.html`,
392
- * видалення AVIF-сиріт. Не валідує `package.json`/`lint-image` — це вже у `image-compress`.
392
+ * видалення AVIF-сиріт. Не валідує image-compress cache/dependency policy — це окреме правило.
393
393
  * @param {string} [cwd] корінь репозиторію
394
394
  * @returns {Promise<number>} 0 — все OK, 1 — є проблеми
395
395
  */
@@ -15,7 +15,7 @@ docgen:
15
15
  3. **Rewrite-пасс** — для кожного workspace-пакета (без opt-out) переписує raster-посилання у `.vue`/`.html` на `<...>.avif`, якщо двійник реально існує на диску. Якщо двійника немає (наприклад, оригіналу теж нема) — фейлить конкретне посилання.
16
16
  4. **Cleanup-пасс** — видаляє AVIF-сироти (`<...>.avif`, на які не лишилось жодного посилання у `.vue`/`.html` репозиторію), реалізуючи умову «AVIF лишається лише там, де заміна вдалася».
17
17
 
18
- Модуль свідомо не дублює перевірки скрипта `lint-image` (заборона `--avif` у ньому) — це залишається у правилі `image-compress`. Правило `image-avif` самостійне й вмикається лише там, де AVIF підтримується (адмінки), а не у публічних сайтах.
18
+ Модуль свідомо не дублює перевірки cache/dependency policy з правила `image-compress`. Правило `image-avif` самостійне й вмикається лише там, де AVIF підтримується (адмінки), а не у публічних сайтах.
19
19
 
20
20
  ## Експорти / API
21
21
 
@@ -1,20 +1,19 @@
1
1
  ---
2
- description: Оптимізація raster/SVG через @nitra/minify-image у локальному lint
2
+ description: Оптимізація raster/SVG через @nitra/minify-image у n-cursor lint
3
3
  version: '1.2'
4
4
  globs: "**/*.{png,jpg,jpeg,gif,svg}"
5
5
  alwaysApply: false
6
6
  ---
7
7
 
8
- CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **3.3.1**) запускається через `npx` і **не** додається в `dependencies` / `devDependencies`. Канонічний `lint-image` авто-оптимізація з прапорцем `--write`: стискає raster/SVG на місці. **AVIF-генерація (`--avif`) у `lint-image` заборонена** — її виконує окреме правило `image-avif`.
8
+ CLI [`@nitra/minify-image`](https://www.npmjs.com/package/@nitra/minify-image) (≥ **4.0.1**) запускається через `npx` і **не** додається в `dependencies` / `devDependencies`. Запуск — через **`n-cursor lint image-compress`**: локально (fix) виконує `npx @nitra/minify-image --src=. --write`, у `--read-only` виконує `npx @nitra/minify-image --src=. --json` і падає, якщо `summary.needsCompression > 0`. **AVIF-генерація (`--avif`) у `image-compress` не входить** — її виконує окреме правило `image-avif`.
9
9
 
10
- Перевірка лише локальна — у CI `lint-image` не запускаємо. Окремий workflow `lint-image.yml` створювати не треба.
10
+ Окремий workflow `lint-image.yml` створювати не треба; якщо потрібен CI-gate для зображень, використовуй `n-cursor lint image-compress --read-only`.
11
11
 
12
12
  ## `package.json`
13
13
 
14
- - Канон `lint-image` (substring requirements): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
15
14
  - Заборонені залежності `@nitra/minify-image` у deps/devDeps: [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
16
15
 
17
- Якщо в `package.json` уже є агрегований `lint`, додай у його ланцюжок `bun run lint-image` (як `bun run lint-text`, `bun run lint-js`, `bun run lint-ga`). Так розробник, що локально гонить `bun run lint`, перед фіксацією одразу бачить, чи зросли зображення.
16
+ Окремий `package.json`-скрипт `lint-image` не потрібен і не перевіряється єдина точка входу `n-cursor lint image-compress`.
18
17
 
19
18
  ## Кеш
20
19
 
@@ -8,4 +8,5 @@ resource: npm/rules/image-compress/js/
8
8
 
9
9
  | Файл | Тип |
10
10
  |---|---|
11
+ | [lint.mjs](lint.md) | JS Module |
11
12
  | [package_setup.mjs](package_setup.md) | JS Module |
@@ -0,0 +1,24 @@
1
+ ---
2
+ type: JS Module
3
+ title: lint.mjs
4
+ resource: npm/rules/image-compress/js/lint.mjs
5
+ ---
6
+
7
+ Адаптер підключає `@nitra/minify-image` до єдиної точки входу `n-cursor lint image-compress`.
8
+
9
+ ## Поведінка
10
+
11
+ 1. У fix-режимі запускає `npx @nitra/minify-image --src=. --write`.
12
+ 2. У read-only режимі запускає `npx @nitra/minify-image --src=. --json`.
13
+ 3. Парсить JSON-звіт і падає, якщо `summary.needsCompression > 0`.
14
+ 4. Повертає exit code дочірнього процесу або reporter exit code.
15
+
16
+ ## Публічний API
17
+
18
+ `lint` — оркестраторний entrypoint для правила `image-compress`.
19
+
20
+ ## Гарантії поведінки
21
+
22
+ - Read-only режим не стискає файли і не пише cache, покладаючись на `--json` detect-mode у `@nitra/minify-image`.
23
+ - Fix-режим делегує запис тільки `@nitra/minify-image --write`.
24
+ - За помилки запуску або невалідного JSON повертає ненульовий exit code.
@@ -18,7 +18,7 @@ docgen:
18
18
 
19
19
  ## Публічний API
20
20
 
21
- check — Перевіряє, чи відсутній у `.gitignore` файл `.n-minify-image.tsv` та чи видалено `.minify-image-cache.tsv`. Вказує, що CI-workflow для image не потрібен, оскільки лінтування зображень відбувається лише локально. (image-compress.mdc)
21
+ check — Перевіряє, чи відсутній у `.gitignore` файл `.n-minify-image.tsv` та чи видалено `.minify-image-cache.tsv`. Package-level стискання зображень виконує `n-cursor lint image-compress`, а read-only CI-gate використовує JSON detect-mode `@nitra/minify-image --json`. (image-compress.mdc)
22
22
 
23
23
  ## Гарантії поведінки
24
24
 
@@ -0,0 +1,78 @@
1
+ /** @see ./docs/lint.md */
2
+ import { spawnSync } from 'node:child_process'
3
+
4
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
5
+
6
+ const JSON_MAX_BUFFER = 20 * 1024 * 1024
7
+
8
+ /**
9
+ * Парсить JSON-звіт `@nitra/minify-image --json`.
10
+ * @param {string} stdout stdout дочірнього процесу
11
+ * @returns {{ summary?: { needsCompression?: unknown, total?: unknown } }} розпарсений звіт
12
+ */
13
+ function parseMinifyJson(stdout) {
14
+ return JSON.parse(stdout)
15
+ }
16
+
17
+ /**
18
+ * Read-only detect-mode для `image-compress`: `@nitra/minify-image --json` не стискає файли і не
19
+ * пише cache, але повідомляє скільки зображень не відповідають `.n-minify-image.tsv`.
20
+ * @param {string} cwd корінь
21
+ * @returns {number} exit code
22
+ */
23
+ function runJsonDetect(cwd) {
24
+ const reporter = createCheckReporter()
25
+ const { pass, fail } = reporter
26
+
27
+ const r = spawnSync('npx', ['@nitra/minify-image', '--src=.', '--json'], {
28
+ cwd,
29
+ encoding: 'utf8',
30
+ env: process.env,
31
+ maxBuffer: JSON_MAX_BUFFER
32
+ })
33
+ if (r.error) {
34
+ fail(`image-compress: не вдалося запустити npx @nitra/minify-image --json: ${r.error.message}`)
35
+ return reporter.getExitCode()
36
+ }
37
+ if (r.status !== 0) {
38
+ const detail = [r.stdout, r.stderr].filter(Boolean).join('\n').trim()
39
+ fail(`image-compress: @nitra/minify-image --json завершився з кодом ${r.status}${detail ? `:\n${detail}` : ''}`)
40
+ return reporter.getExitCode()
41
+ }
42
+
43
+ let report
44
+ try {
45
+ report = parseMinifyJson(r.stdout)
46
+ } catch {
47
+ fail('image-compress: @nitra/minify-image --json повернув невалідний JSON')
48
+ return reporter.getExitCode()
49
+ }
50
+
51
+ const needsCompression = Number(report.summary?.needsCompression ?? 0)
52
+ const total = Number(report.summary?.total ?? 0)
53
+ if (needsCompression > 0) {
54
+ fail(
55
+ `image-compress: ${needsCompression}/${total} image-файлів потребують стиснення — запусти \`n-cursor lint image-compress\` локально`
56
+ )
57
+ } else {
58
+ pass(`image-compress: ${total} image-файлів синхронізовані з .n-minify-image.tsv`)
59
+ }
60
+ return reporter.getExitCode()
61
+ }
62
+
63
+ /**
64
+ * Оркестраторний адаптер `n-cursor lint image-compress`.
65
+ * @param {string[] | undefined} _files ігнорується; `@nitra/minify-image` сам обходить дерево
66
+ * @param {string} [cwd] корінь
67
+ * @param {{ readOnly?: boolean }} [opts] readOnly → `--json`, fix → `--write`
68
+ * @returns {Promise<number>} exit code
69
+ */
70
+ export function lint(_files, cwd = process.cwd(), opts = {}) {
71
+ if (opts.readOnly === true) return Promise.resolve(runJsonDetect(cwd))
72
+ const r = spawnSync('npx', ['@nitra/minify-image', '--src=.', '--write'], { cwd, env: process.env, stdio: 'inherit' })
73
+ if (r.error) {
74
+ console.error(`image-compress: не вдалося запустити npx @nitra/minify-image --write: ${r.error.message}`)
75
+ return Promise.resolve(1)
76
+ }
77
+ return Promise.resolve(typeof r.status === 'number' ? r.status : 1)
78
+ }
@@ -1 +1 @@
1
- { "auto": { "glob": "**/*.{png,jpg,jpeg,gif,svg}" } }
1
+ { "auto": { "glob": "**/*.{png,jpg,jpeg,gif,svg}" }, "lint": "full" }
@@ -1,25 +1,11 @@
1
1
  # Перевірка `package.json` (image-compress.mdc).
2
2
  #
3
- # Канон надходить через --data: { "template": { "contains": ..., "deny": ... } }
4
- # Структура --data сформована з template/package.json.{contains,deny}.json.
5
- #
6
- # Логіка, що ЛИШАЄТЬСЯ у rego (inverse-patterns, не виносяться у template):
7
- # - `--avif` ЗАБОРОНЕНИЙ підрядок у `lint-image` (anti-contains);
8
- # - агрегатор `lint` (якщо `lint-image` присутній) має містити `bun run lint-image`.
3
+ # Канон надходить через --data: { "template": { "deny": ... } }.
4
+ # Структура --data сформована з template/package.json.deny.json.
9
5
  package image_compress.package_json
10
6
 
11
7
  import rego.v1
12
8
 
13
- # ── deny: scripts.<name> має містити кожен substring з template.contains ─
14
-
15
- deny contains msg if {
16
- some script_name, needles in data.template.contains.scripts
17
- actual := object.get(object.get(input, "scripts", {}), script_name, "")
18
- some needle in needles
19
- not contains(actual, needle)
20
- msg := sprintf("package.json: scripts.%s має містити %q (image-compress.mdc)", [script_name, needle])
21
- }
22
-
23
9
  # ── deny: top-level deps/devDeps з template.deny ─────────────────────────
24
10
 
25
11
  deny contains msg if {
@@ -33,21 +19,3 @@ deny contains msg if {
33
19
  pkg in object.keys(object.get(input, "devDependencies", {}))
34
20
  msg := sprintf("package.json: devDependencies.%s — %s", [pkg, reason])
35
21
  }
36
-
37
- # ── deny: `--avif` заборонений у `lint-image` (anti-contains, у rego) ────
38
-
39
- deny contains msg if {
40
- lint_image := object.get(object.get(input, "scripts", {}), "lint-image", "")
41
- contains(lint_image, "--avif")
42
- msg := "package.json: lint-image не має містити `--avif` — AVIF-генерацію виконує check image-avif (image-compress.mdc)"
43
- }
44
-
45
- # ── deny: агрегатор `lint` (якщо `lint-image` є) ─────────────────────────
46
-
47
- deny contains msg if {
48
- "lint-image" in object.keys(object.get(input, "scripts", {}))
49
- lint := object.get(object.get(input, "scripts", {}), "lint", "")
50
- lint != ""
51
- not contains(lint, "bun run lint-image")
52
- msg := "package.json: агрегований `lint` має містити `bun run lint-image` (image-compress.mdc)"
53
- }
@@ -0,0 +1,14 @@
1
+ /** @see ./docs/lint.md */
2
+ import { runLintK8s } from '../lint/lint.mjs'
3
+
4
+ /**
5
+ * Оркестраторний адаптер `n-cursor lint k8s` (лінтер-фаза): kubeconform + kubescape по деревах
6
+ * `.../k8s/*.yaml` через `runLintK8s` (read-only тули — мутацій немає, тож `opts` ігнорується).
7
+ * Структурні k8s.mdc-перевірки (manifest/kustomization/network_policy) — у конформність-фазі.
8
+ * Без `.../k8s`-маніфестів крок — no-op.
9
+ * @param {string[] | undefined} _files ігнорується (whole-repo обхід `.../k8s`)
10
+ * @returns {Promise<number>} exit code
11
+ */
12
+ export function lint(_files) {
13
+ return runLintK8s()
14
+ }
package/rules/k8s/k8s.mdc CHANGED
@@ -43,17 +43,7 @@ alwaysApply: false
43
43
 
44
44
  Підстав свою `attributes.name` (рядок або regex), якщо ConfigMap зветься інакше; виключай контрольно, а не глобально (не додавай винятки без `attributes.name`/`labels`, бо тоді C-0012 знімається для усіх ConfigMap-ів проєкту і реальні витоки credentials теж пройдуть).
45
45
 
46
- У репозиторії пакета **`@nitra/cursor`** скрипт **`lint-k8s`** делегує до CLI **`n-cursor lint-k8s`** (реалізація — **`npm/rules/k8s/js/run.mjs`**). У інших проєктах достатньо встановити **`@nitra/cursor`** у `devDependencies` бінарка **`n-cursor`** буде у **`node_modules/.bin/`**.
47
-
48
- ```json title="package.json"
49
- {
50
- "scripts": {
51
- "lint-k8s": "n-cursor lint-k8s"
52
- }
53
- }
54
- ```
55
-
56
- Якщо правило **`k8s`** підключено в **`.n-cursor.json`** (масив **`rules`**), у **кореневому** `package.json` **мають** бути скрипт **`lint-k8s`** і виклик **`bun run lint-k8s`** у агрегованому **`lint`** (див. **`bun.mdc`**). Це перевіряє **`npx @nitra/cursor fix bun`**.
46
+ Лінт запускається через правило **`n-cursor lint k8s`** (реалізація — **`npm/rules/k8s/js/lint.mjs`**, делегує kubeconform/kubescape у **`npm/rules/k8s/lint/lint.mjs`**). Окремий `package.json`-скрипт `lint-k8s` не потрібен і не перевіряється.
57
47
 
58
48
  Додай workflow **`.github/workflows/lint-k8s.yml`** (гілки **`dev`** і **`main`**, лише **`.yml`**, узгоджено з **`ga.mdc`**). **Не** дублюй **`setup-node`**, **`oven-sh/setup-bun`**, **`actions/cache`** і **`bun install`** у job — після **`checkout`** використовуй локальний composite **`setup-bun-deps`** (шлях **`./.github/actions/setup-bun-deps`** або **`./npm/github-actions/setup-bun-deps`**, як у репозиторії). Встановлення **kubeconform** / **kubescape** лишаються окремими кроками.
59
49
 
@@ -103,7 +93,7 @@ jobs:
103
93
  # (kubectl уже доступний на github-hosted runner'ах: https://github.com/actions/runner-images).
104
94
 
105
95
  - name: Lint K8s
106
- run: bun run lint-k8s
96
+ run: n-cursor lint k8s --read-only
107
97
  ```
108
98
 
109
99
  ## Deployment: `resources.requests` (CPU і memory)
@@ -317,12 +317,10 @@ CLI-режим: при прямому виконанні скрипта (`bun np
317
317
 
318
318
  ## Потік виконання / Використання
319
319
 
320
- ### CLI-режим (підкоманда `lint-k8s`)
320
+ ### CLI-режим
321
321
 
322
322
  ```
323
- bun run lint-k8s # через root-script (workspace)
324
- # або
325
- n-cursor lint-k8s # CLI-обгортка
323
+ n-cursor lint k8s # rule orchestration entrypoint
326
324
  # або (наприкінці файлу — прямий запуск)
327
325
  bun npm/rules/k8s/lint/lint.mjs
328
326
  ```
@@ -1 +1 @@
1
- { "auto": { "glob": "**/k8s/**" } }
1
+ { "auto": { "glob": "**/k8s/**" }, "lint": "full" }
@@ -11,12 +11,12 @@ docgen:
11
11
 
12
12
  ## Огляд
13
13
 
14
- Модуль відповідає за визначення та виконання процесу лінтування коду. Функція `selectLintRules` вибирає та сортує ідентифікатори правил лінтування на основі конфігурацій, визначених у meta.json. Функція `runLint` запускає перевірку обраних правил для змінених або всіх файлів репозиторію.
14
+ Модуль відповідає за визначення та виконання процесу лінтування коду. Unscoped linter-фаза бере активні правила з `.n-cursor.json`, а `meta.json#lint` використовує лише як класифікацію scope (`per-file` або `full`). Функція `runLint` запускає перевірку обраних правил для змінених або всіх файлів репозиторію.
15
15
 
16
16
  ## Поведінка
17
17
 
18
- selectLintRules вибирає і сортує ідентифікатори правил для лінтування на основі їхніх конфігурацій, включаючи можливість включення правил, що застосовуються до всього репозиторію.
19
- runLint запускає процес лінтування, виконуючи перевірку правил для змінених файлів або для всього репозиторію, залежно від наданих опцій, і може виконувати форматування.
18
+ selectLintRules вибирає і сортує ідентифікатори правил для лінтування з уже активованого списку `.n-cursor.json`, включаючи можливість включення правил, що застосовуються до всього репозиторію.
19
+ runLint запускає процес лінтування, виконуючи перевірку активних правил для змінених файлів або для всього репозиторію, залежно від наданих опцій, і може виконувати форматування. Scoped режим `lint <rule…>` запускає названі правила напряму та не потребує `.n-cursor.json` для linter-фази.
20
20
 
21
21
  ## Публічний API
22
22
 
@@ -8,6 +8,7 @@ import { spawnSync } from 'node:child_process'
8
8
  import { parseRuleLintSpec, readRuleMetaRaw } from '../../../scripts/lib/rule-meta.mjs'
9
9
  import { collectChangedFilesSince, resolveChangedBase } from '../../../scripts/lib/changed-files.mjs'
10
10
  import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
11
+ import { isRuleEnabled, readNCursorConfigLite } from '../../../scripts/lib/read-n-cursor-config-lite.mjs'
11
12
 
12
13
  // Цей файл: npm/rules/lint/js/orchestrate.mjs → PACKAGE_ROOT = npm (чотири dirname угору).
13
14
  const PACKAGE_ROOT = dirname(dirname(dirname(dirname(fileURLToPath(import.meta.url)))))
@@ -42,17 +43,33 @@ async function runConformance(cwd, readOnly, log, filter = []) {
42
43
  * Вибирає id правил для контексту, алфавітно.
43
44
  * @param {Record<string, {lint?: unknown}>} metaById мапа id → meta-обʼєкт
44
45
  * @param {boolean} full `false` → лише `per-file` правила; `true` → усі (`per-file` ∪ `full`)
46
+ * @param {string[]} enabledRuleIds активні rule-id з `.n-cursor.json`
45
47
  * @returns {string[]} відсортовані id
46
48
  */
47
- export function selectLintRules(metaById, full) {
49
+ export function selectLintRules(metaById, full, enabledRuleIds) {
50
+ const enabled = new Set(enabledRuleIds)
48
51
  const out = []
49
52
  for (const [id, raw] of Object.entries(metaById)) {
53
+ if (!enabled.has(id)) continue
50
54
  const scope = parseRuleLintSpec(raw?.lint)
51
55
  if (scope === 'per-file' || (full && scope === 'full')) out.push(id)
52
56
  }
53
57
  return out.toSorted((a, b) => a.localeCompare(b))
54
58
  }
55
59
 
60
+ /**
61
+ * Активні правила для unscoped linter-фази. `.n-cursor.json` — єдине джерело
62
+ * whitelist/disable, `meta.json#lint` нижче використовується лише як scope (`per-file`/`full`).
63
+ * @param {Record<string, unknown>} metaById доступні bundled правила
64
+ * @param {string} cwd корінь
65
+ * @returns {Promise<string[]>} активні rule-id з конфіга, що існують у пакеті
66
+ */
67
+ async function readEnabledLintRuleIds(metaById, cwd) {
68
+ const config = await readNCursorConfigLite(cwd)
69
+ if (!config.exists) return []
70
+ return Object.keys(metaById).filter(id => isRuleEnabled(config, id))
71
+ }
72
+
56
73
  /**
57
74
  * Зчитує meta всіх правил пакета.
58
75
  * @param {string} rulesDir каталог rules
@@ -130,7 +147,7 @@ async function runFullConformancePhase(cwd, readOnly, log) {
130
147
  * @param {(s: string) => void} log логер
131
148
  * @returns {Promise<number>} код виходу oxfmt (0 — OK або пропущено)
132
149
  */
133
- async function runFormat(cwd, log) {
150
+ function runFormat(cwd, log) {
134
151
  const oxfmt = resolveCmd('oxfmt')
135
152
  if (!oxfmt) {
136
153
  log('ℹ️ lint: oxfmt недоступний у PATH — формат-крок пропущено.\n')
@@ -201,7 +218,8 @@ export async function runLint(opts = {}) {
201
218
  }
202
219
 
203
220
  const metaById = readAllMeta(rulesDir)
204
- const ids = selectLintRules(metaById, full)
221
+ const enabledRuleIds = await readEnabledLintRuleIds(metaById, cwd)
222
+ const ids = selectLintRules(metaById, full, enabledRuleIds)
205
223
  const perFile = await runPerFileRules(ids, { rulesDir, changed, cwd, readOnly, metaById, log })
206
224
  if (perFile.stop) return perFile.code
207
225
  let worst = perFile.code
@@ -219,7 +237,7 @@ export async function runLint(opts = {}) {
219
237
  // Формат-крок (oxfmt): fix-режим — завжди (будь-який scope); read-only пропускаємо (нуль
220
238
  // мутацій). Кастомний rulesDir (юніт-тести) — реальний пакет недоступний, тож пропускаємо.
221
239
  if (!readOnly && opts.rulesDir === undefined) {
222
- const fmtCode = await runFormat(cwd, log)
240
+ const fmtCode = runFormat(cwd, log)
223
241
  if (fmtCode !== 0) worst = fmtCode
224
242
  }
225
243
  return worst
@@ -8,4 +8,5 @@ resource: npm/rules/php/js/
8
8
 
9
9
  | Файл | Тип |
10
10
  |---|---|
11
+ | [lint.mjs](lint.md) | JS Module |
11
12
  | [tooling.mjs](tooling.md) | JS Module |
@@ -0,0 +1,20 @@
1
+ ---
2
+ type: JS Module
3
+ title: lint.mjs
4
+ resource: npm/rules/php/js/lint.mjs
5
+ docgen:
6
+ crc: 7de6b473
7
+ score: 100
8
+ ---
9
+
10
+ Оркестраторний адаптер правила `php` для `n-cursor lint`. Делегує лінтер-фазу наявній функції `run` із `../lint/lint.mjs` (composer audit, php-cs-fixer у `--dry-run`, phpstan/psalm). Режиму на рівні окремих файлів немає — composer-інструменти працюють по всьому проєкту, тож параметр `files` ігнорується. Інструменти read-only (`--dry-run`/`analyse`), тож мутацій робочого дерева немає.
11
+
12
+ ## Поведінка
13
+
14
+ 1. Викликає `run` — лінтер-фаза php по всьому проєкту.
15
+ 2. Повертає код виходу інструменту.
16
+
17
+ ## Гарантії поведінки
18
+
19
+ - Read-only: інструменти не мутують робоче дерево.
20
+ - Не звертається до мережі напряму (composer-кроки можуть, але це поведінка делегата, не цього модуля).
@@ -0,0 +1,15 @@
1
+ /** @see ./docs/lint.md */
2
+ import { run } from '../lint/lint.mjs'
3
+ import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
4
+
5
+ /**
6
+ * Оркестраторний адаптер `n-cursor lint php` (лінтер-фаза): composer audit + php-cs-fixer
7
+ * (`--dry-run`) + phpstan/psalm через `run` (read-only — мутацій немає, тож `opts` ігнорується).
8
+ * Структурні php.mdc-перевірки — у конформність-фазі. Без composer-інструментів крок — no-op.
9
+ * @param {string[] | undefined} _files ігнорується (whole-repo обхід)
10
+ * @param {string} [cwd] корінь
11
+ * @returns {Promise<number>} exit code
12
+ */
13
+ export function lint(_files, cwd = process.cwd()) {
14
+ return runStandardLint(import.meta.dirname, () => run(cwd))
15
+ }
@@ -63,13 +63,14 @@ function runTool(label, abs, args, pass, fail) {
63
63
 
64
64
  /**
65
65
  * Запускає `lint-php`.
66
+ * @param {string} [cwd] корінь репозиторію
66
67
  * @returns {number} 0 — OK, 1 — є помилки
67
68
  */
68
- export function run() {
69
+ export function run(cwd = process.cwd()) {
69
70
  const reporter = createCheckReporter()
70
71
  const { pass, fail } = reporter
71
72
 
72
- const root = process.cwd()
73
+ const root = cwd
73
74
  if (!existsSync(join(root, 'composer.json'))) {
74
75
  pass('lint-php: немає composer.json у корені — кроки PHP пропущено')
75
76
  return reporter.getExitCode()
@@ -1 +1 @@
1
- { "auto": { "glob": "composer.json" } }
1
+ { "auto": { "glob": "composer.json" }, "lint": "full" }
package/rules/php/php.mdc CHANGED
@@ -63,9 +63,7 @@ composer audit
63
63
 
64
64
  ## lint-php
65
65
 
66
- `composer`-інструмененти не мають єдиного CLI, який сам обходить репозиторій, тому `lint-php` зручно делегувати у JS-скрипт-обгортку (як `lint-docker`, `lint-k8s`).
67
-
68
- - Канон `package.json#scripts.lint-php` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
66
+ `composer`-інструмененти не мають єдиного CLI, який сам обходить репозиторій, тому php-лінт делегується у JS-скрипт-обгортку. Запуск — через **`n-cursor lint php`** (CI — `--read-only`); окремого `package.json`-скрипта немає.
69
67
 
70
68
  Скрипт `run-php.mjs`:
71
69
 
@@ -44,4 +44,4 @@ jobs:
44
44
  run: composer install --no-interaction --no-progress --prefer-dist
45
45
 
46
46
  - name: Lint PHP
47
- run: bun run lint-php
47
+ run: n-cursor lint php --read-only
@@ -38,4 +38,4 @@ jobs:
38
38
  run: uv sync --frozen
39
39
 
40
40
  - name: Lint Python
41
- run: bun run lint-python
41
+ run: n-cursor lint python --read-only
@@ -27,9 +27,7 @@ Python-проєкти ведуться **виключно** на [uv](https://do
27
27
 
28
28
  ## lint-python
29
29
 
30
- Інструменти uv-екосистеми не мають єдиного CLI, що сам обходить репозиторій, тому `lint-python` делегується у JS-скрипт-обгортку (як `lint-php`, `lint-docker`).
31
-
32
- - Канон `package.json#scripts.lint-python` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
30
+ Інструменти uv-екосистеми не мають єдиного CLI, що сам обходить репозиторій, тому python-лінт делегується у JS-скрипт-обгортку. Запуск — через **`n-cursor lint python`** (CI — `--read-only`); окремого `package.json`-скрипта немає.
33
31
 
34
32
  Скрипт `rules/python/lint/lint.mjs`:
35
33
 
@@ -9,3 +9,4 @@ resource: npm/rules/rust/js/
9
9
  | Файл | Тип |
10
10
  |---|---|
11
11
  | [applies.mjs](applies.md) | JS Module |
12
+ | [lint.mjs](lint.md) | JS Module |
@@ -0,0 +1,21 @@
1
+ ---
2
+ type: JS Module
3
+ title: lint.mjs
4
+ resource: npm/rules/rust/js/lint.mjs
5
+ docgen:
6
+ crc: 5d7c4123
7
+ score: 100
8
+ ---
9
+
10
+ Оркестраторний адаптер правила `rust` для `n-cursor lint`: rustfmt + clippy через `cargo`. Запускається на `n-cursor lint rust`. За відсутності `Cargo.toml` у корені — no-op (вихід 0). `cargo`/`rustfmt`/`clippy` резолвляться з PATH (Rust toolchain через rustup), не з npm-залежностей; якщо `cargo` відсутній за наявного `Cargo.toml` — помилка.
11
+
12
+ ## Поведінка
13
+
14
+ 1. `readOnly` (CI): `cargo fmt --all -- --check` + `cargo clippy --all-targets --all-features -- -D warnings` — детект без мутацій.
15
+ 2. fix-режим: `cargo fmt --all` + `cargo clippy --fix` + фінальний `cargo clippy … -D warnings`.
16
+ 3. Перший ненульовий cargo-крок спиняє ланцюг і повертає його код.
17
+
18
+ ## Гарантії поведінки
19
+
20
+ - Read-only за наявності `readOnly`: cargo не мутує робоче дерево (`--check`, без `--fix`).
21
+ - Не звертається до мережі напряму (cargo-кроки можуть тягнути crates, але це поведінка тулчейну).
@@ -0,0 +1,79 @@
1
+ /** @see ./docs/lint.md */
2
+ import { spawnSync } from 'node:child_process'
3
+ import { existsSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+
6
+ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
7
+ import { runStandardLint } from '../../../scripts/lib/run-standard-lint.mjs'
8
+ import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
9
+
10
+ /**
11
+ * Запускає cargo-крок і репортить результат.
12
+ * @param {string} label назва кроку
13
+ * @param {string} cargo абсолютний шлях до `cargo`
14
+ * @param {string[]} args аргументи
15
+ * @param {(m: string) => void} pass callback pass
16
+ * @param {(m: string) => void} fail callback fail
17
+ * @returns {boolean} true якщо крок успішний
18
+ */
19
+ function runCargo(label, cargo, args, pass, fail) {
20
+ const r = spawnSync(cargo, args, { stdio: 'inherit', shell: false })
21
+ if (r.status === 0) {
22
+ pass(`lint-rust: ${label} — OK`)
23
+ return true
24
+ }
25
+ const code = typeof r.status === 'number' ? r.status : 1
26
+ fail(`lint-rust: ${label} — помилка (код ${code}, rust.mdc)`)
27
+ return false
28
+ }
29
+
30
+ /**
31
+ * Оркестраторний адаптер `n-cursor lint rust`: rustfmt + clippy через cargo. Без `Cargo.toml` —
32
+ * no-op (0). `cargo`/`rustfmt`/`clippy` — Rust toolchain (rustup), не npm-залежності.
33
+ * readOnly (CI): `cargo fmt --all -- --check` + `cargo clippy … -D warnings` (нуль мутацій).
34
+ * fix: `cargo fmt --all` + `cargo clippy --fix` + фінальний `cargo clippy … -D warnings`.
35
+ * @param {string[] | undefined} _files ігнорується (cargo обходить crate сам)
36
+ * @param {string} [cwd] корінь
37
+ * @param {{ readOnly?: boolean }} [opts] readOnly → без мутацій
38
+ * @returns {number} exit code
39
+ */
40
+ function runRustLint(cwd = process.cwd(), opts = {}) {
41
+ const readOnly = opts.readOnly === true
42
+ const reporter = createCheckReporter()
43
+ const { pass, fail } = reporter
44
+
45
+ if (!existsSync(join(cwd, 'Cargo.toml'))) {
46
+ pass('lint-rust: немає Cargo.toml — кроки Rust пропущено')
47
+ return reporter.getExitCode()
48
+ }
49
+
50
+ const cargo = resolveCmd('cargo')
51
+ if (!cargo) {
52
+ fail('lint-rust: `cargo` не знайдено в PATH (Rust toolchain через rustup, rust.mdc)')
53
+ return reporter.getExitCode()
54
+ }
55
+
56
+ const fmtArgs = readOnly ? ['fmt', '--all', '--', '--check'] : ['fmt', '--all']
57
+ if (!runCargo(readOnly ? 'cargo fmt --check' : 'cargo fmt', cargo, fmtArgs, pass, fail)) {
58
+ return reporter.getExitCode()
59
+ }
60
+
61
+ if (!readOnly) {
62
+ const fixArgs = ['clippy', '--fix', '--allow-staged', '--allow-dirty', '--all-targets', '--all-features']
63
+ if (!runCargo('cargo clippy --fix', cargo, fixArgs, pass, fail)) return reporter.getExitCode()
64
+ }
65
+
66
+ runCargo('cargo clippy -D warnings', cargo, ['clippy', '--all-targets', '--all-features', '--', '-D', 'warnings'], pass, fail)
67
+ return reporter.getExitCode()
68
+ }
69
+
70
+ /**
71
+ * Locked orchestration entry point for `n-cursor lint rust`.
72
+ * @param {string[] | undefined} _files ігнорується (cargo обходить crate сам)
73
+ * @param {string} [cwd] корінь
74
+ * @param {{ readOnly?: boolean }} [opts] readOnly → без мутацій
75
+ * @returns {Promise<number>} exit code
76
+ */
77
+ export function lint(_files, cwd = process.cwd(), opts = {}) {
78
+ return runStandardLint(import.meta.dirname, () => runRustLint(cwd, opts))
79
+ }
@@ -1 +1 @@
1
- { "auto": { "glob": "**/Cargo.toml" } }
1
+ { "auto": { "glob": "**/Cargo.toml" }, "lint": "full" }
@@ -5,12 +5,10 @@ alwaysApply: false
5
5
  version: '1.4'
6
6
  ---
7
7
 
8
- **rustfmt** ([rust-lang/rustfmt](https://github.com/rust-lang/rustfmt)) — форматер; **clippy** ([rust-lang/rust-clippy](https://github.com/rust-lang/rust-clippy)) — лінтер. У скрипті **`lint-rust`** локально йдуть три кроки в одному рядку: `cargo fmt --all` → `cargo clippy --fix --allow-staged --allow-dirty --all-targets --all-features` → фінальний `cargo clippy --all-targets --all-features -- -D warnings`. У CIбез `--fix`: `cargo fmt --all -- --check` і `cargo clippy ... -- -D warnings` (див. `lint-rust.yml`).
8
+ **rustfmt** ([rust-lang/rustfmt](https://github.com/rust-lang/rustfmt)) — форматер; **clippy** ([rust-lang/rust-clippy](https://github.com/rust-lang/rust-clippy)) — лінтер. Запуск через **`n-cursor lint rust`** (адаптер `js/lint.mjs`): локально (fix) `cargo fmt --all` → `cargo clippy --fix --allow-staged --allow-dirty --all-targets --all-features` → фінальний `cargo clippy --all-targets --all-features -- -D warnings`; у `--read-only` (детект) — `cargo fmt --all -- --check` + `cargo clippy ... -- -D warnings`. Окремого `package.json`-скрипта немає. У CI cargo викликається напряму (див. `lint-rust.yml`).
9
9
 
10
10
  `cargo`, `rustfmt`, `clippy` не додавай у `devDependencies` — це Rust toolchain, ставиться через `rustup` локально або через `dtolnay/rust-toolchain@stable` у CI.
11
11
 
12
- Канон `scripts.lint-rust` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
13
-
14
12
  У `.vscode/extensions.json` `recommendations` мають містити `rust-lang.rust-analyzer` і `tamasfe.even-better-toml`: [extensions.json.snippet.json](./policy/vscode_extensions/template/extensions.json.snippet.json)
15
13
 
16
14
  Канон workflow `.github/workflows/lint-rust.yml`: [lint-rust.yml.snippet.yml](./policy/lint_rust_yml/template/lint-rust.yml.snippet.yml)
@@ -21,7 +19,7 @@ version: '1.4'
21
19
 
22
20
  Tauri-проєкт завжди має `src-tauri/Cargo.toml`, тому правило `rust` активується автоматично разом з `tauri`. Поділ обов'язків:
23
21
 
24
- - `rust` — `lint-rust` скрипт, `rust-analyzer`, `even-better-toml`, CI workflow.
22
+ - `rust` — лінт через `n-cursor lint rust`, `rust-analyzer`, `even-better-toml`, CI workflow.
25
23
  - `tauri` — `tauri-apps.tauri-vscode` (див. **tauri.mdc**).
26
24
 
27
25
  Обидва правила перевіряють `.vscode/extensions.json` за `contains`-семантикою; конкурентного запису немає.
@@ -1,5 +0,0 @@
1
- {
2
- "scripts": {
3
- "lint-image": ["npx @nitra/minify-image", "--src=.", "--write"]
4
- }
5
- }
@@ -1,16 +0,0 @@
1
- # Перевірка `package.json` (php.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "contains": ... } }
4
- # Структура --data сформована з template/package.json.contains.json.
5
- # FS-перевірки (`composer.json`, наявність `package.json` як такого) — у JS.
6
- package php.package_json
7
-
8
- import rego.v1
9
-
10
- deny contains msg if {
11
- some script_name, needles in data.template.contains.scripts
12
- actual := object.get(object.get(input, "scripts", {}), script_name, "")
13
- some needle in needles
14
- not contains(actual, needle)
15
- msg := sprintf("package.json: scripts.%s має містити %q (php.mdc)", [script_name, needle])
16
- }
@@ -1,4 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "single": "package.json" }
4
- }
@@ -1,5 +0,0 @@
1
- {
2
- "scripts": {
3
- "lint-php": ["bun"]
4
- }
5
- }
@@ -1,16 +0,0 @@
1
- # Перевірка `package.json` (python.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "contains": ... } }
4
- # Структура --data сформована з template/package.json.contains.json.
5
- # FS-перевірки (наявність `package.json` як такого) — у JS.
6
- package python.package_json
7
-
8
- import rego.v1
9
-
10
- deny contains msg if {
11
- some script_name, needles in data.template.contains.scripts
12
- actual := object.get(object.get(input, "scripts", {}), script_name, "")
13
- some needle in needles
14
- not contains(actual, needle)
15
- msg := sprintf("package.json: scripts.%s має містити %q (python.mdc)", [script_name, needle])
16
- }
@@ -1,4 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "single": "package.json" }
4
- }
@@ -1,5 +0,0 @@
1
- {
2
- "scripts": {
3
- "lint-python": ["bun"]
4
- }
5
- }
@@ -1,18 +0,0 @@
1
- # Перевірка `package.json` для правила rust (rust.mdc).
2
- #
3
- # Канон надходить через --data: { "template": { "contains": ... } }
4
- # Структура --data сформована з template/package.json.contains.json.
5
- # Перевіряємо substring-вимоги до scripts.lint-rust: усі три кроки
6
- # (`cargo fmt`, `cargo clippy --fix`, фінальний `cargo clippy ... -D warnings`)
7
- # мають бути присутніми у значенні скрипта.
8
- package rust.package_json
9
-
10
- import rego.v1
11
-
12
- deny contains msg if {
13
- some script_name, needles in data.template.contains.scripts
14
- actual := object.get(object.get(input, "scripts", {}), script_name, "")
15
- some needle in needles
16
- not contains(actual, needle)
17
- msg := sprintf("package.json: scripts.%s має містити %q (rust.mdc)", [script_name, needle])
18
- }
@@ -1,5 +0,0 @@
1
- {
2
- "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
- "files": { "single": "package.json", "required": true },
4
- "missingMessage": "package.json не існує — створи зі scripts.lint-rust (rust.mdc)"
5
- }
@@ -1,9 +0,0 @@
1
- {
2
- "scripts": {
3
- "lint-rust": [
4
- "cargo fmt --all",
5
- "cargo clippy --fix --allow-staged --allow-dirty",
6
- "cargo clippy --all-targets --all-features -- -D warnings"
7
- ]
8
- }
9
- }