@nitra/cursor 1.13.62 → 1.13.63

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
@@ -4,6 +4,19 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.13.63] - 2026-05-20
8
+
9
+ ### Fixed
10
+
11
+ - **`check changelog`**: новий local-only воркспейс (маніфест відсутній на merge-base з `dev`/`main`, напр. `demo/` на `main`) більше не вимагає штучного bump — достатньо початкової `version` і запису в `CHANGELOG.md` (раніше `Vbase === ∅` помилково трактувалось як «version не підвищено»).
12
+ - **`check changelog`**: на гілці **`main`** база порівняння — **`origin/main`** (або `HEAD~1` без remote), не `dev`; коли `origin/main` збігається з `HEAD`, diff порожній (не fallback на `HEAD~1`); feature-гілки — `merge-base` з `dev`, інакше з `main` (репо без `dev`).
13
+
14
+ ### Changed
15
+
16
+ - Правило **`changelog`** ([changelog.mdc](rules/changelog/changelog.mdc) `2.5`): блок **STOP** перенесено на початок (тригер шляхів, інверсія, три кроки до завершення відповіді) — щоб агент не пропускав bump після правок у `npm/skills/` тощо, коли чеклист губився внизу довгого alwaysApply-правила.
17
+ - **`.cursor/rules/scripts.mdc`**: секція «Завершення задачі після правок у пакетному workspace» — cross-STOP з **n-changelog** (останні кроки сесії перед відповіддю).
18
+ - **`hk.pkl`**: pre-commit крок **`npm-changelog`** (`glob: npm/**`, `bun ./npm/bin/n-cursor.js check changelog`) — програмний стоп-кран при commit, якщо агент забув bump.
19
+
7
20
  ## [1.13.62] - 2026-05-20
8
21
 
9
22
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.62",
3
+ "version": "1.13.63",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,9 +1,25 @@
1
1
  ---
2
2
  description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
3
- version: '2.4'
3
+ version: '2.5'
4
4
  alwaysApply: true
5
5
  ---
6
6
 
7
+ ## STOP — перед завершенням відповіді агента
8
+
9
+ > **Якщо в цій сесії ти змінив(ла) файли в пакетному workspace** (код, rego, правила, скіли, скрипти, конфіги, тести — **не** лише `docs/` / `doc/`) — **не завершуй задачу**, поки не виконаєш **усі три** кроки нижче в **тому ж** наборі змін. Це не «опційно після синку» — це частина PR.
10
+
11
+ 1. **`version`** у `<ws>/package.json` (або `[project].version` у `pyproject.toml`) → **patch +1** відносно `git show HEAD:<ws>/package.json`, якщо ще не піднято.
12
+ 2. **`CHANGELOG.md`** того workspace → **нова** секція `## [версія] - YYYY-MM-DD` **зверху** (не bullet-и в стару версію).
13
+ 3. **`npx @nitra/cursor check changelog`** (у репо `@nitra/cursor`: `bun ./npm/bin/n-cursor.js check changelog`) → exit **`0`**.
14
+
15
+ **Тригер шляхів (приклади):** `npm/**`, `packages/foo/**`, будь-який каталог з власним `package.json` / `pyproject.toml`, куди потрапили правки.
16
+
17
+ **Інверсія (bump не потрібен):** лише `docs/` / `doc/`; лише `.gitignore`; лише сам релізний крок (`CHANGELOG.md` + `version`).
18
+
19
+ **Pre-commit (людина):** `hk` у цьому репо також запускає `check changelog` при змінах під `npm/**` — агент не покладайся лише на commit hook; виконай кроки 1–3 **до** фінальної відповіді.
20
+
21
+ ---
22
+
7
23
  У кожному **пакетному** workspace (каталог із `package.json` або `pyproject.toml`) має бути власний **`CHANGELOG.md`**. Спільного на репозиторій змісту змін **не існує** — кожен пакет веде свій.
8
24
 
9
25
  **Маніфест версії:**
@@ -13,7 +29,9 @@ alwaysApply: true
13
29
 
14
30
  Каталоги лише з `pyproject.toml` (без `package.json`) теж враховуються; `node_modules/`, `.venv/`, `venv/` при пошуку ігноруються.
15
31
 
16
- ## Чеклист агента (будь-який репозиторій з цим правилом)
32
+ ## Чеклист агента (деталі)
33
+
34
+ Повний алгоритм — у блоці **STOP** вище; тут лише уточнення.
17
35
 
18
36
  **Інверсія (за замовчуванням не вимагають bump/CHANGELOG):**
19
37
 
@@ -21,13 +39,7 @@ alwaysApply: true
21
39
  - файли під **`.gitignore`**;
