@nitra/cursor 1.17.4 → 1.18.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.
Files changed (49) hide show
  1. package/.claude-template/hooks/capture-decisions.sh +79 -0
  2. package/.claude-template/hooks/normalize-decisions.sh +98 -0
  3. package/CHANGELOG.md +29 -0
  4. package/bin/rename-yaml-extensions.mjs +1 -1
  5. package/package.json +1 -1
  6. package/rules/abie/fix.mjs +1 -1
  7. package/rules/adr/adr.mdc +5 -1
  8. package/rules/adr/fix.mjs +1 -1
  9. package/rules/bun/fix.mjs +1 -1
  10. package/rules/capacitor/fix.mjs +1 -1
  11. package/rules/changelog/fix.mjs +1 -1
  12. package/rules/ci4/fix.mjs +1 -1
  13. package/rules/docker/fix.mjs +1 -1
  14. package/rules/docker/lint/lint.mjs +1 -1
  15. package/rules/efes/fix.mjs +1 -1
  16. package/rules/feedback/fix.mjs +1 -1
  17. package/rules/ga/fix.mjs +1 -1
  18. package/rules/graphql/fix.mjs +1 -1
  19. package/rules/hasura/fix.mjs +1 -1
  20. package/rules/image-avif/fix.mjs +1 -1
  21. package/rules/image-compress/fix.mjs +1 -1
  22. package/rules/js-bun-db/fix.mjs +1 -1
  23. package/rules/js-bun-redis/fix.mjs +1 -1
  24. package/rules/js-lint/fix.mjs +1 -1
  25. package/rules/js-mssql/fix.mjs +1 -1
  26. package/rules/js-run/fix.mjs +1 -1
  27. package/rules/k8s/fix.mjs +1 -1
  28. package/rules/k8s/lint/lint.mjs +1 -1
  29. package/rules/nginx-default-tpl/fix.mjs +1 -1
  30. package/rules/npm-module/fix.mjs +1 -1
  31. package/rules/php/fix.mjs +1 -1
  32. package/rules/php/lint/lint.mjs +1 -1
  33. package/rules/rego/fix.mjs +1 -1
  34. package/rules/rego/lint/lint.mjs +1 -1
  35. package/rules/rust/fix.mjs +1 -1
  36. package/rules/security/fix.mjs +1 -1
  37. package/rules/style-lint/fix.mjs +1 -1
  38. package/rules/tauri/fix.mjs +1 -1
  39. package/rules/test/fix.mjs +1 -1
  40. package/rules/text/fix.mjs +1 -1
  41. package/rules/text/lint/run-dotenv-linter.mjs +1 -1
  42. package/rules/text/lint/run-shellcheck.mjs +1 -1
  43. package/rules/text/lint/run-v8r.mjs +1 -1
  44. package/rules/text/policy/cspell/template/.cspell.json.snippet.json +2 -1
  45. package/rules/text/text.mdc +5 -3
  46. package/rules/vue/fix.mjs +1 -1
  47. package/scripts/cli-entry.mjs +19 -9
  48. package/scripts/lib/run-rule-cli.mjs +5 -11
  49. package/skills/adr-normalize/SKILL.md +2 -0
@@ -34,6 +34,62 @@ mkdir -p "$ADR_DIR" "$LOG_DIR"
34
34
 
35
35
  log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
36
36
 
