@nitra/cursor 3.26.0 → 3.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/n-cursor.js +29 -9
  3. package/package.json +1 -1
  4. package/rules/abie/js/applies.mjs +1 -5
  5. package/rules/abie/js/env_dns.mjs +1 -9
  6. package/rules/abie/js/firebase_hosting.mjs +1 -5
  7. package/rules/abie/js/hc_pairing.mjs +1 -8
  8. package/rules/abie/js/ua_http_route.mjs +1 -10
  9. package/rules/abie/js/ua_node_selector.mjs +1 -8
  10. package/rules/adr/js/hooks.mjs +1 -20
  11. package/rules/bun/js/layout.mjs +1 -19
  12. package/rules/capacitor/js/platforms.mjs +1 -23
  13. package/rules/changelog/js/consistency.mjs +1 -29
  14. package/rules/ci4/js/marksman_config.mjs +1 -19
  15. package/rules/docker/js/lint.mjs +1 -34
  16. package/rules/ga/docs/fix.md +16 -149
  17. package/rules/ga/js/docs/lint.md +12 -93
  18. package/rules/ga/js/docs/workflows.md +28 -213
  19. package/rules/ga/js/workflows.mjs +1 -16
  20. package/rules/ga/lint/docs/lint.md +24 -206
  21. package/rules/graphql/js/tooling.mjs +1 -9
  22. package/rules/hasura/js/internal_urls.mjs +1 -24
  23. package/rules/image-avif/js/avif_generation.mjs +1 -27
  24. package/rules/image-compress/js/package_setup.mjs +1 -18
  25. package/rules/js-bun-db/js/safety.mjs +1 -31
  26. package/rules/js-bun-redis/js/imports.mjs +1 -12
  27. package/rules/js-lint/js/docs/lint-findings.md +30 -0
  28. package/rules/js-lint/js/lint-findings.mjs +1 -7
  29. package/rules/js-lint/js/lint.mjs +1 -10
  30. package/rules/js-lint/js/tooling.mjs +1 -13
  31. package/rules/js-lint/js/utils_imports.mjs +1 -18
  32. package/rules/js-lint-ci/js/lint.mjs +1 -6
  33. package/rules/js-mssql/js/deps.mjs +1 -10
  34. package/rules/js-run/js/runtime.mjs +1 -37
  35. package/rules/js-run/lib/docs/temporal-scan.md +25 -0
  36. package/rules/k8s/js/manifests.mjs +1 -137
  37. package/rules/nginx-default-tpl/js/template.mjs +1 -18
  38. package/rules/npm-module/js/docs/header_doc_pointer.md +25 -0
  39. package/rules/npm-module/js/header_doc_pointer.mjs +82 -0
  40. package/rules/npm-module/js/package_structure.mjs +1 -28
  41. package/rules/npm-module/js/rule_meta.mjs +1 -10
  42. package/rules/npm-module/js/skill_meta.mjs +1 -13
  43. package/rules/php/js/tooling.mjs +1 -11
  44. package/rules/python/js/applies.mjs +1 -8
  45. package/rules/python/js/tooling.mjs +1 -21
  46. package/rules/rego/js/applies.mjs +1 -11
  47. package/rules/rust/js/applies.mjs +1 -7
  48. package/rules/security/js/sample_secret.mjs +1 -28
  49. package/rules/security/js/trufflehog.mjs +1 -8
  50. package/rules/style-lint/js/lint.mjs +1 -5
  51. package/rules/style-lint/js/tooling.mjs +1 -19
  52. package/rules/tauri/js/cargo_mutants_config.mjs +1 -20
  53. package/rules/tauri/js/tooling.mjs +1 -21
  54. package/rules/test/js/cargo_mutants_config.mjs +1 -12
  55. package/rules/test/js/location.mjs +1 -9
  56. package/rules/test/js/no-process-chdir.mjs +1 -21
  57. package/rules/test/js/no-relative-fs-path.mjs +1 -23
  58. package/rules/test/js/stryker_config.mjs +4 -25
  59. package/rules/test/js/vitest-config-pool-forks.mjs +1 -17
  60. package/rules/text/js/forbidden-prettier.mjs +1 -10
  61. package/rules/text/js/formatting.mjs +1 -31
  62. package/rules/vue/js/packages.mjs +1 -24
  63. package/scripts/docs/coverage-fix-extract.md +32 -0
  64. package/scripts/docs/lint-cli.md +25 -0
  65. package/scripts/docs/post-tool-use-fix.md +27 -0
  66. package/scripts/docs/rename-yaml-extensions.md +36 -0
  67. package/scripts/docs/skills-cli.md +35 -0
  68. package/scripts/docs/sync-claude-config.md +52 -0
  69. package/scripts/docs/sync-setup-bun-deps-action.md +26 -0
  70. package/scripts/docs/upgrade-nitra-cursor-and-install.md +29 -0
  71. package/scripts/docs/worktree-cli.md +46 -0
  72. package/scripts/lib/docs/assert-project-root.md +28 -0
  73. package/scripts/lib/docs/diff-added-lines.md +34 -0
  74. package/scripts/lib/docs/read-n-cursor-config-lite.md +28 -0
  75. package/scripts/lib/docs/resolve-target-files.md +34 -0
  76. package/scripts/lib/docs/root-notice.md +28 -0
  77. package/scripts/lib/docs/rule-meta-helpers.md +34 -0
  78. package/scripts/lib/docs/rule-meta.md +34 -0
  79. package/scripts/lib/docs/rule-predicates.md +30 -0
  80. package/scripts/lib/docs/run-conftest-batch.md +26 -0
  81. package/scripts/lib/docs/run-lint-step.md +25 -0
  82. package/scripts/lib/docs/run-rule-cli.md +27 -0
  83. package/scripts/lib/docs/run-rule.md +32 -0
  84. package/scripts/lib/docs/run-standard-lint.md +22 -0
  85. package/scripts/lib/docs/run-standard-rule.md +24 -0
  86. package/scripts/lib/docs/skill-meta.md +31 -0
  87. package/scripts/lib/docs/sync-gitignore-worktree.md +31 -0
  88. package/scripts/lib/docs/template.md +40 -0
  89. package/scripts/lib/docs/timing-summary.md +24 -0
  90. package/scripts/lib/docs/workspaces.md +30 -0
  91. package/scripts/lib/docs/worktree-notice.md +27 -0
  92. package/scripts/lib/docs/worktree.md +38 -0
  93. package/scripts/utils/docs/ast-scan-utils.md +50 -0
  94. package/scripts/utils/docs/ensure-gitignore-entries.md +28 -0
  95. package/scripts/utils/docs/find-package-json-paths.md +26 -0
  96. package/scripts/utils/docs/lock-cache-dir.md +25 -0
  97. package/scripts/utils/docs/pass.md +25 -0
  98. package/scripts/utils/docs/resolve-cargo-manifest.md +23 -0
  99. package/scripts/utils/docs/resolve-cmd.md +29 -0
  100. package/scripts/utils/docs/resolve-js-root.md +25 -0
  101. package/scripts/utils/docs/test-helpers.md +36 -0
  102. package/scripts/utils/docs/walk-cache.md +27 -0
  103. package/scripts/utils/docs/walkDir.md +32 -0
  104. package/scripts/utils/docs/with-lock.md +25 -0
  105. package/scripts/utils/docs/worktree-fingerprint.md +27 -0
  106. package/skills/docgen/js/docgen-batch.mjs +95 -0
  107. package/skills/docgen/js/docgen-extract.mjs +33 -18
  108. package/skills/docgen/js/docgen-gen.mjs +258 -30
  109. package/skills/docgen/js/docgen-ignore.mjs +4 -7
  110. package/skills/docgen/js/docgen-prompts.mjs +40 -23
  111. package/skills/docgen/js/docgen-scan.mjs +1 -8
  112. package/skills/docgen/js/docs/docgen-extract.md +28 -0
  113. package/skills/docgen/js/docs/docgen-gen.md +41 -0
  114. package/skills/docgen/js/docs/docgen-ignore.md +24 -0
  115. package/skills/docgen/js/docs/docgen-prompts.md +24 -0
  116. package/skills/docgen/js/docs/docgen-scan.md +48 -0
  117. package/skills/fix/SKILL.md +5 -31
  118. package/skills/fix/js/docs/llm-worker.md +27 -0
  119. package/skills/fix/js/docs/orchestrator.md +32 -0
  120. package/skills/fix/js/docs/t0.md +29 -0
  121. package/skills/fix/js/llm-worker.mjs +216 -0
  122. package/skills/fix/js/orchestrator.mjs +119 -0
  123. package/skills/fix/js/t0.mjs +213 -0
  124. package/skills/fix/meta.json +1 -1
  125. package/skills/start-check/js/check.mjs +1 -16
  126. package/skills/start-check/js/docs/check.md +34 -0
  127. package/skills/taze/js/diff.mjs +1 -15
  128. package/skills/taze/js/docs/diff.md +33 -0
