@nitra/cursor 12.3.0 → 12.3.2

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.2] - 2026-06-20
4
+
5
+ ### Fixed
6
+
7
+ - fix-каскад: per-tier timeout (локалі fail-fast ~45s замість стіни 120s, env N_LOCAL_FIX_TIMEOUT_MS/N_CLOUD_FIX_TIMEOUT_MS) + хмарний транспортний збій (pi ETIMEDOUT/spawn) обриває драбину замість ескалації на cloud-avg — не палиться avg-бюджет
8
+
9
+ ## [12.3.1] - 2026-06-20
10
+
11
+ ### Changed
12
+
13
+ - npm-module конформність: module-level JSDoc → pointer (`/** @see ./docs/… */`) у docgen-crc/extract-anchors/files-batch/judge-measure, doc-files/lint, lint/orchestrate, doc-aggregate/docgen-ignore; doc-CRC перештамповано; eslint --fix-стан прийнято як канон
14
+
3
15
  ## [12.3.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.3.0",
3
+ "version": "12.3.2",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -68,4 +68,4 @@
68
68
  "skills": "skills",
69
69
  "extensions": ".pi-template/extensions"
70
70
  }
71
- }
71
+ }
@@ -1,34 +1,4 @@
1
- /**
2
- * CRC32 джерела + YAML-frontmatter файлової документації.
3
- *
4
- * Кожна файлова дока несе у frontmatter контрольну суму байтів джерела на момент
5
- * генерації. Це детермінований маркер застарілості: `crc32(поточне джерело)` звіряється
6
- * з `crc` у доці — розбіжність (або відсутня дока) означає, що дока відстала від коду.
7
- * CRC не залежить від git-стану (rebase, незакомічене, гілки), тож придатний і для
8
- * per-edit hook (бачить лише змінений файл), і для повного сканування.
9
- *
10
- * Degraded-маркер (ADR 260610-2228): якщо локальний конвеєр не дотягнув до порогу
11
- * якості, дока все одно пишеться, а frontmatter додатково несе `score` (det-оцінка)
12
- * та `issues` (коди проблем). CRC при цьому свіжий — Stop-гейт не блокує задачі через
13
- * слабкість моделі; борг видимий через `check --degraded` і автоматично доретраюється
14
- * наступним `gen` (рівно один раз на версію джерела — далі `retried: true` у frontmatter).
15
- *
16
- * Frontmatter — єдиний дозволений виняток із правила «чистий Markdown без HTML»:
17
- * це машинні метадані, не контент. Формат:
18
- *
19
- * ---
20
- * docgen:
21
- * source: src/lib/foo.js
22
- * crc: a3f1c9e0
23
- * model: omlx/gemma-4-e4b-it-OptiQ-4bit
24
- * score: 55
25
- * issues: short-behavior,internal-name:bar
26
- * ---
27
- *
28
- * `model` — повний id моделі-генератора (як повертає resolveModel, із префіксом
29
- * провайдера). Пасивна метадата: маркер «віку» доки за моделлю на додачу до CRC
30
- * джерела. На staleness НЕ впливає — звіряється лише `crc`.
31
- */
1
+ /** @see ./docs/docgen-crc.md */
32
2
  import { existsSync, readFileSync } from 'node:fs'
33
3
  import { basename, extname } from 'node:path'
34
4
  import { crc32 as zlibCrc32 } from 'node:zlib'