37
+ # Структурний скіп ADR-генерації для "tooling-only" сесій.
38
+ # Вхід: рядки-шляхи у stdin (один шлях на лінію), відносні до $PROJECT_ROOT
39
+ # або абсолютні з префіксом $PROJECT_ROOT (нормалізуємо тут).
40
+ # Вихід: 0 — усі шляхи в allowlist; 1 — є хоч один змістовний шлях.
41
+ # Bash 3.2: без mapfile/асоц. масивів.
42
+ is_tooling_only_change() {
43
+ local proj="$1"
44
+ local had_file=0
45
+ local f rel
46
+ while IFS= read -r f; do
47
+ [ -z "$f" ] && continue
48
+ had_file=1
49
+ case "$f" in
50
+ "$proj"/*) rel="${f#"$proj"/}" ;;
51
+ /*) return 1 ;;
52
+ *) rel="$f" ;;
53
+ esac
54
+ case "$rel" in
55
+ .cspell.json) ;;
56
+ docs/adr/*.md) ;;
57
+ AGENTS.md|CLAUDE.md) ;;
58
+ CHANGELOG.md) ;;
59
+ */CHANGELOG.md) ;;
60
+ package.json|*/package.json)
61
+ if ! git_diff_only_version_field "$proj" "$rel"; then
62
+ return 1
63
+ fi
64
+ ;;
65
+ *) return 1 ;;
66
+ esac
67
+ done
68
+ [ "$had_file" = "1" ] && return 0
69
+ return 1
70
+ }
71
+
72
+ # Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
73
+ # Поза git-репо або при помилці — вертаємо 1 (не tooling).
74
+ git_diff_only_version_field() {
75
+ local proj="$1" path="$2"
76
+ [ -d "$proj/.git" ] || return 1
77
+ local diff
78
+ diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
79
+ [ -z "$diff" ] && return 1
80
+ local line
81
+ while IFS= read -r line; do
82
+ case "$line" in
83
+ '+++ '*|'--- '*|'@@ '*|'') continue ;;
84
+ [+-]*'"version":'*) continue ;;
85
+ [+-]*) return 1 ;;
86
+ esac
87
+ done <<EOF
88
+ $diff
89
+ EOF
90
+ return 0
91
+ }
92
+
37
93
  log "fired: $SESSION_ID"
38
94
 
39
95
  if [[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]]; then
@@ -86,6 +142,29 @@ if [[ -z "$TRANSCRIPT" ]]; then
86
142
  exit 0
87
143
  fi
88
144
 
145
+ # Structural skip: якщо в сесії змінювалися лише tooling-файли — не викликаємо LLM.
146
+ # ENV `ADR_NORMALIZE_SKIP_TOOLING_ONLY=0` вимикає скіп.
147
+ if [[ "${ADR_NORMALIZE_SKIP_TOOLING_ONLY:-1}" = "1" ]]; then
148
+ CHANGED_FILES=$(jq -r '
149
+ select(.type == "assistant" or .role == "assistant")
150
+ | .message as $m
151
+ | ($m.content // [])
152
+ | if type == "array" then
153
+ map(select(.type == "tool_use" and (.name == "Edit" or .name == "Write" or .name == "MultiEdit"))
154
+ | .input.file_path // empty)
155
+ | .[]
156
+ else empty end
157
+ ' "$TRANSCRIPT_PATH" 2>/dev/null | sort -u || true)
158
+
159
+ if [[ -n "$CHANGED_FILES" ]]; then
160
+ if printf '%s\n' "$CHANGED_FILES" | is_tooling_only_change "$PROJECT_ROOT"; then
161
+ log " → skipping ADR capture: tooling-only session"
162
+ log " files: $(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')"
163
+ exit 0
164
+ fi
165
+ fi
166
+ fi
167
+
89
168
  PROMPT=$(cat <<'EOF'
90
169
  You analyze an AI coding session transcript and produce durable decision documentation.
91
170
 
@@ -39,6 +39,65 @@ mkdir -p "$LOG_DIR"
39
39
 
40
40
  log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
41
41
 
42
+ # Структурний скіп ADR-нормалізації для "tooling-only" сесій.
43
+ # Дублікат із capture-decisions.sh: `.claude-template/hooks/` копіюється плоско,
44
+ # спільний helper-файл туди не вписується.
45
+ is_tooling_only_change() {
46
+ proj="$1"
47
+ had_file=0
48
+ while IFS= read -r f; do
49
+ [ -z "$f" ] && continue
50
+ had_file=1
51
+ case "$f" in
52
+ "$proj"/*) rel="${f#"$proj"/}" ;;
53
+ /*) return 1 ;;
54
+ *) rel="$f" ;;
55
+ esac
56
+ case "$rel" in
57
+ .cspell.json) ;;
58
+ docs/adr/*.md) ;;
59
+ AGENTS.md|CLAUDE.md) ;;
60
+ CHANGELOG.md) ;;
61
+ */CHANGELOG.md) ;;
62
+ package.json|*/package.json)
63
+ if ! git_diff_only_version_field "$proj" "$rel"; then
64
+ return 1
65
+ fi
66
+ ;;
67
+ *) return 1 ;;
68
+ esac
69
+ done
70
+ [ "$had_file" = "1" ] && return 0
71
+ return 1
72
+ }
73
+
74
+ # Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
75
+ git_diff_only_version_field() {
76
+ proj="$1"; path="$2"
77
+ [ -d "$proj/.git" ] || return 1
78
+ diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
79
+ [ -z "$diff" ] && return 1
80
+ while IFS= read -r line; do
81
+ case "$line" in
82
+ '+++ '*|'--- '*|'@@ '*|'') continue ;;
83
+ [+-]*'"version":'*) continue ;;
84
+ [+-]*) return 1 ;;
85
+ esac
86
+ done <<EOF
87
+ $diff
88
+ EOF
89
+ return 0
90
+ }
91
+
92
+ # Витягає поле `transcript:` з YAML frontmatter ADR-чернетки.
93
+ draft_transcript_path() {
94
+ awk '
95
+ NR==1 && /^---$/ { fm=1; next }
96
+ fm && /^---$/ { exit }
97
+ fm && /^transcript: / { sub(/^transcript: /, ""); print; exit }
98
+ ' "$1" 2>/dev/null
99
+ }
100
+
42
101
  # Skip if repo is mid-rebase / mid-merge — editing files now would tangle the user.