22
40
  - правки **лише** `CHANGELOG.md` або поля `version` у маніфесті як сам релізний крок.
23
41
 
24
- **Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, конфіги, тести тощо).
25
-
26
- Перед завершенням задачі:
27
-
28
- 1. **`version`** у `<ws>/package.json` або `[project].version` у `<ws>/pyproject.toml` підвищено (build/patch +1 на PR), якщо зміни входять у реліз.
29
- 2. **`CHANGELOG.md`** — **нова** секція `## [версія] - YYYY-MM-DD` зверху (не дописуй bullet-и в стару версію).
30
- 3. **`npx @nitra/cursor check changelog`** — exit `0`.
42
+ **Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, скіли, конфіги, тести тощо).
31
43
 
32
44
  Перевірка програмна (`changelog/fix/consistency/check.mjs`).
33
45
 
@@ -48,14 +60,14 @@ alwaysApply: true
48
60
 
49
61
  ### local-only
50
62
 
51
- **npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. База = **`dev` або `main`** (перша наявна), `git merge-base`:
63
+ **npm:** `private: true` або без `files`. **Python:** без пари name+version для реєстру. База залежить від гілки:
52
64
 
53
65
  1. На **`dev`** local-only не активний (крім незакомічених registry-published).
54
- 2. На feature-гілціbump + CHANGELOG **раз на PR**.
55
- 3. Після merge на інтеграційну гілку diff порожній pass.
56
- 4. Direct-commit на `main` ловиться так само.
66
+ 2. На **`main`**diff від **`origin/main`** (попередній опублікований `main`); без remote — від `HEAD~1`. **`dev` не використовується** як база на `main`.
67
+ 3. На **feature-гілці** — `merge-base` з **`dev`**, якщо є; інакше з **`main`** (репо без `dev`).
68
+ 4. Bump + CHANGELOG **раз на PR** / direct-commit на `main`.
57
69
 
58
- Якщо немає git або немає `dev`/`main` — local-only пропускається.
70
+ Якщо немає git або немає `dev`/`main`/`origin/main` — local-only пропускається.
59
71
 
60
72
  ## Формат CHANGELOG.md
61
73
 
@@ -10,8 +10,9 @@
10
10
  * Якщо локальна версія відрізняється — потрібен CHANGELOG; для npm також `"CHANGELOG.md"`
11
11
  * у `files`. Якщо версії збігаються, але в git є релевантні зміни без bump — fail.
12
12
  *
13
- * 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру): PR-scoped
14
- * перевірка проти `dev` / `main` через `git merge-base`.
13
+ * 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру):
14
+ * feature-гілка `merge-base` з `dev`, інакше з `main`; на `main` diff від
15
+ * `origin/main` (попередній опублікований main) або `HEAD~1` без remote.
15
16
  *
16
17
  * Усі `git` і зовнішні виклики — через `execFile` / `fetch`, без shell-інтерполяції.
17
18
  */