@@ -0,0 +1,48 @@
1
+ # docgen-scan.mjs
2
+
3
+ ## Огляд
4
+
5
+ `docgen scanner` обходить проєкт, щоб створити JSON-список кодових файлів, розташованих у теці `docs/` поряд із джерелом. Цей список використовується іншими скілами для генерації документації. Інструмент забезпечує детермінований обхід проєкту та відстежує наявність файлів, не використовуючи мережеві ресурси чи LLM.
6
+
7
+ ## Поведінка
8
+
9
+ - `isSourceFile`: Перевіряє, чи файл є джерелом коду, враховуючи розширення та тестові файли.
10
+ - `docPathForSource`: Генерує шлях до файлу документації для заданого джерела, враховуючи відносні шляхи.
11
+ - `scanForDocgen`: Рекурсивно обходить дерево проєкту, визначаючи кодові файли, які потрібно документувати, та їх відповідні шляхи до документації.
12
+ - `slugForModule`: Генерує унікальний slug для модуля на основі його відносного шляху.
13
+ - `findModuleRoots`: Знаходить абсолютні шляхи коренів модулів проєкту.
14
+ - `nearestModuleRoot`: Знаходить найближчий модуль-предок для заданого файлу.
15
+ - `scanForModules`: Лістить модулі проєкту, визначаючи їхні члени (sourcePath-и) та шляхи до документації.
16
+ - `resolveRoot`: Визначає абсолютний корінь проєкту на основі аргументів командного рядка.
17
+ - `runDocgenScanCli`: Запускає сканування docgen як CLI, виводить JSON-масив у stdout та повертає код виходу.
18
+ - `runDocgenModulesCli`: Запускає сканування модулів docgen як CLI, виводить JSON-масив у stdout та повертає код виходу.
19
+
20
+ ## Публічний API
21
+
22
+ - isSourceFile — Визначає, чи є файл кодовим джерелом для створення документації.
23
+ - docPathForSource — Генерує шлях до md-файлу, пов'язаний з кодовим файлом.
24
+ - scanForDocgen — Рекурсивно обстежує структуру проєкту, виявляючи файли для документування.
25
+ - slugForModule — Створює унікальний ідентифікатор (slug) для модуля на основі його шляху.
26
+ - findModuleRoots — Знаходить корені модулів, визначаючи директорії з файлом `package.json`.
27
+ - nearestModuleRoot — Визначає найближчий модуль-предок для заданого файлу.
28
+ - scanForModules — Створює список логічних модулів проєкту, включаючи файли та інформацію про них.
29
+ - resolveRoot — Встановлює базову директорію для сканування, використовуючи аргумент командного рядка або поточну директорію.
30
+ - runDocgenScanCli — Запускає сканування для виявлення файлів документації та виводить результат у форматі JSON.
31
+ - runDocgenModulesCli — Запускає сканування модулів та виводить результат у форматі JSON.
32
+
33
+ ## Гарантії поведінки
34
+
35
+ - `isSourceFile` повертає `true`, якщо вказаний шлях до файлу є джерелом коду.
36
+ - `isSourceFile` повертає `false` в іншому випадку.
37
+ - `docPathForSource` повертає шлях до відповідної папки `docs/` для вказаного джерела.
38
+ - `scanForDocgen` повертає JSON-список шляхів до кодових файлів, які потрібно обробити.
39
+ - `scanForDocgen` встановлює прапор `exists` у випадку, якщо файл або папка вже існують.
40
+ - `slugForModule` повертає унікальний URL-шлях для модуля.
41
+ - `findModuleRoots` повертає список кореневих модулів проєкту.
42
+ - `nearestModuleRoot` повертає найближчий кореневий модуль до вказаного шляху.
43
+ - `scanForModules` повертає список модулів проєкту.
44
+ - `resolveRoot` повертає кореневу папку проєкту.
45
+ - `runDocgenScanCli` запускає процес сканування для `docgen`.
46
+ - `runDocgenModulesCli` запускає процес сканування для модулів.
47
+ - У разі невдачі повертається `false` та `null`.
48
+ -
@@ -8,42 +8,16 @@ description: >-
8
8
 
