@nitra/cursor 1.13.66 → 1.13.68

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.
@@ -165,6 +165,8 @@ PROMPT_HEADER=$(cat <<'EOF'
165
165
  ]
166
166
  }
167
167
 
168
+ Принцип вибору операції: уникай дрібних дублів. Перш ніж обрати `rewrite`, звір тему драфта з clean-списком і з рештою драфтів цього батча. Якщо рішення по суті вже зафіксоване — у наявному clean-ADR або в драфті, який ти переписуєш через `rewrite`, — і драфт лише уточнює / доповнює / виправляє / продовжує його, обери `merge-into`, а не `rewrite`. `rewrite` (новий файл) виправданий лише для справді самостійного, нового рішення. Краще один повний наскрізний ADR, ніж кілька майже однакових файлів.
169
+
168
170
  Правила:
169
171
 
170
172
  1. `delete` — драфт тривіальний / повністю покритий іншим існуючим clean-ADR-ом / порожній. Поясни короткою причиною українською.
@@ -180,14 +182,17 @@ PROMPT_HEADER=$(cat <<'EOF'
180
182
  - У `## More Information` перенеси файли, команди, публічні API, конфіги й transcript facts. Якщо нема — `Додаткової інформації в transcript не зафіксовано.`
181
183
  - `slug` — kebab-case українською (наприклад `ланцюжок-запуску-abie`, `npm-publish-flow`). Без розширення `.md`. Літери малі, дозволено цифри, дефіс, кирилиця. Якщо тема технічна англійською (назва пакету, ключове слово) — лиши англійською без транслітерації.
182
184
 
183
- 3. `merge-into` — драфт повторює тему вже існуючого clean-файлу зі списку нижче. `target` точна назва файлу зі списку (з `.md`). `additions` — лише новий зміст, який варто дописати в кінець target-файлу під підзаголовком `## Update YYYY-MM-DD` (date з `captured` драфта). Якщо нічого нового додати використовуй `delete`.
185
+ 3. `merge-into` — рішення драфта НЕ самостійне: воно лише уточнює, доповнює, виправляє або продовжує рішення, яке вже зафіксоване або (а) в clean-файлі зі списку нижче, або (б) у драфті цього ж батча, який ти переписуєш через `rewrite`. `target`:
186
+ - для (а) — точна назва clean-файлу зі списку (з `.md`);
187
+ - для (б) — `<slug>.md`, де `<slug>` дорівнює полю `slug` тієї `rewrite`-операції (timestamp-префікс скрипт додасть сам — не дописуй його).
188
+ `additions` — лише новий зміст, який варто дописати в кінець target-файлу під підзаголовком `## Update YYYY-MM-DD` (date з `captured` драфта). Якщо нічого нового додати — використовуй `delete`.
184
189
 
185
190
  Жорсткі обмеження:
186
191
 
187
192
  - Поверни валідний JSON, нічого крім нього. Жодних code-fence, жодних коментарів.
188
193
  - Кожен файл з вхідного списку має зʼявитися у `operations` рівно один раз.
189
194
  - Слаги не повторювати між операціями того самого батча. Якщо дві чернетки про одну тему — одна `rewrite`, інша `merge-into target: <slug>.md` з тим самим slug-ом.
190
- - Не вигадуй target, якого нема у списку clean-файлів.
195
+ - `target` у `merge-into` це або файл зі списку clean-файлів, або `<slug>.md` rewrite-операції цього ж батча. Іншого target не вигадуй.
191
196
  - Не вигадуй альтернативи, decision drivers, наслідки, людей або зовнішній контекст. Якщо даних бракує — явно напиши, що transcript цього не містить.
192
197
 
193
198
  Вхідні драфти і clean-список — нижче.
@@ -278,7 +283,22 @@ resolve_unique_slug_path() {
278
283
  APPLIED=0
279
284
  SKIPPED=0
280
285
 
281
- jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
286
+ # Apply operations in two ordered groups delete/rewrite first, merge-into
287
+ # last — so a merge-into can target a clean file that a rewrite of the same
288
+ # batch only just created. Looping over a file (not a pipe) keeps the loop in
289
+ # the main shell, so APPLIED/SKIPPED survive to the final summary line.
290
+ OPS_FILE="$TMP_DIR/ops.jsonl"
291
+ {
292
+ jq -c '.operations[] | select(.op != "merge-into")' "$RESPONSE_CLEAN_FILE"
293
+ jq -c '.operations[] | select(.op == "merge-into")' "$RESPONSE_CLEAN_FILE"
294
+ } > "$OPS_FILE"
295
+
296
+ # slug → created clean-file path: written by rewrite ops, read by merge-into
297
+ # ops (one tab-separated "slug<TAB>path" line per rewrite).
298
+ SLUG_MAP="$TMP_DIR/slug-map.txt"
299
+ : > "$SLUG_MAP"
300
+
301
+ while IFS= read -r op_json; do
282
302
  OP=$(printf '%s' "$op_json" | jq -r '.op // empty')
283
303
  FILE=$(printf '%s' "$op_json" | jq -r '.file // empty')
284
304
  SRC_PATH="$ADR_DIR/$FILE"
@@ -349,6 +369,9 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
349
369
  DEST_PATH=$(resolve_unique_slug_path "$DEST_SLUG")
350
370
  printf '%s\n' "$CONTENT" > "$DEST_PATH"
351
371
  rm -- "$SRC_PATH"
372
+ # Record bare slug → final path so a same-batch merge-into can target
373
+ # this freshly created file by `<slug>.md` despite the timestamp prefix.
374
+ printf '%s\t%s\n' "$SLUG" "$DEST_PATH" >> "$SLUG_MAP"
352
375
  log "rewrite: $FILE → $(basename "$DEST_PATH")"
353
376
  APPLIED=$(( APPLIED + 1 ))
354
377
  ;;
@@ -367,7 +390,32 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
367
390
  continue
368
391
  ;;
369
392
  esac
393
+ # Resolve the target clean file. The LLM gives a bare `<slug>.md`, but the
394
+ # real file usually carries a `YYYYMMDD-HHMMSS-` prefix. Try, in order:
395
+ # 1. exact name in docs/adr/,
396
+ # 2. a rewrite of this batch that produced that slug (SLUG_MAP),
397
+ # 3. a unique existing clean file whose name ends with `-<slug>.md`.
370
398
  TARGET_PATH="$ADR_DIR/$TARGET"
399
+ if [ ! -f "$TARGET_PATH" ]; then
400
+ TSLUG="${TARGET%.md}"
401
+ MAPPED=$(awk -F'\t' -v s="$TSLUG" '$1 == s { print $2; exit }' "$SLUG_MAP")
402
+ if [ -z "$MAPPED" ]; then
403
+ SUFFIX_HITS=0
404
+ for cf in "$ADR_DIR"/*-"$TSLUG".md; do
405
+ [ -f "$cf" ] || continue
406
+ MAPPED="$cf"
407
+ SUFFIX_HITS=$(( SUFFIX_HITS + 1 ))
408
+ done
409
+ if [ "$SUFFIX_HITS" -gt 1 ]; then
410
+ log "skip merge-into: target '$TARGET' ambiguous ($SUFFIX_HITS matches)"
411
+ SKIPPED=$(( SKIPPED + 1 ))
412
+ continue
413
+ fi
414
+ fi
415
+ if [ -n "$MAPPED" ]; then
416
+ TARGET_PATH="$MAPPED"
417
+ fi
418
+ fi
371
419
  if [ ! -f "$TARGET_PATH" ]; then
372
420
  log "skip merge-into: target '$TARGET' missing"
373
421
  SKIPPED=$(( SKIPPED + 1 ))
@@ -380,7 +428,7 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
380
428
  fi
381
429
  printf '\n%s\n' "$ADDITIONS" >> "$TARGET_PATH"
382
430
  rm -- "$SRC_PATH"
383
- log "merge-into: $FILE → $TARGET"
431
+ log "merge-into: $FILE → $(basename "$TARGET_PATH")"
384
432
  APPLIED=$(( APPLIED + 1 ))
385
433
  ;;
386
434
  *)
@@ -388,6 +436,6 @@ jq -c '.operations[]' "$RESPONSE_CLEAN_FILE" | while IFS= read -r op_json; do
388
436
  SKIPPED=$(( SKIPPED + 1 ))
389
437
  ;;
390
438
  esac
391
- done
439
+ done < "$OPS_FILE"
392
440
 
393
- log "done"
441
+ log "done (applied $APPLIED, skipped $SKIPPED)"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@
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.68] - 2026-05-21
8
+
9
+ ### Changed
10
+
11
+ - ADR-хук **`normalize-decisions.sh`**: нормалізація тепер активніше повторно використовує наявні ADR замість створення нових файлів. У промпт додано принцип вибору операції — перш ніж `rewrite` (новий файл), агент звіряє тему драфта з clean-списком і рештою драфтів батча; якщо рішення по суті вже зафіксоване і драфт лише уточнює/доповнює/виправляє його — обирає `merge-into`. Правило `merge-into` тепер явно дозволяє `target` двох видів: clean-файл зі списку або `<slug>.md` `rewrite`-операції цього ж батча; суперечливе обмеження «не вигадуй target поза clean-списком» узгоджено з цим. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
12
+
13
+ ### Fixed
14
+
15
+ - ADR-хук **`normalize-decisions.sh`**: `merge-into` більше не падає в `skip … target missing`, коли драфт треба влити в clean-ADR, який створює `rewrite` того самого батча, або в наявний clean-ADR, на який LLM послався голим `<slug>.md` без timestamp-префікса. Операції тепер застосовуються двома впорядкованими групами (спершу `delete`/`rewrite`, потім `merge-into`), а `target` резолвиться за трьома кроками: точна назва → slug-мапа rewrite-ів цього батча → єдиний наявний clean-файл із суфіксом `-<slug>.md`. Цикл застосування переведено з pipe на читання з файлу — лічильники `applied`/`skipped` виживають і потрапляють у фінальний рядок логу `done (applied N, skipped M)`. Зачеплено: [normalize-decisions.sh](.claude-template/hooks/normalize-decisions.sh).
16
+
17
+ ## [1.13.67] - 2026-05-21
18
+
19
+ ### Changed
20
+
21
+ - Правило **`changelog`**: перевірка `changelog/consistency` більше не вимагає version-bump і запису в `CHANGELOG.md` за зміни синхронізованого з `@nitra/cursor` інструментарію. Інверсію шляхів розширено: до `docs/` / `doc/` додано префікси `.cursor/` (канонічні правила та скіли) і `.claude/` (ADR-хуки). Причина: синк tooling-пакета — це дзеркало `@nitra/cursor`, а не зміна логіки воркспейсу, тож раніше кожен `npx @nitra/cursor` тягнув за собою зайвий bump і секцію CHANGELOG, де описувалося лише оновлення інструментарію. Кореневі `AGENTS.md` / `CLAUDE.md` окремого запису в інверсії не потребують — їх покриває пропуск кореня монорепо (нижче). Джерело правил у самому репо `@nitra/cursor` лежить під `npm/`, тож на нього інверсія не поширюється — реальні зміни правил і далі вимагають bump. Зачеплено: [check.mjs](rules/changelog/fix/consistency/check.mjs) (`CHANGELOG_IGNORE_PATH_PREFIXES`), [changelog.mdc](rules/changelog/changelog.mdc) (секція «Інверсія», bump `2.5` → `2.6`).
22
+ - Правило **`changelog`**: корінь монорепо (воркспейс `.` за наявності підпакетів) більше не перевіряється на bump/CHANGELOG. Причина: кореневий `package.json` монорепо — це glue/конфіг/tooling (`private`, `workspaces`), власного продуктового CHANGELOG він не веде, а помітні зміни документують підпакети. Раніше будь-яка правка в корені (конфіги, синк правил, bump `@nitra/cursor` у `devDependencies`) хибно вимагала bump кореневої `version`. Одно-пакетні репозиторії (корінь = єдиний воркспейс) перевіряються як і раніше. Зачеплено: [check.mjs](rules/changelog/fix/consistency/check.mjs) (`isMonorepoRoot` у `check()`), [changelog.mdc](rules/changelog/changelog.mdc).
23
+
24
+ ### Fixed
25
+
26
+ - Правило **`changelog`**: перевірка `changelog/consistency` коректно опрацьовує файли з не-ASCII іменами (кирилиця тощо). `git diff` / `git ls-files` без `-z` застосовують `core.quotePath` і повертають такі шляхи у C-quoted формі `"docs/\320\262..."` — рядок не збігався з префіксами інверсії, тож, наприклад, чернетка ADR з кириличною назвою під `docs/` хибно вважалася зміною, що потребує bump, і валила перевірку. Усі переліки шляхів тепер читаються через `-z` (`NUL`-розділення, без quoting). Зачеплено: [check.mjs](rules/changelog/fix/consistency/check.mjs) (`splitNulPaths`, `listChangedPathsAgainstBase`), [check.test.mjs](rules/changelog/fix/consistency/check.test.mjs) (тести quotePath, синку tooling і пропуску кореня монорепо).
27
+
7
28
  ## [1.13.66] - 2026-05-20
8
29
 
9
30
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.13.66",
3
+ "version": "1.13.68",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
3
- version: '2.5'
3
+ version: '2.6'
4
4
  alwaysApply: true
5
5
  ---
6
6
 
@@ -14,7 +14,7 @@ alwaysApply: true
14
14
 
15
15
  **Тригер шляхів (приклади):** `npm/**`, `packages/foo/**`, будь-який каталог з власним `package.json` / `pyproject.toml`, куди потрапили правки.
16
16
 
17
- **Інверсія (bump не потрібен):** лише `docs/` / `doc/`; лише `.gitignore`; лише сам релізний крок (`CHANGELOG.md` + `version`).
17
+ **Інверсія (bump не потрібен):** лише `docs/` / `doc/`; синхронізований із `@nitra/cursor` інструментарій (`.cursor/`, `.claude/`); лише `.gitignore`; лише сам релізний крок (`CHANGELOG.md` + `version`). **Корінь монорепо** (воркспейс `.` за наявності підпакетів) не перевіряється взагалі — отже й кореневі `AGENTS.md` / `CLAUDE.md` та bump `@nitra/cursor` у `devDependencies`.
18
18
 
19
19
  **Pre-commit (людина):** `hk` у цьому репо також запускає `check changelog` при змінах під `npm/**` — агент не покладайся лише на commit hook; виконай кроки 1–3 **до** фінальної відповіді.
20
20
 
@@ -36,10 +36,12 @@ alwaysApply: true
36
36
  **Інверсія (за замовчуванням не вимагають bump/CHANGELOG):**
37
37
 
38
38
  - зміни **лише** під `docs/` або `doc/`;
39
+ - синхронізований із `@nitra/cursor` інструментарій під `.cursor/` (канонічні правила та скіли) і `.claude/` (ADR-хуки) — це дзеркало tooling-пакета, а не логіка воркспейсу;
40
+ - будь-які зміни в **корені монорепо** (воркспейс `.` за наявності підпакетів) — корінь веде glue/конфіг/tooling, власного CHANGELOG не має; помітні зміни документують підпакети. Сюди потрапляють і кореневі `AGENTS.md` / `CLAUDE.md`, і bump `@nitra/cursor` у `devDependencies`;
39
41
  - файли під **`.gitignore`**;
40
42
  - правки **лише** `CHANGELOG.md` або поля `version` у маніфесті як сам релізний крок.
41
43
 
42
- **Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, скіли, конфіги, тести тощо).
44
+ **Вимагають bump + нову секцію CHANGELOG** — усі інші зміни в каталозі workspace (код, rego, правила, скіли, конфіги, тести тощо). Виняток `.cursor/` / `.claude/` **не** поширюється на джерело правил у репо `@nitra/cursor` — воно лежить під `npm/`, тож зміни в ньому далі вимагають bump.
43
45
 
44
46
  Перевірка програмна (`changelog/fix/consistency/check.mjs`).
45
47
 
@@ -38,11 +38,14 @@ const FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])
38
38
  /** Гілка `dev`: local-only не активний (крім незакомічених registry-published). */
39
39
  const LOCAL_ONLY_SKIP_BRANCH = 'dev'
40
40
 
41
- /** Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc). */
42
- const CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/'])
43
-
44
- /** Точні шляхи каталогів документації (posix), без bump. */
45
- const CHANGELOG_IGNORE_PATH_EXACT = Object.freeze(['docs', 'doc'])
41
+ /**
42
+ * Префікси шляхів (posix), які не вважаються релізними змінами — інверсія glob (n-changelog.mdc):
43
+ * документація (`docs/`, `doc/`) та синхронізований із `@nitra/cursor` інструментарій
44
+ * (`.cursor/` канонічні правила й скіли, `.claude/` — ADR-хуки). Останнє — дзеркало tooling-пакета,
45
+ * не логіка самого воркспейсу, тож bump CHANGELOG не потрібен. Джерело правил у репо `@nitra/cursor`
46
+ * лежить під `npm/`, тож на нього ця інверсія не поширюється.
47
+ */
48
+ const CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/', '.cursor/', '.claude/'])
46
49
 
47
50
  /** Таймаут на `npm view` / PyPI (мс) */
48
51
  const REGISTRY_TIMEOUT_MS = 10_000
@@ -117,9 +120,6 @@ async function resolveBranchRef(branchName) {
117
120
  */
118
121
  function isChangelogIgnoredPath(relPath) {
119
122
  const p = relPath.replaceAll('\\', '/').replace(LEADING_DOTSLASH_RE, '')
120
- if (CHANGELOG_IGNORE_PATH_EXACT.includes(p)) {
121
- return true
122
- }
123
123
  return CHANGELOG_IGNORE_PATH_PREFIXES.some(prefix => p.startsWith(prefix))
124
124
  }
125
125
 
@@ -196,29 +196,31 @@ function pathspecForWorkspace(ws, subWorkspaces) {
196
196
  return ['.', ...subWorkspaces.filter(s => s !== '.').map(s => `:(exclude)${s}/`)]
197
197
  }
198
198
 
199
+ /**
200
+ * Шляхи з `NUL`-розділеного виводу git (прапорець `-z`).
201
+ *
202
+ * `-z` критичний: без нього git застосовує `core.quotePath` і повертає не-ASCII імена файлів
203
+ * (кирилиця тощо) у C-quoted формі `"docs/\320\262..."`. Такий рядок не збігається з
204
+ * префіксами інверсії (`docs/`, `.cursor/`, ...), тож файл хибно вважався б зміною, що потребує bump.
205
+ * @param {string | null} nulSeparated сирий вивід git або `null`
206
+ * @returns {string[]} шляхи без обгортки/escape
207
+ */
208
+ function splitNulPaths(nulSeparated) {
209
+ if (typeof nulSeparated !== 'string') {
210
+ return []
211
+ }
212
+ return nulSeparated.split('\0').filter(p => p.length > 0)
213
+ }
214
+
199
215
  /**
200
216
  * @param {string} baseRef параметр
201
217
  * @param {string[]} pathspec параметр
202
218
  * @returns {Promise<string[]>} результат
203
219
  */
204
220
  async function listChangedPathsAgainstBase(baseRef, pathspec) {
205
- /**
206
- @type {string[]}
207
- */
208
- const out = []
209
- const diffArgs =
210
- baseRef === 'HEAD'
211
- ? ['diff', '--name-only', 'HEAD', '--', ...pathspec]
212
- : ['diff', '--name-only', baseRef, '--', ...pathspec]
213
- const diffOut = await gitOrNull(diffArgs)
214
- if (typeof diffOut === 'string' && diffOut.trim().length > 0) {
215
- out.push(...diffOut.trim().split('\n'))
216
- }
217
- const untrackedOut = await gitOrNull(['ls-files', '--others', '--exclude-standard', '--', ...pathspec])
218
- if (typeof untrackedOut === 'string' && untrackedOut.trim().length > 0) {
219
- out.push(...untrackedOut.trim().split('\n'))
220
- }
221
- return [...new Set(out)]
221
+ const diffOut = await gitOrNull(['diff', '--name-only', '-z', baseRef, '--', ...pathspec])
222
+ const untrackedOut = await gitOrNull(['ls-files', '--others', '--exclude-standard', '-z', '--', ...pathspec])
223
+ return [...new Set([...splitNulPaths(diffOut), ...splitNulPaths(untrackedOut)])]
222
224
  }
223
225
 
224
226
  /**
@@ -551,6 +553,9 @@ export async function check(opts = {}) {
551
553
 
552
554
  const workspaces = await getMonorepoProjectRootDirs(process.cwd())
553
555
  const subWorkspaces = workspaces.filter(w => w !== '.')
556
+ // Корінь монорепо (`.` за наявності підпакетів) — це glue/конфіг/tooling, а не логіка
557
+ // продукту: власного CHANGELOG він не веде, помітні зміни документують підпакети.
558
+ const isMonorepoRoot = subWorkspaces.length > 0
554
559
 
555
560
  /**
556
561
  @type {import('../../../../scripts/utils/package-manifest.mjs').PackageManifest[]}
@@ -562,6 +567,12 @@ export async function check(opts = {}) {
562
567
  const localOnly = []
563
568
 
564
569
  for (const ws of workspaces) {
570
+ if (ws === '.' && isMonorepoRoot) {
571
+ pass(
572
+ '<root>: корінь монорепо (glue/конфіг/tooling) — перевірку CHANGELOG пропущено; помітні зміни документують підпакети'
573
+ )
574
+ continue
575
+ }
565
576
  const manifest = await readPackageManifest(ws)
566
577
  if (!manifest) {
567
578
  continue