@nitra/cursor 12.1.0 → 12.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [12.3.0] - 2026-06-19
4
+
5
+ ### Added
6
+
7
+ - lint --full: резюме викликів моделей у stdout (локальна / cloud-min / cloud-avg) через reportRunStats/summarizeCalls
8
+
9
+ ## [12.2.0] - 2026-06-19
10
+
11
+ ### Changed
12
+
13
+ - lint fail-fast лише для --read-only: у fix-режимі per-file правило з ненульовим кодом не спиняє — прогін доходить до кроку виправлення (конформність-драбина). + applyChanges створює батьківську теку (mkdirSync) перед записом. + детермінований T0-патерн changelog-create-change-file (writeChange замість LLM)
14
+
3
15
  ## [12.1.0] - 2026-06-19
4
16
 
5
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "12.1.0",
3
+ "version": "12.3.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: orchestrate.mjs
4
4
  resource: npm/rules/lint/js/orchestrate.mjs
5
5
  docgen:
6
- crc: 69bc0daf
6
+ crc: 0f1d717c
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -14,8 +14,9 @@ docgen:
14
14
 
15
15
  Поведінка
16
16
  selectLintRules вибирає і сортує ідентифікатори правил на основі їхнього обсягу дії (`per-file` або `full`) та прапорця `--full`.
17
- runLint запускає оркестрацію лінтування: або виконує перевірку конформності для заданих правил, або ітерує по алфавітно відсортованих правилах, запускаючи лінтер для змінених файлів (за замовчуванням), або виконує перевірку конформності всього репозиторію при використанні прапорця `--full`.
18
- У режимі `--full` без `--read-only` після конформність-фази runLint викликає escalation-аналітику (`analyze-escalation.mjs`): фіксує зсув escalation-логу до фази, післяаналізує записи саме цього прогону. Аналіз не впливає на exit-код lint.
17
+ runLint запускає оркестрацію лінтування: або виконує перевірку конформності для заданих правил, або ітерує по алфавітно відсортованих правилах (`runPerFileRules`), запускаючи лінтер для змінених файлів (за замовчуванням), або виконує перевірку конформності всього репозиторію при використанні прапорця `--full`.
18
+ **Fail-fast лише в `--read-only`** (CI/детект): перший ненульовий код спиняє. У fix-режимі (default) ненульовий код per-file правила НЕ спиняєпроганяються всі правила й виконується крок виправлення (конформність-драбина), а повертається найгірший код.
19
+ У режимі `--full` без `--read-only` після конформність-фази (`runFullConformancePhase`) друкується резюме викликів моделей за прогін (`reportRunStats`: локальна / cloud-min / cloud-avg) і викликається escalation-аналітика (`analyze-escalation.mjs`): фіксує зсув escalation-логу до фази, після — аналізує записи саме цього прогону. Жодне з цього не впливає на exit-код lint.
19
20
 
20
21
  ## Публічний API
21
22
 
@@ -9,7 +9,9 @@
9
9
  * викликає `rules/<id>/js/lint.mjs` → `lint(files, cwd, { readOnly })`:
10
10
  * - default scope: `files` = змінені відносно origin (`collectChangedFilesSince`);
11
11
  * - `--full`: `files = undefined` — весь проєкт.
12
- * Порядок правил — алфавітний. Fail-fast: перший ненульовий код спиняє.
12
+ * Порядок правил — алфавітний. Fail-fast **лише в `--read-only`** (CI/детект): перший
13
+ * ненульовий код спиняє. У fix-режимі (default) ненульовий код НЕ спиняє — проганяємо всі
14
+ * правила й доходимо до кроку виправлення (конформність-драбина), повертаючи найгірший код.
13
15
  */
14
16
  import { existsSync, readdirSync } from 'node:fs'
15
17
  import { dirname, join } from 'node:path'
@@ -80,6 +82,58 @@ function readAllMeta(rulesDir) {
80
82
  return out
81
83
  }
82
84
 
