@nitra/cursor 11.1.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/bin/n-cursor.js +2 -1
- package/lib/llm.mjs +27 -0
- package/package.json +1 -1
- package/rules/doc-files/js/docgen-crc.mjs +20 -9
- package/rules/doc-files/js/docgen-files-batch.mjs +34 -60
- package/rules/doc-files/js/docgen-gen.mjs +2 -2
- package/rules/doc-files/js/docgen-scan.mjs +2 -2
- package/rules/doc-files/js/lint.mjs +4 -3
- package/rules/lint/js/orchestrate.mjs +10 -2
- package/rules/text/js/lint.mjs +3 -2
- package/rules/text/lint/cspell-fix.mjs +125 -49
- package/rules/text/lint/lint.mjs +7 -5
- package/rules/text/meta.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [11.3.0] - 2026-06-15
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- lint: opt-in `meta.json: llmFix:true` тепер реально дротується (раніше прапор був декоративний — opportunistic LLM-fix біг просто на `!readOnly`). `runLint` читає `llmFix` з meta правила й передає в `lint(files, cwd, { readOnly, llmFix })`; правило без прапора лишається detect-only. Це й забезпечує safety-тріаж зі спеки (логічні лінтери не вмикають LLM-fix випадково). doc-files і text позначені `llmFix:true`; cspell-класифікація гейтиться через `llmFix` (проведено `runLintTextCli`/`runLintTextSteps`/`runCspellText`), standalone `lint-text` передає `llmFix:true`. Принагідно: justified `no-unsanitized/method`-disable на package-internal динамічний import у `runLint` (pre-existing).
|
|
8
|
+
|
|
9
|
+
## [11.2.0] - 2026-06-15
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- cspell-fix: заміна зламаного whole-file rewrite на classify→.cspell.json. Стара схема (llmLintFix whole-file) давала timeout 120с та parse-fail на реальних файлах; нова класифікує знахідки через bounded omlx-запит (≤80 слів → компактний JSON) і авто-дописує валідні слова у .cspell.json словник (sorted+dedup), а ймовірні одруки емітує на рев'ю без авто-застосування. Підтверджено на репо: 73 слова у словник за ≈5с (vs 120с timeout раніше).
|
|
14
|
+
- cspell-fix: whole-file omlx-апплай → класифікація+словник (нова схема, спека docs/specs/2026-06-15-opportunistic-llm-fix-tier.md). Старий підхід (модель повертає весь файл як JSON) на укр+тех-репо операційно ламався — 120с-таймаути, memory-guard reject, parse-fail (вимір: 1406 знахідок / 292 файли, ~90% — валідні терміни, не одруки). Новий fix-режим робить **один bounded omlx-виклик**: класифікує distinct «Unknown word» → валідні слова авто-дописує у `.cspell.json#words` (sorted/dedup, видно в diff) → ймовірні одруки лишає списком на рев'ю (НЕ авто-виправляє — апплай небезпечний) → re-detect. read-only незмінний (нуль мутацій). Єдиний knob моделі — `N_LOCAL_MIN_MODEL` (прибрано `N_CURSOR_FIX_MODEL` із цього шляху). Preflight omlx (fast-skip замість приречених викликів), cap класифікації з логом надлишку. `groupFindingsByFile` → `unknownWords`/`appendWordsToDict`.
|
|
15
|
+
- llm: спільний `preflightLocalModel(model)` у `npm/lib/llm.mjs` — omlx health-check (memory-guard / down / auth) як одна цеглина opportunistic LLM-fix tier (спека docs/specs/2026-06-15-opportunistic-llm-fix-tier.md). Прибрано дубльовані локальні `preflightProblem` із `doc-files/js/docgen-files-batch.mjs` (генерація) і `text/lint/cspell-fix.mjs` (класифікація) — обидва тепер кличуть спільний хелпер із `N_LOCAL_MIN_MODEL`. Per-target loop/circuit-breaker лишається у doc-files (cspell — single-call). Поведінка незмінна.
|
|
16
|
+
|
|
3
17
|
## [11.1.0] - 2026-06-15
|
|
4
18
|
|
|
5
19
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -1568,7 +1568,8 @@ try {
|
|
|
1568
1568
|
case 'lint-text': {
|
|
1569
1569
|
// Канонічний lint-text: cspell → shellcheck → dotenv → markdownlint → v8r (text.mdc).
|
|
1570
1570
|
// `--read-only` (CI): без авто-фіксу (markdownlint/shellcheck/dotenv) — нуль мутацій.
|
|
1571
|
-
|
|
1571
|
+
// `llmFix:true` — text llmFix-capable, тож standalone lint-text робить omlx-класифікацію cspell.
|
|
1572
|
+
process.exitCode = await runLintTextCli({ readOnly: args.includes('--read-only'), llmFix: true })
|
|
1572
1573
|
|
|
1573
1574
|
break
|
|
1574
1575
|
}
|
package/lib/llm.mjs
CHANGED
|
@@ -168,3 +168,30 @@ export function omlxHealthCheck(opts = {}) {
|
|
|
168
168
|
return { ok: false, reason: 'error', detail }
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Спільний preflight локальної fix/gen-моделі (opportunistic LLM-fix tier, спека
|
|
174
|
+
* docs/specs/2026-06-15-opportunistic-llm-fix-tier.md): чи можна звати модель зараз.
|
|
175
|
+
* Health-check лише для omlx-бекенду (pi/cloud — завжди дозволено). Використовують
|
|
176
|
+
* doc-files (генерація) і text/cspell (класифікація) — fast-skip замість приречених викликів.
|
|
177
|
+
* @param {string} model model-id (зазвичай `N_LOCAL_MIN_MODEL`)
|
|
178
|
+
* @returns {string|null} людинозрозумілий текст проблеми, або null якщо можна викликати
|
|
179
|
+
*/
|
|
180
|
+
export function preflightLocalModel(model) {
|
|
181
|
+
if (!model) {
|
|
182
|
+
return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.'
|
|
183
|
+
}
|
|
184
|
+
if (pickBackend(model) !== 'omlx') return null
|
|
185
|
+
const hc = omlxHealthCheck({ model })
|
|
186
|
+
if (hc.ok) return null
|
|
187
|
+
if (hc.reason === 'memory-guard') {
|
|
188
|
+
return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}`
|
|
189
|
+
}
|
|
190
|
+
if (hc.reason === 'down') {
|
|
191
|
+
return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}`
|
|
192
|
+
}
|
|
193
|
+
if (hc.reason === 'auth') {
|
|
194
|
+
return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}`
|
|
195
|
+
}
|
|
196
|
+
return `omlx помилка: ${hc.detail}`
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* Degraded-маркер (ADR 260610-2228): якщо локальний конвеєр не дотягнув до порогу
|
|
11
11
|
* якості, дока все одно пишеться, а frontmatter додатково несе `score` (det-оцінка)
|
|
12
12
|
* та `issues` (коди проблем). CRC при цьому свіжий — Stop-гейт не блокує задачі через
|
|
13
|
-
* слабкість моделі; борг видимий через `check --degraded` і
|
|
14
|
-
*
|
|
13
|
+
* слабкість моделі; борг видимий через `check --degraded` і автоматично доретраюється
|
|
14
|
+
* наступним `gen` (рівно один раз на версію джерела — далі `retried: true` у frontmatter).
|
|
15
15
|
*
|
|
16
16
|
* Frontmatter — єдиний дозволений виняток із правила «чистий Markdown без HTML»:
|
|
17
17
|
* це машинні метадані, не контент. Формат:
|
|
@@ -54,6 +54,8 @@ const CRC_RE = /^[ \t]{0,8}crc:[ \t]{0,8}(.+)$/mu
|
|
|
54
54
|
const MODEL_RE = /^[ \t]{0,8}model:[ \t]{0,8}(.+)$/mu
|
|
55
55
|
const SCORE_RE = /^[ \t]{0,8}score:[ \t]{0,8}(\d+)$/mu
|
|
56
56
|
const ISSUES_RE = /^[ \t]{0,8}issues:[ \t]{0,8}(.+)$/mu
|
|
57
|
+
const RETRIED_RE = /^[ \t]{0,8}retried:[ \t]{0,8}true[ \t]*$/mu
|
|
58
|
+
const JUDGE_MODEL_RE = /^[ \t]{0,8}judgeModel:[ \t]{0,8}(.+)$/mu
|
|
57
59
|
const LEADING_NEWLINES_RE = /^\n+/u
|
|
58
60
|
const ISSUE_CODE_TAIL_RE = /[,:]$/u
|
|
59
61
|
|
|
@@ -62,7 +64,7 @@ const ISSUE_CODE_TAIL_RE = /[,:]$/u
|
|
|
62
64
|
* Поля `model`/`score`/`issues` опційні (back-compat зі старими доками): без них —
|
|
63
65
|
* `model:null`, `score:null`, `issues:[]`.
|
|
64
66
|
* @param {string} md вміст md-файлу
|
|
65
|
-
* @returns {{ data: { source: string|null, crc: string|null, model: string|null, score: number|null, issues: string[] }|null, body: string }} метадані + тіло без frontmatter
|
|
67
|
+
* @returns {{ data: { source: string|null, crc: string|null, model: string|null, score: number|null, issues: string[], retried: boolean, judgeModel: string|null }|null, body: string }} метадані + тіло без frontmatter
|
|
66
68
|
*/
|
|
67
69
|
export function parseDocFrontmatter(md) {
|
|
68
70
|
const match = md.match(FRONTMATTER_RE)
|
|
@@ -81,7 +83,9 @@ export function parseDocFrontmatter(md) {
|
|
|
81
83
|
.split(',')
|
|
82
84
|
.map(s => s.trim())
|
|
83
85
|
.filter(Boolean)
|
|
84
|
-
: []
|
|
86
|
+
: [],
|
|
87
|
+
retried: RETRIED_RE.test(block),
|
|
88
|
+
judgeModel: block.match(JUDGE_MODEL_RE)?.[1].trim() ?? null
|
|
85
89
|
},
|
|
86
90
|
body: md.slice(match[0].length)
|
|
87
91
|
}
|
|
@@ -107,7 +111,7 @@ function issueCodes(issues) {
|
|
|
107
111
|
* Будує frontmatter-блок із шляхом джерела, CRC, (опційно) моделлю-генератором і якістю.
|
|
108
112
|
* @param {string} source відносний шлях джерела
|
|
109
113
|
* @param {string} crc CRC32 джерела у hex
|
|
110
|
-
* @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка
|
|
114
|
+
* @param {{ score: number, issues?: string[], retried?: boolean, judge?: {model?: string} }|null} [quality] det-оцінка доки (+ опц. `retried`-маркер і `judge.model` хмарного судді); null — без полів якості
|
|
111
115
|
* @param {string|null} [model] повний id моделі-генератора; null — без поля `model`
|
|
112
116
|
* @returns {string} рядок `---\ndocgen:\n source: …\n crc: …[\n model: …][\n score: …][\n issues: …]\n---\n`
|
|
113
117
|
*/
|
|
@@ -118,6 +122,8 @@ export function buildDocFrontmatter(source, crc, quality = null, model = null) {
|
|
|
118
122
|
lines.push(`score: ${quality.score}`)
|
|
119
123
|
const codes = issueCodes(quality.issues ?? [])
|
|
120
124
|
if (codes.length > 0) lines.push(`issues: ${codes.join(',')}`)
|
|
125
|
+
if (quality.retried) lines.push('retried: true')
|
|
126
|
+
if (quality.judge && quality.judge.model) lines.push(`judgeModel: ${quality.judge.model}`)
|
|
121
127
|
}
|
|
122
128
|
const indented = lines.map(l => ' ' + l).join('\n')
|
|
123
129
|
return `---\ndocgen:\n${indented}\n---\n`
|
|
@@ -128,7 +134,7 @@ export function buildDocFrontmatter(source, crc, quality = null, model = null) {
|
|
|
128
134
|
* @param {string} md тіло доки (з frontmatter або без)
|
|
129
135
|
* @param {string} source відносний шлях джерела
|
|
130
136
|
* @param {string} crc CRC32 джерела у hex
|
|
131
|
-
* @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки
|
|
137
|
+
* @param {{ score: number, issues?: string[], retried?: boolean, judge?: {model?: string} }|null} [quality] det-оцінка доки (+ опц. `retried`-маркер і `judge.model` хмарного судді)
|
|
132
138
|
* @param {string|null} [model] повний id моделі-генератора; null — без поля `model`
|
|
133
139
|
* @returns {string} md зі свіжим frontmatter
|
|
134
140
|
*/
|
|
@@ -150,12 +156,17 @@ export function readDocCrc(docAbsPath) {
|
|
|
150
156
|
/**
|
|
151
157
|
* Якість, збережена у frontmatter доки.
|
|
152
158
|
* @param {string} docAbsPath абсолютний шлях md-доки
|
|
153
|
-
* @returns {{ score: number|null, issues: string[] }} `score:null` — доки немає або поле
|
|
159
|
+
* @returns {{ score: number|null, issues: string[], retried: boolean, judgeModel: string|null }} `score:null` — доки немає або поле відсутнє; `retried` — чи док уже доретраювали при цьому CRC; `judgeModel` — хмарна модель-суддя, що позначила док (або null)
|
|
154
160
|
*/
|
|
155
161
|
export function readDocQuality(docAbsPath) {
|
|
156
|
-
if (!existsSync(docAbsPath)) return { score: null, issues: [] }
|
|
162
|
+
if (!existsSync(docAbsPath)) return { score: null, issues: [], retried: false, judgeModel: null }
|
|
157
163
|
const data = parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data
|
|
158
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
score: data?.score ?? null,
|
|
166
|
+
issues: data?.issues ?? [],
|
|
167
|
+
retried: data?.retried ?? false,
|
|
168
|
+
judgeModel: data?.judgeModel ?? null
|
|
169
|
+
}
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
/**
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Уся черга/батчинг/CRC-штамп живуть тут, а не в контексті моделі — тому
|
|
5
5
|
* масовий перший прогін на сотні файлів не «заморює» агента. Конвеєр суто
|
|
6
6
|
* локальний: жодних cloud-ескалацій; якщо det-score нижче порогу — дока все
|
|
7
|
-
* одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а
|
|
8
|
-
* `gen
|
|
7
|
+
* одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а наступний
|
|
8
|
+
* `gen` автоматично доретраює такі доки (один раз на версію джерела — далі `retried:true`).
|
|
9
9
|
*
|
|
10
10
|
* Перед масовим прогоном — health-check omlx: memory-guard зайнятої 8GB машини
|
|
11
11
|
* означає «відклади прогін», а не сотні хибних «✗» у звіті.
|
|
@@ -14,7 +14,7 @@ import { readFileSync, mkdirSync, writeFileSync, existsSync, statSync } from 'no
|
|
|
14
14
|
import { dirname, join } from 'node:path'
|
|
15
15
|
|
|
16
16
|
import { isRunAsCli } from '../../../scripts/cli-entry.mjs'
|
|
17
|
-
import {
|
|
17
|
+
import { classifyOmlxError, preflightLocalModel } from '../../../lib/llm.mjs'
|
|
18
18
|
import { generateDoc, DEFAULT_LOCAL_MODEL } from './docgen-gen.mjs'
|
|
19
19
|
import { crc32, stampDoc, readDocQuality, readDocModel, QUALITY_THRESHOLD } from './docgen-crc.mjs'
|
|
20
20
|
import { resolveRoot, scanForDocFiles } from './docgen-scan.mjs'
|
|
@@ -22,7 +22,7 @@ import { resolveRoot, scanForDocFiles } from './docgen-scan.mjs'
|
|
|
22
22
|
/**
|
|
23
23
|
* Парсить `--limit N` / `--from N` / прапори режимів для дозапуску великого прогону.
|
|
24
24
|
* @param {string[]} argv аргументи
|
|
25
|
-
* @returns {{ from: number, limit: number, overwrite: boolean
|
|
25
|
+
* @returns {{ from: number, limit: number, overwrite: boolean }} зріз і режими
|
|
26
26
|
*/
|
|
27
27
|
function parseGenArgs(argv) {
|
|
28
28
|
const num = (flag, dflt) => {
|
|
@@ -32,65 +32,38 @@ function parseGenArgs(argv) {
|
|
|
32
32
|
return {
|
|
33
33
|
from: num('--from', 0),
|
|
34
34
|
limit: num('--limit', Infinity),
|
|
35
|
-
overwrite: argv.includes('--overwrite')
|
|
36
|
-
retryDegraded: argv.includes('--retry-degraded')
|
|
35
|
+
overwrite: argv.includes('--overwrite')
|
|
37
36
|
}
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
/**
|
|
41
|
-
* Цілі
|
|
42
|
-
* - default
|
|
43
|
-
* - `--overwrite`
|
|
44
|
-
*
|
|
40
|
+
* Цілі генерації:
|
|
41
|
+
* - default → застарілі (stale) АБО degraded-доки, які ще не доретраювали при цьому CRC;
|
|
42
|
+
* - `--overwrite` → усі.
|
|
43
|
+
* Degraded-док отримує рівно ОДИН доретрай на версію джерела: після невдалого доретраю
|
|
44
|
+
* (лишився degraded) штампується `retried: true` і його більше не чіпають до зміни джерела
|
|
45
|
+
* (нова версія → CRC-mismatch → stale → лічильник скидається). Конвеєр сходиться без прапора.
|
|
45
46
|
* @param {string} root абсолютний корінь
|
|
46
47
|
* @param {Array<object>} all результат scanForDocFiles
|
|
47
|
-
* @param {{ overwrite: boolean
|
|
48
|
+
* @param {{ overwrite: boolean }} mode режими
|
|
48
49
|
* @returns {Array<object>} відфільтровані цілі
|
|
49
50
|
*/
|
|
50
|
-
function selectTargets(root, all, { overwrite
|
|
51
|
-
if (retryDegraded) {
|
|
52
|
-
return all.filter(f => {
|
|
53
|
-
if (f.stale) return false
|
|
54
|
-
const { score } = readDocQuality(join(root, f.docPath))
|
|
55
|
-
return score !== null && score < QUALITY_THRESHOLD
|
|
56
|
-
})
|
|
57
|
-
}
|
|
51
|
+
export function selectTargets(root, all, { overwrite }) {
|
|
58
52
|
if (overwrite) return all
|
|
59
|
-
return all.filter(f =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* @returns {string|null} текст фатальної проблеми або null якщо можна генерувати
|
|
65
|
-
*/
|
|
66
|
-
export function preflightProblem() {
|
|
67
|
-
if (!DEFAULT_LOCAL_MODEL) {
|
|
68
|
-
return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.'
|
|
69
|
-
}
|
|
70
|
-
if (pickBackend(DEFAULT_LOCAL_MODEL) !== 'omlx') return null
|
|
71
|
-
const hc = omlxHealthCheck({ model: DEFAULT_LOCAL_MODEL })
|
|
72
|
-
if (hc.ok) return null
|
|
73
|
-
if (hc.reason === 'memory-guard') {
|
|
74
|
-
return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}`
|
|
75
|
-
}
|
|
76
|
-
if (hc.reason === 'down') {
|
|
77
|
-
return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}`
|
|
78
|
-
}
|
|
79
|
-
if (hc.reason === 'auth') {
|
|
80
|
-
return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}`
|
|
81
|
-
}
|
|
82
|
-
return `omlx помилка: ${hc.detail}`
|
|
53
|
+
return all.filter(f => {
|
|
54
|
+
if (f.stale) return true
|
|
55
|
+
const { score, retried } = readDocQuality(join(root, f.docPath))
|
|
56
|
+
return score !== null && score < QUALITY_THRESHOLD && !retried
|
|
57
|
+
})
|
|
83
58
|
}
|
|
84
59
|
|
|
85
60
|
/**
|
|
86
61
|
* Текст-суфікс режиму для прогрес-рядка.
|
|
87
|
-
* @param {{ overwrite: boolean
|
|
88
|
-
* @returns {string} ` (--overwrite)`
|
|
62
|
+
* @param {{ overwrite: boolean }} mode режими
|
|
63
|
+
* @returns {string} ` (--overwrite)` або порожній рядок
|
|
89
64
|
*/
|
|
90
|
-
function modeSuffix({ overwrite
|
|
91
|
-
|
|
92
|
-
if (retryDegraded) return ' (--retry-degraded)'
|
|
93
|
-
return ''
|
|
65
|
+
function modeSuffix({ overwrite }) {
|
|
66
|
+
return overwrite ? ' (--overwrite)' : ''
|
|
94
67
|
}
|
|
95
68
|
|
|
96
69
|
/**
|
|
@@ -146,8 +119,13 @@ async function generateOne(file, root, progress, stats) {
|
|
|
146
119
|
const result = await generateDoc(sourceAbs, { existingMd })
|
|
147
120
|
const crc = crc32(readFileSync(sourceAbs))
|
|
148
121
|
mkdirSync(dirname(docAbs), { recursive: true })
|
|
122
|
+
// retried: НЕ stale (отже це доретрай при тому ж CRC) і лишився degraded → штампуємо,
|
|
123
|
+
// щоб наступні `gen` його не чіпали до зміни джерела (сходимість без прапора).
|
|
124
|
+
const retried = !file.stale && result.degraded
|
|
149
125
|
const quality =
|
|
150
|
-
result.score === null
|
|
126
|
+
result.score === null
|
|
127
|
+
? null
|
|
128
|
+
: { score: result.score, issues: result.degraded ? result.issues : [], retried, judge: result.judge }
|
|
151
129
|
writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality, result.model))
|
|
152
130
|
stats.ok++
|
|
153
131
|
if (result.degraded) {
|
|
@@ -189,7 +167,7 @@ function reportStats(stats) {
|
|
|
189
167
|
for (const e of stats.skipped) console.log(` - ${e}`)
|
|
190
168
|
}
|
|
191
169
|
if (stats.degraded > 0) {
|
|
192
|
-
console.log(
|
|
170
|
+
console.log('Degraded-доки автоматично доретраюються наступним `gen` (один раз на версію джерела).')
|
|
193
171
|
}
|
|
194
172
|
}
|
|
195
173
|
|
|
@@ -200,22 +178,18 @@ function reportStats(stats) {
|
|
|
200
178
|
*/
|
|
201
179
|
export async function runDocFilesGenCli(argv) {
|
|
202
180
|
const root = resolveRoot(argv)
|
|
203
|
-
const { from, limit, overwrite
|
|
181
|
+
const { from, limit, overwrite } = parseGenArgs(argv)
|
|
204
182
|
|
|
205
183
|
const all = scanForDocFiles(root)
|
|
206
|
-
const targets = selectTargets(root, all, { overwrite
|
|
184
|
+
const targets = selectTargets(root, all, { overwrite }).slice(from, from + limit)
|
|
207
185
|
|
|
208
186
|
if (targets.length === 0) {
|
|
209
|
-
console.log(
|
|
210
|
-
retryDegraded
|
|
211
|
-
? '✓ doc-files: degraded-док немає. Нічого переганяти.'
|
|
212
|
-
: '✓ doc-files: усі файлові доки свіжі. Нічого генерувати.'
|
|
213
|
-
)
|
|
187
|
+
console.log('✓ doc-files: усі файлові доки свіжі й не-degraded. Нічого генерувати.')
|
|
214
188
|
return 0
|
|
215
189
|
}
|
|
216
190
|
|
|
217
191
|
return runGenerationBatch(targets, root, {
|
|
218
|
-
headline: `📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite
|
|
192
|
+
headline: `📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite })}`
|
|
219
193
|
})
|
|
220
194
|
}
|
|
221
195
|
|
|
@@ -230,7 +204,7 @@ export async function runDocFilesGenCli(argv) {
|
|
|
230
204
|
* @returns {Promise<number>} 0 — без помилок; 1 — фейл preflight або є помилки; 2 — systemic-abort
|
|
231
205
|
*/
|
|
232
206
|
export async function runGenerationBatch(targets, root, { headline } = {}) {
|
|
233
|
-
const problem =
|
|
207
|
+
const problem = preflightLocalModel(DEFAULT_LOCAL_MODEL)
|
|
234
208
|
if (problem) {
|
|
235
209
|
console.error(`✗ fix-doc-files: ${problem}`)
|
|
236
210
|
return 1
|
|
@@ -9,7 +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
|
+
import { JUDGE_ENABLED, JUDGE_MODEL, judgeDoc, judgeFailsDoc } from './docgen-judge.mjs'
|
|
13
13
|
import {
|
|
14
14
|
oneShotMessages,
|
|
15
15
|
sectionMessages,
|
|
@@ -455,7 +455,7 @@ export function generateDoc(file, { model = DEFAULT_LOCAL_MODEL, threshold = QUA
|
|
|
455
455
|
let judge = null
|
|
456
456
|
if (JUDGE_ENABLED && score >= threshold) {
|
|
457
457
|
try {
|
|
458
|
-
judge = judgeDoc(src, r.md)
|
|
458
|
+
judge = { ...judgeDoc(src, r.md), model: JUDGE_MODEL }
|
|
459
459
|
if (judgeFailsDoc(judge)) issues = [...issues, `judge:inaccurate:${judge.confidence}`]
|
|
460
460
|
} catch (error) {
|
|
461
461
|
issues = [...issues, `judge:error: ${error.message.slice(0, 80)}`]
|
|
@@ -234,7 +234,7 @@ function toRelSource(root, candidate) {
|
|
|
234
234
|
/**
|
|
235
235
|
* `doc-files check --degraded` — інформаційний список свіжих за CRC док зі
|
|
236
236
|
* `score < QUALITY_THRESHOLD` (локальний конвеєр не дотягнув; ADR 260610-2228).
|
|
237
|
-
* Не блокує (exit 0): degraded —
|
|
237
|
+
* Не блокує (exit 0): degraded — борг, що автоматично доретраюється наступним `gen`, а не гейт.
|
|
238
238
|
* @param {string} root абсолютний корінь
|
|
239
239
|
* @returns {number} exit-код: завжди 0
|
|
240
240
|
*/
|
|
@@ -256,7 +256,7 @@ function runDegradedReport(root) {
|
|
|
256
256
|
})
|
|
257
257
|
.join('\n')
|
|
258
258
|
console.log(
|
|
259
|
-
`⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→
|
|
259
|
+
`⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ доретраюються автоматично наступним \`gen\` (один раз на версію джерела).`
|
|
260
260
|
)
|
|
261
261
|
return 0
|
|
262
262
|
}
|
|
@@ -98,13 +98,14 @@ function collectStale(files, cwd) {
|
|
|
98
98
|
* Крок агрегатора lint для doc-files (opportunistic LLM-fix tier).
|
|
99
99
|
* @param {string[] | undefined} files quick: лише ці файли; undefined: весь репозиторій
|
|
100
100
|
* @param {string} [cwd] корінь репо
|
|
101
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly: лише детект (CI/hook)
|
|
101
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly: лише детект (CI/hook);
|
|
102
|
+
* llmFix: opt-in opportunistic-генерація (з `meta.json: llmFix:true`) — без нього detect-only
|
|
102
103
|
* @returns {Promise<number>} 0 — доки свіжі; 1 — є застарілі (детект, fix пропущено чи помилка генерації)
|
|
103
104
|
*/
|
|
104
|
-
export async function lint(files, cwd = process.cwd(), { readOnly = false } = {}) {
|
|
105
|
+
export async function lint(files, cwd = process.cwd(), { readOnly = false, llmFix = false } = {}) {
|
|
105
106
|
const stale = collectStale(files, cwd)
|
|
106
107
|
if (stale.length === 0) return 0
|
|
107
|
-
if (readOnly) return reportStale(stale)
|
|
108
|
+
if (readOnly || !llmFix) return reportStale(stale)
|
|
108
109
|
|
|
109
110
|
// fix-by-default: opportunistic-генерація через спільне ядро (preflight omlx →
|
|
110
111
|
// батч із circuit-breaker'ом). omlx недоступний → runGenerationBatch друкує причину
|
|
@@ -108,15 +108,23 @@ export async function runLint(opts = {}) {
|
|
|
108
108
|
return 0
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const
|
|
111
|
+
const metaById = readAllMeta(rulesDir)
|
|
112
|
+
const ids = selectLintRules(metaById, full)
|
|
112
113
|
for (const id of ids) {
|
|
113
114
|
const lintPath = join(rulesDir, id, 'js', 'lint.mjs')
|
|
114
115
|
if (!existsSync(lintPath)) {
|
|
115
116
|
log(`⚠️ lint: правило ${id} має lint-фазу, але немає js/lint.mjs — пропускаю.\n`)
|
|
116
117
|
continue
|
|
117
118
|
}
|
|
119
|
+
// lintPath = join(rulesDir, id, …) — суто package-internal (rulesDir пакета + id зі
|
|
120
|
+
// selectLintRules за власним meta), не зовнішній вхід → ін'єкції немає.
|
|
121
|
+
// eslint-disable-next-line no-unsanitized/method
|
|
118
122
|
const mod = await import(lintPath)
|
|
119
|
-
|
|
123
|
+
// `llmFix` (opt-in opportunistic LLM-fix, спека 2026-06-15): лише правила з
|
|
124
|
+
// `meta.json: llmFix:true` отримують fix-сходинку; решта — detect-only. Це й
|
|
125
|
+
// забезпечує safety-тріаж (логічні лінтери не вмикають LLM-fix випадково).
|
|
126
|
+
const llmFix = metaById[id]?.llmFix === true
|
|
127
|
+
const code = await mod.lint(changed, cwd, { readOnly, llmFix })
|
|
120
128
|
if (code !== 0) return code
|
|
121
129
|
}
|
|
122
130
|
|
package/rules/text/js/lint.mjs
CHANGED
|
@@ -6,9 +6,10 @@ import { runLintTextCli } from '../lint/lint.mjs'
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {string[] | undefined} _files ігнорується (whole-repo аналіз)
|
|
8
8
|
* @param {string} [_cwd] корінь (ігнорується — CLI працює від process.cwd())
|
|
9
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій)
|
|
9
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу (нуль мутацій);
|
|
10
|
+
* llmFix → opt-in omlx-класифікація cspell (з `meta.json: llmFix:true`)
|
|
10
11
|
* @returns {Promise<number>} exit code
|
|
11
12
|
*/
|
|
12
13
|
export function lint(_files, _cwd, opts = {}) {
|
|
13
|
-
return runLintTextCli({ readOnly: opts.readOnly === true })
|
|
14
|
+
return runLintTextCli({ readOnly: opts.readOnly === true, llmFix: opts.llmFix === true })
|
|
14
15
|
}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cspell у ланцюжку lint-text із omlx
|
|
2
|
+
* cspell у ланцюжку lint-text із omlx-класифікацією (нова схема — спека
|
|
3
|
+
* docs/specs/2026-06-15-opportunistic-llm-fix-tier.md).
|
|
3
4
|
*
|
|
4
|
-
* cspell не має нативного `--fix
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* cspell не має нативного `--fix`, а емпірично ~90% «Unknown word» на укр+тех-репо —
|
|
6
|
+
* валідні терміни, не одруки (вимір: 1406 знахідок / 292 файли, ~90% словникові
|
|
7
|
+
* кандидати). Тому fix-режим НЕ переписує файли (старий whole-file `llmLintFix`
|
|
8
|
+
* таймаутив/парс-фейлив — bounded-output принцип спеки), а **класифікує** знахідки:
|
|
9
|
+
* detect → omlx-класифікація distinct-слів (bounded JSON-вихід) → валідні слова
|
|
10
|
+
* авто-дописуються у `.cspell.json#words` (sorted/dedup, видно в diff) → ймовірні
|
|
11
|
+
* одруки лишаються списком на рев'ю (НЕ авто-виправляються — апплай небезпечний) →
|
|
12
|
+
* re-detect. read-only: лише детект (нуль мутацій).
|
|
13
|
+
*
|
|
14
|
+
* Гейт: валідні слова після дописування у словник зникають; нерозкласифіковані та
|
|
15
|
+
* typo лишаються → cspell повертає !=0 → exit 1 (людина доправляє одруки вручну).
|
|
8
16
|
*/
|
|
9
17
|
import { spawnSync } from 'node:child_process'
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { join } from 'node:path'
|
|
10
20
|
|
|
11
21
|
import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs'
|
|
12
|
-
import {
|
|
22
|
+
import { callLlm, preflightLocalModel } from '../../../lib/llm.mjs'
|
|
23
|
+
|
|
24
|
+
/** Слово у рядку cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */
|
|
25
|
+
const UNKNOWN_WORD_RE = /Unknown word \(([^)]+)\)/u
|
|
26
|
+
/** Максимум distinct-слів під класифікацію за прогін (без тихого обрізання — логуємо надлишок). */
|
|
27
|
+
const MAX_CLASSIFY_WORDS = 80
|
|
13
28
|
|
|
14
|
-
/**
|
|
15
|
-
const
|
|
16
|
-
/** Максимум файлів під omlx-фікс за прогін (без тихого обрізання — логуємо надлишок). */
|
|
17
|
-
const MAX_FIX_FILES = 25
|
|
29
|
+
/** Локальна fix-модель (рішення: єдиний knob `N_LOCAL_MIN_MODEL`). */
|
|
30
|
+
const fixModel = () => process.env.N_LOCAL_MIN_MODEL || ''
|
|
18
31
|
|
|
19
32
|
/**
|
|
20
33
|
* Запускає `cspell .` із захопленням виводу.
|
|
@@ -28,39 +41,82 @@ function detectCspell(cwd, bin) {
|
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
/**
|
|
31
|
-
*
|
|
44
|
+
* Унікальні «Unknown word» зі stdout cspell.
|
|
32
45
|
* @param {string} out вивід cspell
|
|
33
|
-
* @returns {
|
|
46
|
+
* @returns {string[]} distinct-слова у порядку першої появи
|
|
34
47
|
*/
|
|
35
|
-
export function
|
|
36
|
-
|
|
37
|
-
const byFile = new Map()
|
|
48
|
+
export function unknownWords(out) {
|
|
49
|
+
const set = new Set()
|
|
38
50
|
for (const line of out.split('\n')) {
|
|
39
|
-
const m =
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
const m = UNKNOWN_WORD_RE.exec(line)
|
|
52
|
+
if (m) set.add(m[1])
|
|
53
|
+
}
|
|
54
|
+
return [...set]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Промпт класифікації: для укр+тех-репо bias у «valid» (додати валідне слово безпечно,
|
|
59
|
+
* «виправити» валідне — шкода). Вихід bounded — JSON-масив вердиктів.
|
|
60
|
+
* @param {string[]} words distinct-слова
|
|
61
|
+
* @returns {string} prompt
|
|
62
|
+
*/
|
|
63
|
+
function classifyPrompt(words) {
|
|
64
|
+
return [
|
|
65
|
+
'You triage cspell "unknown word" findings for a Ukrainian + technical codebase.',
|
|
66
|
+
'For each word decide:',
|
|
67
|
+
'- "valid": correct technical term, identifier, abbreviation, transliteration, jargon, or intentional Ukrainian word → dictionary candidate.',
|
|
68
|
+
'- "typo": a genuine misspelling of a real word.',
|
|
69
|
+
'Default to "valid" when unsure (adding a real word to the dictionary is safe; "fixing" a valid word is harmful).',
|
|
70
|
+
'Return ONLY a JSON array, no markdown fences: [{"w":"<word>","verdict":"valid"|"typo","fix":"<correction or null>"}]',
|
|
71
|
+
'Words:',
|
|
72
|
+
...words.map(w => `- ${w}`)
|
|
73
|
+
].join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Витягує JSON-масив із відповіді моделі (бере від першої «[» до останньої «]» — зрізає прозу й markdown-обрамлення).
|
|
78
|
+
* @param {string} text відповідь
|
|
79
|
+
* @returns {Array<{w:string, verdict:string, fix:string|null}>|null} вердикти або null
|
|
80
|
+
*/
|
|
81
|
+
function parseClassify(text) {
|
|
82
|
+
const start = text.indexOf('[')
|
|
83
|
+
const end = text.lastIndexOf(']')
|
|
84
|
+
if (start === -1 || end <= start) return null
|
|
85
|
+
try {
|
|
86
|
+
const arr = JSON.parse(text.slice(start, end + 1))
|
|
87
|
+
return Array.isArray(arr) ? arr : null
|
|
88
|
+
} catch {
|
|
89
|
+
return null
|
|
44
90
|
}
|
|
45
|
-
return byFile
|
|
46
91
|
}
|
|
47
92
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Дописує слова у `.cspell.json#words` (sorted/dedup) — видно в git diff для рев'ю.
|
|
95
|
+
* @param {string} cwd корінь
|
|
96
|
+
* @param {string[]} words валідні слова
|
|
97
|
+
* @returns {number} к-сть фактично доданих (нових) слів
|
|
98
|
+
*/
|
|
99
|
+
export function appendWordsToDict(cwd, words) {
|
|
100
|
+
const cfgPath = join(cwd, '.cspell.json')
|
|
101
|
+
if (words.length === 0 || !existsSync(cfgPath)) return 0
|
|
102
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
|
|
103
|
+
const set = new Set(cfg.words)
|
|
104
|
+
const before = set.size
|
|
105
|
+
for (const w of words) set.add(w)
|
|
106
|
+
if (set.size === before) return 0
|
|
107
|
+
cfg.words = [...set].toSorted((a, b) => a.localeCompare(b))
|
|
108
|
+
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`)
|
|
109
|
+
return set.size - before
|
|
110
|
+
}
|
|
56
111
|
|
|
57
112
|
/**
|
|
58
|
-
* cspell-крок lint-text
|
|
113
|
+
* cspell-крок lint-text: класифікація → словник (нова схема).
|
|
59
114
|
* @param {string} [cwd] корінь
|
|
60
115
|
* @param {boolean} [readOnly] true → лише детект (нуль мутацій)
|
|
116
|
+
* @param {boolean} [llmFix] opt-in omlx-класифікація (з `meta.json: llmFix:true`); без нього — лише детект
|
|
61
117
|
* @returns {number} 0 — чисто; 1 — лишились знахідки / помилка середовища
|
|
62
118
|
*/
|
|
63
|
-
export function runCspellText(cwd = process.cwd(), readOnly = false) {
|
|
119
|
+
export function runCspellText(cwd = process.cwd(), readOnly = false, llmFix = false) {
|
|
64
120
|
const bin = resolveCmd('npx')
|
|
65
121
|
if (!bin) {
|
|
66
122
|
process.stderr.write('❌ npx не знайдено в PATH (cspell).\n')
|
|
@@ -69,35 +125,55 @@ export function runCspellText(cwd = process.cwd(), readOnly = false) {
|
|
|
69
125
|
|
|
70
126
|
const first = detectCspell(cwd, bin)
|
|
71
127
|
if (first.code === 0) return 0
|
|
72
|
-
if (readOnly) {
|
|
128
|
+
if (readOnly || !llmFix) {
|
|
129
|
+
process.stdout.write(first.out)
|
|
130
|
+
return first.code
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fix-режим: класифікація знахідок (bounded JSON-вихід), валідні → у словник.
|
|
134
|
+
const model = fixModel()
|
|
135
|
+
const problem = preflightLocalModel(model)
|
|
136
|
+
if (problem) {
|
|
137
|
+
process.stdout.write(`⚠️ cspell: класифікацію пропущено (${problem})\n`)
|
|
73
138
|
process.stdout.write(first.out)
|
|
74
139
|
return first.code
|
|
75
140
|
}
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
142
|
+
const words = unknownWords(first.out)
|
|
143
|
+
const batch = words.slice(0, MAX_CLASSIFY_WORDS)
|
|
144
|
+
if (words.length > MAX_CLASSIFY_WORDS) {
|
|
145
|
+
process.stdout.write(`ℹ️ cspell: класифікація перших ${MAX_CLASSIFY_WORDS}/${words.length} слів (решта — наступний прогін)\n`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let text
|
|
149
|
+
try {
|
|
150
|
+
text = callLlm([{ role: 'user', content: classifyPrompt(batch) }], model, { caller: 'cspell-classify', maxTokens: 4000 })
|
|
151
|
+
} catch (error) {
|
|
152
|
+
process.stdout.write(`⚠️ cspell: omlx-класифікація впала (${error.message}) — без авто-словника\n`)
|
|
81
153
|
process.stdout.write(first.out)
|
|
82
154
|
return first.code
|
|
83
155
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
156
|
+
|
|
157
|
+
const parsed = parseClassify(text)
|
|
158
|
+
if (!parsed) {
|
|
159
|
+
process.stdout.write('⚠️ cspell: не вдалося розпарсити класифікацію — без авто-словника\n')
|
|
160
|
+
process.stdout.write(first.out)
|
|
161
|
+
return first.code
|
|
87
162
|
}
|
|
88
163
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
const valid = parsed.filter(x => x.verdict === 'valid' && typeof x.w === 'string').map(x => x.w)
|
|
165
|
+
const typos = parsed.filter(x => x.verdict === 'typo' && typeof x.w === 'string')
|
|
166
|
+
const added = appendWordsToDict(cwd, valid)
|
|
167
|
+
process.stdout.write(`✓ cspell: +${added} валідних слів у .cspell.json (з ${valid.length} класифікованих)\n`)
|
|
168
|
+
if (typos.length > 0) {
|
|
169
|
+
process.stdout.write("⚠️ cspell: ймовірні одруки на рев'ю (НЕ виправлено авто):\n")
|
|
170
|
+
for (const t of typos) {
|
|
171
|
+
const arrow = t.fix ? ` → ${t.fix}` : ''
|
|
172
|
+
process.stdout.write(` - ${t.w}${arrow}\n`)
|
|
173
|
+
}
|
|
98
174
|
}
|
|
99
175
|
|
|
100
|
-
// Re-detect:
|
|
176
|
+
// Re-detect: валідні тепер у словнику → лишаються одруки/нерозкласифіковане → exit 1.
|
|
101
177
|
const second = detectCspell(cwd, bin)
|
|
102
178
|
if (second.code !== 0) process.stdout.write(second.out)
|
|
103
179
|
return second.code
|
package/rules/text/lint/lint.mjs
CHANGED
|
@@ -97,9 +97,10 @@ function preflight(dep) {
|
|
|
97
97
|
/**
|
|
98
98
|
* Внутрішні кроки `lint-text` без локу.
|
|
99
99
|
* @param {boolean} [readOnly] true → лише детект без авто-фіксу (нуль мутацій — CI/pre-commit)
|
|
100
|
+
* @param {boolean} [llmFix] opt-in omlx-класифікація cspell (інші кроки фіксяться детерміновано за readOnly)
|
|
100
101
|
* @returns {number} 0 — все OK, інакше — код першого кроку, що впав
|
|
101
102
|
*/
|
|
102
|
-
function runLintTextSteps(readOnly = false) {
|
|
103
|
+
function runLintTextSteps(readOnly = false, llmFix = false) {
|
|
103
104
|
// Auto-install: throws on failure → propagates as exit 1 from runStandardLint
|
|
104
105
|
ensureTool('shellcheck')
|
|
105
106
|
ensureTool('dotenv-linter')
|
|
@@ -107,8 +108,8 @@ function runLintTextSteps(readOnly = false) {
|
|
|
107
108
|
// patch потрібен лише для авто-фіксу shellcheck; у read-only пропускаємо preflight.
|
|
108
109
|
if (!readOnly && !preflight(PATCH_PREFLIGHT)) return 1
|
|
109
110
|
|
|
110
|
-
console.log(`\n▶ cspell (${readOnly
|
|
111
|
-
const cspellCode = runCspellText(process.cwd(), readOnly)
|
|
111
|
+
console.log(`\n▶ cspell (${!readOnly && llmFix ? 'omlx-класифікація + словник + перевірка' : 'перевірка'})`)
|
|
112
|
+
const cspellCode = runCspellText(process.cwd(), readOnly, llmFix)
|
|
112
113
|
if (cspellCode !== 0) return cspellCode
|
|
113
114
|
|
|
114
115
|
console.log(`\n▶ shellcheck (${readOnly ? 'перевірка' : 'авто-фікс + фінальна перевірка'} *.sh)`)
|
|
@@ -129,8 +130,9 @@ function runLintTextSteps(readOnly = false) {
|
|
|
129
130
|
|
|
130
131
|
/**
|
|
131
132
|
* Публічна CLI-форма: серіалізує через `withLock('lint-text')` + дедуп за станом git-дерева.
|
|
132
|
-
* @param {{ readOnly?: boolean }} [opts] readOnly → детект без
|
|
133
|
+
* @param {{ readOnly?: boolean, llmFix?: boolean }} [opts] readOnly → детект без авто-фіксу;
|
|
134
|
+
* llmFix → omlx-класифікація cspell (opt-in із `meta.json: llmFix:true`)
|
|
133
135
|
* @returns {Promise<number>} код виходу
|
|
134
136
|
*/
|
|
135
137
|
export const runLintTextCli = (opts = {}) =>
|
|
136
|
-
runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true))
|
|
138
|
+
runStandardLint(import.meta.dirname, () => runLintTextSteps(opts.readOnly === true, opts.llmFix === true))
|
package/rules/text/meta.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{ "auto": "завжди", "lint": "per-file" }
|
|
1
|
+
{ "auto": "завжди", "lint": "per-file", "llmFix": true }
|