@nitra/cursor 11.2.0 → 11.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.4.0] - 2026-06-15
4
+
5
+ ### Changed
6
+
7
+ - Додано логіку відсіювання неактуальних правил/скілів за `availableRules`/`availableSkills
8
+
9
+ ## [11.3.0] - 2026-06-15
10
+
11
+ ### Changed
12
+
13
+ - lint: opt-in `meta.json: llmFix:true` тепер реально дротується (раніше прапор був декоративний — opportunistic LLM-fix біг просто на `!readOnly`). `runLint` читає `llmFix` з meta правила й передає в `lint(files, cwd, { readOnly, llmFix })`; правило без прапора лишається detect-only. Це й забезпечує safety-тріаж зі спеки (логічні лінтери не вмикають LLM-fix випадково). doc-files і text позначені `llmFix:true`; cspell-класифікація гейтиться через `llmFix` (проведено `runLintTextCli`/`runLintTextSteps`/`runCspellText`), standalone `lint-text` передає `llmFix:true`. Принагідно: justified `no-unsanitized/method`-disable на package-internal динамічний import у `runLint` (pre-existing).
14
+
3
15
  ## [11.2.0] - 2026-06-15
4
16
 
5
17
  ### Changed
@@ -173,7 +173,7 @@
173
173
  #### Вкладена normalizeConfigWithAutoRules(parsedConfig)
174
174
 
175
175
  - **Сигнатура:** `async function normalizeConfigWithAutoRules(parsedConfig: Record<string, unknown>): Promise<Record<string, unknown>>`
176
- - **Призначення:** перевіряє типи полів, обчислює `auto-detected rules` (`detectAutoRules`), будує ефективний список правил (поточні + auto, мінус `disable-rules`), за яким `detectAutoSkills` визначає скіли. Далі `mergeConfigWithAutoDetected` зливає дані, після чого `$schema` вирівнюється до `CONFIG_SCHEMA_URL`, додаються `disable-rules`/`disable-skills` (якщо непорожні), результат проходить через `sortConfigIdArrays`.
176
+ - **Призначення:** перевіряє типи полів, обчислює `auto-detected rules` (`detectAutoRules`), будує ефективний список правил (поточні + auto, мінус `disable-rules`), за яким `detectAutoSkills` визначає скіли. Далі `mergeConfigWithAutoDetected` зливає дані (передаючи `availableRules`/`availableSkills` із каталогів пакета, щоб відсіяти з `rules`/`skills` неактуальні id, яких уже немає у пакеті — прибрані логуються через `🧹`), після чого `$schema` вирівнюється до `CONFIG_SCHEMA_URL`, додаються `disable-rules`/`disable-skills` (якщо непорожні), результат проходить через `sortConfigIdArrays`.
177
177
 
178
178
  ### logRuleMigrationsIfAny(parsedConfig)
179
179
 
package/bin/n-cursor.js CHANGED
@@ -25,7 +25,7 @@
25
25
  * `npx \@nitra/cursor lint-text` — канонічний lint-text (text.mdc): `cspell` → `shellcheck` (з auto-fix) →
26
26
  * `markdownlint-cli2 --fix` → `v8r` (json/json5/yaml/yml/toml)
27
27
  * `npx \@nitra/cursor lint-doc-files` — детермінований детектор застарілості файлових док (`stale`: `missing`|`crc-mismatch`); правило doc-files (ignore-glob у `npm/rules/doc-files/js/docgen-ignore.mjs`; тека `docs/` поряд із джерелом). Режими: повний (exit 1), `--json` (exit 0), `--missing-only`, `--hook`/`--git` (hook-протокол, exit 2), `--degraded`
28
- * `npx \@nitra/cursor fix-doc-files` — JS-оркестрована генерація файлових док (роутинг local/cloud) зі штампом CRC (`--limit`/`--from`/`--overwrite`/`--retry-degraded`); `--stamp` — детерміноване перештампування CRC без LLM
28
+ * `npx \@nitra/cursor fix-doc-files` — JS-оркестрована генерація файлових док (роутинг local/cloud) зі штампом CRC (`--limit`/`--from`/`--overwrite`); `--stamp` — детерміноване перештампування CRC без LLM
29
29
  * `npx \@nitra/cursor doc-aggregate modules` — JSON-лістинг логічних модулів (межі за `package.json`) для Tier 2 скілу doc-aggregate
30
30
  * `npx \@nitra/cursor skill list` — скіли пакета без синку в проєкт
31
31
  * `npx \@nitra/cursor skill taze` — промпт на stdout
@@ -315,9 +315,18 @@ async function readConfig(paths = {}) {
315
315
  const merged = mergeConfigWithAutoDetected({
316
316
  config: parsedConfig,
317
317
  detectedRules: autoDetectedRules.rules,
318
- detectedSkills: autoDetectedSkills.skills
318
+ detectedSkills: autoDetectedSkills.skills,
319
+ availableRules,
320
+ availableSkills
319
321
  })
320
322
 
323
+ if (merged.pruned) {
324
+ const parts = []
325
+ if (merged.pruned.rules.length > 0) parts.push(`rules: ${merged.pruned.rules.join(', ')}`)
326
+ if (merged.pruned.skills.length > 0) parts.push(`skills: ${merged.pruned.skills.join(', ')}`)
327
+ console.log(`🧹 Прибрано з ${CONFIG_FILE} неактуальні (немає у пакеті) — ${parts.join('; ')}\n`)
328
+ }
329
+
321
330
  const rest = Object.fromEntries(Object.entries(parsedConfig).filter(([k]) => k !== '$schema'))
322
331
  const normalized = {
323
332
  $schema: CONFIG_SCHEMA_URL,
@@ -1568,7 +1577,8 @@ try {
1568
1577
  case 'lint-text': {
1569
1578
  // Канонічний lint-text: cspell → shellcheck → dotenv → markdownlint → v8r (text.mdc).
1570
1579
  // `--read-only` (CI): без авто-фіксу (markdownlint/shellcheck/dotenv) — нуль мутацій.
1571
- process.exitCode = await runLintTextCli({ readOnly: args.includes('--read-only') })
1580
+ // `llmFix:true` text llmFix-capable, тож standalone lint-text робить omlx-класифікацію cspell.
1581
+ process.exitCode = await runLintTextCli({ readOnly: args.includes('--read-only'), llmFix: true })
1572
1582
 
1573
1583
  break
1574
1584
  }
@@ -1649,7 +1659,7 @@ try {
1649
1659
  }
1650
1660
  case 'fix-doc-files': {
1651
1661
  // n-cursor fix-doc-files — local-only генерація файлових док (omlx) + CRC-штамп
1652
- // (--limit/--from/--overwrite/--retry-degraded); --stamp — детерміноване
1662
+ // (--limit/--from/--overwrite); --stamp — детерміноване
1653
1663
  // перештампування source+crc без LLM. У CI не запускається (потрібна локальна модель).
1654
1664
  if (args.includes('--stamp')) {
1655
1665
  const { runDocFilesStampCli } = await import('../rules/doc-files/js/docgen-files-batch.mjs')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "11.2.0",
3
+ "version": "11.4.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -98,13 +98,14 @@ function collectStale(files, cwd) {
98
98
  * Крок агрегатора lint для doc-files (opportunistic LLM-fix tier).
99
99
  * @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
100
100
  * @param {string} [cwd] корінь репо
101
- * @param {{ readOnly?: boolean }} [opts] readOnly: лише детект (CI/hook), без мутацій/LLM
101
+ * @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly: лише детект (CI/hook);
102
+ * llmFix: opt-in opportunistic-генерація (з `meta.json: llmFix:true`) — без нього detect-only
102
103
  * @returns {Promise<number>} 0 — доки свіжі; 1 — є застарілі (детект, fix пропущено чи помилка генерації)
103
104
  */
104
- export async function lint(files, cwd = process.cwd(), { readOnly = false } = {}) {
105
+ export async function lint(files, cwd = process.cwd(), { readOnly = false, llmFix = false } = {}) {
105
106
  const stale = collectStale(files, cwd)
106
107
  if (stale.length === 0) return 0
107
- if (readOnly) return reportStale(stale)
108
+ if (readOnly || !llmFix) return reportStale(stale)
108
109
 
109
110
  // fix-by-default: opportunistic-генерація через спільне ядро (preflight omlx →
110
111
  // батч із circuit-breaker'ом). omlx недоступний → runGenerationBatch друкує причину
@@ -108,15 +108,23 @@ export async function runLint(opts = {}) {
108
108
  return 0
109
109
  }
110
110
 
111
- const ids = selectLintRules(readAllMeta(rulesDir), full)
111
+ const metaById = readAllMeta(rulesDir)
112
+ const ids = selectLintRules(metaById, full)
112
113
  for (const id of ids) {
113
114
  const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
114
115
  if (!existsSync(lintPath)) {
115
116
  log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
116
117
  continue
117
118
  }
119
+ // lintPath = join(rulesDir, id, …) — суто package-internal (rulesDir пакета + id зі
120
+ // selectLintRules за власним meta), не зовнішній вхід → ін'єкції немає.
121
+ // eslint-disable-next-line no-unsanitized/method
118
122
  const mod = await import(lintPath)
119
- const code = await mod.lint(changed, cwd, { readOnly })
123
+ // `llmFix` (opt-in opportunistic LLM-fix, спека 2026-06-15): лише правила з
124
+ // `meta.json: llmFix:true` отримують fix-сходинку; решта — detect-only. Це й
125
+ // забезпечує safety-тріаж (логічні лінтери не вмикають LLM-fix випадково).
126
+ const llmFix = metaById[id]?.llmFix === true
127
+ const code = await mod.lint(changed, cwd, { readOnly, llmFix })
120
128
  if (code !== 0) return code
121
129
  }
122
130
 
@@ -6,9 +6,10 @@ import { runLintTextCli } from '../lint/lint.mjs'
6
6
  /**
7
7
  * @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
8
8
  * @param {string} [_cwd] корінь (ігнорується — CLI працює від process.cwd())
9
- * @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій)
9
+ * @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій);
10
+ * llmFix → opt-in omlx-класифікація cspell (з `meta.json: llmFix:true`)
10
11
  * @returns {Promise<number>} exit code
11
12
  */
12
13
  export function lint(_files, _cwd, opts = {}) {
13
- return runLintTextCli({ readOnly: opts.readOnly === true })
14
+ return runLintTextCli({ readOnly: opts.readOnly === true, llmFix: opts.llmFix === true })
14
15
  }
@@ -113,9 +113,10 @@ export function appendWordsToDict(cwd, words) {
113
113
  * cspell-крок lint-text: класифікація → словник (нова схема).
114
114
  * @param {string} [cwd] корінь
115
115
  * @param {boolean} [readOnly] true → лише детект (нуль мутацій)
116
+ * @param {boolean} [llmFix] opt-in omlx-класифікація (з `meta.json: llmFix:true`); без нього — лише детект
116
117
  * @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
117
118
  */
118
- export function runCspellText(cwd = process.cwd(), readOnly = false) {
119
+ export function runCspellText(cwd = process.cwd(), readOnly = false, llmFix = false) {
119
120
  const bin = resolveCmd('npx')
120
121
  if (!bin) {
121
122
  process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
@@ -124,7 +125,7 @@ export function runCspellText(cwd = process.cwd(), readOnly = false) {
124
125
 
125
126
  const first = detectCspell(cwd, bin)
126
127
  if (first.code === 0) return 0
127
- if (readOnly) {
128
+ if (readOnly || !llmFix) {
128
129
  process.stdout.write(first.out)
129
130
  return first.code
130
131
  }
@@ -97,9 +97,10 @@ function preflight(dep) {
97
97
  /**
98
98
  * Внутрішні кроки `lint-text` без локу.
99
99
  * @param {boolean} [readOnly] true → лише детект без авто-фіксу (нуль мутацій — CI/pre-commit)
100
+ * @param {boolean} [llmFix] opt-in omlx-класифікація cspell (інші кроки фіксяться детерміновано за readOnly)
100
101
  * @returns {number} 0 — все OK, інакше — код першого кроку, що впав
101
102
  */
102
- function runLintTextSteps(readOnly = false) {
103
+ function runLintTextSteps(readOnly = false, llmFix = false) {
103
104
  // Auto-install: throws on failure → propagates as exit 1 from runStandardLint
104
105
  ensureTool('shellcheck')
105
106
  ensureTool('dotenv-linter')
@@ -107,8 +108,8 @@ function runLintTextSteps(readOnly = false) {
107
108
  // patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight.
108
109
  if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1
109
110
 
110
- console.log(`\n▶ cspell (${readOnly ? 'перевірка' : 'omlx-автофікс одруків + перевірка'})`)
111
- const cspellCode = runCspellText(process.cwd(), readOnly)
111
+ console.log(`\n▶ cspell (${!readOnly && llmFix ? 'omlx-класифікація + словник + перевірка' : 'перевірка'})`)
112
+ const cspellCode = runCspellText(process.cwd(), readOnly, llmFix)
112
113
  if (cspellCode !== 0) return cspellCode
113
114
 
114
115
  console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`)
