@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 +12 -0
- package/package.json +1 -1
- package/rules/lint/js/docs/orchestrate.md +4 -3
- package/rules/lint/js/orchestrate.mjs +66 -28
- package/rules/release/docs/release.md +14 -212
- package/rules/tool-surface/docs/fix.md +32 -0
- package/rules/tool-surface/docs/index.md +11 -0
- package/scripts/lib/adr/docs/normalize-pipeline.md +29 -20
- package/scripts/lib/docs/worktree-notice.md +14 -15
- package/scripts/lib/fix/analyze-escalation.mjs +35 -0
- package/scripts/lib/fix/docs/analyze-escalation.md +3 -1
- package/scripts/lib/fix/docs/llm-fix-apply.md +2 -2
- package/scripts/lib/fix/docs/t0.md +5 -6
- package/scripts/lib/fix/llm-fix-apply.mjs +7 -3
- package/scripts/lib/fix/t0.mjs +53 -7
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
|
@@ -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:
|
|
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 запускає оркестрацію лінтування: або виконує перевірку конформності для заданих правил, або ітерує по алфавітно відсортованих
|
|
18
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
132
|
-
//
|
|
171
|
+
// Конформність-фаза: whole-repo, лише у `--full`. Кастомний rulesDir (юніт-тести
|
|
172
|
+
// селектора) — реальний пакет недоступний, тож пропускаємо.
|
|
133
173
|
if (full && opts.rulesDir === undefined) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
12
|
+
## Огляд
|
|
10
13
|
|
|
11
|
-
|
|
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
|
-
|
|
16
|
+
## Поведінка
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
release агрегує change-файли для всіх робочих просторів, оновлює версії в маніфестах, додає секції до `CHANGELOG.md`, видаляє використані change-файли, створює коміт та анотовані теги для всіх зібраних релізів, а потім намагається пушити їх у апстрім з повторними спробами.
|
|
19
|
+
runReleaseCli виконує процес релізу, викликаючи функцію release, та виводить відповідний статус у консоль.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
- single-package репо (`workspaces === ['.']`) — релізиться сам root.
|
|
21
|
+
## Публічний API
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
release — агрегує change-файли для кожного робочого простору у version-bump та CHANGELOG, комітить зміни, ставить тег `<name>@<version>` та видаляє використані change-файли.
|
|
24
|
+
runReleaseCli — виконує команди командного інтерфейсу для запуску процесу випуску.
|
|
26
25
|
|
|
27
|
-
##
|
|
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
|
+
- Кешує результати в межах одного прогону.
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
17
|
-
draftTitle
|
|
18
|
-
isNoDecision визначає, чи
|
|
19
|
-
buildEdges
|
|
20
|
-
validateMadr
|
|
21
|
-
|
|
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 —
|
|
28
|
-
isNoDecision —
|
|
29
|
-
buildEdges —
|
|
30
|
-
validateMadr —
|
|
31
|
-
|
|
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
|
-
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
14
|
-
WORKTREE_END
|
|
15
|
-
injectWorktreeNotice
|
|
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 —
|
|
20
|
-
WORKTREE_END —
|
|
21
|
-
injectWorktreeNotice —
|
|
24
|
+
WORKTREE_START — Позначає початок секції, що описує робоче дерево.
|
|
25
|
+
WORKTREE_END — Позначає кінець секції, що описує робоче дерево.
|
|
26
|
+
injectWorktreeNotice — Вставляє, змінює або видаляє блок інформації про робоче дерево у файлі `SKILL.md`.
|
|
22
27
|
|
|
23
28
|
## Гарантії поведінки
|
|
24
29
|
|
|
25
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
6
|
+
crc: 524d91e2
|
|
7
7
|
model: omlx/gemma-4-e4b-it-OptiQ-4bit
|
|
8
8
|
score: 100
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
-
Модуль керує
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
}
|
package/scripts/lib/fix/t0.mjs
CHANGED
|
@@ -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
|
-
|
|
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(', ')}`)
|