9
9
  ## Scope
10
10
 
11
- Цей скіл відповідає **лише за структуру** проєкту: щоб `.cursor/rules/` + `npx @nitra/cursor fix` були задоволені (наявність конфігів, залежностей, скриптів, GitHub workflows, відсутність заборонених файлів). **Лінт-порушення у самому коді** (ESLint, oxlint, jscpd, cspell, knip, sonarjs, stylelint тощо) — **поза скоупом**; їх діагностує й виправляє **`/n-lint`** (`bun run lint`). Не запускай `bun run lint` із цього скілу і не намагайся виправляти його порушення тут — це задача `/n-lint`. Якщо `npx @nitra/cursor fix` чистий, а `bun run lint` лишився червоним — запусти `/n-lint` окремо.
11
+ Цей скіл відповідає **лише за структуру** проєкту: щоб `.cursor/rules/` + `npx @nitra/cursor fix` були задоволені (наявність конфігів, залежностей, скриптів, GitHub workflows, відсутність заборонених файлів). **Лінт-порушення у самому коді** (ESLint, oxlint, jscpd, cspell, knip, sonarjs, stylelint тощо) — **поза скоупом**; їх діагностує й виправляє **`/n-lint`** (`bun run lint`).
12
12
 
13
13
  ## Workflow
14
14
 