@@ -129,8 +130,9 @@ function runLintTextSteps(readOnly = false) {
129
130
 
130
131
  /**
131
132
  * Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
132
- * @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу
133
+ * @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу;
134
+ * llmFix → omlx-класифікація cspell (opt-in із `meta.json: llmFix:true`)
133
135
  * @returns {Promise<number>} код виходу
134
136
  */
135
137
  export const runLintTextCli = (opts = {}) =>
136
- runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true))
138
+ runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true, opts.llmFix === true))
@@ -1 +1 @@
1
- { "auto": "завжди", "lint": "per-file" }
1
+ { "auto": "завжди", "lint": "per-file", "llmFix": true }
@@ -392,14 +392,45 @@ export async function detectAutoRules({
392
392
  }
393
393
 
394
394
  /**
395
- * Доповнює конфіг автодетектом (лише додає; існуючі вручну задані елементи не прибирає).
395
+ * Розділяє список id на доступні в пакеті й застарілі (відсутні).
396
+ * Без `available` нічого не прибирає — усе вважається доступним.
397
+ * @param {string[]} ids перелік id (rules або skills)
398
+ * @param {string[] | undefined} available id, що реально є у каталозі пакета
399
+ * @returns {{ kept: string[], pruned: string[] }} відфільтровані й прибрані id
400
+ */
401
+ function partitionByAvailability(ids, available) {
402
+ if (!available) return { kept: ids, pruned: [] }
403
+ const availableSet = new Set(available)
404
+ const kept = []
405
+ const pruned = []
406
+ for (const id of ids) {
407
+ if (availableSet.has(id)) kept.push(id)
408
+ else pruned.push(id)
409
+ }
410
+ return { kept, pruned }
411
+ }
412
+
413
+ /**
414
+ * Доповнює конфіг автодетектом (лише додає; існуючі вручну задані елементи не прибирає),
415
+ * а за наявності `availableRules`/`availableSkills` ще й прибирає з `rules`/`skills`
416
+ * неактуальні id, яких уже немає у пакеті (наприклад, правило чи скіл видалено з нової
417
+ * версії \@nitra/cursor) — інакше sync щоразу падав би на завантаженні відсутнього
418
+ * `rules/<id>.mdc` чи `skills/<id>/`. Прибрані id повертаються у полі `pruned` (для логу).
396
419
  * @param {object} params параметри оновлення
397
420
  * @param {{ rules: unknown, skills?: unknown, ['disable-rules']?: unknown, ['disable-skills']?: unknown }} params.config розпарсений `.n-cursor.json`
398
421
  * @param {string[]} params.detectedRules правила, визначені автодетектом
399
422
  * @param {string[]} params.detectedSkills skills, визначені автодетектом
400
- * @returns {{ rules: string[], skills: string[] } & Record<string, unknown>} новий нормалізований конфіг
423
+ * @param {string[]} [params.availableRules] id правил, наявних у каталозі `rules/` пакета (для відсіву неактуальних)
424
+ * @param {string[]} [params.availableSkills] id skills, наявних у каталозі `skills/` пакета (для відсіву неактуальних)
425
+ * @returns {{ rules: string[], skills: string[], pruned?: { rules: string[], skills: string[] } } & Record<string, unknown>} новий нормалізований конфіг
401
426
  */
402
- export function mergeConfigWithAutoDetected({ config, detectedRules, detectedSkills }) {
427
+ export function mergeConfigWithAutoDetected({
428
+ config,
429
+ detectedRules,
430
+ detectedSkills,
431
+ availableRules,
432
+ availableSkills
433
+ }) {
403
434
  const existingRules = migrateRuleIds(normalizeIdList(config.rules))
404
435
  const existingSkills = normalizeIdList(config.skills)
405
436
  const disableRules = migrateRuleIds(normalizeIdList(config['disable-rules']))
@@ -419,13 +450,19 @@ export function mergeConfigWithAutoDetected({ config, detectedRules, detectedSki
419
450
  }
420
451
  }
421
452
 
422
- /** @type {{ rules: string[], skills: string[] } & Record<string, unknown>} */
423
- const normalized = { rules, skills }
453
+ const { kept: keptRules, pruned: prunedRules } = partitionByAvailability(rules, availableRules)
454
+ const { kept: keptSkills, pruned: prunedSkills } = partitionByAvailability(skills, availableSkills)
455
+
456
+ /** @type {{ rules: string[], skills: string[], pruned?: { rules: string[], skills: string[] } } & Record<string, unknown>} */
457
+ const normalized = { rules: keptRules, skills: keptSkills }
424
458
  if (disableRules.length > 0) {
425
459
  normalized['disable-rules'] = disableRules
426
460
  }
427
461
  if (disableSkills.length > 0) {
428
462
  normalized['disable-skills'] = disableSkills
429
463
  }
464
+ if (prunedRules.length > 0 || prunedSkills.length > 0) {
465
+ normalized.pruned = { rules: prunedRules, skills: prunedSkills }
466
+ }
430
467
  return normalized
431
468
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  docgen:
3
3
  source: npm/scripts/auto-rules.mjs
4
- crc: 972b56fc
4
+ crc: d50b922f
5
5
  score: 90
6
6
  ---
7
7
 
@@ -29,7 +29,7 @@ detectAutoRules
29
29
  Визначає активні правила на основі spec, перевіряючи їх проти згенерованих фактів.
30
30
 
31
31
  mergeConfigWithAutoDetected
32
- Доповнює конфігурацію, додаючи визначені автоправила та налаштування, з урахуванням legacy-ID.
32
+ Доповнює конфігурацію, додаючи визначені автоправила та налаштування, з урахуванням legacy-ID; за наявності `availableRules`/`availableSkills` ще й відсіює з `rules`/`skills` неактуальні id, яких немає у пакеті (повертає їх у полі `pruned`).
33
33
 
34
34
  ## Публічний API
35
35