@nitra/cursor 12.3.3 → 12.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.4.0] - 2026-06-21
4
+
5
+ ### Changed
6
+
7
+ - Уніфіковано selection linter-фази: unscoped n-cursor lint бере правила з .n-cursor.json, а meta.json#lint лишається тільки scope-класифікацією.
8
+
3
9
  ## [12.3.3] - 2026-06-20
4
10
 
5
11
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.3.3",
3
+ "version": "12.4.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`** напряму (без обгорток).
@@ -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,13 @@
1
+ /** @see ./docs/lint.md */
2
+ import { run } from '../lint/lint.mjs'
3
+
4
+ /**
5
+ * Оркестраторний адаптер `n-cursor lint php` (лінтер-фаза): composer audit + php-cs-fixer
6
+ * (`--dry-run`) + phpstan/psalm через `run` (read-only — мутацій немає, тож `opts` ігнорується).
7
+ * Структурні php.mdc-перевірки — у конформність-фазі. Без composer-інструментів крок — no-op.
8
+ * @param {string[] | undefined} _files ігнорується (whole-repo обхід)
9
+ * @returns {number} exit code
10
+ */
11
+ export function lint(_files) {
12
+ return run()
13
+ }
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,67 @@
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 { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
8
+
9
+ /**
10
+ * Запускає cargo-крок і репортить результат.
11
+ * @param {string} label назва кроку
12
+ * @param {string} cargo абсолютний шлях до `cargo`
13
+ * @param {string[]} args аргументи
14
+ * @param {(m: string) => void} pass callback pass
15
+ * @param {(m: string) => void} fail callback fail
16
+ * @returns {boolean} true якщо крок успішний
17
+ */
18
+ function runCargo(label, cargo, args, pass, fail) {
19
+ const r = spawnSync(cargo, args, { stdio: 'inherit', shell: false })
20
+ if (r.status === 0) {
21
+ pass(`lint-rust: ${label} — OK`)
22
+ return true
23
+ }
24
+ const code = typeof r.status === 'number' ? r.status : 1
25
+ fail(`lint-rust: ${label} — помилка (код ${code}, rust.mdc)`)
26
+ return false
27
+ }
28
+
29
+ /**
30
+ * Оркестраторний адаптер `n-cursor lint rust`: rustfmt + clippy через cargo. Без `Cargo.toml` —
31
+ * no-op (0). `cargo`/`rustfmt`/`clippy` — Rust toolchain (rustup), не npm-залежності.
32
+ * readOnly (CI): `cargo fmt --all -- --check` + `cargo clippy … -D warnings` (нуль мутацій).
33
+ * fix: `cargo fmt --all` + `cargo clippy --fix` + фінальний `cargo clippy … -D warnings`.
34
+ * @param {string[] | undefined} _files ігнорується (cargo обходить crate сам)
35
+ * @param {string} [cwd] корінь
36
+ * @param {{ readOnly?: boolean }} [opts] readOnly → без мутацій
37
+ * @returns {number} exit code
38
+ */
39
+ export function lint(_files, cwd = process.cwd(), opts = {}) {
40
+ const readOnly = opts.readOnly === true
41
+ const reporter = createCheckReporter()
42
+ const { pass, fail } = reporter
43
+
44
+ if (!existsSync(join(cwd, 'Cargo.toml'))) {
45
+ pass('lint-rust: немає Cargo.toml — кроки Rust пропущено')
46
+ return reporter.getExitCode()
47
+ }
48
+
49
+ const cargo = resolveCmd('cargo')
50
+ if (!cargo) {
51
+ fail('lint-rust: `cargo` не знайдено в PATH (Rust toolchain через rustup, rust.mdc)')
52
+ return reporter.getExitCode()
53
+ }
54
+
55
+ const fmtArgs = readOnly ? ['fmt', '--all', '--', '--check'] : ['fmt', '--all']
56
+ if (!runCargo(readOnly ? 'cargo fmt --check' : 'cargo fmt', cargo, fmtArgs, pass, fail)) {
57
+ return reporter.getExitCode()
58
+ }
59
+
60
+ if (!readOnly) {
61
+ const fixArgs = ['clippy', '--fix', '--allow-staged', '--allow-dirty', '--all-targets', '--all-features']
62
+ if (!runCargo('cargo clippy --fix', cargo, fixArgs, pass, fail)) return reporter.getExitCode()
63
+ }
64
+
65
+ runCargo('cargo clippy -D warnings', cargo, ['clippy', '--all-targets', '--all-features', '--', '-D', 'warnings'], pass, fail)
66
+ return reporter.getExitCode()
67
+ }
@@ -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,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
- }