@nitra/cursor 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [11.1.0] - 2026-06-15
4
+
5
+ ### Changed
6
+
7
+ - doc-files lint-крок: opportunistic LLM-fix tier (спека docs/specs/2026-06-15-opportunistic-llm-fix-tier.md). У fix-by-default (не `--read-only`) крок тепер не лише детектить застарілі доки, а й намагається їх **згенерувати** локальною моделлю, якщо omlx піднято; недоступний omlx → fix пропускається з повідомленням і лишається exit 1 (гейт тримається, без false-green). `--read-only` (CI/hook) лишається суто детектом — нуль мутацій/LLM. Прапор `meta.json: llmFix:true` (схема `rule-meta.json`) — opt-in лише для контент-правил; логічні лінтери НЕ вмикати (LLM-правка коду може змінити поведінку). Спільне ядро генерації винесено в `runGenerationBatch`/`preflightProblem` (експорт з `docgen-files-batch.mjs`) — перевикористовують і батч-CLI `fix-doc-files`, і lint-крок.
8
+
3
9
  ## [11.0.0] - 2026-06-15
4
10
 
5
11
  ### Removed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "11.0.0",
3
+ "version": "11.1.0",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -63,7 +63,7 @@ function selectTargets(root, all, { overwrite, retryDegraded }) {
63
63
  * Preflight локального бекенда: для omlx-моделі — мінімальний chat-виклик.
64
64
  * @returns {string|null} текст фатальної проблеми або null якщо можна генерувати
65
65
  */
66
- function preflightProblem() {
66
+ export function preflightProblem() {
67
67
  if (!DEFAULT_LOCAL_MODEL) {
68
68
  return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.'
69
69
  }
@@ -214,13 +214,29 @@ export async function runDocFilesGenCli(argv) {
214
214
  return 0
215
215
  }
216
216
 
217
+ return runGenerationBatch(targets, root, {
218
+ headline: `📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite, retryDegraded })}`
219
+ })
220
+ }
221
+
222
+ /**
223
+ * Спільне ядро генерації: preflight локального бекенда → послідовний прогін
224
+ * `targets` через `generateOne` з circuit-breaker'ом (K systemic-збоїв підряд →
225
+ * abort) → підсумковий звіт. Перевикористовують і батч-CLI (`runDocFilesGenCli`),
226
+ * і opportunistic lint-крок doc-files (scoped-набір змінених файлів).
227
+ * @param {Array<object>} targets елементи scanForDocFiles (sourcePath/docPath)
228
+ * @param {string} root абсолютний корінь
229
+ * @param {{ headline?: string }} [opts] headline — рядок-шапка прогону у stdout
230
+ * @returns {Promise<number>} 0 — без помилок; 1 — фейл preflight або є помилки; 2 — systemic-abort
231
+ */
232
+ export async function runGenerationBatch(targets, root, { headline } = {}) {
217
233
  const problem = preflightProblem()
218
234
  if (problem) {
219
235
  console.error(`✗ fix-doc-files: ${problem}`)
220
236
  return 1
221
237
  }
222
238
 
223
- console.log(`📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite, retryDegraded })}`)
239
+ if (headline) console.log(headline)
224
240
  const stats = { ok: 0, degraded: 0, err: 0, errors: [], skipped: [] }
225
241
 
226
242
  let done = 0
@@ -9,6 +9,7 @@ import { docPathForSource } from './docgen-scan.mjs'
9
9
  import { extractFacts } from './docgen-extract.mjs'
10
10
  import { extractAnchors, anchorTokens } from './docgen-extract-anchors.mjs'
11
11
  import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
12
+ import { JUDGE_ENABLED, judgeDoc, judgeFailsDoc } from './docgen-judge.mjs'
12
13
  import {
13
14
  oneShotMessages,
14
15
  sectionMessages,
@@ -449,6 +450,18 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
449
450
  }
450
451
  }
451
452
 
453
+ // Stage 3 (опц.): семантичний judge-гейт — лише за N_CURSOR_DOCGEN_JUDGE=1 і на
454
+ // доках, що ПРОЙШЛИ det-скорер (там ховаються false-positives). Scope: inaccurate.
455
+ let judge = null
456
+ if (JUDGE_ENABLED && score >= threshold) {
457
+ try {
458
+ judge = judgeDoc(src, r.md)
459
+ if (judgeFailsDoc(judge)) issues = [...issues, `judge:inaccurate:${judge.confidence}`]
460
+ } catch (error) {
461
+ issues = [...issues, `judge:error: ${error.message.slice(0, 80)}`]
462
+ }
463
+ }
464
+
452
465
  return {
453
466
  ...r,
454
467
  ms: Date.now() - t0,
@@ -456,7 +469,8 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
456
469
  llmCalls: llmMeter.calls,
457
470
  score,
458
471
  issues,
459
- degraded: score < threshold,
472
+ judge,
473
+ degraded: score < threshold || judgeFailsDoc(judge),
460
474
  model
461
475
  }
462
476
  }
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Usage:
16
16
  * node docgen-judge-measure.mjs <file1> <file2> ...
17
- * MEASURE_CACHE=/tmp/x N_CURSOR_DOCGEN_JUDGE_MODEL=openai-codex/gpt-5.4 node docgen-judge-measure.mjs ...
17
+ * MEASURE_CACHE=/tmp/x N_CLOUD_MIN_MODEL=openai-codex/gpt-5.4 node docgen-judge-measure.mjs ...
18
18
  */
19
19
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
20
20
  import { createHash } from 'node:crypto'
@@ -25,7 +25,7 @@ import { QUALITY_THRESHOLD } from './docgen-crc.mjs'
25
25
 
26
26
  const env = process.env
27
27
  const GEN_MODEL = env.N_LOCAL_MIN_MODEL ?? 'omlx/gemma-4-e4b-it-OptiQ-4bit'
28
- const JUDGE_MODEL = env.N_CURSOR_DOCGEN_JUDGE_MODEL ?? 'openai-codex/gpt-5.4-mini'
28
+ const JUDGE_MODEL = env.N_CLOUD_MIN_MODEL ?? 'openai-codex/gpt-5.4-mini'
29
29
  const THRESHOLD = Number(env.N_CURSOR_DOC_FILES_THRESHOLD ?? QUALITY_THRESHOLD) || 70
30
30
  const CACHE_DIR = env.MEASURE_CACHE ?? '/tmp/docgen-judge-measure'
31
31
  const JUDGE_TIMEOUT = Number(env.MEASURE_JUDGE_TIMEOUT_MS ?? 120_000)
@@ -48,7 +48,12 @@ function cacheSet(key, val) {
48
48
  writeFileSync(join(CACHE_DIR, key + '.json'), JSON.stringify(val))
49
49
  }
50
50
 
51
- /** Генерує (з кешем за хешем src). */
51
+ /**
52
+ * Генерує док (з кешем за хешем src).
53
+ * @param {string} file абсолютний шлях джерела
54
+ * @param {string} src вміст файлу
55
+ * @returns {{md: string, score: number|null, issues: string[], degraded: boolean, cached: boolean}} результат генерації
56
+ */
52
57
  function genCached(file, src) {
53
58
  const key = 'gen-' + sha(GEN_MODEL + '\0' + src)
54
59
  const hit = cacheGet(key)
@@ -59,7 +64,12 @@ function genCached(file, src) {
59
64
  return { ...out, cached: false }
60
65
  }
61
66
 
62
- /** Судить (з кешем за хешем src+doc). */
67
+ /**
68
+ * Судить док сильною моделлю (з кешем за хешем src+doc).
69
+ * @param {string} src вміст вихідного файлу
70
+ * @param {string} doc згенерована документація
71
+ * @returns {{verdict: string, confidence: number, reason: string, offending?: string[], cached: boolean}} verdict судді
72
+ */
63
73
  function judgeCached(src, doc) {
64
74
  const key = 'judge-' + sha(JUDGE_MODEL + '\0' + src + '\0' + doc)
65
75
  const hit = cacheGet(key)
@@ -67,7 +77,7 @@ function judgeCached(src, doc) {
67
77
  const user = `SOURCE FILE:\n\`\`\`\n${src.slice(0, 12000)}\n\`\`\`\n\nGENERATED DOC:\n\`\`\`md\n${doc.slice(0, 8000)}\n\`\`\`\n\nReturn the JSON verdict.`
68
78
  const raw = callLlm([{ role: 'system', content: SYSTEM }, { role: 'user', content: user }], JUDGE_MODEL, { timeoutMs: JUDGE_TIMEOUT, temperature: 0 })
69
79
  const a = raw.indexOf('{'), b = raw.lastIndexOf('}')
70
- if (a < 0 || b < 0) throw new Error('no JSON in judge reply: ' + raw.slice(0, 160))
80
+ if (a === -1 || b === -1) throw new Error('no JSON in judge reply: ' + raw.slice(0, 160))
71
81
  const v = JSON.parse(raw.slice(a, b + 1))
72
82
  cacheSet(key, v)
73
83
  return { ...v, cached: false }
@@ -85,11 +95,11 @@ function main() {
85
95
  for (const [i, file] of files.entries()) {
86
96
  const tag = `(${i + 1}/${files.length}) ${file}`
87
97
  let src
88
- try { src = readFileSync(file, 'utf8') } catch (e) { console.error(`[skip] ${tag}: read ${e.message}`); continue }
98
+ try { src = readFileSync(file, 'utf8') } catch (error) { console.error(`[skip] ${tag}: read ${error.message}`); continue }
89
99
 
90
100
  let gen
91
- try { gen = genCached(file, src) } catch (e) { console.error(`[gen-err] ${tag}: ${e.message.slice(0, 120)}`); rows.push({ file, error: 'gen', detail: e.message.slice(0, 200) }); continue }
92
- if (gen.score == null) { console.error(`[unsupported] ${tag}`); rows.push({ file, score: null, unsupported: true }); continue }
101
+ try { gen = genCached(file, src) } catch (error) { console.error(`[gen-err] ${tag}: ${error.message.slice(0, 120)}`); rows.push({ file, error: 'gen', detail: error.message.slice(0, 200) }); continue }
102
+ if (gen.score === null) { console.error(`[unsupported] ${tag}`); rows.push({ file, score: null, unsupported: true }); continue }
93
103
 
94
104
  const passed = gen.score >= THRESHOLD
95
105
  const row = { file, score: gen.score, degraded: gen.degraded, passed, genCached: gen.cached }
@@ -100,7 +110,7 @@ function main() {
100
110
  const v = judgeCached(src, gen.md)
101
111
  row.verdict = v.verdict; row.confidence = v.confidence; row.reason = v.reason; row.offending = v.offending; row.judgeCached = v.cached
102
112
  console.error(` [judge${v.cached ? '*' : ''}] ${v.verdict} (${v.confidence}) — ${(v.reason || '').slice(0, 90)}`)
103
- } catch (e) { row.judgeError = e.message.slice(0, 200); console.error(` [judge-err] ${e.message.slice(0, 120)}`) }
113
+ } catch (error) { row.judgeError = error.message.slice(0, 200); console.error(` [judge-err] ${error.message.slice(0, 120)}`) }
104
114
  }
105
115
  rows.push(row)
106
116
  }
@@ -0,0 +1,72 @@
1
+ /** @see ./docs/docgen-judge.md */
2
+ /**
3
+ * docgen-judge — опціональний семантичний verdict-гейт (spec
4
+ * `docs/specs/2026-06-14-docgen-judge-design.md`).
5
+ *
6
+ * Доповнює детермінований `scoreDoc`: ловить `inaccurate`-доки (твердження, що
7
+ * суперечать джерелу), яких структурно-лексичний скорер не бачить у принципі.
8
+ * Активується АВТОМАТИЧНО, якщо задано `N_CLOUD_MIN_MODEL` (бо суддя потребує
9
+ * сильнішої за генератор хмарної моделі — без неї судити нема чим). Працює лише на
10
+ * доках що ПРОЙШЛИ det-скорер (`score ≥ threshold`) — саме там ховаються
11
+ * false-positives. Scope строго `inaccurate` (вимір показав generic=0%). Без
12
+ * `N_CLOUD_MIN_MODEL` → 0 змін поведінки. Патерн дзеркалить `scripts/coverage-classify`.
13
+ */
14
+ import { env } from 'node:process'
15
+ import { callLlm } from '../../../lib/llm.mjs'
16
+ import { CLOUD_MIN } from '../../../lib/models.mjs'
17
+
18
+ /** Модель-суддя = `N_CLOUD_MIN_MODEL` (хмарний cloud-min tier). */
19
+ export const JUDGE_MODEL = CLOUD_MIN
20
+ /** Гейт активується АВТОМАТИЧНО, коли задано `N_CLOUD_MIN_MODEL` (без нього нема надійного судді). */
21
+ export const JUDGE_ENABLED = Boolean(CLOUD_MIN)
22
+ /** Мін. впевненість, щоб verdict `inaccurate` позначив док як degraded. */
23
+ export const JUDGE_CONFIDENCE = Number(env.N_CURSOR_DOCGEN_JUDGE_THRESHOLD ?? 0.7) || 0.7
24
+
25
+ const JUDGE_SYSTEM = `You are a strict technical-documentation reviewer. You receive a SOURCE file and an auto-generated Markdown DOC describing it. Classify the DOC into exactly one verdict:
26
+ - "accurate": specific to THIS file AND every factual claim is supported by the source.
27
+ - "generic": vague/boilerplate; could describe almost any file of this kind.
28
+ - "inaccurate": contains at least one claim NOT supported by, or contradicted by, the source code (e.g. wrong return behavior, false "no network"/"read-only", invented symbols/fields).
29
+ Prefer "inaccurate" if any claim is wrong. Respond with ONLY a JSON object, no prose:
30
+ {"verdict":"accurate|generic|inaccurate","confidence":0.0-1.0,"reason":"<10-300 chars>"}`
31
+
32
+ const VERDICTS = new Set(['accurate', 'generic', 'inaccurate'])
33
+
34
+ /**
35
+ * Витягує й валідує verdict-JSON із сирої відповіді LLM (як `parseVerdict` у coverage-classify).
36
+ * @param {string} rawText сира текстова відповідь судді
37
+ * @returns {{verdict: string, confidence: number, reason: string}} провалідований verdict
38
+ * @throws {Error} якщо JSON відсутній/невалідний або не відповідає схемі
39
+ */
40
+ export function parseDocVerdict(rawText) {
41
+ const a = rawText.indexOf('{')
42
+ const b = rawText.lastIndexOf('}')
43
+ if (a === -1 || b === -1) throw new Error('judge: no JSON object in response')
44
+ const v = JSON.parse(rawText.slice(a, b + 1))
45
+ if (!VERDICTS.has(v.verdict)) throw new Error(`judge: bad verdict "${v.verdict}"`)
46
+ if (typeof v.confidence !== 'number' || v.confidence < 0 || v.confidence > 1) {
47
+ throw new Error('judge: bad confidence')
48
+ }
49
+ return { verdict: v.verdict, confidence: v.confidence, reason: String(v.reason ?? '').slice(0, 500) }
50
+ }
51
+
52
+ /**
53
+ * Судить згенерований док сильною моделлю проти джерела.
54
+ * @param {string} src вміст вихідного файлу
55
+ * @param {string} doc згенерована документація
56
+ * @param {{model?: string, timeoutMs?: number}} [opts] override моделі/таймауту
57
+ * @returns {{verdict: string, confidence: number, reason: string}} verdict судді
58
+ */
59
+ export function judgeDoc(src, doc, { model = JUDGE_MODEL, timeoutMs = 120_000 } = {}) {
60
+ const user = `SOURCE FILE:\n\`\`\`\n${src.slice(0, 12_000)}\n\`\`\`\n\nGENERATED DOC:\n\`\`\`md\n${doc.slice(0, 8000)}\n\`\`\`\n\nReturn the JSON verdict.`
61
+ const raw = callLlm([{ role: 'system', content: JUDGE_SYSTEM }, { role: 'user', content: user }], model, { timeoutMs, temperature: 0 })
62
+ return parseDocVerdict(raw)
63
+ }
64
+
65
+ /**
66
+ * Чи позначає verdict док як degraded (лише `inaccurate` із достатньою впевненістю).
67
+ * @param {{verdict: string, confidence: number}|null} verdict verdict судді або null
68
+ * @returns {boolean} true якщо док треба вважати degraded через семантичну неточність
69
+ */
70
+ export function judgeFailsDoc(verdict) {
71
+ return verdict !== null && verdict.verdict === 'inaccurate' && verdict.confidence >= JUDGE_CONFIDENCE
72
+ }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  docgen:
3
3
  source: npm/rules/doc-files/js/docgen-judge-measure.mjs
4
- crc: b40b626c
4
+ crc: 7f72e520
5
5
  model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
6
  score: 100
7
7
  ---
@@ -10,20 +10,21 @@ docgen:
10
10
 
11
11
  ## Огляд
12
12
 
13
- Файл аналізує вміст визначеного списку файлів, створюючи технічну документацію на основі вихідного коду. Процес спирається на конфігурацію, визначену в report.json. Для кожного файлу відбувається кешування результатів у межах прогону. Якість згенерованої документації оцінюється хмарною моделлю шляхом порівняння з пороговими значеннями, заданими в report.json. Результати аналізу агрегуються, обчислюється відсоток хибнопозитивних спрацювань та зберігаються у report.json.
13
+ Файл зчитує список файлів для аналізу, спираючись на конфігурацію `report.json`. Для кожного файлу генерується технічна документація за допомогою локальної моделі. Після генерації документація оцінюється хмарною моделлю. Результати оцінки кешуються у межах прогону, а потім збираються у звіт. Звіт зберігається у `report.json` та виводиться у консоль.
14
14
 
15
15
  ## Поведінка
16
16
 
17
- 1. Зчитує список файлів для аналізу.
17
+ 1. Зчитує список файлів для аналізу з командного рядка.
18
18
  2. Для кожного файлу зчитує його вміст.
19
- 3. Генерує технічну документацію для файлу, використовуючи кеш за вмістом вихідного коду.
20
- 4. Якщо документація згенерована, перевіряє її якість за встановленим порогом.
21
- 5. Якщо якість документації відповідає порогу, передає вміст вихідного коду та згенеровану документацію для оцінки.
22
- 6. Оцінює документацію за допомогою сильної хмарної моделі, використовуючи кеш за вмістом вихідного коду та документації.
23
- 7. Збирає результати для кожного файлу.
24
- 8. Агрегує результати, обчислюючи відсоток хибнопозитивних спрацювань (false-positive rate) серед документів, які пройшли порогову перевірку та були оцінені.
25
- 9. Зберігає повний звіт у файл `report.json` у директорії кешу.
26
- 10. Виводить консольний звіт про результати вимірювання.
19
+ 3. Генерує документацію для файлу, використовуючи локальну модель. Результат кешується за хешем вмісту джерела.
20
+ 4. Якщо генерація документації неможлива або не пройшла встановлений поріг якості, фіксує це як помилку або непідтримуваний файл.
21
+ 5. Якщо документація пройшла поріг якості, вона передається для оцінки.
22
+ 6. Оцінює згенеровану документацію за допомогою потужної хмарної моделі. Результат оцінки кешується за хешем вмісту джерела та документації.
23
+ 7. Якщо оцінка неможлива, фіксує помилку оцінки.
24
+ 8. Збирає результати для всіх файлів.
25
+ 9. Обчислює звіт, включаючи загальну кількість файлів, кількість успішно згенерованих та оцінених, а також відсоток хибнопозитивних результатів (false-positive rate).
26
+ 10. Зберігає повний звіт у файл `report.json` у каталозі кешу.
27
+ 11. Виводить консольний звіт з ключовими показниками.
27
28
 
28
29
  ## Гарантії поведінки
29
30
 
@@ -0,0 +1,35 @@
1
+ ---
2
+ docgen:
3
+ source: npm/rules/doc-files/js/docgen-judge.mjs
4
+ crc: c6ab093a
5
+ model: omlx/gemma-4-e4b-it-OptiQ-4bit
6
+ score: 100
7
+ ---
8
+
9
+ # docgen-judge.mjs
10
+
11
+ ## Огляд
12
+
13
+ Модуль оцінює згенеровану документацію, порівнюючи її з вихідним файлом за допомогою великої мовної моделі. Це дозволяє визначити відповідність та якість документації. Модуль надає функції для запуску оцінки (`judgeDoc`, `judgeFailsDoc`), отримання результатів (`parseDocVerdict`), визначення моделі (`JUDGE_MODEL`), перевірки статусу оцінювача (`JUDGE_ENABLED`) та отримання рівня впевненості (`JUDGE_CONFIDENCE`).
14
+
15
+ ## Поведінка
16
+
17
+ JUDGE_MODEL — Вказує модель, яку використовує суддя для оцінки документації.
18
+ JUDGE_ENABLED — Позначає, чи активна функціональність судді.
19
+ JUDGE_CONFIDENCE — Визначає мінімальний рівень впевненості, необхідний для позначення документації як деградованої.
20
+ parseDocVerdict — Витягує та валідує об'єкт з оцінкою документації з сирого текстового виводу судді.
21
+ judgeDoc — Виконує оцінку згенерованої документації проти вмісту вихідного файлу за допомогою великої мовної моделі.
22
+ judgeFailsDoc — Визначає, чи слід вважати документацію деградованою на основі оцінки судді.
23
+
24
+ ## Публічний API
25
+
26
+ JUDGE_MODEL — Визначає модель-суддю як `N_CLOUD_MIN_MODEL` (хмарний мінімальний рівень).
27
+ JUDGE_ENABLED — Автоматично вмикає механізм судді, якщо обрано `N_CLOUD_MIN_MODEL`.
28
+ JUDGE_CONFIDENCE — Встановлює мінімальний рівень впевненості, необхідний для позначення документа як погіршеного (degraded) за неточним вердиктом.
29
+ parseDocVerdict — Витягує та перевіряє структуру вердикту у форматі JSON із сирих даних від великої мовної моделі.
30
+ judgeDoc — Оцінює згенерований документ потужною моделлю, порівнюючи його з вихідним джерелом.
31
+ judgeFailsDoc — Визначає, чи повинен документ бути позначений як погіршений (degraded) на підставі вердикту, якщо він неточний і має достатню впевненість.
32
+
33
+ ## Гарантії поведінки
34
+
35
+ - Read-only: не виконує операцій запису (ФС/БД).
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Адаптер агрегатора `n-cursor lint` для правила doc-files.
2
+ * Адаптер агрегатора `n-cursor lint` для правила doc-files (opportunistic LLM-fix
3
+ * tier, спека docs/specs/2026-06-15-opportunistic-llm-fix-tier.md).
3
4
  *
4
5
  * Quick-фаза отримує список змінених файлів і мапить їх у пари в **обидва** боки:
5
6
  * - змінене **джерело** (`.js/.mjs/.ts/.vue/.py/.rs`) → перевірка його доки `<dir>/docs/<stem>.md`;
@@ -7,8 +8,11 @@
7
8
  * (той самий stem у каталозі над текою `docs`).
8
9
  * Ci-фаза (files === undefined) проганяє повний скан дерева.
9
10
  *
10
- * Порушення — `missing` ∪ `crc-mismatch` (детермінований CRC-детект, 0 LLM-токенів);
11
- * degraded не блокує. Exit 1 є stale; 0 — все свіже (конвенція агрегатора).
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.
12
16
  */
13
17
  import { join, dirname, basename, extname } from 'node:path'
14
18
  import { existsSync, readdirSync } from 'node:fs'
@@ -79,18 +83,34 @@ function reportStale(stale) {
79
83
  }
80
84
 
81
85
  /**
82
- * Крок агрегатора lint для doc-files.
86
+ * Збирає застарілі (missing crc-mismatch) описи у scope кроку.
83
87
  * @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
84
- * @param {string} [cwd] корінь репо
85
- * @returns {Promise<number>} 0 OK, 1 — є застарілі доки
88
+ * @param {string} cwd корінь репо
89
+ * @returns {Array<{sourcePath:string, docPath:string, reason:string|null}>} stale-описи (готові як targets генерації)
86
90
  */
87
- export function lint(files, cwd = process.cwd()) {
88
- if (files === undefined) {
89
- const stale = scanForDocFiles(cwd).filter(f => f.stale)
90
- return Promise.resolve(reportStale(stale))
91
- }
91
+ function collectStale(files, cwd) {
92
+ if (files === undefined) return scanForDocFiles(cwd).filter(f => f.stale)
92
93
  const sources = sourcesFromChanged(files, cwd)
93
- if (sources.length === 0) return Promise.resolve(0)
94
- const stale = sources.map(src => describeFile(cwd, src)).filter(f => f.stale)
95
- return Promise.resolve(reportStale(stale))
94
+ return sources.map(src => describeFile(cwd, src)).filter(f => f.stale)
95
+ }
96
+
97
+ /**
98
+ * Крок агрегатора lint для doc-files (opportunistic LLM-fix tier).
99
+ * @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
100
+ * @param {string} [cwd] корінь репо
101
+ * @param {{ readOnly?: boolean }} [opts] readOnly: лише детект (CI/hook), без мутацій/LLM
102
+ * @returns {Promise<number>} 0 — доки свіжі; 1 — є застарілі (детект, fix пропущено чи помилка генерації)
103
+ */
104
+ export async function lint(files, cwd = process.cwd(), { readOnly = false } = {}) {
105
+ const stale = collectStale(files, cwd)
106
+ if (stale.length === 0) return 0
107
+ if (readOnly) return reportStale(stale)
108
+
109
+ // fix-by-default: opportunistic-генерація через спільне ядро (preflight omlx →
110
+ // батч із circuit-breaker'ом). omlx недоступний → runGenerationBatch друкує причину
111
+ // й повертає !=0; ми re-detect'имо й через reportStale віддаємо exit 1 (гейт тримається).
112
+ process.stdout.write(`ℹ️ doc-files: ${stale.length} застарілих — пробую авто-фікс (omlx)…\n`)
113
+ const { runGenerationBatch } = await import('./docgen-files-batch.mjs')
114
+ await runGenerationBatch(stale, cwd, { headline: `📋 doc-files: генерація ${stale.length} файл(ів)` })
115
+ return reportStale(collectStale(files, cwd))
96
116
  }
@@ -1 +1 @@
1
- { "auto": "завжди", "lint": "per-file" }
1
+ { "auto": "завжди", "lint": "per-file", "llmFix": true }
@@ -11,6 +11,10 @@
11
11
  "enum": ["per-file", "full"],
12
12
  "description": "Scope lint-кроку: per-file (декомпозиція по змінених файлах, дельта vs origin) або full (нероздільно крос-файловий, лише `lint --full`)."
13
13
  },
14
+ "llmFix": {
15
+ "type": "boolean",
16
+ "description": "Opt-in opportunistic LLM-fix: у fix-by-default (не --read-only) lint-крок намагається виправити порушення локальною моделлю (omlx), якщо її піднято; інакше пропускає й лишає exit 1. Лише для контент-правил (doc-files, cspell) — НЕ для логічних лінтерів (зміна коду може змінити поведінку). Деталі: docs/specs/2026-06-15-opportunistic-llm-fix-tier.md."
17
+ },
14
18
  "auto": {
15
19
  "description": "Умова автоактивації правила: \"завжди\", масив id правил-залежностей, glob, або іменований предикат.",
16
20
  "oneOf": [