85
+ /**
86
+ * Per-file фаза: проганяє лінтер кожного правила. Fail-fast лише в read-only.
87
+ * @param {string[]} ids id правил (алфавітно)
88
+ * @param {{ rulesDir: string, changed: string[]|undefined, cwd: string, readOnly: boolean, metaById: Record<string, {llmFix?: boolean}>, log: (s: string) => void }} ctx контекст
89
+ * @returns {Promise<{ stop: boolean, code: number }>} `stop` — read-only fail-fast; `code` — найгірший код
90
+ */
91
+ async function runPerFileRules(ids, ctx) {
92
+ const { rulesDir, changed, cwd, readOnly, metaById, log } = ctx
93
+ let worst = 0
94
+ for (const id of ids) {
95
+ const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
96
+ if (!existsSync(lintPath)) {
97
+ log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
98
+ continue
99
+ }
100
+ // lintPath = join(rulesDir, id, …) — суто package-internal (rulesDir пакета + id зі
101
+ // selectLintRules за власним meta), не зовнішній вхід → ін'єкції немає.
102
+ // eslint-disable-next-line no-unsanitized/method
103
+ const mod = await import(lintPath)
104
+ // `llmFix` (opt-in opportunistic LLM-fix, спека 2026-06-15): лише правила з
105
+ // `meta.json: llmFix:true` отримують fix-сходинку; решта — detect-only.
106
+ const llmFix = metaById[id]?.llmFix === true
107
+ const code = await mod.lint(changed, cwd, { readOnly, llmFix })
108
+ if (code !== 0) {
109
+ if (readOnly) return { stop: true, code } // read-only — fail-fast (детект для CI)
110
+ worst = code // fix-режим — фіксуємо, але йдемо далі до кроку виправлення
111
+ }
112
+ }
113
+ return { stop: false, code: worst }
114
+ }
115
+
116
+ /**
117
+ * Конформність-фаза `--full` (поглинула `fix`): escalation-аналітику обрамляє зсувом логу
118
+ * (записи саме цього прогону), у fix-режимі по конформності викликає аналіз.
119
+ * @param {string} cwd корінь
120
+ * @param {boolean} readOnly лише детект
121
+ * @param {(s: string) => void} log логер
122
+ * @returns {Promise<number>} код конформності
123
+ */
124
+ async function runFullConformancePhase(cwd, readOnly, log) {
125
+ const { escalationLogSize, maybeAnalyzeEscalation, reportRunStats } = await import(
126
+ '../../../scripts/lib/fix/analyze-escalation.mjs'
127
+ )
128
+ const escOffset = readOnly ? 0 : escalationLogSize()
129
+ const conformanceCode = await runConformance(cwd, readOnly, log)
130
+ if (!readOnly) {
131
+ reportRunStats(escOffset, log) // резюме викликів моделей (локальна / cloud-min / cloud-avg)
132
+ maybeAnalyzeEscalation(cwd, escOffset, log)
133
+ }
134
+ return conformanceCode
135
+ }
136
+
83
137
  /**
84
138
  * Запускає lint-оркестрацію.
85
139
  * @param {{ full?: boolean, readOnly?: boolean, rules?: string[], cwd?: string, rulesDir?: string, log?: (s: string) => void }} [opts] параметри
@@ -110,34 +164,18 @@ export async function runLint(opts = {}) {
110
164
 
111
165
  const metaById = readAllMeta(rulesDir)
112
166
  const ids = selectLintRules(metaById, full)
113
- for (const id of ids) {
114
- const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
115
- if (!existsSync(lintPath)) {
116
- log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
117
- continue
118
- }
119
- // lintPath = join(rulesDir, id, …) — суто package-internal (rulesDir пакета + id зі
120
- // selectLintRules за власним meta), не зовнішній вхід → ін'єкції немає.
121
- // eslint-disable-next-line no-unsanitized/method
122
- const mod = await import(lintPath)
123
- // `llmFix` (opt-in opportunistic LLM-fix, спека 2026-06-15): лише правила з
124
- // `meta.json: llmFix:true` отримують fix-сходинку; решта — detect-only. Це й
125
- // забезпечує safety-тріаж (логічні лінтери не вмикають LLM-fix випадково).
126
- const llmFix = metaById[id]?.llmFix === true
127
- const code = await mod.lint(changed, cwd, { readOnly, llmFix })
128
- if (code !== 0) return code
129
- }
167
+ const perFile = await runPerFileRules(ids, { rulesDir, changed, cwd, readOnly, metaById, log })
168
+ if (perFile.stop) return perFile.code
169
+ let worst = perFile.code
130
170
 
131
- // Конформність-фаза (поглинула `fix`): whole-repo, лише у `--full`. Кастомний rulesDir
132
- // (юніт-тести селектора) — реальний пакет недоступний, тож пропускаємо.
171
+ // Конформність-фаза: whole-repo, лише у `--full`. Кастомний rulesDir (юніт-тести
172
+ // селектора) — реальний пакет недоступний, тож пропускаємо.
133
173
  if (full && opts.rulesDir === undefined) {
134
- // Escalation-аналітика: фіксуємо зсув логу ДО конформності, після — аналізуємо
135
- // записи саме цього прогону (fix-режим). Аналіз не має валити lint.
136
- const { escalationLogSize, maybeAnalyzeEscalation } = await import('../../../scripts/lib/fix/analyze-escalation.mjs')
137
- const escOffset = readOnly ? 0 : escalationLogSize()
138
- const conformanceCode = await runConformance(cwd, readOnly, log)
139
- if (!readOnly) maybeAnalyzeEscalation(cwd, escOffset, log)
140
- if (conformanceCode !== 0) return conformanceCode
174
+ const conformanceCode = await runFullConformancePhase(cwd, readOnly, log)
175
+ if (conformanceCode !== 0) {
176
+ if (readOnly) return conformanceCode
177
+ worst = conformanceCode
178
+ }
141
179
  }
142
- return 0
180
+ return worst
143
181
  }
@@ -3,224 +3,26 @@ type: JS Module
3
3
  title: release.mjs
4
4
  resource: npm/rules/release/release.mjs
5
5
  docgen:
6
- crc: 9aa2796b
6
+ crc: 06d3556b
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ judgeModel: openai-codex/gpt-5.4-mini
7
10
  ---
8
11
 
9
- Модуль `npm/rules/release/release.mjs` — це ядро команди `n-cursor release`. Він агрегує per-workspace change-файли (накопичені у `CHANGES_DIR` кожного воркспейсу) у:
12
+ ## Огляд
10
13
 
11
- 1. version-bump у маніфесті пакета (`package.json` для npm-пакетів або `pyproject.toml` для Python),
12
- 2. новий розділ у `CHANGELOG.md` відповідного воркспейсу,
13
- 3. git-коміт зі стандартизованим subject `release: <name@version>, ...`,
14
- 4. git-теги у форматі `<name>@<version>` для кожного зрелізованого пакета,
15
- 5. фізичне видалення «спожитих» change-файлів,
16
- 6. `git push --follow-tags`.
14
+ Файл автоматизує процес випуску версій. Він агрегує зміна-файли для всіх робочих просторів у `version-bump` та `CHANGELOG`, комітить зміни, ставить тег `<name>@<version>` та видаляє використані зміна-файли. Цей процес виконується у CI на гілці `main` (n-cursor-release-design, варіант A). Публічні функції, що надає модуль, — `release` та `runReleaseCli`.
17
15
 
18
- Модуль розрахований на запуск у CI на гілці `main` (варіант A з ADR `n-cursor-release-design`). Сам він **нічого не публікує** у реєстри (npm/PyPI) — цим займаються окремі CI-кроки, орієнтовані на створені теги.
16
+ ## Поведінка
19
17
 
20
- Підтримуються:
18
+ release агрегує change-файли для всіх робочих просторів, оновлює версії в маніфестах, додає секції до `CHANGELOG.md`, видаляє використані change-файли, створює коміт та анотовані теги для всіх зібраних релізів, а потім намагається пушити їх у апстрім з повторними спробами.
19
+ runReleaseCli виконує процес релізу, викликаючи функцію release, та виводить відповідний статус у консоль.
21
20
 
22
- - monorepo (root має суб-воркспейси) — root-пакет автоматично пропускається, релізиться лише кожен суб-воркспейс окремо;
23
- - single-package репо (`workspaces === ['.']`) — релізиться сам root.
21
+ ## Публічний API
24
22
 
25
- Якщо явних change-файлів у воркспейсі немає, але в історії з останнього тегу `<name>@<version>` є коміти, модуль робить **fallback-синтез** запису з commit log (через `synthesizeChangeFromCommits`).
23
+ release агрегує change-файли для кожного робочого простору у version-bump та CHANGELOG, комітить зміни, ставить тег `<name>@<version>` та видаляє використані change-файли.
24
+ runReleaseCli — виконує команди командного інтерфейсу для запуску процесу випуску.
26
25
 
27
- ## Експорти / API
26
+ ## Гарантії поведінки
28
27
 
29
- | Символ | Тип | Призначення |
30
- | ----------------------------- | ---------------- | ------------------------------------------------------------------------------------------------- |
31
- | `release(opts?)` | `async function` | Програмний API: виконує повний реліз-цикл і повертає масив зрелізованих пакетів. |
32
- | `runReleaseCli(_args, opts?)` | `async function` | CLI-обгортка: запускає `release`, друкує підсумок у stdout/stderr, повертає exit-код (`0` / `1`). |
33
-
34
- Внутрішні (не експортовані) функції-помічники: `writeManifestVersion`, `prependWorkspaceChangelog`, `collectChangeFiles`.
35
-
36
- Внутрішні константи: `SEMVER_LINE_RE`, `PY_VERSION_LINE_RE` — regex для in-place заміни рядка `version` у відповідних типах маніфесту.
37
-
38
- ## Функції
39
-
40
- ### `writeManifestVersion(cwd, manifest, newVersion)` (внутрішня)
41
-
42
- Записує нову version у маніфест пакета, зберігаючи форматування файлу.
43
-
44
- - **Сигнатура:** `async (cwd: string, manifest: PackageManifest, newVersion: string) => Promise<void>`
45
- - **Параметри:**
46
- - `cwd` — абсолютний шлях кореня репо (root проекту);
47
- - `manifest` — об'єкт маніфесту з типу `PackageManifest`, що містить поля `ws` (відносний шлях до воркспейсу, або `.` для root), `manifestRel` (відносний шлях до файлу маніфесту в межах воркспейсу) і `kind` (`'npm'` чи інший — трактується як Python);
48
- - `newVersion` — новий рядок версії (SemVer для npm, PEP 440 / SemVer для Python — не валідується тут).
49
- - **Повертає:** `Promise<void>` після успішного запису.
50
- - **Side effects:**
51
- - читає файл маніфесту з диска;
52
- - перезаписує його зі зміненим рядком версії;
53
- - **обирає regex за типом маніфесту**: `SEMVER_LINE_RE` для `'npm'`, `PY_VERSION_LINE_RE` інакше;
54
- - **кидає `Error`**, якщо в файлі не знайдено патерн рядка `version` (тобто `text.replace(...)` не змінив текст).
55
-
56
- ### `prependWorkspaceChangelog(cwd, ws, sectionBlock)` (внутрішня)
57
-
58
- Доклеює (prepend) новий блок CHANGELOG до початку `<ws>/CHANGELOG.md`; якщо файл не існує — створює.
59
-
60
- - **Сигнатура:** `async (cwd: string, ws: string, sectionBlock: string) => Promise<void>`
61
- - **Параметри:**
62
- - `cwd` — корінь репо;
63
- - `ws` — відносний шлях воркспейсу від `cwd` (наприклад, `'npm'`, `'.'`);
64
- - `sectionBlock` — готовий markdown-блок нового розділу (формат сформований `aggregateWorkspace`).
65
- - **Повертає:** `Promise<void>`.
66
- - **Side effects:** читає (за наявності) і пише `<cwd>/<ws>/CHANGELOG.md`. Логіку конкатенації керує `prependChangelogSection` з `./lib/aggregate.mjs` — модуль `release.mjs` лише викликає її, не дублюючи правил вставки.
67
-
68
- ### `collectChangeFiles(cwd, manifest, runGit)` (внутрішня)
69
-
70
- Збирає всі change-записи для воркспейсу: спочатку явні файли з `CHANGES_DIR`, інакше — fallback-синтез з історії комітів.
71
-
72
- - **Сигнатура:** `async (cwd: string, manifest: PackageManifest, runGit: (args: string[]) => Promise<string | null>) => Promise<Array<{ file: string | null, entry: { bump: string, section: string, description: string } }>>`
73
- - **Параметри:**
74
- - `cwd` — корінь репо;
75
- - `manifest` — маніфест воркспейсу;
76
- - `runGit` — git-раннер; повертає stdout як рядок, або `null`, якщо команда зафейлилася.
77
- - **Повертає:** масив об'єктів `{ file, entry }`:
78
- - `file` — ім'я change-файлу у `CHANGES_DIR` (для подальшого видалення), або `null` для синтезованого запису;
79
- - `entry` — нормалізований опис зміни: `{ bump, section, description }` (типи `bump`/`section` визначаються форматом change-файлу і `synthesizeChangeFromCommits`).
80
- - **Поведінка:**
81
- 1. Викликає `readChangeFiles(manifest.ws, cwd)`; якщо результат непустий — повертає його (явні мають пріоритет).
82
- 2. Інакше: якщо у маніфесті немає `name` — повертає `[]` (без імені неможливо знайти попередній тег для fallback).
83
- 3. Інакше викликає `synthesizeChangeFromCommits(name, ws, { runGit })`; якщо нічого не синтезувалося — `[]`.
84
- 4. Якщо синтезовано — друкує в stderr попередження `⚠️ <ws>: немає change-файлів — синтезовано запис із комітів (fallback)` і повертає `[{ file: null, entry: synthesized }]`.
85
- - **Side effects:** виклики git через `runGit` (всередині `synthesizeChangeFromCommits`); `console.warn` при fallback.
86
-
87
- ### `release(opts?)` (експорт)
88
-
89
- Основний програмний вхід — виконує повний реліз-цикл для всіх релевантних воркспейсів.
90
-
91
- - **Сигнатура:**
92
- ```
93
- async (opts?: {
94
- cwd?: string,
95
- date?: string,
96
- runGit?: (args: string[]) => Promise<string | null>,
97
- }) => Promise<Array<{ ws: string, name: string | null, newVersion: string }>>
98
- ```
99
- - **Параметри `opts` (усі необов'язкові):**
100
- - `cwd` — корінь репо; за замовчуванням `process.cwd()`;
101
- - `date` — рядок у форматі `YYYY-MM-DD` для дати релізу в CHANGELOG; за замовчуванням сьогоднішня UTC-дата (`new Date().toISOString().slice(0, 10)`);
102
- - `runGit` — інжектований git-раннер; за замовчуванням `defaultRunGit(cwd)` (виконує справжні git-команди у `cwd`).
103
- - **Повертає:** масив зрелізованих пакетів, кожен — `{ ws, name, newVersion }`. Якщо релізити нічого не було — повертає `[]` (без коміту/тегу/пушу).
104
- - **Алгоритм:**
105
- 1. Отримує список воркспейсів через `getMonorepoProjectRootDirs(cwd)`.
106
- 2. Визначає, чи це monorepo: `subWorkspaces = workspaces.filter(w => w !== '.')`, `isMonorepoRoot = subWorkspaces.length > 0`.
107
- 3. Для кожного `ws`:
108
- - якщо `ws === '.'` і `isMonorepoRoot` — пропустити (root у monorepo не релізиться сам по собі);
109
- - читає маніфест через `readPackageManifest(ws, cwd)`; якщо манfest відсутній або без `version` — пропустити;
110
- - збирає change-записи через `collectChangeFiles`;
111
- - викликає `aggregateWorkspace({ currentVersion, changeFiles, date })` — якщо повертає `null` (нема чого релізити, всі записи відфільтровано) — пропустити;
112
- - інакше: записує нову версію у маніфест (`writeManifestVersion`), prepend новий розділ у `CHANGELOG.md` (`prependWorkspaceChangelog`), видаляє кожен спожитий change-файл (`rm` у `<cwd>/<ws>/<CHANGES_DIR>/<file>`), додає запис у `released`, а якщо є `manifest.name` — формує тег `<name>@<newVersion>`.
113
- 4. Якщо `released.length > 0`:
114
- - формує subject коміту: список тегів через `, `, або (якщо тегів нема — наприклад, у пакетів без `name`) — `<ws>@<newVersion>`-значення;
115
- - `git add -A`;
116
- - `git commit -m "release: <subject>"` — якщо `runGit` повертає `null` (коміт не вдався), кидає `Error('release: git commit не вдався — теги та push скасовано')`;
117
- - для кожного тегу — `git tag <tag>`;
118
- - `git push --follow-tags`.
119
- - **Side effects:**
120
- - читання/запис файлів маніфестів і `CHANGELOG.md`;
121
- - видалення change-файлів з диска (`rm`);
122
- - виконання git-команд через `runGit` (включно з push до remote);
123
- - `console.warn` при fallback (через `collectChangeFiles`).
124
- - **Помилки:** кидаються через `throw new Error(...)` у двох місцях:
125
- - не знайдено патерн `version` у маніфесті (`writeManifestVersion`);
126
- - не вдався `git commit` (`runGit` повернув `null`).
127
-
128
- ### `runReleaseCli(_args, opts?)` (експорт)
129
-
130
- CLI-фасад: викликає `release(opts)`, друкує підсумок, мапить помилки на exit-код.
131
-
132
- - **Сигнатура:** `async (_args: string[], opts?: { cwd?: string, date?: string, runGit?: ... }) => Promise<number>`
133
- - **Параметри:**
134
- - `_args` — позиційні CLI-аргументи (поточна імплементація опцій з CLI не приймає, тому ігнорується; параметр підкреслений `_` для лінту);
135
- - `opts` — ті самі опції, що в `release` (використовується тестами для інжекції `cwd`/`date`/`runGit`).
136
- - **Повертає:** `Promise<number>` — exit-код:
137
- - `0` — успіх (включно з випадком «немає що релізити»);
138
- - `1` — будь-яка помилка з `release`.
139
- - **Поведінка:**
140
- - якщо `released.length === 0` — друкує `release: немає змін для релізу`;
141
- - інакше для кожного запису — `console.log` рядок `✅ <name або ws>@<newVersion>`;
142
- - при exception — `console.error("❌ <message>")` і повертає `1`. Підтримує і `Error` (бере `.message`), і не-`Error`-значення (приводить через `String(...)`).
143
-
144
- ## Залежності
145
-
146
- ### Стандартна бібліотека Node.js
147
-
148
- - `node:fs` — `existsSync` (для перевірки існування `CHANGELOG.md`);
149
- - `node:fs/promises` — `readFile`, `writeFile`, `rm`;
150
- - `node:path` — `join`.
151
-
152
- ### Внутрішні модулі проекту
153
-
154
- - `../changelog/lib/package-manifest.mjs`:
155
- - `getMonorepoProjectRootDirs(cwd)` — повертає список воркспейсів (включно з `.`);
156
- - `readPackageManifest(ws, cwd)` — читає маніфест воркспейсу;
157
- - тип `PackageManifest` (через JSDoc-імпорт).
158
- - `./lib/aggregate.mjs`:
159
- - `aggregateWorkspace({ currentVersion, changeFiles, date })` — обчислює `newVersion` і `sectionBlock` CHANGELOG за зібраними change-записами; повертає `null`, якщо немає змін до релізу;
160
- - `prependChangelogSection(existing, sectionBlock)` — формує новий вміст `CHANGELOG.md` зі вставкою блоку на початок.
161
- - `./lib/change-file.mjs`:
162
- - константа `CHANGES_DIR` — назва теки з change-файлами всередині воркспейсу;
163
- - `readChangeFiles(ws, cwd)` — читає всі change-файли воркспейсу.
164
- - `./lib/fallback.mjs`:
165
- - `defaultRunGit(cwd)` — фабрика git-раннера з прив'язкою до `cwd`;
166
- - `synthesizeChangeFromCommits(name, ws, { runGit })` — синтезує change-запис із commit-історії з останнього тегу `<name>@*`.
167
-
168
- ### Зовнішні залежності
169
-
170
- Жодних npm-пакетів — лише вбудовані модулі Node.js та внутрішні модулі проекту.
171
-
172
- ## Потік виконання / Використання
173
-
174
- ### CLI-використання (через диспатчер `n-cursor`)
175
-
176
- `runReleaseCli` під'єднується до точки входу `n-cursor release`. Зазвичай викликається в CI на `main` після злиття PR:
177
-
178
- ```bash
179
- n-cursor release
180
- ```
181
-
182
- Exit-код `0` — навіть якщо нічого не зрелізовано (в стандартний випадок «нічого не змінилось»). Exit-код `1` — фейл (помилка запису маніфесту, фейл `git commit`, тощо).
183
-
184
- ### Програмне використання (у тестах / інших скриптах)
185
-
186
- ```js
187
- import { release, runReleaseCli } from './release.mjs'
188
-
189
- // 1) Прямий виклик
190
- const released = await release({
191
- cwd: '/abs/path/to/repo',
192
- date: '2026-06-03',
193
- runGit: async args => '...stdout...' // або null при помилці
194
- })
195
-
196
- // 2) Через CLI-фасад
197
- const code = await runReleaseCli([], { cwd, date, runGit })
198
- process.exit(code)
199
- ```
200
-
201
- ### Послідовність кроків у `release()`
202
-
203
- 1. **Дискавер воркспейсів** — `getMonorepoProjectRootDirs(cwd)`.
204
- 2. **Класифікація** — root пропускається у monorepo (де є суб-воркспейси).
205
- 3. **Для кожного воркспейсу:**
206
- - читання маніфесту;
207
- - збір change-файлів (явні → fallback-синтез з комітів);
208
- - агрегація → `newVersion` + `sectionBlock`;
209
- - запис маніфесту;
210
- - prepend CHANGELOG;
211
- - видалення «спожитих» change-файлів;
212
- - реєстрація запису і (за наявності `name`) тегу.
213
- 4. **Якщо є зрелізоване хоча б одне:**
214
- - формування subject коміту;
215
- - `git add -A` → `git commit -m "release: <subject>"`;
216
- - перевірка успішності коміту (інакше throw);
217
- - проставляння всіх тегів;
218
- - `git push --follow-tags`.
219
- 5. **Повернення масиву** зрелізованих пакетів.
220
-
221
- ### Інваріанти
222
-
223
- - Жодних версій/тегів/коміту, якщо немає реальних змін у жодному воркспейсі — масив `released` залишається порожнім, і блок git взагалі не виконується.
224
- - Якщо хоча б у одному воркспейсі не вдалось оновити маніфест — функція кидає виняток до `git add -A`; часткові зміни на диску можуть залишитися (виклик не транзакційний — це відповідальність CI/runner-а відкотити робоче дерево).
225
- - Якщо `git commit` зафейлився — теги не проставляються і push не виконується; кидається явна помилка.
226
- - Усі git-операції проходять через інжектований `runGit`, що дає змогу тестувати функцію без реальних git-викликів.
28
+ - (специфічних машинно-виведених гарантій немає)
@@ -0,0 +1,32 @@
1
+ ---
2
+ type: JS Module
3
+ title: fix.mjs
4
+ resource: npm/rules/tool-surface/fix.mjs
5
+ docgen:
6
+ crc: 38cf876b
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ issues: judge:inaccurate:0.93
10
+ judgeModel: openai-codex/gpt-5.4-mini
11
+ ---
12
+
13
+ ## Огляд
14
+
15
+ Цей файл запускає логіку перевірки відповідності політики через функцію `run`, використовуючи контекст прогону, який містить дані, кешовані у межах цього прогону. При самостійному запуску скрипт ініціює повний цикл: завантажує конфігурацію, перевіряє білий список та формує підсумок. Результат виконання визначається кодом виходу, що сигналізує про успішне виконання або виявлені порушення.
16
+
17
+ ## Поведінка
18
+
19
+ 1. Викликається функція `run` для запуску правила.
20
+ 2. Правило виконується, використовуючи контекст прогону, що може містити кеш.
21
+ 3. Якщо скрипт виконується як окрема програма (standalone), запускається оркестрація правила.
22
+ 4. Оркестрація виконує завантаження конфігурації, перевірку білого списку та підсумок.
23
+ 5. Код завершує роботу з кодом виходу, який вказує на успіх або наявність порушень.
24
+
25
+ ## Публічний API
26
+
27
+ run — виконує послідовність перевірок: застосовує правила, аналізує JS-занепокоєння, перевіряє політику та посилання MDC.
28
+
29
+ ## Гарантії поведінки
30
+
31
+ - Read-only: не виконує операцій запису (ФС/БД).
32
+ - Кешує результати в межах одного прогону.
@@ -0,0 +1,11 @@
1
+ ---
2
+ type: Directory Index
3
+ title: npm/rules/tool-surface
4
+ resource: npm/rules/tool-surface/
5
+ ---
6
+
7
+ # npm/rules/tool-surface
8
+
9
+ | Файл | Тип |
10
+ |---|---|
11
+ | [fix.mjs](fix.md) | JS Module |
@@ -3,36 +3,45 @@ type: JS Module
3
3
  title: normalize-pipeline.mjs
4
4
  resource: npm/scripts/lib/adr/normalize-pipeline.mjs
5
5
  docgen:
6
- crc: 6eb6ba69
6
+ crc: 9f2d42d3
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
+ issues: judge:inaccurate:0.99
10
+ judgeModel: openai-codex/gpt-5.4-mini
9
11
  ---
10
12
 
11
- Файл реалізує локально-орієнтований конвеєр для нормалізації чернеток ADR. Він використовує LLM лише для вузьких, верифікованих бінарних суджень. Конвеєр працює у послідовних стадіях: JS виконує пошук кандидатів-ребер на основі лексичної схожості, LLM оцінює ці ребра (Stage 1: `same/different`) та драфти (Stage 1b: `standalone/trivial`), JS кластеризує підтверджені ребра (використовуючи `union-find`), LLM реформатує анотера (Stage 2: `gen-MADR`), а LLM генерує доповнення для злиття (Stage 3: `gen-merge`). Глобальний стан (кластери, слаги, покриття) зберігається в JS. Конвеєр повертає операції у форматі `operations[]`, сумісного з контрактом `apply-ops`.
13
+ ## Огляд
14
+
15
+ Файл реалізує локально-орієнтований конвеєр для нормалізації чернеток архітектурних рішень (ADR). Він працює за принципом інверсії керування: JavaScript оркеструє процес, а LLM відповідає лише на вузькі, верифіковані запитання, будучи заточеним під малу локальну модель (omlx/gemma-4b). LLM використовується лише для бінарного судження схожості між записами (наприклад, через `edge-judge`) та витягування змісту секцій у JSON (через `gen-MADR`). Конвеєр збирає, валідує та складає повний MADR-каркас, використовуючи кластеризацію (`cluster` та `union-find`) та проходить через `validation gate`, повертаючи результат у вигляді операцій для застосування.
12
16
 
13
17
  ## Поведінка
14
18
 
15
- tokenize токенізує назву або слаг у множину значущих токенів, виключаючи стоп-слова.
16
- jaccard обчислює Jaccard-схожість між двома множинами токенів.
17
- draftTitle витягує заголовок чернетки, надаючи пріоритет заголовку ADR.
18
- isNoDecision визначає, чи не прийнято рішення у чернетці, аналізуючи секцію Decision Outcome.
19
- buildEdges будує кандидати-ребра між чернетками та між чернетками і чистими ADR на основі лексичної схожості.
20
- validateMadr перевіряє згенерований контент на відповідність канону чистого MADR.
21
- normalizePipeline виконує повний конвеєр нормалізації, групуючи чернетки, оцінюючи їх та генеруючи операції для застосування.
19
+ tokenize: Токенізує назву або слаг, видаляючи стоп-слова та розширюючи його на значущі токени.
20
+ jaccard: Обчислює коефіцієнт Jaccard між двома множинами токенів.
21
+ draftTitle: Витягує заголовок з тіла чернетки, використовуючи ADR-шаблон або перший не-MADR заголовок.
22
+ isNoDecision: Детерміновано визначає, чи рішення в чернетці явно не прийняте.
23
+ buildEdges: Будує кандидати-ребра між чернетками та між чернетками і чистими ADR-файлами на основі лексичної схожості.
24
+ validateMadr: Перевіряє згенерований MADR-текст на відповідність вимогам OKF та структурним елементам.
25
+ madrDate: Детерміновано визначає ISO-дату для поля **Date:**, використовуючи дані з чернетки або імені файлу.
26
+ normalizeSections: Нормалізує сирий JSON-вивід LLM у строгу структуру секцій MADR, толерантно до дрібних відхилень моделі.
27
+ assembleMadr: Збирає канонічний MADR-markdown, використовуючи заголовок, дату та нормалізований контент секцій.
28
+ genMadr: Витягує зміст архітектурного рішення з чернетки у JSON, а потім збирає його у валідний MADR-текст.
29
+ normalizePipeline: Виконує повний конвеєр нормалізації, кластеризуючи чернетки та генеруючи операції для застосування.
22
30
 
23
31
  ## Публічний API
24
32
 
25
- tokenize — розбиває назву чи слаг на значущі частини, замінюючи пробіли та дефіси, і відсіюючи загальні слова.
26
- jaccard — обчислює ступінь подібності між двома наборами слів.
27
- draftTitle — витягує заголовок з чернетки, надаючи пріоритет заголовку у форматі `## ADR <title>`, а у відсутності такого — використовує перший не-ADR заголовок або ім'я файлу.
28
- isNoDecision — визначає, чи не містить чернетка чіткого рішення, що робить її непотрібною для окремого ADR.
29
- buildEdges — створює потенційні зв'язки між елементами на основі схожості слів.
30
- validateMadr — перевіряє якість згенерованого документа ADR.
31
- normalizePipelineвиконує повний процес обробки, повертаючи список виконаних кроків та статистику.
33
+ tokenize — Розбиває назву чи слаг на значущі слова, ігноруючи стоп-слова.
34
+ jaccard — Вимірює схожість двох наборів слів.
35
+ draftTitle — Витягує заголовок з чернетки, надаючи пріоритет заголовку ADR.
36
+ isNoDecision — Визначає, чи не було прийнято рішення у чернетці, щоб уникнути створення зайвих ADR.
37
+ buildEdges — Створює потенційні зв'язки між елементами на основі схожості слів.
38
+ validateMadr — Перевіряє якість згенерованого документа ADR.
39
+ madrDateФормує стандартизовану дату для документа, використовуючи метадані або ім'я файлу.
40
+ normalizeSections — Приводить неструктурований вивід генеративної моделі до чіткого формату секцій.
41
+ assembleMadr — Збирає повний, стандартизований документ ADR, використовуючи фіксовані шаблони.
42
+ genMadr — Створює чернетку документа ADR на основі вхідних даних.
43
+ normalizePipeline — Виконує повний потік обробки даних, повертаючи результати та статистику.
32
44
 
33
45
  ## Гарантії поведінки
34
46
 
35
- - Read-only: файл не виконує операцій запису у файлову систему.
36
- - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
37
- - За невдачі повертає значення помилки (`false`/`null`/`Err`) замість генерування винятку чи паніки.
38
- - Не звертається до мережі.
47
+ - (специфічних машинно-виведених гарантій немає)
@@ -3,29 +3,28 @@ type: JS Module
3
3
  title: worktree-notice.mjs
4
4
  resource: npm/scripts/lib/worktree-notice.mjs
5
5
  docgen:
6
- crc: dc4fba22
6
+ crc: 1f7d5e0d
7
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
+ score: 100
9
+ judgeModel: openai-codex/gpt-5.4-mini
7
10
  ---
8
11
 
9
- Цей файл вбудовує інструкції щодо використання git-worktree, коли `meta.json.worktree` встановлено в `true`. Він забезпечує паралельне виконання скілу лише в окремому git-worktree, запобігаючи потенційним проблемам з паралелізмом. Цей механізм дозволяє уникнути гонки з CDN та забезпечує надійний запуск скілу з локальною копією CLI.
12
+ ## Огляд
13
+
14
+ Цей файл вшиває worktree-інструкцію у синкнутий `SKILL.md` (рішення D2 зі spec). Коли `meta.json.worktree === true`, скіл вставляє/замінює ідемпотентний ре-синкнутий блок, що містить маркери WORKTREE_START та WORKTREE_END, забезпечуючи виконання скілу в окремому git-worktree та запобігаючи паралелізації. Функція `injectWorktreeNotice` керує наявністю або відсутністю цього блоку в `SKILL.md` на основі конфігурації.
10
15
 
11
16
  ## Поведінка
12
17
 
13
- WORKTREE_START: вставляє маркер початку worktree-блоку.
14
- WORKTREE_END: вставляє маркер кінця worktree-блоку.
15
- injectWorktreeNotice: вставляє або видаляє worktree-блок у `SKILL.md` на основі значення `meta.json.worktree`. Якщо `meta.json.worktree` `true`, вставляє блок; інакше видаляє. Якщо блок вже існує, замінює його; якщо ні додає. Враховує наявність YAML-frontmatter та вставляє блок після нього. Використовує транслітерацію для створення суфікса гілки. Реалізує retry-обгортку для `npx` з обмеженням часу та інтервалом для перевірки.
18
+ WORKTREE_START Маркер початку блоку інструкцій для роботи в окремому git-worktree.
19
+ WORKTREE_END Маркер кінця блоку інструкцій для роботи в окремому git-worktree.
20
+ injectWorktreeNotice — Вставляє, оновлює або видаляє блок інструкцій для роботи в worktree у вмісті SKILL.md залежно від булевого значення.
16
21
 
17
22
  ## Публічний API
18
23
 
19
- WORKTREE_START — Початок блоку worktree.
20
- WORKTREE_END — Кінець блоку worktree.
21
- injectWorktreeNotice — Змінює вміст SKILL.md, додаючи, оновлюючи або видаляючи worktree-блок.
24
+ WORKTREE_START — Позначає початок секції, що описує робоче дерево.
25
+ WORKTREE_END — Позначає кінець секції, що описує робоче дерево.
26
+ injectWorktreeNotice — Вставляє, змінює або видаляє блок інформації про робоче дерево у файлі `SKILL.md`.
22
27
 
23
28
  ## Гарантії поведінки
24
29
 
25
- Якщо `meta.json.worktree === true`, то скіл виконується в окремому git-worktree.
26
- Скіл не паралелізується.
27
- Після створення worktree виконується `bun install` у цьому worktree.
28
- Виконується shell-обгортка `n_cursor_npx` навколо `npx` для bootstrap-виклику.
29
- Обгортка `n_cursor_npx` виконує retry на транзитні помилки реєстру/мережі (інтервал 30с, дефолт 5 хв, `N_CURSOR_NPX_RETRY_MAX_MIN`, ceiling 10 хв).
30
- При виникненні nonzero CLI повертається одразу.
31
- Команди, що вимагають command substitution, виконуються після створення worktree.
30
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -107,6 +107,41 @@ export function readEscalationRecords(path, sinceOffset = 0) {
107
107
  return out
108
108
  }
109
109
 
110
+ /** Маркер skip-запису avg-рунга (кеп вичерпано) — НЕ фактичний виклик моделі. */
111
+ const AVG_SKIP_MARKER = 'cloud-avg cap reached'
112
+
113
+ /**
114
+ * Рахує фактичні виклики моделей за тирами (skip-записи avg-кепу не рахуються).
115
+ * @param {object[]} records записи рунгів
116
+ * @returns {{ local: number, cloudMin: number, cloudAvg: number }} лічильники викликів
117
+ */
118
+ export function summarizeCalls(records) {
119
+ const stats = { local: 0, cloudMin: 0, cloudAvg: 0 }
120
+ for (const r of records) {
121
+ if (r.callError === AVG_SKIP_MARKER) continue
122
+ if (r.tier === 'cloud-avg') stats.cloudAvg++
123
+ else if (r.tier === 'cloud-min') stats.cloudMin++
124
+ else if (typeof r.tier === 'string' && r.tier.startsWith('local')) stats.local++
125
+ }
126
+ return stats
127
+ }
128
+
129
+ /**
130
+ * Друкує резюме викликів моделей за цей прогін (локальна / cloud-min / cloud-avg).
131
+ * No-op, якщо викликів не було. Читає записи від `sinceOffset`.
132
+ * @param {number} sinceOffset байтовий зсув логу перед прогоном
133
+ * @param {(s: string) => void} log логер
134
+ * @returns {void}
135
+ */
136
+ export function reportRunStats(sinceOffset, log) {
137
+ const { local, cloudMin, cloudAvg } = summarizeCalls(readEscalationRecords(escalationLogPath(), sinceOffset))
138
+ if (local + cloudMin + cloudAvg === 0) return
139
+ log(
140
+ `\n📊 LLM-виклики fix-конформності (цей прогін): ` +
141
+ `локальна ${local} · cloud-min ${cloudMin} · cloud-avg ${cloudAvg}\n`
142
+ )
143
+ }
144
+
110
145
  /**
111
146
  * Стискає запис до полів, важливих для аналізу (без ts/ms-шуму).
112
147
  * @param {object} r сирий запис рунга
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: analyze-escalation.mjs
4
4
  resource: npm/scripts/lib/fix/analyze-escalation.mjs
5
5
  docgen:
6
- crc: f802e47f
6
+ crc: 5a586df6
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -16,6 +16,8 @@ docgen:
16
16
 
17
17
  ## Публічний API
18
18
 
19
+ - `summarizeCalls(records)` — лічильники фактичних викликів за тирами `{ local, cloudMin, cloudAvg }` (skip-записи avg-кепу не рахуються).
20
+ - `reportRunStats(sinceOffset, log)` — друкує резюме викликів моделей за прогін (no-op, якщо викликів не було).
19
21
  - `analysisEnabled()` — чи дозволено авто-аналіз (kill-switch `N_CURSOR_FIX_ANALYZE`).
20
22
  - `escalationLogSize(path?)` — розмір логу в байтах (since-offset).
21
23
  - `readEscalationRecords(path, sinceOffset?)` — записи від зсуву.
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: llm-fix-apply.mjs
4
4
  resource: npm/scripts/lib/fix/llm-fix-apply.mjs
5
5
  docgen:
6
- crc: 80befb00
6
+ crc: 62e475fa
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -14,7 +14,7 @@ docgen:
14
14
 
15
15
  parseChangesResponse парсить сирий текст відповіді моделі, щоб витягнути структуру змін у форматі патчу або повернути null у разі невдачі парсингу.
16
16
  readFilesForFix зчитує вміст файлів, зазначених у списку відносних шляхів, відносно кореня проєкту, повертаючи список об'єктів з шляхом та вмістом або null для неіснуючих файлів.
17
- applyChanges записує нові вмісти в файли, використовуючи наданий список змін, відносно кореня проєкту, повертаючи статус успіху або помилки.
17
+ applyChanges записує нові вмісти в файли, використовуючи наданий список змін, відносно кореня проєкту; перед записом створює батьківську теку (`mkdirSync recursive`) — модель може запропонувати новий файл у ще неіснуючому каталозі. Повертає статус успіху або помилки.
18
18
 
19
19
  ## Публічний API
20
20
 
@@ -3,19 +3,18 @@ type: JS Module
3
3
  title: t0.mjs
4
4
  resource: npm/scripts/lib/fix/t0.mjs
5
5
  docgen:
6
- crc: 0321ecc1
6
+ crc: 524d91e2
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
10
10
 
11
- Модуль керує автоматичним застосуванням паттернів до правил для виправлення виводів порушень. Він визначає, які автоматичні паттерни можуть бути застосовані до правил, використовуючи `filterT0AutoRules`, і запускає повний процес виправлення для всіх відповідних правил через `applyT0Auto` та `runT0AutoCli`. Код працює у режимі fail-safe, перехоплюючи помилки та не викидаючи винятків назовні. Робота модуля залежить від конфігурацій `extensions.json` та `package-lock.json`.
11
+ Модуль керує детермінованим (без LLM) застосуванням паттернів до виводів порушень конформності. Паттерни: `vscode-ext-add` (дописати розширення), `rm-forbidden-file` (видалити заборонений файл), `changelog-create-change-file` (створити change-файл через канонічну `writeChange` для воркспейсів із «немає change-файлу» прибирає ескалацію в LLM на цьому кейсі). `filterT0AutoRules` визначає, які правила мають придатний паттерн; `applyT0Auto`/`runT0AutoCli` застосовують. Паттерн може бути sync або async (await нормалізує). Fail-safe: помилки перехоплюються.
12
12
 
13
13
  ## Поведінка
14
14
 
15
- Поведінка
16
- applyT0Auto застосовує визначені автоматичні паттерни до одного виводу порушення, щоб виправити його.
17
- filterT0AutoRules повертає список ідентифікаторів правил, для яких існують відповідні автоматичні паттерни.
18
- runT0AutoCli запускає процес виправлення T0-автоматично для всіх провальних правил, застосовуючи паттерни, повторно перевіряючи стан і виводячи підсумок.
15
+ applyT0Auto застосовує придатні паттерни до одного виводу порушення (await на apply — паттерн може бути async, як `changelog-create-change-file`).
16
+ filterT0AutoRules повертає id правил, для яких існує придатний паттерн.
17
+ runT0AutoCli запускає T0-auto для всіх провальних правил, повторно перевіряє check-gate і виводить підсумок.
19
18
 
20
19
  ## Публічний API
21
20
 
@@ -3,8 +3,8 @@
3
3
  * під фікс і застосування змін. Використовують і `llm-worker.mjs` (конформність), і
4
4
  * `llm-lint-fix.mjs` (per-tool лінтер-фіксери) — щоб не дублювати парс/apply (knip/jscpd).
5
5
  */