@@ -173,6 +143,9 @@ export function buildDocFrontmatter(source, crc, quality = null, model = null) {
173
143
  */
174
144
  const LEADING_H1_RE = /^#[^\n]*\n+/u
175
145
 
146
+ /**
147
+ *
148
+ */
176
149
  export function stampDoc(md, source, crc, quality = null, model = null) {
177
150
  const { body } = parseDocFrontmatter(md)
178
151
  const cleanBody = body.replace(LEADING_NEWLINES_RE, '').replace(LEADING_H1_RE, '')
@@ -1,18 +1,4 @@
1
- /**
2
- * E1 (Fact-anchoring): детермінований витяг «анкорів» — конкретних фрагментів
3
- * з коду, які LLM зобовʼязана згадати в документації, щоб не зісковзнути на
4
- * generic-фрази.
5
- *
6
- * Категорії анкорів:
7
- * - urls : усі https?://… у вихідному коді
8
- * - magicStrings : export const X = '…' з непорожнім value (≤120 символів)
9
- * - errorMarkers : суфікси повідомлень про помилки виду `(rule.mdc)`
10
- * - configRefs : посилання на .json-конфіги проєкту (.n-cursor.json, …)
11
- * - examples : ```…```-блоки у file-header JSDoc (першому коментарі файла)
12
- *
13
- * Всі регулярки — на сирому src без AST: дешево, безпечно, без false-positive
14
- * критичної ваги (надмір — менша проблема, ніж пропуск).
15
- */
1
+ /** @see ./docs/docgen-extract-anchors.md */
16
2
 
17
3
  const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
18
4
  // Після обрізання template-частини URL має лишитися host (R10).
@@ -1,15 +1,4 @@
1
- /**
2
- * JS-оркестрація генерації файлових док (local-only, ADR 260610-2228).
3
- *
4
- * Уся черга/батчинг/CRC-штамп живуть тут, а не в контексті моделі — тому
5
- * масовий перший прогін на сотні файлів не «заморює» агента. Конвеєр суто
6
- * локальний: жодних cloud-ескалацій; якщо det-score нижче порогу — дока все
7
- * одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а наступний
8
- * `gen` автоматично доретраює такі доки (один раз на версію джерела — далі `retried:true`).
9
- *
10
- * Перед масовим прогоном — health-check omlx: memory-guard зайнятої 8GB машини
11
- * означає «відклади прогін», а не сотні хибних «✗» у звіті.
12
- */
1
+ /** @see ./docs/docgen-files-batch.md */
13
2
  import { readFileSync, readdirSync, mkdirSync, writeFileSync, existsSync, statSync } from 'node:fs'
14
3
  import { basename, dirname, join, relative } from 'node:path'
15
4
 
@@ -1,21 +1,4 @@
1
- #!/usr/bin/env node
2
- /**
3
- * docgen-judge-measure.mjs — Q4 офлайн-вимірювач (spec 2026-06-14-docgen-judge-design).
4
- *
5
- * Міряє false-positive rate детермінованого `scoreDoc`: серед доків, що ПРОЙШЛИ
6
- * (score ≥ threshold), який % сильна хмарна модель-суддя класифікує як
7
- * `generic`/`inaccurate`. Це число вирішує, чи будувати рантайм-judge-гейт.
8
- *
9
- * Генерація: локальна (N_LOCAL_MIN_MODEL, omlx/* → прямий HTTP) — реальний пайплайн.
10
- * Суддя: openai-codex/gpt-5.4-mini (сильніша хмара, ніж генератор — інакше вимір беззмістовний).
11
- * Обидва — через існуючий `../../../lib/llm.mjs callLlm` (маршрутизація за префіксом).
12
- *
13
- * Кеш на диску (за хешем контенту) → повторні прогони не регенерують і не пересуджують.
14
- *
15
- * Usage:
16
- * node docgen-judge-measure.mjs <file1> <file2> ...
17
- * MEASURE_CACHE=/tmp/x N_CLOUD_MIN_MODEL=openai-codex/gpt-5.4 node docgen-judge-measure.mjs ...
18
- */
1
+ /** @see ./docs/docgen-judge-measure.md */
19
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
20
3
  import { createHash } from 'node:crypto'
21
4
  import { join } from 'node:path'
@@ -39,10 +22,16 @@ Prefer "inaccurate" over "generic" if any claim is wrong. Respond with ONLY a JS
39
22
 
40
23
  const sha = s => createHash('sha256').update(s).digest('hex').slice(0, 16)
41
24
 
25
+ /**
26
+ *
27
+ */
42
28
  function cacheGet(key) {
43
29
  const p = join(CACHE_DIR, key + '.json')
44
30
  return existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : null
45
31
  }
32
+ /**
33
+ *
34
+ */
46
35
  function cacheSet(key, val) {
47
36
  if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true })
48
37
  writeFileSync(join(CACHE_DIR, key + '.json'), JSON.stringify(val))
@@ -83,6 +72,9 @@ function judgeCached(src, doc) {
83
72
  return { ...v, cached: false }
84
73
  }
85
74
 
75
+ /**
76
+ *
77
+ */
86
78
  function main() {
87
79
  const files = process.argv.slice(2).filter(f => !f.startsWith('--'))
88
80
  if (!files.length) {
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docgen-crc.mjs
4
4
  resource: npm/rules/doc-files/js/docgen-crc.mjs
5
5
  docgen:
6
- crc: 6baf999b
6
+ crc: cca0a79f
7
7
  ---
8
8
 
9
9
  Детермінований маркер актуальності файлових док: контрольна сума джерела у frontmatter плюс опційний degraded-маркер якості. Єдине джерело правди про «дока свіжа/застаріла/неякісна» для генерації, перевірок і хуків.
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docgen-extract-anchors.mjs
4
4
  resource: npm/rules/doc-files/js/docgen-extract-anchors.mjs
5
5
  docgen:
6
- crc: 49749e9c
6
+ crc: b675f159
7
7
  score: 80
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docgen-files-batch.mjs
4
4
  resource: npm/rules/doc-files/js/docgen-files-batch.mjs
5
5
  docgen:
6
- crc: 975d6cfa
6
+ crc: 18c96a58
7
7
  score: 95
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docgen-judge-measure.mjs
4
4
  resource: npm/rules/doc-files/js/docgen-judge-measure.mjs
5
5
  docgen:
6
- crc: 7f72e520
6
+ crc: e6e19732
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: lint.mjs
4
4
  resource: npm/rules/doc-files/js/lint.mjs
5
5
  docgen:
6
- crc: fcdc1c2a
6
+ crc: ca055f8f
7
7
  score: 100
8
8
  ---
9
9
 
@@ -1,19 +1,4 @@
1
- /**
2
- * Адаптер агрегатора `n-cursor lint` для правила doc-files (opportunistic LLM-fix
3
- * tier, спека docs/specs/2026-06-15-opportunistic-llm-fix-tier.md).
4
- *
5
- * Quick-фаза отримує список змінених файлів і мапить їх у пари в **обидва** боки:
6
- * - змінене **джерело** (`.js/.mjs/.ts/.vue/.py/.rs`) → перевірка його доки `<dir>/docs/<stem>.md`;
7
- * - змінена/видалена **дока** (`<dir>/docs/<stem>.md`) → перевірка відповідного джерела
8
- * (той самий stem у каталозі над текою `docs`).
9
- * Ci-фаза (files === undefined) проганяє повний скан дерева.
10
- *
11
- * Детект — `missing` ∪ `crc-mismatch` (детермінований CRC, 0 LLM-токенів); degraded не блокує.
12
- * Поведінка за осями (правило має `meta.json: llmFix:true`):
13
- * - `readOnly` (CI/hook): **лише детект** — нуль мутацій/LLM, exit 1 на stale (детермінований гейт);
14
- * - fix-by-default + omlx **піднято**: opportunistic-генерація stale-доків → re-detect → 0 якщо полагоджено;
15
- * - fix-by-default + omlx **недоступно**: fix пропущено (повідомлення) + exit 1 — гейт тримається, без false-green.
16
- */
1
+ /** @see ./docs/lint.md */
17
2
  import { join, dirname, basename, extname } from 'node:path'
18
3
  import { existsSync, readdirSync } from 'node:fs'
19
4
 
@@ -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: 0f1d717c
6
+ crc: 0ab5b22c
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -1,18 +1,4 @@
1
- /**
2
- * Оркестратор `n-cursor lint` — дві ортогональні осі (spec 2026-06-14-lint-rule-consolidation
3
- * + компаньйон 2026-06-14-lint-orchestrator-fix-readonly-unification):
4
- * - **scope** (`--full`): default = дельта vs origin (лише `per-file` правила);
5
- * `--full` = весь репо (`per-file` ∪ `full` правила);
6
- * - **behavior** (`--read-only`): default = fix; `--read-only` = лише детект без мутацій.
7
- *
8
- * Data-driven: сканує `rules/<id>/meta.json` за полем `lint` (`per-file`|`full`),
9
- * викликає `rules/<id>/js/lint.mjs` → `lint(files, cwd, { readOnly })`:
10
- * - default scope: `files` = змінені відносно origin (`collectChangedFilesSince`);
11
- * - `--full`: `files = undefined` — весь проєкт.
12
- * Порядок правил — алфавітний. Fail-fast **лише в `--read-only`** (CI/детект): перший
13
- * ненульовий код спиняє. У fix-режимі (default) ненульовий код НЕ спиняє — проганяємо всі
14
- * правила й доходимо до кроку виправлення (конформність-драбина), повертаючи найгірший код.
15
- */
1
+ /** @see ./docs/orchestrate.md */
16
2
  import { existsSync, readdirSync } from 'node:fs'
17
3
  import { dirname, join } from 'node:path'
18
4
  import { fileURLToPath } from 'node:url'
@@ -14,6 +14,7 @@
14
14
  * Гейт: валідні слова після дописування у словник зникають; нерозкласифіковані та
15
15
  * typo лишаються → cspell повертає !=0 → exit 1 (людина доправляє одруки вручну).
16
16
  */
17
+ import { env } from 'node:process'
17
18
  import { spawnSync } from 'node:child_process'
18
19
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
19
20
  import { join } from 'node:path'
@@ -27,7 +28,7 @@ const UNKNOWN_WORD_RE = /Unknown word \(([^)]+)\)/u
27
28
  const MAX_CLASSIFY_WORDS = 80
28
29
 
29
30
  /** Локальна fix-модель (рішення: єдиний knob `N_LOCAL_MIN_MODEL`). */
30
- const fixModel = () => process.env.N_LOCAL_MIN_MODEL || ''
31
+ const fixModel = () => env.N_LOCAL_MIN_MODEL || ''
31
32
 
32
33
  /**
33
34
  * Запускає `cspell .` із захопленням виводу.
@@ -3,24 +3,30 @@ type: JS Module
3
3
  title: cspell-fix.mjs
4
4
  resource: npm/rules/text/lint/cspell-fix.mjs
5
5
  docgen:
6
- crc: b5585e0f
6
+ crc: 7b40e8f9
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
- score: 90
8
+ score: 95
9
+ issues: anchor-miss:meta.json,judge:inaccurate:0.98
10
+ judgeModel: openai-codex/gpt-5.4-mini
9
11
  ---
10
12
 
11
- Модуль інтегрує `cspell` у ланцюжок `lint-text` для виявлення орфографічних помилок. У режимі з можливістю виправлення, процес відбувається шляхом: детект (захоплення виводу) $\rightarrow$ групування знахідок по файлах $\rightarrow$ per-file `omlx-фікс` справжніх помилок (`llmLintFix`) $\rightarrow$ re-detect. У read-only режимі виконується лише детект, що гарантує нуль мутацій. Валідні терміни omlx залишаються, їх ловить повторний `cspell` (далі — у словник `@nitra/cspell-dict`).
13
+ ## Огляд
14
+
15
+ Цей модуль інтегрує cspell у ланцюжок lint-text для класифікації невідомих слів згідно зі схемою omlx. Він витягує невідомі слова, класифікує їх за допомогою LLM (detect $\rightarrow$ omlx-класифікація) та автоматично дописує валідні терміни до словника `.cspell.json`. Нерозкласифіковані та ймовірні одруки залишаються для ручного рев'ю. Процес є read-only, оскільки він лише класифікує знахідки, а не переписує файли.
12
16
 
13
17
  ## Поведінка
14
18
 
15
- groupFindingsByFile групує вивід `cspell` за файлами, виділяючи знахідки.
16
- runCspellText запускає `cspell` для виявлення орфографічних помилок, а при вимкненому режимі читання виконує автоматичне виправлення помилок за допомогою зовнішнього інструменту для файлів, що містять справжні помилки, і повторно перевіряє файли.
19
+ unknownWords витягує унікальні невідомі слова з виводу cspell.
20
+ appendWordsToDict дописує класифіковані валідні слова до файлу .cspell.json, оновлюючи словник.
21
+ runCspellText запускає cspell, класифікує знахідки за допомогою LLM (якщо увімкнено) та повторно перевіряє код, повертаючи 0 при чистоті або 1 при знахідках.
17
22
 
18
23
  ## Публічний API
19
24
 
20
- groupFindingsByFileзбирає знайдені помилки cspell у групи за файлами.
21
- runCspellTextвиконує перевірку тексту за правилами cspell, застосовуючи автоматичне виправлення від omlx.
25
+ unknownWordsЗбирає унікальні слова, які не були знайдені у словнику cspell.
26
+ appendWordsToDictДодає зібрані слова до файлу `.cspell.json#words` у відсортованому та унікальному вигляді для перегляду у Git.
27
+ runCspellText — Виконує перевірку тексту за допомогою cspell, класифікуючи слова та оновлюючи словник за новою схемою.
22
28
 
23
29
  ## Гарантії поведінки
24
30
 
25
- - Read-only: файл не виконує операцій запису у файлову систему.
26
- - Не звертається до мережі.
31
+ - Перехоплює помилки і не пропускає винятків назовні (fail-safe).
32
+ - За певних помилок повертає порожнє значення (напр. `null`) замість винятку.
@@ -3,26 +3,27 @@ type: JS Module
3
3
  title: normalize-cli.mjs
4
4
  resource: npm/scripts/lib/adr/normalize-cli.mjs
5
5
  docgen:
6
- crc: ce2f13af
6
+ crc: 63a18347
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 90
9
+ judgeModel: openai-codex/gpt-5.4-mini
9
10
  ---
10
11
 
11
- Цей файл є CLI-обгорткою для локального ADR-нормалізатора (`n-cursor adr-normalize-local`). Він використовується скриптом `.claude/hooks/normalize-decisions.sh` як локальний бекенд для обробки батчу чернеток та списку чистих ADR. Обгортка зчитує шляхи до чернеток та параметри з аргументів командного рядка та змінних середовища, а потім проганяє `normalizePipeline`. Результатом роботи є вивід JSON-контракту з операціями у stdout, який парситься зовнішнім скриптом. Прогрес відображається у stderr.
12
+ ## Огляд
13
+
14
+ Цей файл є CLI-обгорткою для локального нормалізатора ADR. Він зчитує шляхи до чернеток (`--batch <file>`) та список чистих ADR (`--clean <file>`), використовуючи директорію ADR (`--adr-dir <dir>`) для резолву шляхів. Обгортка запускає `normalizePipeline`, яка генерує JSON-контракт у форматі `{ "operations": [...] }` та виводить його у stdout для подальшого парсингу bash-скриптом. Прогрес та помилки записуються у stderr. Поведінка нормалізатора може бути змінена через змінні середовища: `ADR_NORMALIZE_ALLOW_CLOUD` контролює можливість хмарної ескалації, а `ADR_NORMALIZE_VOTES` визначає кількість голосів self-consistency для чистих ADR.
12
15
 
13
16
  ## Поведінка
14
17
 
15
- 1. `runAdrNormalizeLocalCli` парсить вхідні аргументи для визначення шляху до чернеток батчу, списку чистих ADR та каталогу ADR.
16
- 2. Якщо не надано файл батчу, `runAdrNormalizeLocalCli` виводить повідомлення про використання та завершує роботу з кодом помилки.
17
- 3. `runAdrNormalizeLocalCli` зчитує шляхи до чернеток батчу з вказаного файлу.
18
- 4. Для кожної чернетки `runAdrNormalizeLocalCli` зчитує вміст файлу, резолвячи шлях відносно каталогу ADR, і створює об'єкт чернетки.
19
- 5. `runAdrNormalizeLocalCli` зчитує список назв чистих ADR з файлу, якщо він наданий.
20
- 6. `runAdrNormalizeLocalCli` зчитує значення змінних середовища `ADR_NORMALIZE_ALLOW_CLOUD` та `ADR_NORMALIZE_VOTES` для визначення параметрів нормалізації.
21
- 7. `runAdrNormalizeLocalCli` викликає логіку нормалізації, передаючи чернетки, список чистих ADR та параметри, при цьому прогрес виводиться у stderr.
22
- 8. `runAdrNormalizeLocalCli` виводить кількість операцій та статистику нормалізації у stderr.
23
- 9. `runAdrNormalizeLocalCli` виводить деталі рішень нормалізації у stderr.
24
- 10. `runAdrNormalizeLocalCli` друкує JSON-об'єкт, що містить операції, у stdout.
25
- 11. `runAdrNormalizeLocalCli` завершує роботу з кодом успіху.
18
+ 1. Викликати runAdrNormalizeLocalCli.
19
+ 2. Зчитувати список шляхів до чернеток батчу з файлу, вказаного через аргумент `--batch`.
20
+ 3. Зчитувати список імен чистих ADR (кандидатів до злиття) з файлу, вказаного через аргумент `--clean`, якщо він наданий.
21
+ 4. Визначати директорію ADR, використовуючи аргумент `--adr-dir` або за замовчуванням `cwd/docs/adr`.
22
+ 5. Зчитувати вміст кожної чернетки батчу, резолвя шляхи відносно `--adr-dir`.
23
+ 6. Зчитувати значення змінних середовища `ADR_NORMALIZE_ALLOW_CLOUD` та `ADR_NORMALIZE_VOTES`.
24
+ 7. Викликати внутрішній механізм нормалізації, передаючи зібрані чернетки, список чистих ADR, та конфігурацію, отриману з змінних середовища.
25
+ 8. Виводити інформацію про прогрес та статистику в stderr.
26
+ 9. Виводити JSON-об'єкт, що містить операції, у stdout.
26
27
 
27
28
  ## Публічний API
28
29
 
@@ -30,5 +31,4 @@ runAdrNormalizeLocalCli — запускає субкоманду, виводя
30
31
 
31
32
  ## Гарантії поведінки
32
33
 
33
- - Read-only: файл не виконує операцій запису у файлову систему.
34
- - Не звертається до мережі.
34
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -16,6 +16,7 @@
16
16
  * ADR_NORMALIZE_ALLOW_CLOUD=1 дозволити хмарну ескалацію tier-каскаду (default off)
17
17
  * ADR_NORMALIZE_VOTES=N голосів self-consistency для clean-ребер (default 2)
18
18
  */
19
+ import { env } from 'node:process'
19
20
  import { readFileSync } from 'node:fs'
20
21
  import { basename, isAbsolute, join } from 'node:path'
21
22
  import { normalizePipeline } from './normalize-pipeline.mjs'
@@ -57,8 +58,8 @@ export function runAdrNormalizeLocalCli(argv) {
57
58
  })
58
59
  const cleanList = args.clean ? readLines(args.clean).map((c) => basename(c)) : []
59
60
 
60
- const allowCloud = process.env.ADR_NORMALIZE_ALLOW_CLOUD === '1'
61
- const votes = Number(process.env.ADR_NORMALIZE_VOTES) || 2
61
+ const allowCloud = env.ADR_NORMALIZE_ALLOW_CLOUD === '1'
62
+ const votes = Number(env.ADR_NORMALIZE_VOTES) || 2
62
63
 
63
64
  const { operations, stats, trace } = normalizePipeline(drafts, cleanList, {
64
65
  allowCloud,
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: llm-worker.mjs
4
4
  resource: npm/scripts/lib/fix/llm-worker.mjs
5
5
  docgen:
6
- crc: de9eb68c
6
+ crc: 857c510e
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -18,7 +18,7 @@ docgen:
18
18
 
19
19
  ## Публічний API
20
20
 
21
- - `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення; `opts`: `model`, `feedback`, `caller`. Повертає `{ ok, error, changes, diagnosis }`.
21
+ - `runLlmWorker(ruleId, violationOutput, projectRoot, opts)` — виправляє одне порушення; `opts`: `model`, `feedback`, `caller`, `timeoutMs` (per-tier ліміт виклику; драбина задає коротший для локальних рунгів). Повертає `{ ok, error, changes, diagnosis }`.
22
22
 
23
23
  ## Гарантії поведінки
24
24
 
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: orchestrator.mjs
4
4
  resource: npm/scripts/lib/fix/orchestrator.mjs
5
5
  docgen:
6
- crc: 78dfe86b
6
+ crc: d327ab6d
7
7
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
8
8
  score: 100
9
9
  ---
@@ -19,7 +19,8 @@ docgen:
19
19
  - рунг `local-min` — перший прохід без feedback;
20
20
  - рунг `local-min-retry` — той самий локальний тир, але з feedback попереднього рунга (попередні зміни + залишковий violation);
21
21
  - рунги `cloud-min` / `cloud-avg` — хмарні моделі (через pi), теж із feedback.
22
- 5. Достроковий вихід драбини: systemic-помилка локального тиру пропускає рунги тієї ж моделі; відсутній API-ключ на хмарному обриває драбину; вичерпаний avg-кеп пропускає avg-рунг (із записом у лог).
22
+ Кожен рунг має per-tier `timeoutMs`: локальні **fail-fast** (`N_LOCAL_FIX_TIMEOUT_MS`, дефолт 45s не палити стіну 120s на повільному локальному inference), хмарні повний (`N_CLOUD_FIX_TIMEOUT_MS`, дефолт 120s).
23
+ 5. Достроковий вихід драбини: systemic-помилка локального тиру пропускає рунги тієї ж моделі; відсутній API-ключ на хмарному обриває драбину; хмарний транспортний збій (pi таймаут/spawn) обриває драбину, щоб не палити avg-бюджет на ту саму стіну; вичерпаний avg-кеп пропускає avg-рунг (із записом у лог).
23
24
  6. Після обробки всіх правил — фінальна перевірка. Усі чисті → успіх; інакше — ознака нерозв'язаних.
24
25
 
25
26
  ## Публічний API
@@ -117,11 +117,12 @@ function buildPrompt(ruleId, ruleMdc, output, files, feedback = null) {
117
117
  * @param {string} prompt текст промпта
118
118
  * @param {string} model назва моделі (provider/id, `omlx/...` або '')
119
119
  * @param {string} caller мітка викликача для wire-trace (`fix:<rule>:<rung>`)
120
+ * @param {number} [timeoutMs] ліміт виклику (драбина задає per-tier; undefined → дефолт callLlm)
120
121
  * @returns {{ text: string, error?: string }} текст відповіді або повідомлення про помилку
121
122
  */
122
- function callModel(prompt, model, caller) {
123
+ function callModel(prompt, model, caller, timeoutMs) {
123
124
  try {
124
- return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 120_000, caller }) }
125
+ return { text: callLlm([{ role: 'user', content: prompt }], model, { timeoutMs, caller }) }
125
126
  } catch (error) {
126
127
  const msg = String(error.message)
127
128
  if (API_KEY_RE.test(msg)) {
@@ -146,9 +147,10 @@ function callModel(prompt, model, caller) {
146
147
  * @param {string} ruleId ID правила
147
148
  * @param {string} violationOutput output з fix check для цього rule
148
149
  * @param {string} projectRoot абсолютний шлях до кореня проєкту
149
- * @param {{ model?: string, feedback?: object|null, caller?: string }} opts опції:
150
+ * @param {{ model?: string, feedback?: object|null, caller?: string, timeoutMs?: number }} opts опції:
150
151
  * `model` — перевизначення моделі; `feedback` — контекст попереднього рунга
151
- * драбини (retry-with-feedback); `caller` — мітка для wire-trace
152
+ * драбини (retry-with-feedback); `caller` — мітка для wire-trace; `timeoutMs` —
153
+ * per-tier ліміт виклику (драбина: локалі fail-fast, хмара повний)
152
154
  * @returns {{ ok: boolean, error?: string, changes: Array<{path:string}>, diagnosis: string|null }}
153
155
  * статус виправлення, помилка, запропоновані зміни і само-аналіз моделі
154
156
  */
@@ -156,6 +158,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
156
158
  const model = opts.model ?? MODEL
157
159
  const feedback = opts.feedback ?? null
158
160
  const caller = opts.caller ?? 'fix'
161
+ const timeoutMs = opts.timeoutMs
159
162
 
160
163
  // 1. Читаємо rule .mdc
161
164
  const mdcPath = join(projectRoot, '.cursor', 'rules', `n-${ruleId}.mdc`)
@@ -166,7 +169,7 @@ export function runLlmWorker(ruleId, violationOutput, projectRoot, opts = {}) {
166
169
 
167
170
  // 3. Будуємо prompt і викликаємо модель
168
171
  const prompt = buildPrompt(ruleId, ruleMdc, violationOutput, files, feedback)
169
- const { text, error: modelError } = callModel(prompt, model, caller)
172
+ const { text, error: modelError } = callModel(prompt, model, caller, timeoutMs)
170
173
 
171
174
  if (modelError) return { ok: false, error: modelError, changes: [], diagnosis: null }
172
175
  if (!text) return { ok: false, error: 'model returned empty response', changes: [], diagnosis: null }
@@ -1,5 +1,6 @@
1
1
  /** @see ./docs/orchestrator.md */
2
2
 
3
+ import { env } from 'node:process'
3
4
  import { runFixCheck } from './run-fix-check.mjs'
4
5
  import { runT0AutoCli } from './t0.mjs'
5
6
  import { logEscalation } from './escalation-log.mjs'
@@ -13,9 +14,24 @@ import { CLOUD_AVG, CLOUD_MIN, LOCAL_MIN } from '../../../lib/models.mjs'
13
14
  */
14
15
  const DEFAULT_MAX_AVG = 3
15
16
 
17
+ /**
18
+ * Timeout одного LLM-виклику за тиром. Локальні рунги **fail-fast**: не палити
19
+ * стіну 120s на повільному 4b (curl exit 28) — швидше абортнути й ескалувати.
20
+ * Хмарні — повний. Перевизначення: `N_LOCAL_FIX_TIMEOUT_MS` / `N_CLOUD_FIX_TIMEOUT_MS`.
21
+ */
22
+ const LOCAL_TIMEOUT_MS = Number(env.N_LOCAL_FIX_TIMEOUT_MS) || 45_000
23
+ const CLOUD_TIMEOUT_MS = Number(env.N_CLOUD_FIX_TIMEOUT_MS) || 120_000
24
+
16
25
  /** Маркер дружнього повідомлення про відсутній API-ключ (з `llm-worker.callModel`). */
17
26
  const NO_KEY_RE = /немає ключа|api key/i
18
27
 
28
+ /**
29
+ * Хмарний транспорт (pi) упав на рівні процесу: таймаут/spawn-помилка. Стіна часу
30
+ * однакова для всіх cloud-рунгів (та сама pi-транспортна стіна), а cloud-avg — інша
31
+ * модель, не більший timeout. Ескалація на неї лише спалить avg-бюджет → обрив.
32
+ */
33
+ const CLOUD_TRANSPORT_RE = /etimedout|timed out|pi error/i
34
+
19
35
  /**
20
36
  * Будує драбину ескалації за наявними тирами (спека 2026-06-19-fix-escalation-cascade):
21
37
  * 1. `local-min` — `N_LOCAL_MIN_MODEL`, перший прохід;
@@ -24,14 +40,14 @@ const NO_KEY_RE = /немає ключа|api key/i
24
40
  * 4. `cloud-avg` — `N_CLOUD_AVG_MODEL` (через pi), з feedback, під avg-кепом.
25
41
  * Рунги з незаданим тиром (`''`) відсіюються — драбина стискається до доступних.
26
42
  * @param {{ localMin: string, cloudMin: string, cloudAvg: string }} models тири з env
27
- * @returns {Array<{ tier: string, model: string, feedback: boolean, local: boolean, isAvg: boolean }>} драбина
43
+ * @returns {Array<{ tier: string, model: string, feedback: boolean, local: boolean, isAvg: boolean, timeoutMs: number }>} драбина
28
44
  */
29
45
  export function buildLadder({ localMin, cloudMin, cloudAvg }) {
30
46
  return [
31
- { tier: 'local-min', model: localMin, feedback: false, local: true, isAvg: false },
32
- { tier: 'local-min-retry', model: localMin, feedback: true, local: true, isAvg: false },
33
- { tier: 'cloud-min', model: cloudMin, feedback: true, local: false, isAvg: false },
34
- { tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true }
47
+ { tier: 'local-min', model: localMin, feedback: false, local: true, isAvg: false, timeoutMs: LOCAL_TIMEOUT_MS },
48
+ { tier: 'local-min-retry', model: localMin, feedback: true, local: true, isAvg: false, timeoutMs: LOCAL_TIMEOUT_MS },
49
+ { tier: 'cloud-min', model: cloudMin, feedback: true, local: false, isAvg: false, timeoutMs: CLOUD_TIMEOUT_MS },
50
+ { tier: 'cloud-avg', model: cloudAvg, feedback: true, local: false, isAvg: true, timeoutMs: CLOUD_TIMEOUT_MS }
35
51
  ].filter(r => r.model)
36
52
  }
37
53
 
@@ -40,6 +56,8 @@ export function buildLadder({ localMin, cloudMin, cloudAvg }) {
40
56
  * - `break` — відсутній API-ключ на хмарному (інші хмарні рунги теж без ключа);
41
57
  * - `skip-model` — systemic-помилка локального тиру (memory-guard/auth/down): повтор
42
58
  * тієї ж моделі марний → пропустити рунги з цим model.
59
+ * - `break` — також хмарний транспорт упав (pi таймаут/spawn): решта cloud-рунгів
60
+ * під тією ж стіною → обрив, щоб не палити avg-бюджет.
43
61
  * @param {{ local: boolean }} rung поточний рунг
44
62
  * @param {string|null|undefined} error помилка виклику worker
45
63
  * @returns {'break'|'skip-model'|null} дія для драбини
@@ -48,6 +66,7 @@ function decideAfterFailure(rung, error) {
48
66
  if (!error) return null
49
67
  if (NO_KEY_RE.test(error)) return 'break'
50
68
  if (rung.local && classifyOmlxError(error) === 'systemic') return 'skip-model'
69
+ if (!rung.local && CLOUD_TRANSPORT_RE.test(error)) return 'break'
51
70
  return null
52
71
  }
53
72
 
@@ -93,7 +112,8 @@ export async function escalateRule(rule, cwd, deps) {
93
112
  const res = worker.runLlmWorker(rule.ruleId, currentViolation, cwd, {
94
113
  model: rung.model,
95
114
  feedback: rung.feedback ? feedback : null,
96
- caller: `fix:${rule.ruleId}:${rung.tier}`
115
+ caller: `fix:${rule.ruleId}:${rung.tier}`,
116
+ timeoutMs: rung.timeoutMs
97
117
  })
98
118
  if (rung.isAvg) avgUsed++
99
119
 
@@ -1,9 +1,2 @@
1
- /**
2
- * Re-export спільного списку ignore-глобів із правила doc-files.
3
- *
4
- * Канонічне джерело — `npm/rules/doc-files/js/docgen-ignore.mjs`: скіл doc-aggregate
5
- * і правило doc-files мусять бачити однакове дерево кодових файлів, інакше агрегат
6
- * посилатиметься на файли без док (або навпаки). Залежність спрямована
7
- * doc-aggregate → doc-files за ADR про розбиття docgen.
8
- */
1
+ /** @see ./docs/docgen-ignore.md */
9
2
  export * from '../../../rules/doc-files/js/docgen-ignore.mjs'
@@ -3,7 +3,7 @@ type: JS Module
3
3
  title: docgen-ignore.mjs
4
4
  resource: npm/skills/doc-aggregate/js/docgen-ignore.mjs
5
5
  docgen:
6
- crc: 5faaffd0
6
+ crc: 3b579230
7
7
  ---
8
8
 
9
9
  Re-export спільного списку ignore-глобів зі скіла doc-files: обидва скіли документації (пофайлові доки й агрегати) мусять бачити однакове дерево кодових файлів, інакше агрегат посилатиметься на файли без док або навпаки.