15
- 1. **Діагностика** — запусти перевірку через retry-обгортку `n_cursor_npx` (визначена у worktree-preflight, крок 0.1: переживає транзитну CDN-гонку щойно опублікованої версії, а реальний `❌` від `fix` віддає одразу). Прапорець `--json` дає **структурований** результат у stdout, щоб не парсити термінальний текст. За замовчуванням — лише правила з `.cursor/rules/*.mdc`, для яких у пакеті є programmatic check; повний набір — явні аргументи: `n_cursor_npx fix bun ga --json`:
16
-
17
- ```bash
18
- n_cursor_npx fix --json
19
- ```
20
-
21
- 2. **Аналіз** — розбери JSON `{ total, failed, rules: [{ ruleId, ok, output }] }`. Працюй **лише** з елементами `ok:false`; їх `output` містить готові `❌`-повідомлення правила (не парси stdout вручну, не визначай правила з тексту). Якщо `failed === 0` — нічого виправляти.
22
-
23
- 3. **Виправлення** — для кожного `❌` відкрий відповідне правило з `.cursor/rules/` і виправ:
24
- - Створи відсутні конфігураційні файли (`.cspell.json`, `.oxfmtrc.json`, `eslint.config.js`, тощо)
25
- - Додай відсутні залежності до `package.json`
26
- - Створи або оновити `.vscode/settings.json` та `extensions.json`
27
- - Створи відсутні GitHub Actions workflows у `.github/workflows/`
28
- - Видали заборонені файли та залежності (`package-lock.json`, `yarn.lock`, prettier, тощо)
29
- - Оновити скрипти в `package.json`
30
-
31
- 4. **Встановлення** — якщо були змінені залежності:
32
-
33
15
  ```bash
34
- bun i
16
+ n_cursor_npx fix
35
17
  ```
36
18
 
37
- 5. **Форматування**відформатуй змінені файли:
19
+ Exit 0 = чисто, 1 = є unresolved (перевір вивід буде список правил що не закрились після 3 ітерацій).
38
20
 
39
- ```bash
40
- oxfmt .
41
- ```
42
-
43
- 6. **Верифікація** — перевір що все виправлено (та сама retry-обгортка `n_cursor_npx`); чекаєш `failed === 0`:
44
-
45
- ```bash
46
- n_cursor_npx fix --json
47
- ```
21
+ Якщо змінились залежності — `bun i`. Якщо змінились JS/TS файли — `oxfmt .`.
48
22
 