@@ -31,11 +32,11 @@ import {
31
32
 
32
33
  const execFileAsync = promisify(execFile)
33
34
 
34
- /** Кандидати інтеграційної гілки (перша наявна в репо; див. n-changelog.mdc) */
35
- const BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
35
+ /** Кандидати інтеграційної гілки для feature-гілок (перша наявна; див. n-changelog.mdc). */
36
+ const FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
36
37
 
37
- /** Гілки, на яких local-only перевірку пропускаємо (крім незакомічених registry-published). */
38
- const INTEGRATION_BRANCHES = Object.freeze(['dev', 'main'])
38
+ /** Гілка `dev`: local-only не активний (крім незакомічених registry-published). */
39
+ const LOCAL_ONLY_SKIP_BRANCH = 'dev'
39
40
 
40
41
  /** Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc). */
41
42
  const CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/'])
@@ -78,14 +79,6 @@ async function currentBranchName() {
78
79
  return typeof out === 'string' ? out.trim() : null
79
80
  }
80
81
 
81
- /**
82
- * @param {string | null} branch параметр
83
- * @returns {boolean} результат
84
- */
85
- function isIntegrationBranch(branch) {
86
- return branch !== null && INTEGRATION_BRANCHES.includes(branch)
87
- }
88
-
89
82
  /**
90
83
  * @param {string} ref параметр
91
84
  * @returns {string} результат
@@ -94,6 +87,30 @@ function baseRefLabel(ref) {
94
87
  return ref.startsWith('origin/') ? ref.slice('origin/'.length) : ref
95
88
  }
96
89
 
90
+ /**
91
+ * @param {string} ancestor предок
92
+ * @param {string} descendant нащадок
93
+ * @returns {Promise<boolean>} результат
94
+ */
95
+ async function isGitAncestor(ancestor, descendant) {
96
+ const out = await gitOrNull(['merge-base', '--is-ancestor', ancestor, descendant])
97
+ return typeof out === 'string' && out.trim() === 'true'
98
+ }
99
+
100
+ /**
101
+ * @param {string} branchName локальна або remote-tracking гілка
102
+ * @returns {Promise<string | null>} ref для git або null
103
+ */
104
+ async function resolveBranchRef(branchName) {
105
+ for (const ref of [branchName, `origin/${branchName}`]) {
106
+ const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
107
+ if (typeof out === 'string' && out.trim().length > 0) {
108
+ return ref
109
+ }
110
+ }
111
+ return null
112
+ }
113
+
97
114
  /**
98
115
  * @param {string} relPath параметр
99
116
  * @returns {boolean} результат
@@ -119,21 +136,6 @@ async function isPathGitIgnored(relPath) {
119
136
  }
120
137
  }
121
138
 
122
- /**
123
- * @returns {Promise<string | null>} результат
124
- */
125
- async function resolveBaseRef() {
126
- for (const name of BASE_BRANCH_CANDIDATES) {
127
- for (const ref of [name, `origin/${name}`]) {
128
- const out = await gitOrNull(['rev-parse', '--verify', '--quiet', ref])
129
- if (typeof out === 'string' && out.trim().length > 0) {
130
- return ref
131
- }
132
- }
133
- }
134
- return null
135
- }
136
-
137
139
  /**
138
140
  * @param {string} baseRef параметр
139
141
  * @returns {Promise<string | null>} результат
@@ -145,6 +147,47 @@ async function resolveMergeBase(baseRef) {
145
147
  return sha.length > 0 ? sha : null
146
148
  }
147
149
 
150
+ /**
151
+ * Точка порівняння git для changelog (ref або SHA для `git diff` / `git show`).
152
+ * @param {string | null} branch поточна гілка
153
+ * @returns {Promise<{ ref: string, label: string } | null>} результат
154
+ */
155
+ async function resolveChangelogComparisonPoint(branch) {
156
+ if (branch === LOCAL_ONLY_SKIP_BRANCH) {
157
+ return null
158
+ }
159
+
160
+ if (branch === 'main') {
161
+ const originMainSha = (await gitOrNull(['rev-parse', '--verify', '--quiet', 'origin/main']))?.trim()
162
+ const headSha = (await gitOrNull(['rev-parse', 'HEAD']))?.trim()
163
+ if (
164
+ originMainSha &&
165
+ headSha &&
166
+ (originMainSha === headSha || (await isGitAncestor('origin/main', 'HEAD')))
167
+ ) {
168
+ return { ref: 'origin/main', label: 'main' }
169
+ }
170
+ const parent = await gitOrNull(['rev-parse', '--verify', '--quiet', 'HEAD~1'])
171
+ if (typeof parent === 'string' && parent.trim().length > 0) {
172
+ return { ref: parent.trim(), label: 'main~1' }
173
+ }
174
+ return null
175
+ }
176
+
177
+ for (const name of FEATURE_BASE_BRANCH_CANDIDATES) {
178
+ const baseRef = await resolveBranchRef(name)
179
+ if (!baseRef) {
180
+ continue
181
+ }
182
+ const mergeBase = await resolveMergeBase(baseRef)
183
+ if (!mergeBase) {
184
+ continue
185
+ }
186
+ return { ref: mergeBase, label: baseRefLabel(baseRef) }
187
+ }
188
+ return null
189
+ }
190
+
148
191
  /**
149
192
  * @param {string} ws параметр
150
193
  * @param {string[]} subWorkspaces параметр
@@ -355,7 +398,8 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
355
398
  }
356
399
 
357
400
  const branch = await currentBranchName()
358
- if (isIntegrationBranch(branch)) {
401
+
402
+ if (branch === LOCAL_ONLY_SKIP_BRANCH) {
359
403
  if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces)) {
360
404
  fail(
361
405
  `${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
@@ -365,28 +409,32 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
365
409
  return
366
410
  }
367
411
 
368
- const baseRef = await resolveBaseRef()
369
- if (!baseRef) {
370
- return
371
- }
372
- const mergeBase = await resolveMergeBase(baseRef)
373
- if (!mergeBase) {
374
- return
375
- }
376
- if (!(await workspaceHasRelevantChangesAgainstBase(mergeBase, manifest.ws, subWorkspaces))) {
377
- return
412
+ const comparison = await resolveChangelogComparisonPoint(branch)
413
+ if (comparison && (await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) {
414
+ const Vbase = await readBaseVersion(comparison.ref, manifest)
415
+ const baseLabel = comparison.label
416
+ if (Vbase === null) {
417
+ pass(
418
+ `${label}: новий registry-published воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`
419
+ )
420
+ await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail)
421
+ checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
422
+ } else if (Vbase === Vcurrent) {
423
+ fail(
424
+ `${label}: у цій гілці є зміни в registry-published пакеті, але version у ${mf} ` +
425
+ `не підвищено (на ${baseLabel} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові (n-changelog.mdc)`
426
+ )
427
+ } else {
428
+ pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
429
+ }
378
430
  }
379
431
 
380
- const Vbase = await readBaseVersion(mergeBase, manifest)
381
- const baseLabel = baseRefLabel(baseRef)
382
- if (Vbase === null || Vbase === Vcurrent) {
432
+ if (branch === 'main' && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces))) {
383
433
  fail(
384
- `${label}: у цій гілці є зміни в registry-published пакеті, але version у ${mf} ` +
385
- `не підвищено (на ${baseLabel} ${Vbase ?? '∅'}). Bump + запис у CHANGELOG.md обов'язкові на PR (n-changelog.mdc)`
434
+ `${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
435
+ `Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
386
436
  )
387
- return
388
437
  }
389
- pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
390
438
  }
391
439
 
392
440
  /**
@@ -426,13 +474,13 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
426
474
  }
427
475
 
428
476
  /**
429
- * @param {string} mergeBase параметр
477
+ * @param {string} comparisonRef ref/SHA для `git diff` / `git show`
430
478
  * @param {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest} manifest параметр
431
479
  * @param {string} baseLabel параметр
432
480
  * @param {(msg: string) => void} pass параметр
433
481
  * @param {(msg: string) => void} fail параметр
434
482
  */
435
- async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pass, fail) {
483
+ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail) {
436
484
  const label = workspaceLabel(manifest)
437
485
  const mf = manifestFilePath(manifest.ws, manifest)
438
486
  const Vcurrent = manifest.version
@@ -440,10 +488,16 @@ async function checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pa
440
488
  fail(`${label}: у ${mf} відсутнє поле version (потрібне для запису в CHANGELOG)`)
441
489
  return
442
490
  }
