@nitra/cursor 5.2.0 → 5.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.2.1] - 2026-06-11
4
+
5
+ ### Changed
6
+
7
+ - adr
8
+
3
9
  ## [5.2.0] - 2026-06-11
4
10
 
5
11
  ### Changed
@@ -1,30 +1,37 @@
1
+ ---
2
+ docgen:
3
+ source: npm/lib/models.mjs
4
+ crc: feb82992
5
+ score: 100
6
+ ---
7
+
1
8
  # models.mjs
2
9
 
3
10
  ## Огляд
4
11
 
5
- Файл визначає ієрархічну класифікацію моделей для системи pi. Класифікація встановлює зв'язок між локальними та хмарними провайдерами. Функція resolveModel забезпечує маршрутизацію вибору моделі залежно від заданого рівня доступності.
12
+ Файл визначає глобальну класифікацію моделей для системи pi, встановлюючи конфігураційні моделі для локального та хмарного інференсу через змінні середовища (наприклад, `N_LOCAL_MIN_MODEL`). Значення моделі мають формат "provider/model-id".
13
+
14
+ Система надає механізм каскадного вибору моделі через функцію `resolveModel`. Цей механізм послідовно перевіряє локальні тири (`LOCAL_MIN` $\rightarrow$ `LOCAL_AVG` $\rightarrow$ `LOCAL_MAX`), а потім хмарні тири, якщо попередні не визначені. Це забезпечує прозору роботу, навіть якщо локальні моделі відсутні. Прямі константи (наприклад, `LOCAL_MIN`) залишені для випадків, що вимагають явного контролю над вибором моделі.
6
15
 
7
16
  ## Поведінка
8
17
 
9
- LOCAL_MIN встановлює мінімальний локальний провайдер
10
- LOCAL_AVG встановлює середній локальний провайдер
11
- LOCAL_MAX встановлює максимальний локальний провайдер
12
- CLOUD_MIN встановлює мінімальний хмарний провайдер
13
- CLOUD_AVG встановлює середній хмарний провайдер
14
- CLOUD_MAX встановлює максимальний хмарний провайдер
15
- resolveModel повертає перший непорожній model-id з каскадного перевірки локальних та хмарних провайдерів
16
- resolveModel приймає тир min avg або max
17
- resolveModel повертає model-id або порожній рядок якщо жоден тир не задано
18
+ LOCAL_MIN повертає модель для швидкого локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
19
+ LOCAL_AVG повертає модель для середнього локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
20
+ LOCAL_MAX повертає модель для максимального локального інференсу, або порожній рядок, якщо змінна середовища не встановлена.
21
+ CLOUD_MIN повертає модель для мінімального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
22
+ CLOUD_AVG повертає модель для середнього хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
23
+ CLOUD_MAX повертає модель для максимального хмарного інференсу, або порожній рядок, якщо змінна середовища не встановлена.
24
+ resolveModel повертає перший непорожній model-id для запитаного тиру, каскадно перевіряючи локальні тири, а потім хмарний еквівалент, або порожній рядок, якщо жоден тир не задано.
18
25
 
19
26
  ## Публічний API
20
27
 
21
- LOCAL_MIN — Виконує швидкий локальний inference.
22
- LOCAL_AVG — Виконує середній локальний inference.
23
- LOCAL_MAX — Виконує максимальний локальний inference.
24
- CLOUD_MIN — Виконує мінімальний хмарний inference.
25
- CLOUD_AVG — Виконує середній хмарний inference.
26
- CLOUD_MAX — Виконує максимальний хмарний inference.
27
- resolveModel — Повертає перший непорожній model-id для запиту, перевіряючи спочатку локальні, а потім хмарні варіанти.
28
+ LOCAL_MIN — Швидке виконання моделі на локальному пристрої.
29
+ LOCAL_AVG — Середнє за продуктивністю виконання моделі на локальному пристрої.
30
+ LOCAL_MAX — Найпотужніше виконання моделі на локальному пристрої.
31
+ CLOUD_MIN — Найменш ресурсомістке виконання моделі в хмарі.
32
+ CLOUD_AVG — Середній рівень продуктивності виконання моделі в хмарі.
33
+ CLOUD_MAX — Найпотужніше виконання моделі в хмарі.
34
+ resolveModel — Знаходить і повертає ідентифікатор моделі, починаючи з локальних варіантів, а потім переходячи до хмарних відповідників.
28
35
 
29
36
  ## Гарантії поведінки
30
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,387 +1,40 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/changelog/js/consistency.mjs
4
+ crc: eaf98d6d
5
+ score: 100
6
+ ---
7
+
1
8
  # consistency.mjs