6
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
7
- import { join } from 'node:path'
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
7
+ import { dirname, join } from 'node:path'
8
8
 
9
9
  const JSON_CODE_BLOCK_RE = /```(?:json)?[ \t]{0,8}\n?([\s\S]*?)```/
10
10
 
@@ -69,7 +69,11 @@ export function applyChanges(changes, projectRoot) {
69
69
  for (const change of changes) {
70
70
  if (!change.path || typeof change.content !== 'string') continue
71
71
  try {
72
- writeFileSync(join(projectRoot, change.path), change.content, 'utf8')
72
+ const abs = join(projectRoot, change.path)
73
+ // Створюємо батьківську теку перед записом: модель може запропонувати новий файл
74
+ // у ще неіснуючому каталозі (напр. `<ws>/.changes/…`) — інакше writeFileSync ENOENT.
75
+ mkdirSync(dirname(abs), { recursive: true })
76
+ writeFileSync(abs, change.content, 'utf8')
73
77
  } catch (error) {
74
78
  return { ok: false, error: `write ${change.path}: ${error.message}` }
75
79
  }
@@ -1,13 +1,33 @@
1
1
  /** @see ./docs/t0.md */
2
+ import { spawnSync } from 'node:child_process'
2
3
  import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
3
4
  import { join } from 'node:path'
4
5
 
5
6
  import { runFixCheck } from './run-fix-check.mjs'
7
+ import { writeChange } from '../../../rules/release/change.mjs'
6
8
 
7
9
  const REC_REQUIRE_RE = /recommendations має містити "[^"]+"/
8
10
  const REC_MATCH_ALL_RE = /recommendations має містити "([^"]+)"/g
