@nitra/cursor 11.3.0 → 11.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.4.1] - 2026-06-16
4
+
5
+ ### Fixed
6
+
7
+ - release: commit-back більше не «вдається» мовчки за відхиленого push — результат `git push` тепер ЯВНО перевіряється (раніше тихий runGit повертав null, а реліз рапортував успіх, через що npm міг піти попереду git). За non-fast-forward (паралельний push у ту саму гілку) release-коміт автоматично rebase-иться на свіжий upstream, теги пересуваються на новий HEAD і push повторюється (до 5 спроб); без upstream або при rebase-конфлікті — кидаємо помилку (exit 1), тож CI-публікація не відбувається без приземленого commit-back.
8
+
9
+ ## [11.4.0] - 2026-06-15
10
+
11
+ ### Changed
12
+
13
+ - Додано логіку відсіювання неактуальних правил/скілів за `availableRules`/`availableSkills
14
+
3
15
  ## [11.3.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,
@@ -1650,7 +1659,7 @@ try {
1650
1659
  }
1651
1660
  case 'fix-doc-files': {
1652
1661
  // n-cursor fix-doc-files — local-only генерація файлових док (omlx) + CRC-штамп
1653
- // (--limit/--from/--overwrite/--retry-degraded); --stamp — детерміноване
1662
+ // (--limit/--from/--overwrite); --stamp — детерміноване
1654
1663
  // перештампування source+crc без LLM. У CI не запускається (потрібна локальна модель).
1655
1664
  if (args.includes('--stamp')) {
1656
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.3.0",
3
+ "version": "11.4.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -64,6 +64,45 @@ async function collectChangeFiles(cwd, manifest, runGit) {
64
64
  return [{ file: null, entry: synthesized }]
65
65
  }
66
66
 
67
+ /**
68
+ * Пушить release-коміт (із тегами) у апстрім, переживаючи паралельні push у ту саму гілку.
69
+ * `runGit` — ТИХИЙ раннер (повертає null при помилці), тож non-fast-forward push не кидає, а
70
+ * повертає null; цей хелпер ЯВНО перевіряє результат, щоб реліз не «вдався» без приземленого
71
+ * commit-back (саме така мовчазна поразка лишала npm попереду git). За відмовою push:
72
+ * fetch + rebase release-коміту на свіжий апстрім, пересунути теги на новий HEAD і повторити
73
+ * (до `attempts` разів). Без апстріму або при rebase-конфлікті — кидаємо, а не маскуємо.
74
+ * @param {(args: string[]) => Promise<string | null>} runGit git-раннер
75
+ * @param {string[]} tags теги релізу (вже створені на поточному HEAD)
76
+ * @param {number} [attempts] максимум спроб push
77
+ * @returns {Promise<void>} результат; кидає, якщо push так і не приземлився
78
+ */
79
+ async function pushReleaseWithRetry(runGit, tags, attempts = 5) {
80
+ for (let attempt = 1; attempt <= attempts; attempt++) {
81
+ const pushed = await runGit(['push', '--follow-tags'])
82
+ if (pushed !== null) return
83
+ if (attempt === attempts) break
84
+ // push відхилено (найімовірніше non-fast-forward — апстрім пішов уперед) → інтегруємо й пробуємо ще
85
+ const upstream = (await runGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}']))?.trim()
86
+ if (!upstream) {
87
+ throw new Error('release: git push відхилено, а upstream для rebase немає — commit-back не приземлився')
88
+ }
89
+ const remote = upstream.includes('/') ? upstream.slice(0, upstream.indexOf('/')) : 'origin'
90
+ await runGit(['fetch', remote])
91
+ const rebased = await runGit(['rebase', upstream])
92
+ if (rebased === null) {
93
+ await runGit(['rebase', '--abort'])
94
+ throw new Error(`release: push відхилено і rebase на ${upstream} дав конфлікт — розв'яжи вручну`)
95
+ }
96
+ // після rebase хеш release-коміту змінився → пересуваємо теги на новий HEAD
97
+ for (const tag of tags) {
98
+ await runGit(['tag', '-f', tag])
99
+ }
100
+ }
101
+ throw new Error(
102
+ `release: git push не вдався після ${attempts} спроб (non-fast-forward?) — commit-back не приземлився, реліз неуспішний`
103
+ )
104
+ }
105
+
67
106
  /**
68
107
  * @param {object} [opts] опції
69
108
  * @param {string} [opts.cwd] корінь
@@ -112,7 +151,7 @@ export async function release(opts = {}) {
112
151
  for (const tag of tags) {
113
152
  await runGit(['tag', tag])
114
153
  }
115
- await runGit(['push', '--follow-tags'])
154
+ await pushReleaseWithRetry(runGit, tags)
116
155
  }
117
156
  return released
118
157
  }
package/rules/vue/vue.mdc CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: Vue
3
- version: '2.2'
3
+ version: '2.3'
4
4
  globs: "**/*.vue"
5
5
  alwaysApply: false
6
6
  ---
@@ -110,6 +110,24 @@ const additionalInstructions = `
110
110
  - **debounce/throttle** для частих подій.
111
111
  - Після ручних **addEventListener** / підписок — прибирай у **onUnmounted**.
112
112
 
113
+ ### Функції в шаблоні
114
+
115
+ Виклики функцій у шаблоні дозволені **лише** в обробниках подій (`@click`, `@change` тощо). У всіх інших місцях — `v-if`, `v-show`, атрибутах (`:prop`), інтерполяціях (`{{ }}`) — замінюй функції на `computed`-властивості: функція виконується при **кожному** render-і, тоді як `computed` кешується і перераховується лише при зміні залежностей.
116
+
117
+ ```vue
118
+ <!-- ❌ функція в умові, атрибуті та інтерполяції -->
119
+ <q-item v-if="getItems(order).length" :label="getLabel(item)">
120
+ {{ formatName(user) }}
121
+ </q-item>
122
+
123
+ <!-- ✅ реактивні змінні / computed / props -->
124
+ <q-item v-if="itemsMap[order.id].length" :label="item.label">
125
+ {{ user.displayName }}
126
+ </q-item>
127
+ <!-- обробник події — виклик функції дозволений -->
128
+ <q-btn @click="doSomething(item)" />
129
+ ```
130
+
113
131
  ### Безпека
114
132
 
115
133
  - Не довіряй **v-html** без санітизації; для форм/API — **CSRF**-захист за потреби; валідація **на сервері** обов’язкова.
@@ -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