43
102
  if [ -d "$PROJECT_ROOT/.git" ]; then
44
103
  for marker in MERGE_HEAD CHERRY_PICK_HEAD REVERT_HEAD rebase-apply rebase-merge; do
@@ -120,6 +179,45 @@ head -n "$BATCH_SIZE" "$DRAFTS_LIST" > "$BATCH_LIST"
120
179
  BATCH_COUNT=$(wc -l < "$BATCH_LIST" | tr -d ' ')
121
180
  log "batch size: $BATCH_COUNT"
122
181
 
182
+ # Structural skip: чернетки з tooling-only сесій видаляємо без виклику LLM.
183
+ if [ "${ADR_NORMALIZE_SKIP_TOOLING_ONLY:-1}" = "1" ]; then
184
+ FILTERED_LIST="$TMP_DIR/batch-filtered.txt"
185
+ : > "$FILTERED_LIST"
186
+ TOOLING_REMOVED=0
187
+ while IFS= read -r draft; do
188
+ [ -f "$draft" ] || continue
189
+ tpath=$(draft_transcript_path "$draft")
190
+ if [ -n "$tpath" ] && [ -f "$tpath" ]; then
191
+ changed=$(jq -r '
192
+ select(.type == "assistant" or .role == "assistant")
193
+ | .message as $m
194
+ | ($m.content // [])
195
+ | if type == "array" then
196
+ map(select(.type == "tool_use" and (.name == "Edit" or .name == "Write" or .name == "MultiEdit"))
197
+ | .input.file_path // empty)
198
+ | .[]
199
+ else empty end
200
+ ' "$tpath" 2>/dev/null | sort -u || true)
201
+ if [ -n "$changed" ] && printf '%s\n' "$changed" | is_tooling_only_change "$PROJECT_ROOT"; then
202
+ rm -f -- "$draft"
203
+ log "tooling-only delete: $(basename "$draft") (files: $(printf '%s' "$changed" | tr '\n' ' '))"
204
+ TOOLING_REMOVED=$(( TOOLING_REMOVED + 1 ))
205
+ continue
206
+ fi
207
+ fi
208
+ printf '%s\n' "$draft" >> "$FILTERED_LIST"
209
+ done < "$BATCH_LIST"
210
+ mv "$FILTERED_LIST" "$BATCH_LIST"
211
+ BATCH_COUNT=$(wc -l < "$BATCH_LIST" | tr -d ' ')
212
+ if [ "$TOOLING_REMOVED" -gt 0 ]; then
213
+ log "after tooling-only filter: $BATCH_COUNT drafts remain (removed $TOOLING_REMOVED)"
214
+ fi
215
+ if [ "$BATCH_COUNT" -eq 0 ]; then
216
+ log "batch is empty after tooling-only filter — exit"
217
+ exit 0
218
+ fi
219
+ fi
220
+
123
221
  # Find clean ADR files at root of docs/adr/ (no `session:`).
124
222
  find "$ADR_DIR" -maxdepth 1 -type f -name '*.md' 2>/dev/null | while IFS= read -r f; do
125
223
  if ! is_draft "$f"; then
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.18.1] - 2026-05-25
8
+
9
+ ### Fixed
10
+
11
+ - **`scripts/cli-entry.mjs::isRunAsCli`** + **`scripts/lib/run-rule-cli.mjs::isRunAsCli`** — функція приймала `()` без аргументів і всередині дивилася на власний `import.meta.url`, а не на caller'а. Через те, що `import.meta` лексично прив'язаний до файлу, де записаний, helper-функція ВСІГДА бачила свій файл — `cli-entry.mjs` / `run-rule-cli.mjs` — і ніколи не дорівнювала `process.argv[1]`. Результат: усі ~40 `if (isRunAsCli())` у `rules/<id>/fix.mjs` / `lint/*.mjs` / `bin/rename-yaml-extensions.mjs` ВСІГДА йшли в else-гілку, і `bun rules/<id>/fix.mjs` мовчки виходив `0` без жодного output'у. `npx @nitra/cursor fix <rule>` → `runFixCommand` → `spawnSync('bun', [fix.mjs])` → exit 0 без жодного reporter-звіту.
12
+ - **Fix:** функція тепер приймає `metaUrl` параметром: `isRunAsCli(import.meta.url)`. Реалізація через `realpathSync(fileURLToPath(metaUrl)) === realpathSync(resolve(process.argv[1]))` — `realpath` знімає різницю «symlink vs canonical» (macOS `/tmp` ↔ `/private/tmp`, pnpm content-addressable links, `node_modules/.bin/*` shim).
13
+ - **Консолідація:** `run-rule-cli.mjs::isRunAsCli` тепер `export { isRunAsCli } from '../cli-entry.mjs'` — одне джерело правди. Existing import paths у callers лишилися без змін.
14
+ - **Callsites:** всі ~40 викликів `isRunAsCli()` оновлено на `isRunAsCli(import.meta.url)`.
15
+ - **Tests:** додано три нові кейси у `scripts/tests/cli-entry.test.mjs` (entry-detection через spawn-fixture, symlink-нормалізація через `/tmp` → `/private/tmp`, no-arg fallback). Fixture — `scripts/tests/fixtures/cli-entry-as-cli.mjs`.
16
+
17
+ ## [1.18.0] - 2026-05-25
18
+
19
+ ### Added
20
+
21
+ - `text`: `docs/adr/**` у канонічному `ignorePaths` правила `.cspell.json` (`policy/cspell/template/.cspell.json.snippet.json`). Машинно-генеровані ADR-документи більше не валідуються cspell-ом — це розриває петлю «правка `.cspell.json` → новий ADR-draft → знову `cspell` ламається на ньому». Локальні розширення `ignorePaths` лишаються дозволені (rego subset-of).
22
+ - `adr`: ENV `ADR_NORMALIZE_SKIP_TOOLING_ONLY` (default `1`) — вимикає structural skip у capture-/normalize-хуках. Документація в `adr.mdc` (таблиця ENV) і `skills/adr-normalize/SKILL.md`.
23
+
24
+ ### Changed
25
+
26
+ - `adr`: `.claude-template/hooks/capture-decisions.sh` — перед LLM-викликом перевіряє список `tool_use`-правок із transcript'у. Якщо всі правки у вузькому allowlist (`.cspell.json`, `docs/adr/*.md`, кореневі `AGENTS.md`/`CLAUDE.md`, `CHANGELOG.md`, `*/package.json` із diff виключно по ключу `version`) — `exit 0` із записом `skipping ADR capture: tooling-only session` у лог. Inline-функція `is_tooling_only_change` + `git_diff_only_version_field`, bash 3.2-сумісно.
27
+ - `adr`: `.claude-template/hooks/normalize-decisions.sh` — після формування батча для кожної чернетки читає `transcript:` із frontmatter і та сама перевірка allowlist'у. Tooling-only чернетки видаляються без виклику LLM; якщо батч порожній — `exit 0`.
28
+ - `text.mdc` 1.29 → 1.30: документація `docs/adr/**` у `ignorePaths`; приклади `.cspell.json` оновлено.
29
+ - `adr.mdc` 2.1 → 2.2: нова секція «Tooling-only skip» у Фазі 1, bullet у Фазі 2, рядок у таблиці ENV.
30
+
31
+ ### Notes
32
+
33
+ - Існуючі ENV (`ADR_NORMALIZE_THRESHOLD`, `…_MIN_INTERVAL_HOURS`, `…_BATCH`, `…_DRY`, recursion-guard `CAPTURE_DECISIONS_RUNNING` / `ADR_NORMALIZE_RUNNING`) поведінку не змінюють.
34
+ - `cspell.rego` subset-of-перевірку зберігає — нічого не зламано для проєктів, де користувач уже руками додав `docs/adr/**` у свій `.cspell.json`.
35
+
7
36
  ## [1.17.4] - 2026-05-24
8
37
 
9
38
  ### Changed
@@ -34,7 +34,7 @@ export async function runRenameYamlExtensionsCli(argv) {
34
34
  return errors.length > 0 ? 1 : 0
35
35
  }
36
36
 
37
- if (isRunAsCli()) {
37
+ if (isRunAsCli(import.meta.url)) {
38
38
  const code = await runRenameYamlExtensionsCli(process.argv.slice(2))
39
39
  if (code !== 0) {
40
40
  process.exitCode = 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.17.4",
3
+ "version": "1.18.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/adr/adr.mdc CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Автоматичний збір ADR/Runbook/Knowledge-чернеток і батч-нормалізація у `docs/adr/` через Stop-хуки Claude Code та Cursor Agent
3
3
  alwaysApply: true
4
- version: '2.1'
4
+ version: '2.2'
5
5
  ---
6
6
 
7
7
  ## MADR v4 і дві фази
@@ -29,6 +29,8 @@ Stop-hook `capture-decisions.sh` зчитує JSONL-транскрипт сес
29
29
 
30
30
  Для Cursor payload скрипт бере `transcript_path`, `conversation_id` / `generation_id` і `workspace_roots[0]`; для Claude Code — `transcript_path`, `session_id` і `CLAUDE_PROJECT_DIR`.
31
31
 
32
+ **Tooling-only skip:** перед викликом LLM `capture-decisions.sh` дивиться у transcript на `tool_use`-правки (`Edit`/`Write`/`MultiEdit`). Якщо всі змінені файли потрапляють у вузький allowlist — `.cspell.json`, `docs/adr/*.md`, `CHANGELOG.md`, кореневі `AGENTS.md`/`CLAUDE.md`, або `package.json` з diff виключно по ключу `version` — хук виходить з `exit 0` без LLM-виклику. Це розриває петлю «`/n-lint` править `.cspell.json` → з'являється новий ADR-draft → наступний `/n-lint` знов псує правопис у цьому draft». Поведінку вимикає `ADR_NORMALIZE_SKIP_TOOLING_ONLY=0`.
33
+
32
34
  ### Фаза 2 — Normalize
33
35
 
34
36
  Stop-hook `normalize-decisions.sh` спрацьовує на тому самому `Stop`-евенті, але:
@@ -38,6 +40,7 @@ Stop-hook `normalize-decisions.sh` спрацьовує на тому самом
38
40
  - Бере не більше **`ADR_NORMALIZE_BATCH`** чернеток (default 10, найстарші за іменем-timestamp), формує один промпт LLM і чекає JSON-відповідь зі списком операцій.
39
41
  - Виходить миттєво, якщо репозиторій у стані `MERGE_HEAD` / `rebase-*` — небезпечно правити файли посеред конфлікту.
40
42
  - Виходить миттєво, якщо інший normalize-запуск тримає `flock` на `.claude/hooks/.normalize.lock` (тільки де `flock` доступний).
43
+ - Перед викликом LLM для кожної чернетки batch'а читає `transcript:` із frontmatter і той самий tool_use-список. Чернетки tooling-only — видаляє без виклику LLM. Якщо після фільтра batch порожній — `exit 0`.
41
44
 
42
45
  LLM повертає масив операцій:
43
46
 
@@ -65,6 +68,7 @@ LLM повертає масив операцій:
65
68
  | `ADR_NORMALIZE_DRY` | `0` | `1` — лише лог запланованих операцій, без змін на диску. |
66
69
  | `ADR_NORMALIZE_MODEL` | `sonnet` | Модель для `claude -p`. |
67
70
  | `ADR_NORMALIZE_CURSOR_MODEL` | `claude-4.6-sonnet-medium` | Модель для `cursor-agent -p`. |
71
+ | `ADR_NORMALIZE_SKIP_TOOLING_ONLY` | `1` | `0` — вимкнути structural skip tooling-only сесій (старий behavior). |
68
72
 
69
73
  Для ручного запуску (поза порогом і поза Stop-хуком) є **`/n-adr-normalize`** — slash-команда тимчасово виставляє `ADR_NORMALIZE_THRESHOLD=0` і `ADR_NORMALIZE_MIN_INTERVAL_HOURS=0` та викликає скрипт напряму.
70
74
 
package/rules/adr/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/bun/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/ci4/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -90,6 +90,6 @@ async function runLintDockerSteps() {
90
90
  */
91
91
  export const runLintDocker = () => runStandardLint(import.meta.dirname, runLintDockerSteps)
92
92
 
93
- if (isRunAsCli()) {
93
+ if (isRunAsCli(import.meta.url)) {
94
94
  process.exitCode = await runLintDocker()
95
95
  }
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/ga/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/k8s/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -354,6 +354,6 @@ async function runLintK8sSteps() {
354
354
  */
355
355
  export const runLintK8s = () => runStandardLint(import.meta.dirname, runLintK8sSteps)
356
356
 
357
- if (isRunAsCli()) {
357
+ if (isRunAsCli(import.meta.url)) {
358
358
  process.exitCode = await runLintK8s()
359
359
  }
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
package/rules/php/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -120,6 +120,6 @@ export function run() {
120
120
  return reporter.getExitCode()
121
121
  }
122
122
 
123
- if (isRunAsCli()) {
123
+ if (isRunAsCli(import.meta.url)) {
124
124
  process.exitCode = run()
125
125
  }
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -143,6 +143,6 @@ export function runLintRegoSteps(cwd = process.cwd()) {
143
143
  */
144
144
  export const runLintRego = () => runStandardLint(import.meta.dirname, () => runLintRegoSteps())
145
145
 
146
- if (isRunAsCli()) {
146
+ if (isRunAsCli(import.meta.url)) {
147
147
  process.exitCode = await runLintRego()
148
148
  }
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -90,6 +90,6 @@ export function runDotenvLinter(cwd = process.cwd()) {
90
90
  return 1
91
91
  }
92
92
 
93
- if (isRunAsCli()) {
93
+ if (isRunAsCli(import.meta.url)) {
94
94
  process.exitCode = runDotenvLinter()
95
95
  }
@@ -215,6 +215,6 @@ function runFinalShellcheck(shellcheck, files, root) {
215
215
  return 1
216
216
  }
217
217
 
218
- if (isRunAsCli()) {
218
+ if (isRunAsCli(import.meta.url)) {
219
219
  process.exitCode = runShellcheckText()
220
220
  }
@@ -76,7 +76,7 @@ export function runV8rWithGlobs(globs = DEFAULT_V8R_GLOBS) {
76
76
  return 0
77
77
  }
78
78
 
79
- if (isRunAsCli()) {
79
+ if (isRunAsCli(import.meta.url)) {
80
80
  const globs = process.argv.length > 2 ? process.argv.slice(2) : DEFAULT_V8R_GLOBS
81
81
  process.exitCode = runV8rWithGlobs(globs)
82
82
  }
@@ -7,6 +7,7 @@
7
7
  ".vscode",
8
8
  "report",
9
9
  "*.svg",
10
- "**/k8s/**/*.yaml"
10
+ "**/k8s/**/*.yaml",
11
+ "docs/adr/**"
11
12
  ]
12
13
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Обробка та перевірка текстових файлів, oxfmt, cspell, shellcheck (sh), dotenv-linter (.env*), markdownlint-cli2, v8r, CI
3
3
  alwaysApply: true
4
- version: '1.29'
4
+ version: '1.30'
5
5
  ---
6
6
 
7
7
  **oxfmt** (`.oxfmtrc.json`, редактор), **cspell**, **shellcheck** (tracked `*.sh` у `lint-text`), **[dotenv-linter](https://dotenv-linter.github.io/)** (`.env*` у `lint-text`), **markdownlint-cli2**, **[v8r](https://chris48s.github.io/v8r/)** ([Schema Store](https://www.schemastore.org/)), розширення **DavidAnson.vscode-markdownlint** / **timonwong.shellcheck**, workflow **`lint-text`**.
@@ -186,7 +186,7 @@ version: '1.29'
186
186
  {
187
187
  "version": "0.2",
188
188
  "language": "nitra",
189
- "ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "report", "*.svg", "**/k8s/**/*.yaml"],
189
+ "ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "report", "*.svg", "**/k8s/**/*.yaml", "docs/adr/**"],
190
190
  "import": ["@nitra/cspell-dict/cspell-ext.json"],
191
191
  "words": []
192
192
  }
@@ -194,6 +194,8 @@ version: '1.29'
194
194
 
195
195
  Канон базових ключів `.cspell.json` (`version`, `ignorePaths`): [.cspell.json.snippet.json](./policy/cspell/template/.cspell.json.snippet.json). Обовʼязковий запис у `import`: [.cspell.json.contains.json](./policy/cspell/template/.cspell.json.contains.json). Заборонено імпортувати окремі `@cspell/dict-*` у `.cspell.json`: [.cspell.json.deny.json](./policy/cspell/template/.cspell.json.deny.json).
196
196
 
197
+ `docs/adr/**` у канонічному `ignorePaths` — машинно-генеровані MADR-документи (драфти `capture-decisions.sh` + clean-ADR-и після `normalize-decisions.sh`). cspell-перевірка там безглузда: чернетка стирається наступним прогоном хук-ланцюжка, а будь-яка ручна правка правопису перезаписується. Локальні розширення `ignorePaths` дозволені — це лише мінімум.
198
+
197
199
  ```json title="package.json"
198
200
  {
199
201
  "scripts": {
@@ -230,7 +232,7 @@ version: '1.29'
230
232
  {
231
233
  "version": "0.2",
232
234
  "language": "en,uk,ru-ru,nitra",
233
- "ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "report", "*.svg", "**/k8s/**/*.yaml"],
235
+ "ignorePaths": ["**/node_modules/**", "**/vscode-extension/**", "**/.git/**", ".vscode", "report", "*.svg", "**/k8s/**/*.yaml", "docs/adr/**"],
234
236
  "import": ["@nitra/cspell-dict/cspell-ext.json"],
235
237
  "words": []
236
238
  }
package/rules/vue/fix.mjs CHANGED
@@ -11,7 +11,7 @@ export function run(ctx) {
11
11
  return runStandardRule(import.meta.dirname, ctx)
12
12
  }
13
13
 
14
- if (isRunAsCli()) {
14
+ if (isRunAsCli(import.meta.url)) {
15
15
  // Standalone: bun rules/<id>/fix.mjs — повний еквівалент `npx @nitra/cursor fix <id>`
16
16
  // (config-loading + whitelist + summary). Дві ролі fix.mjs: library (run) + standalone (main).
17
17
  // eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
@@ -1,23 +1,33 @@
1
1
  /**
2
2
  * Визначення, чи виконується поточний ESM-модуль як точка входу CLI, а не як import у тестах чи інших модулях.
3
3
  *
4
- * Порівняння `import.meta.url` з `process.argv[1]` після `resolve`, щоб `bun path/to/script.mjs`
5
- * і `node path/to/script.mjs` коректно вважалися прямим запуском.
4
+ * Прийом: модуль, що хоче знати свій статус, передає `import.meta.url`
5
+ * `import.meta` лексично прив'язаний до файлу, де він записаний, тому helper-функція
6
+ * без аргументу неминуче дивилася б на свій файл, а не на caller. `realpathSync` на обох
7
+ * сторонах знімає різницю «symlink vs canonical» (macOS `/tmp` ↔ `/private/tmp`,
8
+ * `node_modules/.bin/*` shim, pnpm-style content-addressable links).
6
9
  */
10
+ import { realpathSync } from 'node:fs'
7
11
  import { resolve } from 'node:path'
8
12
  import { fileURLToPath } from 'node:url'
9
13
 
10
14
  /**
11
15
  * Чи виконується модуль як точка входу CLI (прямий запуск), а не через import.
12
- * @returns {boolean} `true`, якщо файл запущено напряму; інакше `false`.
16
+ * @param {string | URL} [metaUrl] `import.meta.url` модуля-caller'а. Без нього завжди `false`.
17
+ * @returns {boolean} `true`, якщо файл, з якого передано `metaUrl`, є `process.argv[1]`.
13
18
  */
14
- export function isRunAsCli() {
19
+ export function isRunAsCli(metaUrl) {
20
+ if (!metaUrl) {
21
+ return false
22
+ }
23
+ const entry = process.argv[1]
24
+ if (!entry) {
25
+ return false
26
+ }
15
27
  try {
16
- const entry = process.argv[1]
17
- if (!entry) {
18
- return false
19
- }
20
- return fileURLToPath(import.meta.url) === resolve(entry)
28
+ const callerPath = realpathSync(fileURLToPath(metaUrl))
29
+ const entryPath = realpathSync(resolve(entry))
30
+ return callerPath === entryPath
21
31
  } catch {
22
32
  return false
23
33
  }
@@ -7,23 +7,17 @@
7
7
  * Library-mode виклик з CLI orchestration — інше: див. `runStandardRule` + `fix.mjs::run(ctx)`.
8
8
  */
9
9
  import { basename } from 'node:path'
10
- import { pathToFileURL } from 'node:url'
11
10
 
12
11
  import { isRuleEnabled, readNCursorConfigLite } from './read-n-cursor-config-lite.mjs'
13
12
  import { runStandardRule } from './run-standard-rule.mjs'
14
13
  import { getOrCreateWalkCache } from '../utils/walk-cache.mjs'
15
14
 
16
- const PACKAGE_NAME = '@nitra/cursor'
15
+ // Re-export для зворотної сумісності: уся `rules/<id>/fix.mjs` уже імпортує `isRunAsCli`
16
+ // саме звідси. Канонічна реалізація — у `scripts/cli-entry.mjs`. Caller передає
17
+ // `import.meta.url`: `if (isRunAsCli(import.meta.url)) …`.
18
+ export { isRunAsCli } from '../cli-entry.mjs'
17
19
 
18
- /**
19
- * Чи поточний модуль запущено як CLI entry-point (`bun rules/<id>/fix.mjs`).
20
- * @returns {boolean} true, коли `import.meta.url` збігається з `process.argv[1]`
21
- */
22
- export function isRunAsCli() {
23
- const entry = process.argv[1]
24
- if (!entry) return false
25
- return import.meta.url === pathToFileURL(entry).href
26
- }
20
+ const PACKAGE_NAME = '@nitra/cursor'
27
21
 
28
22
  /**
29
23
  * @param {string} ruleDir абсолютний шлях до `rules/<id>/`
@@ -62,9 +62,11 @@ description: >-
62
62
  - `ADR_NORMALIZE_BATCH=30` — більший батч (менше викликів LLM, більше токенів за раз).
63
63
  - `ADR_NORMALIZE_MODEL=opus` — інша модель `claude -p`.
64
64
  - `ADR_NORMALIZE_CURSOR_MODEL=…` — інша модель для cursor-agent fallback.
65
+ - `ADR_NORMALIZE_SKIP_TOOLING_ONLY=0` — вимкнути structural skip для tooling-only сесій (default `1`). Корисно лише якщо хочеш зберегти чернетки навіть для правок у `.cspell.json` / `CHANGELOG.md` / `version`-bump-ів.
65
66
 
66
67
  ## Якщо щось пішло не так
67
68
 
68
69
  - LLM повернув криву JSON → у логу буде `invalid JSON response (first 200 chars): …`. Запусти ще раз — нерідко це разовий збій.
69
70
  - Скрипт виходить миттєво без логу → перевір `ADR_NORMALIZE_RUNNING` у env (recursion guard) і чи репо не у стані merge/rebase.
70
71
  - Перейменування зробило дублі імен (`<timestamp>-<slug>-2.md`) → це нормально, скрипт детермінований; під час review можна обʼєднати руками й видалити `-2`.
72
+ - ADR-чернетки видаляються мовчки → це structural tooling-only skip. Перевір лог: `tail .claude/hooks/normalize-decisions.log | grep tooling-only`. Для діагностики на capture-стороні: `tail .claude/hooks/capture-decisions.log | grep tooling-only`. Аби тимчасово вимкнути — `ADR_NORMALIZE_SKIP_TOOLING_ONLY=0 bash .claude/hooks/normalize-decisions.sh`.