9
11
  const FORBIDDEN_FILE_RE = /Знайдено заборонений файл: \S+/
10
12
  const FORBIDDEN_FILE_MATCH_ALL_RE = /Знайдено заборонений файл: (\S+)/g
13
+ // Конформність changelog: «<ws>: є релевантні зміни, але немає change-файлу».
14
+ const MISSING_CHANGE_RE = /є релевантні зміни, але немає change-файлу/
15
+ const MISSING_CHANGE_MATCH_ALL_RE = /(?:^|\s)([\w./@-]+): є релевантні зміни, але немає change-файлу/gm
16
+ /** Дефолти autofix-створеного change-файлу (узгоджено з n-changelog.mdc / consistency.mjs). */
17
+ const CHANGE_BUMP = 'patch'
18
+ const CHANGE_SECTION = 'Changed'
19
+ const CHANGE_FALLBACK_MESSAGE = 'оновлення'
20
+
21
+ /**
22
+ * Опис для авто-створеного change-файлу: subject останнього коміту, інакше fallback.
23
+ * @param {string} cwd корінь репозиторію
24
+ * @returns {string} непорожній опис
25
+ */
26
+ function autoChangeMessage(cwd) {
27
+ const r = spawnSync('git', ['log', '-1', '--format=%s'], { cwd, encoding: 'utf8' })
28
+ const subject = r.status === 0 ? (r.stdout ?? '').trim() : ''
29
+ return subject || CHANGE_FALLBACK_MESSAGE
30
+ }
11
31
 