2
9
 
3
10
  ## Огляд
4
11
 
5
- Модуль `consistency.mjs` реалізує перевірку правила `n-changelog` для монорепозиторіїв із кількома воркспейсами (npm та Python). Його завдання гарантувати, що **будь-яка реліз-релевантна зміна у воркспейсі супроводжується change-файлом** (`<ws>/.changes/*.md`), а поле `version` у маніфесті воркспейсу не зміщене ручним bump-ом поза CI.
6
-
7
- Ключові інваріанти, що їх стверджує перевірка:
8
-
9
- - `version` не повинен дрейфувати відносно бази (опублікованої у реєстрі версії або версії в git-базі гілки). Будь-який ручний bump **fail**, навіть якщо присутній change-файл.
10
- - Bump `version` і генерацію `CHANGELOG.md` виконує **виключно** `n-cursor release` у CI на гілці `main`.
11
- - Релевантні зміни без change-файлу **fail**; зміни лише в інверсних шляхах (`docs/`, `doc/`, `.cursor/`, `.claude/`) — змінами не вважаються.
12
- - npm-пакети, що публікують `CHANGELOG.md` разом із пакетом, повинні мати рядок `"CHANGELOG.md"` у масиві `files` маніфесту, але це перевіряється лише за наявності pending change-файлів.
13
-
14
- Передбачено дві моделі визначення бази на рівні воркспейсу:
15
-
16
- 1. **registry-published** npm-пакети з `name` і `files`, не `private`; Python-проєкти зі статичною `project.version` і `project.name`. База версія, опублікована в npm-реєстрі або PyPI.
17
- 2. **local-only** приватні npm без `files`, Python без імені/версії для реєстру. База визначається через git:
18
- - feature-гілка `merge-base` з `dev`, інакше з `main`;
19
- - гілка `main` diff від `origin/main` (або `HEAD~1` без remote);
20
- - гілка `dev` перевірка пропускається (крім незакомічених registry-published).
21
-
22
- Усі виклики `git` і зовнішні HTTP/CLI — через `execFile` / `fetch`, без shell-інтерполяції (безпека, виключає command injection).
23
-
24
- ## Експорти / API
25
-
26
- | Експорт | Тип | Призначення |
27
- | -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
28
- | `check(opts?)` | `async function` | Єдина публічна точка входу. Запускає весь цикл перевірок для всіх воркспейсів монорепо і повертає exit-код. |
29
-
30
- Сигнатура `check`:
31
-
32
- ```js
33
- export async function check(opts = {}): Promise<number>
34
- ```
35
-
36
- `opts`:
37
-
38
- - `opts.getPublishedVersion?: (name: string, kind?: 'npm' | 'python') => Promise<string | null>` — перевизначення стандартного резолвера опублікованої версії (для юніт-тестів, оффлайн-режимів).
39
- - `opts.cwd?: string` — корінь репозиторію; за замовчуванням `process.cwd()`.
40
- - `opts.autofix?: boolean` — autofix-режим. За замовчуванням береться з env `N_CURSOR_CHANGELOG_AUTOFIX === '1'` (виставляє лише крок `npm-changelog` у `hk.pkl` для pre-commit). Коли увімкнено, замість `fail` на відсутній change-файл правило створює його через `writeChange()` з дефолтами (`bump=patch`, `section=Changed`, `message` = subject останнього коміту, fallback — назва гілки чи `оновлення`) і ставить у git-індекс (`git add`), тож коміт не падає. **Жодної мережі:** у autofix-режимі published-перевірка пропускає реєстровий резолв (`npm view` / PyPI fetch) і drift-перевірку version vs опублікована — лишається лише наявність change-файлу та git-diff. Ручний bump `version` у хуці не ловиться; його далі ловить CI та ручний `fix changelog` без env. Поза хуком режим вимкнено — поведінка лишається fail-on-missing з повною drift-перевіркою, щоб CI не плодив артефактів.
41
-
42
- Повертає **exit-код** (0 — pass, ≠ 0 — fail), отриманий від `createCheckReporter()`.
43
-
44
- ## Функції
45
-
46
- ### `gitOrNull(args, cwd)`
47
-
48
- - **Сигнатура:** `async (args: string[], cwd: string) => Promise<string | null>`
49
- - **Параметри:** `args` — аргументи `git`; `cwd` — робочий каталог процесу.
50
- - **Повертає:** `stdout` команди або `null` при будь-якій помилці.
51
- - **Side effects:** виконує дочірній процес `git` через `execFile`.
52
-
53
- Тиха обгортка над `git`, що ковтає виключення — використовується скрізь, де відсутність гілки/ref/маніфесту є штатним кейсом.
54
-
55
- ### `isInsideGitRepo(cwd)`
56
-
57
- - **Сигнатура:** `async (cwd: string) => Promise<boolean>`
58
- - **Повертає:** `true`, якщо `cwd` всередині git working tree.
59
- - **Side effects:** запит `git rev-parse --is-inside-work-tree`.
60
-
61
- ### `currentBranchName(cwd)`
62
-
63
- - **Сигнатура:** `async (cwd: string) => Promise<string | null>`
64
- - **Повертає:** ім'я поточної гілки (`git rev-parse --abbrev-ref HEAD`) або `null`.
65
-
66
- ### `baseRefLabel(ref)`
67
-
68
- - **Сигнатура:** `(ref: string) => string`
69
- - **Параметри:** `ref` — git-ref.
70
- - **Повертає:** човничок без префіксу `origin/` (наприклад, `origin/main` → `main`); інакше повертає `ref` без змін.
71
- - **Side effects:** немає.
72
-
73
- ### `isGitAncestor(ancestor, descendant, cwd)`
74
-
75
- - **Сигнатура:** `async (ancestor: string, descendant: string, cwd: string) => Promise<boolean>`
76
- - **Повертає:** `true`, якщо `ancestor` є предком `descendant` (через `git merge-base --is-ancestor`).
77
- - **Зауваження:** `git merge-base --is-ancestor` повертає exit-код, тому всередині використовується `gitOrNull`, який ловить ненульовий exit і повертає `null` — у такому випадку результат функції `false`.
78
-
79
- ### `resolveBranchRef(branchName, cwd)`
80
-
81
- - **Сигнатура:** `async (branchName: string, cwd: string) => Promise<string | null>`
82
- - **Поведінка:** для `branchName` пробує спочатку локальний ref, потім `origin/<branchName>`; повертає перший, що верифікується через `git rev-parse --verify --quiet`.
83
-
84
- ### `isChangelogIgnoredPath(relPath)`
85
-
86
- - **Сигнатура:** `(relPath: string) => boolean`
87
- - **Поведінка:** нормалізує шлях до posix (заміна `\` на `/`, обрізання провідного `./`), повертає `true`, якщо починається з одного з префіксів `CHANGELOG_IGNORE_PATH_PREFIXES`.
88
-
89
- ### `isPathGitIgnored(relPath, cwd)`
90
-
91
- - **Сигнатура:** `async (relPath: string, cwd: string) => Promise<boolean>`
92
- - **Поведінка:** виконує `git check-ignore -q -- <relPath>`. Exit-код 0 → ignored (повертає `true`); будь-яка помилка → `false`.
93
- - **Side effects:** дочірній процес `git`.
94
-
95
- ### `resolveMergeBase(baseRef, cwd)`
96
-
97
- - **Сигнатура:** `async (baseRef: string, cwd: string) => Promise<string | null>`
98
- - **Повертає:** SHA `git merge-base baseRef HEAD` або `null`.
99
-
100
- ### `resolveChangelogComparisonPoint(branch, cwd)`
101
-
102
- - **Сигнатура:** `async (branch: string | null, cwd: string) => Promise<{ ref: string, label: string } | null>`
103
- - **Логіка:**
104
- - якщо `branch === 'dev'` → `null` (local-only пропускається);
105
- - якщо `branch === 'main'`:
106
- - якщо `origin/main` верифіковано і `origin/main === HEAD` або `origin/main` — предок `HEAD` → `{ ref: 'origin/main', label: 'main' }`;
107
- - інакше `HEAD~1` → `{ ref: <sha>, label: 'main~1' }`;
108
- - якщо ні те, ні те — `null`.
109
- - feature-гілки: ітерує по `FEATURE_BASE_BRANCH_CANDIDATES` (`['dev', 'main']`); перший, для якого вдається резолвити ref **і** обчислити merge-base, дає `{ ref: <merge-base SHA>, label: baseRefLabel(...) }`.
110
- - **Повертає:** опис точки порівняння (`ref` для `git diff`/`git show`, `label` для повідомлень) або `null`.
111
-
112
- ### `pathspecForWorkspace(ws, subWorkspaces)`
113
-
114
- - **Сигнатура:** `(ws: string, subWorkspaces: string[]) => string[]`
115
- - **Поведінка:**
116
- - для `ws !== '.'` → `[\`${ws}/\`]`;
117
- - для `ws === '.'` (корінь монорепо) → `['.', ':(exclude)<sub>/' для кожного підворкспейсу]`, щоб залишити лише файли кореня без вкладених воркспейсів.
118
- - **Повертає:** масив pathspec-ів для передачі в `git diff -- <pathspec>`.
119
-
120
- ### `splitNulPaths(nulSeparated)`
121
-
122
- - **Сигнатура:** `(nulSeparated: string | null) => string[]`
123
- - **Поведінка:** ділить вхідний рядок по `\0`, відкидає порожні елементи.
124
- - **Чому `-z`:** без прапорця git застосовує `core.quotePath` і повертає не-ASCII імена (наприклад, кирилицю) у C-quoted формі (`"docs/\320\262..."`), що ламає префіксне порівняння для `CHANGELOG_IGNORE_PATH_PREFIXES`.
125
-
126
- ### `listChangedPathsAgainstBase(baseRef, pathspec, cwd)`
127
-
128
- - **Сигнатура:** `async (baseRef: string, pathspec: string[], cwd: string) => Promise<string[]>`
129
- - **Поведінка:** об'єднує два джерела через `Set`:
130
- - `git diff --name-only -z <baseRef> -- <pathspec>` — закомічені/staged зміни;
131
- - `git ls-files --others --exclude-standard -z -- <pathspec>` — нові untracked-файли.
132
- - **Повертає:** дедуплікований масив відносних шляхів.
133
-
134
- ### `workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces, cwd)`
135
-
136
- - **Сигнатура:** `async (baseRef: string, ws: string, subWorkspaces: string[], cwd: string) => Promise<boolean>`
137
- - **Поведінка:** обчислює pathspec для `ws`, отримує всі змінені шляхи, ітерує по них:
138
- - інверсія (`docs/`, `.cursor/`, ...) → пропустити;
139
- - `git check-ignore` → пропустити;
140
- - інакше — повернути `true`.
141
- - **Повертає:** `true`, якщо є хоч один шлях, що вважається релевантною змінною.
142
-
143
- ### `readBaseVersion(baseRef, manifest, cwd)`
144
-
145
- - **Сигнатура:** `async (baseRef: string, manifest: PackageManifest, cwd: string) => Promise<string | null>`
146
- - **Поведінка:** виконує `git show <baseRef>:<wsPath>`, де `wsPath` — шлях до маніфесту відносно репозиторію; парсить:
147
- - `npm` → `JSON.parse(...).version` (`null` при помилці парсу);
148
- - `python` → `parsePyprojectFields(out).version`.
149
- - **Повертає:** версію з маніфесту на `baseRef` або `null`.
150
-
151
- ### `defaultGetPublishedNpmVersion(name)`
152
-
153
- - **Сигнатура:** `async (name: string) => Promise<string | null>`
154
- - **Поведінка:** `npm view <name> version` із таймаутом `REGISTRY_TIMEOUT_MS` (10 с). Trim і повернення; пуста відповідь / помилка → `null`.
155
- - **Side effects:** дочірній процес `npm`, мережа.
156
-
157
- ### `defaultGetPublishedPyPiVersion(name)`
158
-
159
- - **Сигнатура:** `async (name: string) => Promise<string | null>`
160
- - **Поведінка:** `fetch('https://pypi.org/pypi/<encodedName>/json', { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) })`; читає `data.info.version`. Будь-яка помилка / `!res.ok` → `null`.
161
- - **Side effects:** мережа.
162
-
163
- ### `resolvePublishedVersion(manifest, getPublishedVersion)`
164
-
165
- - **Сигнатура:** `(manifest: PackageManifest, getPublishedVersion) => Promise<string | null>`
166
- - **Поведінка:** якщо в маніфесті немає `name` → `Promise.resolve(null)`; інакше делегує до `getPublishedVersion(name, kind)`.
167
-
168
- ### `defaultGetPublishedVersion(name, kind = 'npm')`
169
-
170
- - **Сигнатура:** `(name: string, kind?: 'npm' | 'python') => Promise<string | null>`
171
- - **Поведінка:** диспетчер за `kind` (Python → PyPI, інакше — npm).
172
-
173
- ### `createDefaultGetPublishedVersion()`
174
-
175
- - **Сигнатура:** `() => (name, kind?) => Promise<string | null>`
176
- - **Поведінка:** фабрика, що повертає `defaultGetPublishedVersion`. Використовується як дефолт у `check` для зручної підміни в тестах.
177
-
178
- ### `checkNpmFilesArrayContainsChangelog(manifest, pass, fail)`
179
-
180
- - **Сигнатура:** `(manifest: PackageManifest, pass: (msg)=>void, fail: (msg)=>void) => void`
181
- - **Поведінка:**
182
- - якщо `kind !== 'npm'` або `npmFiles` відсутній — рання терміновка;
183
- - `pass`, якщо `npmFiles` містить `'CHANGELOG.md'`;
184
- - інакше `fail` з рекомендацією додати рядок.
185
-
186
- ### `workspaceLabel(manifest)`
187
-
188
- - **Сигнатура:** `(manifest: PackageManifest) => string`
189
- - **Повертає:** `'<root>'` для `ws === '.'`, інакше `manifest.ws`.
190
-
191
- ### `missingChangeFileMessage(label, mf)`
192
-
193
- - **Сигнатура:** `(label: string, mf: string) => string`
194
- - **Повертає:** уніфікований текст для `fail` про відсутній change-файл, включно з інструкцією для `npx @nitra/cursor change`.
195
-
196
- ### `hasPendingChangeFiles(ws, cwd)`
197
-
198
- - **Сигнатура:** `async (ws: string, cwd: string) => Promise<boolean>`
199
- - **Поведінка:** `(await readChangeFiles(ws, cwd)).length > 0`.
200
-
201
- ### `checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, subWorkspaces, pass, fail, cwd)`
202
-
203
- - **Сигнатура:** `async (...) => Promise<void>`
204
- - **Параметр `_Vcurrent`:** ігнорується (залишений для сумісності сигнатури; bump робить CI).
205
- - **Поведінка:**
206
- 1. Якщо `hasPendingChangeFiles` → `pass` про change-файл(и) + перевірка `CHANGELOG.md` у `files` npm-маніфесту. Вихід.
207
- 2. Якщо не в git-репі — вихід без перевірок.
208
- 3. Беремо `currentBranchName`:
209
- - `branch === 'dev'`: лише перевірка наявності релевантних змін відносно `HEAD` (staged/working tree). Є — `fail` `missingChangeFileMessage`; нема — мовчазний вихід.
210
- - інакше: резолвимо `comparison`; якщо `comparison` + є зміни відносно `comparison.ref` → `fail`.
211
- - на `main` додатково перевіряємо ще й `HEAD` (working/staged) — `fail`, якщо є зміни.
212
-
213
- ### `checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail, cwd)`
214
-
215
- - **Сигнатура:** `async (...) => Promise<void>`
216
- - **Поведінка:**
217
- 1. `manifest.version` відсутній → `fail` («у маніфесті відсутнє поле version»). Вихід.
218
- 2. `manifest.name` відсутній → `fail` («відсутнє ім'я пакета»). Вихід.
219
- 3. `Vpublished = resolvePublishedVersion(...)`; якщо `null` → `pass` («опублікована версія недоступна, перевірку пропущено»). Вихід.
220
- 4. Якщо `Vpublished !== Vcurrent` → `fail` про drift (ручний bump заборонено — навіть із change-файлом). Вихід.
221
- 5. Інакше `pass` про збіг із реєстром і виклик `checkPublishedWorkspacePendingGitChanges`.
222
-
223
- ### `checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail, cwd)`
224
-
225
- - **Сигнатура:** `async (...) => Promise<void>`
226
- - **Поведінка** (виконується для воркспейсів, де `workspaceHasRelevantChangesAgainstBase` дала `true`):
227
- 1. `Vbase = readBaseVersion(comparisonRef, manifest, cwd)`.
228
- 2. Якщо `Vbase && Vcurrent && Vbase !== Vcurrent` → `fail` про drift (`Vbase → Vcurrent`). Вихід.
229
- 3. Якщо `hasPendingChangeFiles` → `pass`. Вихід.
230
- 4. Інакше `fail` `missingChangeFileMessage`.
231
- - Drift-перевірка йде **перед** перевіркою наявності change-файлу: симетрія з registry-published-шляхом (ручний bump заборонено навіть із change-файлом).
232
-
233
- ### `runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail, cwd)`
234
-
235
- - **Сигнатура:** `async (localOnly: PackageManifest[], subWorkspaces: string[], pass, fail, cwd) => Promise<void>`
236
- - **Поведінка:**
237
- 1. Якщо `localOnly` пустий → ранній вихід.
238
- 2. Не git-репозиторій → `pass` про пропуск.
239
- 3. `branch === 'dev'` → `pass` про пропуск.
240
- 4. `comparison` не знайдено (немає `dev`/`main`/`origin/*`) → `pass` про пропуск.
241
- 5. Для кожного `manifest` із `localOnly`: пропустити, якщо немає релевантних змін відносно `comparison.ref`; інакше виставити `checkedAny = true` і викликати `checkLocalOnlyChangedWorkspace`.
242
- 6. Якщо жоден воркспейс не змінено — `pass` («local-only воркспейси без змін відносно `<label>`»).
243
-
244
- ### `check(opts)`
245
-
246
- - **Сигнатура:** `async (opts?: { getPublishedVersion?, cwd? }) => Promise<number>`
247
- - **Покрокове виконання:**
248
- 1. Створюється `reporter = createCheckReporter()`; беруться його `pass` і `fail`.
249
- 2. `getPublishedVersion` — з `opts` або `createDefaultGetPublishedVersion()`.
250
- 3. `cwd` — з `opts` або `process.cwd()`.
251
- 4. `workspaces = await getMonorepoProjectRootDirs(cwd)`; `subWorkspaces = workspaces.filter(w => w !== '.')`.
252
- 5. `isMonorepoRoot = subWorkspaces.length > 0` — корінь монорепо вважається glue/конфіг/tooling.
253
- 6. Розділяємо воркспейси на `published` та `localOnly`:
254
- - корінь `.` за наявності підпакетів → одразу `pass` про пропуск, не читаємо маніфест;
255
- - `readPackageManifest(ws, cwd)` → якщо `null`, ws пропускається;
256
- - `manifest.registryPublishable === true` → у `published`, інакше — у `localOnly`.
257
- 7. Послідовно перевіряємо всі `published` через `checkPublishedWorkspace`.
258
- 8. `runLocalOnlyChecks(localOnly, ...)`.
259
- 9. Повертаємо `reporter.getExitCode()`.
260
-
261
- ## Залежності
262
-
263
- ### Стандартна бібліотека Node.js
264
-
265
- - `node:child_process` → `execFile` — запуск `git`, `npm` без shell-інтерполяції.
266
- - `node:util` → `promisify` — обгортка `execFileAsync = promisify(execFile)`.
267
- - Глобальні: `fetch`, `AbortSignal.timeout` — для PyPI (Node ≥ 18).
268
-
269
- ### Внутрішні модулі
270
-
271
- - `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — створює пару `{ pass, fail }` і обчислює `getExitCode()`.
272
- - `../lib/package-manifest.mjs`:
273
- - `getMonorepoProjectRootDirs(cwd)` — список воркспейсів (включно з `.`);
274
- - `manifestFilePath(ws, manifest)` — шлях до маніфесту в повідомленнях;
275
- - `parsePyprojectFields(text)` — отримання `{ name, version }` із `pyproject.toml`;
276
- - `readPackageManifest(ws, cwd)` — нормалізований опис воркспейсу (тип `PackageManifest`).
277
- - `../../release/lib/change-file.mjs` → `readChangeFiles(ws, cwd)` — список pending change-файлів у `<ws>/.changes/`.
278
-
279
- ### Зовнішні системи / процеси
280
-
281
- - `git` (CLI) — `rev-parse`, `merge-base`, `diff`, `ls-files`, `show`, `check-ignore`.
282
- - `npm` (CLI) — `npm view <name> version` для registry-published npm-пакетів.
283
- - PyPI HTTP API — `https://pypi.org/pypi/<name>/json` для Python-пакетів.
284
-
285
- ### Константи модуля
286
-
287
- - `FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])` — порядок пошуку бази для feature-гілок.
288
- - `LOCAL_ONLY_SKIP_BRANCH = 'dev'` — гілка, де local-only перевірка не активна.
289
- - `CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/', '.cursor/', '.claude/'])` — інверсні префікси (зміни в них не релевантні).
290
- - `REGISTRY_TIMEOUT_MS = 10_000` — таймаут для `npm view` / PyPI fetch.
291
- - `LEADING_DOTSLASH_RE = /^\.\//` — для нормалізації шляхів у `isChangelogIgnoredPath`.
292
-
293
- ## Потік виконання / Використання
294
-
295
- Типовий виклик (із CLI/скрипту):
296
-
297
- ```js
298
- import { check } from './consistency.mjs'
299
-
300
- const exitCode = await check()
301
- process.exit(exitCode)
302
- ```
303
-
304
- Із кастомним резолвером опублікованої версії (наприклад, у тестах):
305
-
306
- ```js
307
- import { check } from './consistency.mjs'
308
-
309
- const exitCode = await check({
310
- cwd: '/tmp/sandbox-repo',
311
- async getPublishedVersion(name, kind) {
312
- if (name === '@scope/pkg-a') return '1.0.0'
313
- return null
314
- }
315
- })
316
- ```
317
-
318
- ### Високорівневий потік `check`
319
-
320
- ```
321
- check(opts)
322
- ├─ createCheckReporter() → { pass, fail, getExitCode }
323
- ├─ getMonorepoProjectRootDirs(cwd) → workspaces
324
- ├─ subWorkspaces = workspaces \ ['.']
325
- ├─ isMonorepoRoot = subWorkspaces.length > 0
326
- ├─ For each ws:
327
- │ ├─ ws === '.' && isMonorepoRoot → pass (root skipped) ; continue
328
- │ ├─ manifest = readPackageManifest(ws, cwd)
329
- │ ├─ !manifest → continue
330
- │ └─ manifest.registryPublishable ? published.push : localOnly.push
331
- ├─ For each published manifest:
332
- │ └─ checkPublishedWorkspace(...)
333
- │ ├─ no version → fail
334
- │ ├─ no name → fail
335
- │ ├─ Vpublished == null → pass (skipped)
336
- │ ├─ drift → fail
337
- │ └─ checkPublishedWorkspacePendingGitChanges(...)
338
- │ ├─ hasPendingChangeFiles → pass + checkNpmFilesArrayContainsChangelog
339
- │ ├─ branch dev → fail iff relevant changes vs HEAD
340
- │ ├─ comparison ref + relevant changes → fail
341
- │ └─ main + relevant changes vs HEAD → fail
342
- ├─ runLocalOnlyChecks(localOnly, ...)
343
- │ ├─ not git → pass (skipped)
344
- │ ├─ branch dev → pass (skipped)
345
- │ ├─ no comparison → pass (skipped)
346
- │ └─ for each localOnly with relevant changes:
347
- │ └─ checkLocalOnlyChangedWorkspace(...)
348
- │ ├─ Vbase != Vcurrent → fail (drift)
349
- │ ├─ hasPendingChangeFiles → pass
350
- │ └─ else fail (missing change file)
351
- └─ return reporter.getExitCode()
352
- ```
353
-
354
- ### Контракти / гарантії
355
-
356
- - **Безпека:** жодних викликів `exec` / `spawn` із інтерполяцією рядків — лише `execFile` із масивом аргументів.
357
- - **Idempotency:** функція виконує лише читання (git/fs/network); не змінює нічого на диску.
358
- - **Деградація:** мережеві / репо-помилки — м'які (повертають `null`); їх результат — `pass` про пропуск, а не `fail`. Виняток: реальні відмінності, які можна спостерігати локально (drift, відсутність change-файлу), завжди дають `fail`.
359
- - **Симетрія шляхів:** registry-published і local-only обидва ставлять drift-перевірку **перед** перевіркою change-файлу, тому ручний bump поза CI стабільно falsies перевірку незалежно від моделі.
360
-
361
- ### Точки розширення
362
-
363
- - `opts.getPublishedVersion` — підміна джерела опублікованих версій (стаб для офлайн-тестів або проксі-реєстру).
364
- - `opts.cwd` — переключення активного репозиторію без `process.chdir`.
365
-
366
- ## Rebuild Test
367
-
368
- Контрольний перелік для відтворення/верифікації поведінки:
369
-
370
- 1. **Експорт API** — модуль експортує єдину `async function check(opts?)`, що повертає `Promise<number>`.
371
- 2. **Дефолти** — `opts.cwd` за замовчуванням `process.cwd()`; `opts.getPublishedVersion` за замовчуванням `defaultGetPublishedVersion` (npm-view для `kind === 'npm'`, PyPI fetch для `kind === 'python'`).
372
- 3. **Корінь монорепо** — для `ws === '.'` за наявності підворкспейсів виставляється `pass` про пропуск без читання маніфесту.
373
- 4. **Класифікація** — `manifest.registryPublishable === true` → `published`; інакше → `localOnly`. Воркспейси без читабельного маніфесту мовчки пропускаються.
374
- 5. **Drift > change-файл** — для обох моделей перевірка drift `version` спрацьовує **раніше** за перевірку наявності change-файлу і `fail` має пріоритет.
375
- 6. **Гілка `dev`** — `runLocalOnlyChecks` повністю пропускає local-only (`pass`); registry-published у `checkPublishedWorkspacePendingGitChanges` на `dev` перевіряє лише робоче дерево/staged відносно `HEAD`.
376
- 7. **Гілка `main`** — точка порівняння: `origin/main`, якщо це предок `HEAD` або збігається; інакше `HEAD~1`; також додаткова перевірка `HEAD` (working/staged), щоб виявити незакомічені зміни.
377
- 8. **Feature-гілка** — точка порівняння визначається ітерацією по `['dev', 'main']`, береться merge-base першої доступної бази; `label` приводиться до короткої форми (`origin/main` → `main`).
378
- 9. **Інверсні шляхи** — `docs/`, `doc/`, `.cursor/`, `.claude/` (із normalize `\` → `/` і обрізанням `./`) не вважаються релевантними змінами.
379
- 10. **`git -z`** — у `git diff --name-only` та `git ls-files --others` обов'язково використовується `-z`, інакше не-ASCII імена потраплять у C-quoted формі й ламатимуть префіксне порівняння.
380
- 11. **Untracked + tracked** — `listChangedPathsAgainstBase` об'єднує `git diff` (відносно `baseRef`) і `git ls-files --others --exclude-standard`, дедуплікація через `Set`.
381
- 12. **gitignored** — кожен кандидат додатково перевіряється через `git check-ignore -q --`; ігноровані пропускаються.
382
- 13. **`checkNpmFilesArrayContainsChangelog`** — викликається лише в гілці «pending change-файли є» для registry-published; для не-npm або відсутнього `npmFiles` — раннє return без `pass`/`fail`.
383
- 14. **Мовчазний skip** — недоступність опублікованої версії (мережа/реєстр) даює `pass` про пропуск, а не `fail`.
384
- 15. **`workspaceLabel`** — `'<root>'` для `.`, інакше шлях ws.
385
- 16. **`missingChangeFileMessage`** — текст fail містить шлях до маніфесту, інструкцію `npx @nitra/cursor change --bump … --section … --message …` і нагадування «bump зробить CI на main (n-changelog.mdc)».
386
- 17. **Послідовність публічних перевірок** — спершу всі `published` (у порядку, повернутому з `getMonorepoProjectRootDirs`), потім `runLocalOnlyChecks`.
387
- 18. **Exit-код** — повертається з `reporter.getExitCode()` (агрегує всі `pass`/`fail`).
12
+ Перевіряє версіонування проектів у монорепозиторії, порівнюючи версії, зазначені в маніфестах, з даними, отриманими з мережі з https://pypi.org/pypi/. Аналізує відповідність версій встановленим правилам, визначеним у конфігурації res.json. Визначає, чи відповідають версії формату, а також перевіряє наявність змін, порівнюючи їх із даними, описаними у (n-changelog.mdc). При невдачі перевірки повертає false/null, не кидаючи винятків.
13
+
14
+ ## Поведінка
15
+
16
+ 1. Ініціалізує репортер для збору результатів перевірки.
17
+ 2. Визначає робочий каталог та стан autofix-режиму.
18
+ 3. Зчитує всі кореневі каталоги проектів у монорепо.
19
+ 4. Класифікує кожен проект як публікувальний (registryPublishable) або локальний.
20
+ 5. Для кожного публікувального проекту виконує перевірку:
21
+ а. Зчитує маніфест проекту.
22
+ б. Якщо у маніфесті відсутнє ім'я або поле version, фіксує помилку.
23
+ в. Якщо є change-файл(и) у .changes/, фіксує успіх, оскільки bump зробить CI (n-changelog.mdc), і перевіряє наявність "CHANGELOG.md" у файлах проекту.
24
+ г. Якщо немає change-файлу, визначає точку порівняння на основі поточної гілки.
25
+ д. Порівнює версію в маніфесті з опублікованою версією (за допомогою npm view або запиту до https://pypi.org/pypi/).
26
+ е. Якщо версія в маніфесті випереджає опубліковану, фіксує помилку, оскільки ручний bump поза CI заборонений.
27
+ ж. Якщо версія в маніфесті позаду опублікованої, фіксує успіх, оскільки це відставання локального репозиторію від реєстру.
28
+ з. Якщо версії збігаються, перевіряє змінений код відносно точки порівняння. Якщо зміни є, але немає change-файлу, викликає механізм фіксації або фіксує помилку, надаючи підказку для використання команди `npx @nitra/cursor change`.
29
+ 6. Для кожного локального проекту виконує перевірку:
30
+ а. Зчитує маніфест проекту.
31
+ б. Визначає точку порівняння на основі поточної гілки.
32
+ в. Перевіряє, чи є релевантні зміни у проекті відносно точки порівняння.
33
+ г. Якщо зміни є, але немає change-файлу, викликає механізм фіксації або фіксує помилку, надаючи підказку для використання команди `npx @nitra/cursor change`.
34
+ 7. Повертає кінцевий код виходу, що відображає результат перевірки.
35
+
36
+ ## Гарантії поведінки
37
+
38
+ - Read-only: файл не виконує операцій запису у файлову систему.
39
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
40
+ - За невдалої перевірки повертає `false`/`null` замість винятку.