443
- const Vbase = await readBaseVersion(mergeBase, manifest)
444
- if (Vbase === null || Vbase === Vcurrent) {
491
+ const Vbase = await readBaseVersion(comparisonRef, manifest)
492
+ if (Vbase === null) {
493
+ pass(`${label}: новий воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`)
494
+ if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail))) return
495
+ checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
496
+ return
497
+ }
498
+ if (Vbase === Vcurrent) {
445
499
  fail(
446
- `${label}: у цій гілці є зміни, але version у ${mf} не підвищено (на ${baseLabel} — ${Vbase ?? '∅'}). Bump + запис у CHANGELOG.md обов'язкові на PR`
500
+ `${label}: у цій гілці є зміни, але version у ${mf} не підвищено (на ${baseLabel} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові на PR`
447
501
  )
448
502
  return
449
503
  }
@@ -466,30 +520,24 @@ async function runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail) {
466
520
  return
467
521
  }
468
522
  const branch = await currentBranchName()
469
- if (branch === 'dev') {
523
+ if (branch === LOCAL_ONLY_SKIP_BRANCH) {
470
524
  pass('changelog: поточна гілка = dev — local-only перевірку пропущено')
471
525
  return
472
526
  }
473
- const baseRef = await resolveBaseRef()
474
- if (!baseRef) {
527
+ const comparison = await resolveChangelogComparisonPoint(branch)
528
+ if (!comparison) {
475
529
  pass('changelog: ref dev/main (та origin/*) не знайдено — local-only перевірку пропущено')
476
530
  return
477
531
  }
478
- const mergeBase = await resolveMergeBase(baseRef)
479
- if (!mergeBase) {
480
- pass(`changelog: merge-base з ${baseRef} не знайдено — local-only перевірку пропущено`)
481
- return
482
- }
483
532
 
484
- const baseLabel = baseRefLabel(baseRef)
485
533
  let checkedAny = false
486
534
  for (const manifest of localOnly) {
487
- if (!(await workspaceHasRelevantChangesAgainstBase(mergeBase, manifest.ws, subWorkspaces))) continue
535
+ if (!(await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces))) continue
488
536
  checkedAny = true
489
- await checkLocalOnlyChangedWorkspace(mergeBase, manifest, baseLabel, pass, fail)
537
+ await checkLocalOnlyChangedWorkspace(comparison.ref, manifest, comparison.label, pass, fail)
490
538
  }
491
539
  if (!checkedAny) {
492
- pass(`changelog: local-only воркспейси без змін відносно merge-base(${baseRef})`)
540
+ pass(`changelog: local-only воркспейси без змін відносно ${comparison.label}`)
493
541
  }
494
542
  }
495
543