12
32
  /**
13
33
  * Патерни T0-auto.
@@ -71,6 +91,31 @@ const PATTERNS = [
71
91
  if (removed.length === 0) return { ok: false, action: 'файлів не знайдено' }
72
92
  return { ok: true, action: `видалено: ${removed.join(', ')}` }
73
93
  }
94
+ },
95
+
96
+ // ── changelog-create-change-file ─────────────────────────────────────────────
97
+ // Violation: «<ws>: є релевантні зміни, але немає change-файлу»
98
+ // Fix: створити change-файл через канонічну `writeChange` (без LLM) — той самий
99
+ // механізм, що autofix changelog-конформності. Прибирає ескалацію в хмару на цьому кейсі.
100
+ {
101
+ id: 'changelog-create-change-file',
102
+ test: out => MISSING_CHANGE_RE.test(out),
103
+ apply: async (out, cwd) => {
104
+ const workspaces = Array.from(out.matchAll(MISSING_CHANGE_MATCH_ALL_RE), m => m[1])
105
+ if (workspaces.length === 0) return { ok: false, action: 'no match' }
106
+
107
+ const message = autoChangeMessage(cwd)
108
+ const created = []
109
+ for (const ws of workspaces) {
110
+ try {
111
+ const rel = await writeChange({ bump: CHANGE_BUMP, section: CHANGE_SECTION, message, ws, cwd })
112
+ created.push(ws === '.' ? rel : join(ws, rel))
113
+ } catch (error) {
114
+ return { ok: false, action: `writeChange ${ws}: ${error.message}` }
115
+ }
116
+ }
117
+ return { ok: true, action: `створено change-файл (${CHANGE_BUMP}/${CHANGE_SECTION}): ${created.join(', ')}` }
118
+ }
74
119
  }
75
120
  ]
76
121
 
@@ -79,15 +124,16 @@ const PATTERNS = [
79
124
  * @param {string} ruleId id правила (для логу)
80
125
  * @param {string} violationOutput рядок з поля `output` у `fix --json`
81
126
  * @param {string} cwd корінь проєкту
82
- * @returns {{ applied: boolean, actions: string[] }} результат: чи щось застосовано і список дій
127
+ * @returns {Promise<{ applied: boolean, actions: string[] }>} результат: чи щось застосовано і список дій
83
128
  */