49
- 7. **Результат** `failed` має стати `0` (усі правила `ok:true`). Якщо лишились `ok:false` — повтори кроки 3-6. Лінт-помилки від `bun run lint` тут **не виправляй** — вони на скіл `/n-lint`.
23
+ Для конкретних правил: `n_cursor_npx fix bun ga`.
@@ -0,0 +1,27 @@
1
+ # llm-worker.mjs
2
+
3
+ ## Огляд
4
+
5
+ Цей файл є LLM-робітником, який збирає контекст для n-fix оркестратора, зокрема правила з файлів .mdc та звіти про порушення. Він передає цей контекст великій мовній моделі (LLM) для генерації JSON з пропозиціями змін. Після отримання змін, скрипт застосовує їх до відповідних ресурсів.
6
+
7
+ ## Поведінка
8
+
9
+ runLlmWorker: Виправляє одне rule-порушення через pi (C1 pattern). Збирає контекст з rule .mdc, файлів з violation output, та передає його в pi для отримання JSON зі змінами. Застосовує зміни, якщо вони були отримані від pi. Якщо pi не повернув JSON, або не зміг застосувати зміни, повертає помилку. Використовує модель Claude HAIKU за замовчуванням, або модель, вказану в опціях.
10
+
11
+ ## Публічний API
12
+
13
+ MODEL_HAIKU — Генерує короткі вірші.
14
+ MODEL_SONNET — Генерує вірші у форматі сонету.
15
+ runLlmWorker — Виправляє порушення правил, використовуючи pi (C1 pattern).
16
+
17
+ ## Гарантії поведінки
18
+
19
+ - Приймає контекст з файлів `.mdc` та файлів з violation.
20
+ - Повертає JSON з пропозиціями змін.
21
+ - Застосовує зміни, отримані від `pi`.
22
+ - Використовує LLM для генерації змін.
23
+ - Викликає LLM через `pi`.
24
+ - Не використовує tool-use.
25
+ - Не кидає винятків.
26
+ - У разі невдачі повертає `false` та `null`.
27
+ - Не використовує кешування.
@@ -0,0 +1,32 @@
1
+ # orchestrator.mjs
2
+
3
+ ## Огляд
4
+
5
+ Файл `convergence-loop` є автономним оркестратором для n-fix, який забезпечує перевірку та виправлення коду без участі LLM. Він ініціює детерміністичні перевірки, regex-парсинг та ітеративні процеси з використанням LLM для вирішення складних проблем. Цей компонент є ключовим елементом системи, що автоматизує процес забезпечення коректності коду n-fix.
6
+
7
+ ## Поведінка
8
+
9
+ 1. **Ініціалізація:** Запускається оркестратор, визначаються максимальна кількість ітерацій (за замовчуванням 3) та порогове значення для ескалації LLM (за замовчуванням 2 невдачі).
10
+ 2. **Первинна перевірка:** Виконується детермінована перевірка (T0) на основі заданих правил, без залучення LLM. Результат перевірки записується.
11
+ 3. **Автоматичний фікс (T0-auto):** Якщо перевірка виявила порушення, запускається автоматичний фікс, який використовує regex для виявлення та виправлення проблем.
12
+ 4. **Ітерація:** Програма повторює наступні кроки до досягнення максимальної кількості ітерацій:
13
+ - **Перевірка (T0):** Виконується детермінована перевірка (T0) на основі поточного стану.
14
+ - **Автоматичний фікс (T0-auto):** Якщо перевірка виявила порушення, запускається автоматичний фікс.
15
+ - **Виклик LLM (T1):** Для кожного порушення, яке не було виправлено автоматичним фіксом, викликається LLM (haiku або sonnet, залежно від кількості попередніх невдач) для генерації рішення.
16
+ - **Оцінка рішення LLM:** LLM генерує рішення, яке оцінюється на предмет успіху.
17
+ - **Оновлення стану:** Якщо рішення LLM успішне, порушення виправляється, і кількість невдалих спроб LLM для цього правила встановлюється на нуль.
18
+ 5. **Фінальна перевірка:** Після завершення всіх ітерацій виконується остаточна перевірка, щоб визначити, чи були виправлені всі порушення.
19
+ 6. **Повернення результату:** Оркестратор повертає код 0, якщо всі правила були виправлені, і код 1, якщо хоча б одне правило залишилося не виправленим. У випадку помилки парсингу JSON, повертається null.
20
+
21
+ ## Гарантії поведінки
22
+
23
+ - Оркестратор запускається при виклику `runOrchestratorCli`.
24
+ - Програма виконує цикл з перевірок, поки не буде досягнуто максимальну кількість ітерацій (`maxIter`).
25
+ - Після кожної ітерації (`T0`, `T0-auto`, `T1`) виконується перевірка.
26
+ - `T0` виконується детерміновано без використання LLM.
27
+ - `T0-auto` виконує regex-парсинг та застосовує програмний фікс у разі виявлення порушення.
28
+ - `T1` використовує LLM через `pi` (ескалація до `haiku` та `sonnet`).
29
+ - `check-gate` повторно запускає `T0` після кожної ітерації.
30
+ - У разі помилки програма повертає `false` або `null`.
31
+ - Програма кешує результати для оптимізації роботи в межах одного запуску.
32
+ - Програма не взаємодіє з мережею.
@@ -0,0 +1,29 @@
1
+ # t0.mjs
2
+
3
+ ## Огляд
4
+
5
+ Файл `t0-auto` забезпечує детермінований рівень виправлень для n-fix оркестратора, автоматично застосовуючи програмні фікси на основі аналізу вихідних даних `n-cursor fix --json`. Він парсить повідомлення про порушення, видобуваючи з них інформацію про цільові файли або рядки для вставки, і застосовує відповідні фікси. Це дозволяє швидко та передбачувано виправляти помилки, не використовуючи LLM, і є першим етапом у конвергентному циклі виправлення.
6
+
7
+ ## Поведінка
8
+
9
+ applyT0Auto: Застосовує всі T0-auto паттерни до вхідного `violationOutput` та повертає об'єкт, що вказує, чи застосовано фікс, та список виконаних дій.
10
+ filterT0AutoRules: Повертає список id правил, для яких є хоч один T0-auto паттерн, на основі `failedRules`.
11
+ runT0AutoCli: Запускає `n-cursor fix --json`, застосовує T0-auto, повторно перевіряє check-gate та виводить підсумок. Повертає 0 у разі успіху, 1 у разі помилки.
12
+
13
+ ## Публічний API
14
+
15
+ - applyT0Auto — Застосовує автоматичні правила виправлення до виявлених проблем.
16
+ - filterT0AutoRules — Визначає ідентифікатори правил, які мають T0-auto паттерни.
17
+ - runT0AutoCli — Запускає CLI-інструмент для автоматичного виправлення, включаючи перевірку та звітність.
18
+
19
+ ## Гарантії поведінки
20
+
21
+ - `applyT0Auto` застосовує програмний фікс, якщо `violationOutput` містить інформацію, яку можна видобути за допомогою регулярних виразів, і `violation-message` містить цільове значення, що відповідає одному з ідентифікаторів правил T0-auto. Результат: `applied` - boolean, `actions` - string[] (список виконаних дій).
22
+ - `listT0AutoRules` повертає список ідентифікаторів правил T0-auto, які мають хоча б один паттерн.
23
+ - `runT0AutoCli` повертає код виходу 0, якщо процес виконано успішно, і 1, якщо виявлено порушення.
24
+ - T0-auto запускається першим у конвергентному циклі.
25
+ - T1 запускається лише для решти.
26
+ - Система перехоплює помилки та не кидає винятки.
27
+ - В системі немає кешування.
28
+ - Вхідні дані `applyT0Auto` повинні відповідати формату JSON, вихідному від `n-cursor fix --json`.
29
+ - Якщо `violation-message` не містить інформації, яку можна видобути за допомогою регулярних виразів, `applyT0Auto` не виконує жод
@@ -0,0 +1,216 @@
1
+ /** @see ./docs/llm-worker.md */
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { env } from 'node:process'
7
+ import { CLOUD_MIN, CLOUD_AVG } from '../../../lib/models.mjs'
8
+
9
+ // Тир за замовчуванням: CLOUD_MIN → CLOUD_AVG при ескалації.
10
+ // Перевизначення через N_CURSOR_FIX_MODEL / N_CURSOR_FIX_MODEL_HEAVY.
11
+ export const MODEL = env.N_CURSOR_FIX_MODEL ?? CLOUD_MIN
12
+ export const MODEL_HEAVY = env.N_CURSOR_FIX_MODEL_HEAVY ?? CLOUD_AVG
13
+
14
+ /**
15
+ * Витягує відносні шляхи файлів із violation output.
16
+ * Розуміє workspace-prefix: `[npm] skills/foo.mjs` → `npm/skills/foo.mjs`.
17
+ *
18
+ * @param {string} output violation output з fix check
19
+ * @returns {string[]} унікальні відносні шляхи (від кореня проєкту)
20
+ */
21
+ function extractFilePaths(output) {
22
+ const seen = new Set()
23
+ const results = []
24
+
25
+ // Патерн з workspace: [npm] skills/foo.mjs або [demo] src/bar.ts
26
+ const wsRe = /\[([\w-]+)\]\s+([\w./][\w./\-]*\.(?:json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py))(?::\d+)?/gm
27
+ for (const m of output.matchAll(wsRe)) {
28
+ const p = `${m[1]}/${m[2]}`
29
+ if (!seen.has(p)) {
30
+ seen.add(p)
31
+ results.push(p)
32
+ }
33
+ }
34
+
35
+ // Патерн без workspace: просто path/to/file.ext або ./file.ext
36
+ const re = /(?:^|\s)(\.?[\w][\w./\-]*\.(?:json|js|mjs|ts|vue|yml|yaml|toml|mdc|md|sh|py))(?::\d+)?/gm
37
+ for (const m of output.matchAll(re)) {
38
+ const p = m[1]
39
+ if (!seen.has(p)) {
40
+ seen.add(p)
41
+ results.push(p)
42
+ }
43
+ }
44
+
45
+ return results
46
+ }
47
+
48
+ /**
49
+ * Будує prompt для pi: правило + порушення + поточний вміст файлів.
50
+ *
51
+ * @param {string} ruleId
52
+ * @param {string} ruleMdc вміст .mdc-файлу правила
53
+ * @param {string} output violation output
54
+ * @param {Array<{path:string, content:string}>} files
55
+ * @returns {string}
56
+ */
57
+ function buildPrompt(ruleId, ruleMdc, output, files) {
58
+ const filesBlock =
59
+ files.length === 0
60
+ ? '(no files identified)'
61
+ : files.map(f => `<file path="${f.path}">\n${f.content}\n</file>`).join('\n\n')
62
+
63
+ return [
64
+ `You fix project structure violations. Return ONLY valid JSON — no explanation, no markdown.`,
65
+ ``,
66
+ `Rule (n-${ruleId}.mdc):`,
67
+ `---`,
68
+ ruleMdc,
69
+ `---`,
70
+ ``,
71
+ `Violation output:`,
72
+ output,
73
+ ``,
74
+ `Current file contents:`,
75
+ filesBlock,
76
+ ``,
77
+ `Return JSON with this exact shape:`,
78
+ `{"changes":[{"path":"relative/path/to/file","content":"full corrected file content"}]}`,
79
+ ``,
80
+ `Rules:`,
81
+ `- "path" is relative to the project root`,
82
+ `- "content" is the complete new file content (not a diff)`,
83
+ `- Only include files that actually need to change`,
84
+ `- If nothing can be fixed automatically, return {"changes":[],"error":"reason"}`
85
+ ].join('\n')
86
+ }
87
+
88
+ /**
89
+ * Запускає pi і повертає stdout як рядок.
90
+ *
91
+ * @param {string} prompt
92
+ * @param {string} model
93
+ * @returns {{ text: string, error?: string }}
94
+ */
95
+ function callPi(prompt, model) {
96
+ const modelArgs = model ? ['--model', model] : []
97
+ const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {
98
+ encoding: 'utf8',
99
+ timeout: 120_000
100
+ })
101
+ if (r.error) return { text: '', error: r.error.message }
102
+ if (r.status !== 0) {
103
+ const stderr = r.stderr?.slice(0, 300) ?? ''
104
+ if (stderr.toLowerCase().includes('no api key') || stderr.toLowerCase().includes('api key')) {
105
+ const provider = model ? model.split('/')[0] : 'дефолтного провайдера'
106
+ return {
107
+ text: '',
108
+ error: [
109
+ `pi: немає ключа для ${provider}.`,
110
+ `Встановіть N_CLOUD_MIN_MODEL=provider/model-id`,
111
+ `(напр.: openai/gpt-5.4-mini, google/gemini-2.5-flash, ollama/gemma3:4b)`,
112
+ ].join(' ')
113
+ }
114
+ }
115
+ return { text: '', error: `pi exit ${r.status}: ${stderr}` }
116
+ }
117
+ return { text: r.stdout?.trim() ?? '' }
118
+ }
119
+
120
+ /**
121
+ * Парсить JSON-відповідь від pi.
122
+ * pi може обгорнути JSON у ```json ... ```, тому пробуємо витягти.
123
+ *
124
+ * @param {string} text
125
+ * @returns {{ changes: Array<{path:string,content:string}>, error?: string } | null}
126
+ */
127
+ function parseResponse(text) {
128
+ // Спроба 1: прямий JSON
129
+ try {
130
+ return JSON.parse(text)
131
+ } catch {
132
+ /* fallthrough */
133
+ }
134
+
135
+ // Спроба 2: витягти з ```json ... ```
136
+ const m = text.match(/```(?:json)?\s*([\s\S]*?)```/)
137
+ if (m) {
138
+ try {
139
+ return JSON.parse(m[1].trim())
140
+ } catch {
141
+ /* fallthrough */
142
+ }
143
+ }
144
+
145
+ // Спроба 3: перший { ... } блок
146
+ const start = text.indexOf('{')
147
+ const end = text.lastIndexOf('}')
148
+ if (start !== -1 && end > start) {
149
+ try {
150
+ return JSON.parse(text.slice(start, end + 1))
151
+ } catch {
152
+ /* fallthrough */
153
+ }
154
+ }
155
+
156
+ return null
157
+ }
158
+
159
+ /**
160
+ * LLM-worker: виправляє одне rule-порушення через pi (C1 pattern).
161
+ *
162
+ * @param {string} ruleId
163
+ * @param {string} violationOutput output з fix check для цього rule
164
+ * @param {string} projectRoot абсолютний шлях до кореня проєкту
165
+ * @param {{ model?: string }} opts
166
+ * @returns {Promise<{ ok: boolean, error?: string }>}
167
+ */
168
+ export async function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
169
+ const model = opts.model ?? MODEL
170
+
171
+ // 1. Читаємо rule .mdc
172
+ const mdcPath = join(projectRoot, '.cursor', 'rules', `n-${ruleId}.mdc`)
173
+ const ruleMdc = existsSync(mdcPath) ? readFileSync(mdcPath, 'utf8') : '(rule file not found)'
174
+
175
+ // 2. Витягуємо файли з violation output і читаємо їх
176
+ const filePaths = extractFilePaths(violationOutput)
177
+ const files = filePaths
178
+ .map(p => {
179
+ const abs = join(projectRoot, p)
180
+ if (!existsSync(abs)) return null
181
+ try {
182
+ return { path: p, content: readFileSync(abs, 'utf8') }
183
+ } catch {
184
+ return null
185
+ }
186
+ })
187
+ .filter(Boolean)
188
+
189
+ // 3. Будуємо prompt і викликаємо pi
190
+ const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files)
191
+ const { text, error: piError } = callPi(prompt, model)
192
+
193
+ if (piError) return { ok: false, error: piError }
194
+ if (!text) return { ok: false, error: 'pi returned empty response' }
195
+
196
+ // 4. Парсимо відповідь
197
+ const parsed = parseResponse(text)
198
+ if (!parsed) return { ok: false, error: `cannot parse pi response: ${text.slice(0, 200)}` }
199
+ if (parsed.error) return { ok: false, error: parsed.error }
200
+
201
+ const changes = parsed.changes ?? []
202
+ if (changes.length === 0) return { ok: false, error: 'pi returned no changes' }
203
+
204
+ // 5. Застосовуємо зміни
205
+ for (const change of changes) {
206
+ if (!change.path || typeof change.content !== 'string') continue
207
+ const abs = join(projectRoot, change.path)
208
+ try {
209
+ writeFileSync(abs, change.content, 'utf8')
210
+ } catch (e) {
211
+ return { ok: false, error: `write ${change.path}: ${e.message}` }
212
+ }
213
+ }
214
+
215
+ return { ok: true }
216
+ }
@@ -0,0 +1,119 @@
1
+ /** @see ./docs/orchestrator.md */
2
+
3
+ import { spawnSync } from 'node:child_process'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { join } from 'node:path'
6
+
7
+ const HERE = fileURLToPath(new URL('.', import.meta.url))
8
+ const N_CURSOR_BIN = join(HERE, '../../../bin/n-cursor.js')
9
+
10
+ const DEFAULT_MAX_ITER = 3
11
+ const ESCALATE_AFTER = 2
12
+
13
+ /**
14
+ * @param {string[]} args CLI аргументи після 'fix'
15
+ * @param {string} cwd корінь проєкту
16
+ * @returns {Promise<number>} 0 = all clean, 1 = unresolved
17
+ */
18
+ export async function runOrchestratorCli(args, cwd) {
19
+ const { runLlmWorker, MODEL, MODEL_HEAVY } = await import('./llm-worker.mjs')
20
+
21
+ const maxIterIdx = args.indexOf('--max-iter')
22
+ const maxIter =
23
+ maxIterIdx !== -1 ? Number(args[maxIterIdx + 1] ?? DEFAULT_MAX_ITER) || DEFAULT_MAX_ITER : DEFAULT_MAX_ITER
24
+ const skipIdxs = new Set(maxIterIdx !== -1 ? [maxIterIdx, maxIterIdx + 1] : [])
25
+ const ruleFilter = args.filter((a, i) => !a.startsWith('-') && !skipIdxs.has(i))
26
+
27
+ /** @type {Map<string, number>} ruleId → кількість LLM-провалів підряд */
28
+ const failCount = new Map()
29
+
30
+ // ── Перша перевірка (тихо) ──
31
+ const initial = runFixCheck(cwd, ruleFilter)
32
+ if (!initial) {
33
+ console.error(`❌ fix: помилка перевірки`)
34
+ return 1
35
+ }
36
+
37
+ let failed = initial.rules.filter(r => !r.ok)
38
+ const total = initial.total
39
+
40
+ // Нічого не зламано — коротка відповідь
41
+ if (failed.length === 0) {
42
+ console.log(`✅ fix: ${total} правил — все чисто`)
43
+ return 0
44
+ }
45
+
46
+ // Є порушення — показуємо прогрес
47
+ console.log(`🔄 fix: ${failed.length}/${total} порушень (${failed.map(r => r.ruleId).join(', ')})`)
48
+ if (ruleFilter.length) console.log(` filter: ${ruleFilter.join(', ')}`)
49
+
50
+ for (let iter = 1; iter <= maxIter; iter++) {
51
+ // ── T0-auto: детермінований фікс без LLM ──
52
+ spawnSync('bun', [N_CURSOR_BIN, 'fix-t0', ...ruleFilter], { cwd, stdio: 'pipe' })
53
+
54
+ const afterT0 = runFixCheck(cwd, ruleFilter)
55
+ const failedAfterT0 = afterT0?.rules.filter(r => !r.ok) ?? failed
56
+ const t0Fixed = failed.filter(r => !failedAfterT0.find(f => f.ruleId === r.ruleId))
57
+
58
+ if (t0Fixed.length > 0) {
59
+ console.log(` ⚙️ T0-auto: ${t0Fixed.map(r => r.ruleId).join(', ')}`)
60
+ }
61
+
62
+ failed = failedAfterT0
63
+ if (failed.length === 0) break
64
+
65
+ // ── T1: LLM через pi ──
66
+ for (const rule of failed) {
67
+ const prevFails = failCount.get(rule.ruleId) ?? 0
68
+ const model = prevFails >= ESCALATE_AFTER ? MODEL_HEAVY : MODEL
69
+ const label = model || 'pi'
70
+
71
+ const result = await runLlmWorker(rule.ruleId, rule.output, cwd, { model })
72
+
73
+ if (result.ok) {
74
+ console.log(` ⚡ LLM (${label}): ${rule.ruleId}`)
75
+ failCount.delete(rule.ruleId)
76
+ } else {
77
+ failCount.set(rule.ruleId, prevFails + 1)
78
+ const hint = (result.error ?? '').slice(0, 200)
79
+ console.log(` ⚡ LLM (${label}): ${rule.ruleId} ❌ ${hint}`)
80
+ }
81
+ }
82
+
83
+ // Перевірка після LLM
84
+ const afterLLM = runFixCheck(cwd, ruleFilter)
85
+ failed = afterLLM?.rules.filter(r => !r.ok) ?? failed
86
+ if (failed.length === 0) break
87
+ }
88
+
89
+ if (failed.length === 0) {
90
+ console.log(`✅ fix: ${total} правил — все чисто`)
91
+ return 0
92
+ }
93
+
94
+ console.log(`❌ fix: ${failed.length} невирішених — ${failed.map(r => r.ruleId).join(', ')}`)
95
+ return 1
96
+ }
97
+
98
+ /**
99
+ * Внутрішня check-gate: запускає fix-перевірки і повертає структурований результат.
100
+ * Не є публічним CLI — викликається лише оркестратором.
101
+ *
102
+ * @param {string} cwd
103
+ * @param {string[]} ruleFilter
104
+ * @returns {{ total: number, failed: number, rules: Array<{ ruleId: string, ok: boolean, output: string }> } | null}
105
+ */
106
+ function runFixCheck(cwd, ruleFilter = []) {
107
+ const r = spawnSync('bun', [N_CURSOR_BIN, '_fix-check', ...ruleFilter], {
108
+ cwd,
109
+ encoding: 'utf8',
110
+ timeout: 120_000
111
+ })
112
+ const stdout = r.stdout?.trim()
113
+ if (!stdout) return null
114
+ try {
115
+ return JSON.parse(stdout)
116
+ } catch {
117
+ return null
118
+ }
119
+ }