@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 +6 -0
- package/package.json +1 -1
- package/rules/doc-files/js/docgen-files-batch.mjs +18 -2
- package/rules/doc-files/js/docgen-gen.mjs +15 -1
- package/rules/doc-files/js/docgen-judge-measure.mjs +19 -9
- package/rules/doc-files/js/docgen-judge.mjs +72 -0
- package/rules/doc-files/js/docs/docgen-judge-measure.md +12 -11
- package/rules/doc-files/js/docs/docgen-judge.md +35 -0
- package/rules/doc-files/js/lint.mjs +34 -14
- package/rules/doc-files/meta.json +1 -1
- package/schemas/rule-meta.json +4 -0
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
|
@@ -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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
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 (
|
|
92
|
-
if (gen.score
|
|
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 (
|
|
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:
|
|
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
|
-
Файл
|
|
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.
|
|
25
|
-
9.
|
|
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
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
86
|
+
* Збирає застарілі (missing ∪ crc-mismatch) описи у scope кроку.
|
|
83
87
|
* @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
|
|
84
|
-
* @param {string}
|
|
85
|
-
* @returns {
|
|
88
|
+
* @param {string} cwd корінь репо
|
|
89
|
+
* @returns {Array<{sourcePath:string, docPath:string, reason:string|null}>} stale-описи (готові як targets генерації)
|
|
86
90
|
*/
|
|
87
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 }
|
package/schemas/rule-meta.json
CHANGED
|
@@ -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": [
|