84
- export function applyT0Auto(ruleId, violationOutput, cwd) {
129
+ export async function applyT0Auto(ruleId, violationOutput, cwd) {
85
130
  const actions = []
86
131
  let applied = false
87
132
 
88
133
  for (const p of PATTERNS) {
89
134
  if (!p.test(violationOutput)) continue
90
- const result = p.apply(violationOutput, cwd)
135
+ // Патерн може бути sync ({ok,action}) або async (Promise) — await нормалізує обидва.
136
+ const result = await p.apply(violationOutput, cwd)
91
137
  actions.push(`[${p.id}] ${result.action}`)
92
138
  if (result.ok) {
93
139
  applied = true
@@ -113,13 +159,13 @@ export function filterT0AutoRules(failedRules) {
113
159
  * Застосовує T0-auto до кожного провального правила, розділяючи на applied/skipped.
114
160
  * @param {Array<{ ruleId: string, output: string }>} failed провальні правила
115
161
  * @param {string} cwd корінь проєкту
116
- * @returns {{ applied: Array<{ ruleId: string, actions: string[] }>, skipped: string[] }} застосовані й пропущені
162
+ * @returns {Promise<{ applied: Array<{ ruleId: string, actions: string[] }>, skipped: string[] }>} застосовані й пропущені
117
163
  */
118
- function applyT0ToFailed(failed, cwd) {
164
+ async function applyT0ToFailed(failed, cwd) {
119
165
  const applied = []
120
166
  const skipped = []
121
167
  for (const r of failed) {
122
- const result = applyT0Auto(r.ruleId, r.output, cwd)
168
+ const result = await applyT0Auto(r.ruleId, r.output, cwd)
123
169
  if (result.applied) {
124
170
  applied.push({ ruleId: r.ruleId, actions: result.actions })
125
171
  } else {
@@ -150,7 +196,7 @@ export async function runT0AutoCli(args, cwd) {
150
196
  }
151
197
 
152
198
  // 2. Застосувати T0-auto
153
- const { applied, skipped } = applyT0ToFailed(failed, cwd)
199
+ const { applied, skipped } = await applyT0ToFailed(failed, cwd)
154
200
 
155
201
  if (applied.length === 0) {
156
202
  console.log(`⏭️ fix-t0: T0-auto паттерн не підходить для: ${failed.map(r => r.ruleId).join(', ')}`)