@nitra/cursor
Advanced tools
+27
-0
@@ -171,1 +171,28 @@ /** | ||
| } | ||
| /** | ||
| * Спільний preflight локальної fix/gen-моделі (opportunistic LLM-fix tier, спека | ||
| * docs/specs/2026-06-15-opportunistic-llm-fix-tier.md): чи можна звати модель зараз. | ||
| * Health-check лише для omlx-бекенду (pi/cloud — завжди дозволено). Використовують | ||
| * doc-files (генерація) і text/cspell (класифікація) — fast-skip замість приречених викликів. | ||
| * @param {string} model model-id (зазвичай `N_LOCAL_MIN_MODEL`) | ||
| * @returns {string|null} людинозрозумілий текст проблеми, або null якщо можна викликати | ||
| */ | ||
| export function preflightLocalModel(model) { | ||
| if (!model) { | ||
| return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.' | ||
| } | ||
| if (pickBackend(model) !== 'omlx') return null | ||
| const hc = omlxHealthCheck({ model }) | ||
| if (hc.ok) return null | ||
| if (hc.reason === 'memory-guard') { | ||
| return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}` | ||
| } | ||
| if (hc.reason === 'down') { | ||
| return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}` | ||
| } | ||
| if (hc.reason === 'auth') { | ||
| return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}` | ||
| } | ||
| return `omlx помилка: ${hc.detail}` | ||
| } |
+1
-1
| { | ||
| "name": "@nitra/cursor", | ||
| "version": "11.1.0", | ||
| "version": "11.2.0", | ||
| "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -13,4 +13,4 @@ /** | ||
| * та `issues` (коди проблем). CRC при цьому свіжий — Stop-гейт не блокує задачі через | ||
| * слабкість моделі; борг видимий через `check --degraded` і адресно перегенеровується | ||
| * через `gen --retry-degraded`. | ||
| * слабкість моделі; борг видимий через `check --degraded` і автоматично доретраюється | ||
| * наступним `gen` (рівно один раз на версію джерела — далі `retried: true` у frontmatter). | ||
| * | ||
@@ -58,2 +58,4 @@ * Frontmatter — єдиний дозволений виняток із правила «чистий Markdown без HTML»: | ||
| const ISSUES_RE = /^[ \t]{0,8}issues:[ \t]{0,8}(.+)$/mu | ||
| const RETRIED_RE = /^[ \t]{0,8}retried:[ \t]{0,8}true[ \t]*$/mu | ||
| const JUDGE_MODEL_RE = /^[ \t]{0,8}judgeModel:[ \t]{0,8}(.+)$/mu | ||
| const LEADING_NEWLINES_RE = /^\n+/u | ||
@@ -67,3 +69,3 @@ const ISSUE_CODE_TAIL_RE = /[,:]$/u | ||
| * @param {string} md вміст md-файлу | ||
| * @returns {{ data: { source: string|null, crc: string|null, model: string|null, score: number|null, issues: string[] }|null, body: string }} метадані + тіло без frontmatter | ||
| * @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 | ||
| */ | ||
@@ -87,3 +89,5 @@ export function parseDocFrontmatter(md) { | ||
| .filter(Boolean) | ||
| : [] | ||
| : [], | ||
| retried: RETRIED_RE.test(block), | ||
| judgeModel: block.match(JUDGE_MODEL_RE)?.[1].trim() ?? null | ||
| }, | ||
@@ -114,3 +118,3 @@ body: md.slice(match[0].length) | ||
| * @param {string} crc CRC32 джерела у hex | ||
| * @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки; null — без полів якості | ||
| * @param {{ score: number, issues?: string[], retried?: boolean, judge?: {model?: string} }|null} [quality] det-оцінка доки (+ опц. `retried`-маркер і `judge.model` хмарного судді); null — без полів якості | ||
| * @param {string|null} [model] повний id моделі-генератора; null — без поля `model` | ||
@@ -126,2 +130,4 @@ * @returns {string} рядок `---\ndocgen:\n source: …\n crc: …[\n model: …][\n score: …][\n issues: …]\n---\n` | ||
| if (codes.length > 0) lines.push(`issues: ${codes.join(',')}`) | ||
| if (quality.retried) lines.push('retried: true') | ||
| if (quality.judge && quality.judge.model) lines.push(`judgeModel: ${quality.judge.model}`) | ||
| } | ||
@@ -137,3 +143,3 @@ const indented = lines.map(l => ' ' + l).join('\n') | ||
| * @param {string} crc CRC32 джерела у hex | ||
| * @param {{ score: number, issues?: string[] }|null} [quality] det-оцінка доки | ||
| * @param {{ score: number, issues?: string[], retried?: boolean, judge?: {model?: string} }|null} [quality] det-оцінка доки (+ опц. `retried`-маркер і `judge.model` хмарного судді) | ||
| * @param {string|null} [model] повний id моделі-генератора; null — без поля `model` | ||
@@ -160,8 +166,13 @@ * @returns {string} md зі свіжим frontmatter | ||
| * @param {string} docAbsPath абсолютний шлях md-доки | ||
| * @returns {{ score: number|null, issues: string[] }} `score:null` — доки немає або поле відсутнє | ||
| * @returns {{ score: number|null, issues: string[], retried: boolean, judgeModel: string|null }} `score:null` — доки немає або поле відсутнє; `retried` — чи док уже доретраювали при цьому CRC; `judgeModel` — хмарна модель-суддя, що позначила док (або null) | ||
| */ | ||
| export function readDocQuality(docAbsPath) { | ||
| if (!existsSync(docAbsPath)) return { score: null, issues: [] } | ||
| if (!existsSync(docAbsPath)) return { score: null, issues: [], retried: false, judgeModel: null } | ||
| const data = parseDocFrontmatter(readFileSync(docAbsPath, 'utf8')).data | ||
| return { score: data?.score ?? null, issues: data?.issues ?? [] } | ||
| return { | ||
| score: data?.score ?? null, | ||
| issues: data?.issues ?? [], | ||
| retried: data?.retried ?? false, | ||
| judgeModel: data?.judgeModel ?? null | ||
| } | ||
| } | ||
@@ -168,0 +179,0 @@ |
@@ -7,4 +7,4 @@ /** | ||
| * локальний: жодних cloud-ескалацій; якщо det-score нижче порогу — дока все | ||
| * одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а | ||
| * `gen --retry-degraded` адресно переганяє лише такі доки пізніше. | ||
| * одно пишеться з degraded-маркером (`score`/`issues` у frontmatter), а наступний | ||
| * `gen` автоматично доретраює такі доки (один раз на версію джерела — далі `retried:true`). | ||
| * | ||
@@ -18,3 +18,3 @@ * Перед масовим прогоном — health-check omlx: memory-guard зайнятої 8GB машини | ||
| import { isRunAsCli } from '../../../scripts/cli-entry.mjs' | ||
| import { omlxHealthCheck, pickBackend, classifyOmlxError } from '../../../lib/llm.mjs' | ||
| import { classifyOmlxError, preflightLocalModel } from '../../../lib/llm.mjs' | ||
| import { generateDoc, DEFAULT_LOCAL_MODEL } from './docgen-gen.mjs' | ||
@@ -27,3 +27,3 @@ import { crc32, stampDoc, readDocQuality, readDocModel, QUALITY_THRESHOLD } from './docgen-crc.mjs' | ||
| * @param {string[]} argv аргументи | ||
| * @returns {{ from: number, limit: number, overwrite: boolean, retryDegraded: boolean }} зріз і режими | ||
| * @returns {{ from: number, limit: number, overwrite: boolean }} зріз і режими | ||
| */ | ||
@@ -38,4 +38,3 @@ function parseGenArgs(argv) { | ||
| limit: num('--limit', Infinity), | ||
| overwrite: argv.includes('--overwrite'), | ||
| retryDegraded: argv.includes('--retry-degraded') | ||
| overwrite: argv.includes('--overwrite') | ||
| } | ||
@@ -45,55 +44,29 @@ } | ||
| /** | ||
| * Цілі генерації за режимом: | ||
| * - default → застарілі (stale); | ||
| * - `--overwrite` → усі; | ||
| * - `--retry-degraded` → свіжі за CRC, але зі `score < QUALITY_THRESHOLD`. | ||
| * Цілі генерації: | ||
| * - default → застарілі (stale) АБО degraded-доки, які ще не доретраювали при цьому CRC; | ||
| * - `--overwrite` → усі. | ||
| * Degraded-док отримує рівно ОДИН доретрай на версію джерела: після невдалого доретраю | ||
| * (лишився degraded) штампується `retried: true` і його більше не чіпають до зміни джерела | ||
| * (нова версія → CRC-mismatch → stale → лічильник скидається). Конвеєр сходиться без прапора. | ||
| * @param {string} root абсолютний корінь | ||
| * @param {Array<object>} all результат scanForDocFiles | ||
| * @param {{ overwrite: boolean, retryDegraded: boolean }} mode режими | ||
| * @param {{ overwrite: boolean }} mode режими | ||
| * @returns {Array<object>} відфільтровані цілі | ||
| */ | ||
| function selectTargets(root, all, { overwrite, retryDegraded }) { | ||
| if (retryDegraded) { | ||
| return all.filter(f => { | ||
| if (f.stale) return false | ||
| const { score } = readDocQuality(join(root, f.docPath)) | ||
| return score !== null && score < QUALITY_THRESHOLD | ||
| }) | ||
| } | ||
| export function selectTargets(root, all, { overwrite }) { | ||
| if (overwrite) return all | ||
| return all.filter(f => f.stale) | ||
| return all.filter(f => { | ||
| if (f.stale) return true | ||
| const { score, retried } = readDocQuality(join(root, f.docPath)) | ||
| return score !== null && score < QUALITY_THRESHOLD && !retried | ||
| }) | ||
| } | ||
| /** | ||
| * Preflight локального бекенда: для omlx-моделі — мінімальний chat-виклик. | ||
| * @returns {string|null} текст фатальної проблеми або null якщо можна генерувати | ||
| */ | ||
| export function preflightProblem() { | ||
| if (!DEFAULT_LOCAL_MODEL) { | ||
| return 'модель не задано. Вистав N_LOCAL_MIN_MODEL (напр. omlx/mlx-community--gemma-4-e4b-it-OptiQ-4bit) і повтори.' | ||
| } | ||
| if (pickBackend(DEFAULT_LOCAL_MODEL) !== 'omlx') return null | ||
| const hc = omlxHealthCheck({ model: DEFAULT_LOCAL_MODEL }) | ||
| if (hc.ok) return null | ||
| if (hc.reason === 'memory-guard') { | ||
| return `omlx memory-guard: модель не влазить у динамічну стелю пам'яті (машина зайнята).\n Звільни пам'ять або повтори прогін пізніше.\n ${hc.detail}` | ||
| } | ||
| if (hc.reason === 'down') { | ||
| return `omlx-сервер не відповідає. Запусти \`omlx serve\` і повтори.\n ${hc.detail}` | ||
| } | ||
| if (hc.reason === 'auth') { | ||
| return `omlx вимагає API-ключ. Вистав N_CURSOR_OMLX_KEY (auth.api_key з ~/.omlx/settings.json).\n ${hc.detail}` | ||
| } | ||
| return `omlx помилка: ${hc.detail}` | ||
| } | ||
| /** | ||
| * Текст-суфікс режиму для прогрес-рядка. | ||
| * @param {{ overwrite: boolean, retryDegraded: boolean }} mode режими | ||
| * @returns {string} ` (--overwrite)` / ` (--retry-degraded)` / порожній рядок | ||
| * @param {{ overwrite: boolean }} mode режими | ||
| * @returns {string} ` (--overwrite)` або порожній рядок | ||
| */ | ||
| function modeSuffix({ overwrite, retryDegraded }) { | ||
| if (overwrite) return ' (--overwrite)' | ||
| if (retryDegraded) return ' (--retry-degraded)' | ||
| return '' | ||
| function modeSuffix({ overwrite }) { | ||
| return overwrite ? ' (--overwrite)' : '' | ||
| } | ||
@@ -154,4 +127,9 @@ | ||
| mkdirSync(dirname(docAbs), { recursive: true }) | ||
| // retried: НЕ stale (отже це доретрай при тому ж CRC) і лишився degraded → штампуємо, | ||
| // щоб наступні `gen` його не чіпали до зміни джерела (сходимість без прапора). | ||
| const retried = !file.stale && result.degraded | ||
| const quality = | ||
| result.score === null ? null : { score: result.score, issues: result.degraded ? result.issues : [] } | ||
| result.score === null | ||
| ? null | ||
| : { score: result.score, issues: result.degraded ? result.issues : [], retried, judge: result.judge } | ||
| writeFileSync(docAbs, stampDoc(result.md, file.sourcePath, crc, quality, result.model)) | ||
@@ -198,3 +176,3 @@ stats.ok++ | ||
| if (stats.degraded > 0) { | ||
| console.log(`Degraded-доки перегенеровуються пізніше: npx @nitra/cursor fix-doc-files --retry-degraded`) | ||
| console.log('Degraded-доки автоматично доретраюються наступним `gen` (один раз на версію джерела).') | ||
| } | ||
@@ -210,13 +188,9 @@ } | ||
| const root = resolveRoot(argv) | ||
| const { from, limit, overwrite, retryDegraded } = parseGenArgs(argv) | ||
| const { from, limit, overwrite } = parseGenArgs(argv) | ||
| const all = scanForDocFiles(root) | ||
| const targets = selectTargets(root, all, { overwrite, retryDegraded }).slice(from, from + limit) | ||
| const targets = selectTargets(root, all, { overwrite }).slice(from, from + limit) | ||
| if (targets.length === 0) { | ||
| console.log( | ||
| retryDegraded | ||
| ? '✓ doc-files: degraded-док немає. Нічого переганяти.' | ||
| : '✓ doc-files: усі файлові доки свіжі. Нічого генерувати.' | ||
| ) | ||
| console.log('✓ doc-files: усі файлові доки свіжі й не-degraded. Нічого генерувати.') | ||
| return 0 | ||
@@ -226,3 +200,3 @@ } | ||
| return runGenerationBatch(targets, root, { | ||
| headline: `📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite, retryDegraded })}` | ||
| headline: `📋 doc-files: до генерації ${targets.length} файл(ів)${modeSuffix({ overwrite })}` | ||
| }) | ||
@@ -242,3 +216,3 @@ } | ||
| export async function runGenerationBatch(targets, root, { headline } = {}) { | ||
| const problem = preflightProblem() | ||
| const problem = preflightLocalModel(DEFAULT_LOCAL_MODEL) | ||
| if (problem) { | ||
@@ -245,0 +219,0 @@ console.error(`✗ fix-doc-files: ${problem}`) |
@@ -12,3 +12,3 @@ /** @see ./docs/docgen-gen.md */ | ||
| import { QUALITY_THRESHOLD } from './docgen-crc.mjs' | ||
| import { JUDGE_ENABLED, judgeDoc, judgeFailsDoc } from './docgen-judge.mjs' | ||
| import { JUDGE_ENABLED, JUDGE_MODEL, judgeDoc, judgeFailsDoc } from './docgen-judge.mjs' | ||
| import { | ||
@@ -459,3 +459,3 @@ oneShotMessages, | ||
| try { | ||
| judge = judgeDoc(src, r.md) | ||
| judge = { ...judgeDoc(src, r.md), model: JUDGE_MODEL } | ||
| if (judgeFailsDoc(judge)) issues = [...issues, `judge:inaccurate:${judge.confidence}`] | ||
@@ -462,0 +462,0 @@ } catch (error) { |
@@ -237,3 +237,3 @@ /** @see ./docs/docgen-scan.md */ | ||
| * `score < QUALITY_THRESHOLD` (локальний конвеєр не дотягнув; ADR 260610-2228). | ||
| * Не блокує (exit 0): degraded — борг для `gen --retry-degraded`, а не гейт. | ||
| * Не блокує (exit 0): degraded — борг, що автоматично доретраюється наступним `gen`, а не гейт. | ||
| * @param {string} root абсолютний корінь | ||
@@ -260,3 +260,3 @@ * @returns {number} exit-код: завжди 0 | ||
| console.log( | ||
| `⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ перегенеруй: npx @nitra/cursor fix-doc-files --retry-degraded` | ||
| `⚠ doc-files: degraded-док ${degraded.length} (score < ${QUALITY_THRESHOLD}):\n${list}\n→ доретраюються автоматично наступним \`gen\` (один раз на версію джерела).` | ||
| ) | ||
@@ -263,0 +263,0 @@ return 0 |
| /** | ||
| * cspell у ланцюжку lint-text із omlx-автофіксом (point 4 спеки). | ||
| * cspell у ланцюжку lint-text із omlx-класифікацією (нова схема — спека | ||
| * docs/specs/2026-06-15-opportunistic-llm-fix-tier.md). | ||
| * | ||
| * cspell не має нативного `--fix`. У fix-режимі: детект (захоплення виводу) → групування | ||
| * знахідок по файлах → per-file omlx-фікс справжніх одруків (`llmLintFix`) → re-detect. | ||
| * У read-only: лише детект (нуль мутацій). Валідні терміни omlx лишає — їх ловить повторний | ||
| * cspell (далі — у словник `@nitra/cspell-dict`). | ||
| * cspell не має нативного `--fix`, а емпірично ~90% «Unknown word» на укр+тех-репо — | ||
| * валідні терміни, не одруки (вимір: 1406 знахідок / 292 файли, ~90% словникові | ||
| * кандидати). Тому fix-режим НЕ переписує файли (старий whole-file `llmLintFix` | ||
| * таймаутив/парс-фейлив — bounded-output принцип спеки), а **класифікує** знахідки: | ||
| * detect → omlx-класифікація distinct-слів (bounded JSON-вихід) → валідні слова | ||
| * авто-дописуються у `.cspell.json#words` (sorted/dedup, видно в diff) → ймовірні | ||
| * одруки лишаються списком на рев'ю (НЕ авто-виправляються — апплай небезпечний) → | ||
| * re-detect. read-only: лише детект (нуль мутацій). | ||
| * | ||
| * Гейт: валідні слова після дописування у словник зникають; нерозкласифіковані та | ||
| * typo лишаються → cspell повертає !=0 → exit 1 (людина доправляє одруки вручну). | ||
| */ | ||
| import { spawnSync } from 'node:child_process' | ||
| import { existsSync, readFileSync, writeFileSync } from 'node:fs' | ||
| import { join } from 'node:path' | ||
| import { resolveCmd } from '../../../scripts/utils/resolve-cmd.mjs' | ||
| import { llmLintFix } from '../../../scripts/lib/fix/llm-lint-fix.mjs' | ||
| import { callLlm, preflightLocalModel } from '../../../lib/llm.mjs' | ||
| /** Рядок cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */ | ||
| const CSPELL_LINE_RE = /^(.+?):\d+:\d+\s+-\s+Unknown word/u | ||
| /** Максимум файлів під omlx-фікс за прогін (без тихого обрізання — логуємо надлишок). */ | ||
| const MAX_FIX_FILES = 25 | ||
| /** Слово у рядку cspell: `<file>:<line>:<col> - Unknown word (xxx)`. */ | ||
| const UNKNOWN_WORD_RE = /Unknown word \(([^)]+)\)/u | ||
| /** Максимум distinct-слів під класифікацію за прогін (без тихого обрізання — логуємо надлишок). */ | ||
| const MAX_CLASSIFY_WORDS = 80 | ||
| /** Локальна fix-модель (рішення: єдиний knob `N_LOCAL_MIN_MODEL`). */ | ||
| const fixModel = () => process.env.N_LOCAL_MIN_MODEL || '' | ||
| /** | ||
@@ -31,30 +44,72 @@ * Запускає `cspell .` із захопленням виводу. | ||
| /** | ||
| * Групує cspell-знахідки за файлом. | ||
| * Унікальні «Unknown word» зі stdout cspell. | ||
| * @param {string} out вивід cspell | ||
| * @returns {Map<string, string[]>} файл → рядки знахідок | ||
| * @returns {string[]} distinct-слова у порядку першої появи | ||
| */ | ||
| export function groupFindingsByFile(out) { | ||
| /** @type {Map<string, string[]>} */ | ||
| const byFile = new Map() | ||
| export function unknownWords(out) { | ||
| const set = new Set() | ||
| for (const line of out.split('\n')) { | ||
| const m = CSPELL_LINE_RE.exec(line.trim()) | ||
| if (!m) continue | ||
| const file = m[1] | ||
| if (!byFile.has(file)) byFile.set(file, []) | ||
| byFile.get(file).push(line.trim()) | ||
| const m = UNKNOWN_WORD_RE.exec(line) | ||
| if (m) set.add(m[1]) | ||
| } | ||
| return byFile | ||
| return [...set] | ||
| } | ||
| const CSPELL_INSTRUCTION = [ | ||
| 'Correct genuine spelling typos in the file(s).', | ||
| 'Each flagged "Unknown word" is listed below.', | ||
| 'ONLY fix obvious misspellings of real words.', | ||
| 'If a flagged token is a valid identifier, technical term, abbreviation, proper noun, URL,', | ||
| 'or an intentional non-English word, leave it UNCHANGED (it will be added to the dictionary).', | ||
| 'Preserve all code, formatting, and unrelated text exactly.' | ||
| ].join(' ') | ||
| /** | ||
| * Промпт класифікації: для укр+тех-репо bias у «valid» (додати валідне слово безпечно, | ||
| * «виправити» валідне — шкода). Вихід bounded — JSON-масив вердиктів. | ||
| * @param {string[]} words distinct-слова | ||
| * @returns {string} prompt | ||
| */ | ||
| function classifyPrompt(words) { | ||
| return [ | ||
| 'You triage cspell "unknown word" findings for a Ukrainian + technical codebase.', | ||
| 'For each word decide:', | ||
| '- "valid": correct technical term, identifier, abbreviation, transliteration, jargon, or intentional Ukrainian word → dictionary candidate.', | ||
| '- "typo": a genuine misspelling of a real word.', | ||
| 'Default to "valid" when unsure (adding a real word to the dictionary is safe; "fixing" a valid word is harmful).', | ||
| 'Return ONLY a JSON array, no markdown fences: [{"w":"<word>","verdict":"valid"|"typo","fix":"<correction or null>"}]', | ||
| 'Words:', | ||
| ...words.map(w => `- ${w}`) | ||
| ].join('\n') | ||
| } | ||
| /** | ||
| * cspell-крок lint-text з omlx-автофіксом. | ||
| * Витягує JSON-масив із відповіді моделі (бере від першої «[» до останньої «]» — зрізає прозу й markdown-обрамлення). | ||
| * @param {string} text відповідь | ||
| * @returns {Array<{w:string, verdict:string, fix:string|null}>|null} вердикти або null | ||
| */ | ||
| function parseClassify(text) { | ||
| const start = text.indexOf('[') | ||
| const end = text.lastIndexOf(']') | ||
| if (start === -1 || end <= start) return null | ||
| try { | ||
| const arr = JSON.parse(text.slice(start, end + 1)) | ||
| return Array.isArray(arr) ? arr : null | ||
| } catch { | ||
| return null | ||
| } | ||
| } | ||
| /** | ||
| * Дописує слова у `.cspell.json#words` (sorted/dedup) — видно в git diff для рев'ю. | ||
| * @param {string} cwd корінь | ||
| * @param {string[]} words валідні слова | ||
| * @returns {number} к-сть фактично доданих (нових) слів | ||
| */ | ||
| export function appendWordsToDict(cwd, words) { | ||
| const cfgPath = join(cwd, '.cspell.json') | ||
| if (words.length === 0 || !existsSync(cfgPath)) return 0 | ||
| const cfg = JSON.parse(readFileSync(cfgPath, 'utf8')) | ||
| const set = new Set(cfg.words) | ||
| const before = set.size | ||
| for (const w of words) set.add(w) | ||
| if (set.size === before) return 0 | ||
| cfg.words = [...set].toSorted((a, b) => a.localeCompare(b)) | ||
| writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`) | ||
| return set.size - before | ||
| } | ||
| /** | ||
| * cspell-крок lint-text: класифікація → словник (нова схема). | ||
| * @param {string} [cwd] корінь | ||
@@ -78,26 +133,46 @@ * @param {boolean} [readOnly] true → лише детект (нуль мутацій) | ||
| // Fix-режим: omlx по файлах зі справжніми одруками. | ||
| const byFile = groupFindingsByFile(first.out) | ||
| const files = [...byFile.keys()] | ||
| if (files.length === 0) { | ||
| // Fix-режим: класифікація знахідок (bounded JSON-вихід), валідні → у словник. | ||
| const model = fixModel() | ||
| const problem = preflightLocalModel(model) | ||
| if (problem) { | ||
| process.stdout.write(`⚠️ cspell: класифікацію пропущено (${problem})\n`) | ||
| process.stdout.write(first.out) | ||
| return first.code | ||
| } | ||
| const targets = files.slice(0, MAX_FIX_FILES) | ||
| if (files.length > MAX_FIX_FILES) { | ||
| process.stdout.write(`ℹ️ cspell: omlx-фікс перших ${MAX_FIX_FILES}/${files.length} файлів (решта — наступний прогін)\n`) | ||
| const words = unknownWords(first.out) | ||
| const batch = words.slice(0, MAX_CLASSIFY_WORDS) | ||
| if (words.length > MAX_CLASSIFY_WORDS) { | ||
| process.stdout.write(`ℹ️ cspell: класифікація перших ${MAX_CLASSIFY_WORDS}/${words.length} слів (решта — наступний прогін)\n`) | ||
| } | ||
| for (const file of targets) { | ||
| const res = llmLintFix({ | ||
| tool: 'cspell', | ||
| instruction: CSPELL_INSTRUCTION, | ||
| findings: byFile.get(file).join('\n'), | ||
| filePaths: [file], | ||
| projectRoot: cwd | ||
| }) | ||
| process.stdout.write(res.ok ? ` ⚡ cspell omlx-фікс: ${file}\n` : ` ⚠️ cspell omlx-фікс пропущено (${file}): ${res.error}\n`) | ||
| let text | ||
| try { | ||
| text = callLlm([{ role: 'user', content: classifyPrompt(batch) }], model, { caller: 'cspell-classify', maxTokens: 4000 }) | ||
| } catch (error) { | ||
| process.stdout.write(`⚠️ cspell: omlx-класифікація впала (${error.message}) — без авто-словника\n`) | ||
| process.stdout.write(first.out) | ||
| return first.code | ||
| } | ||
| // Re-detect: що лишилось (валідні терміни → у словник). | ||
| const parsed = parseClassify(text) | ||
| if (!parsed) { | ||
| process.stdout.write('⚠️ cspell: не вдалося розпарсити класифікацію — без авто-словника\n') | ||
| process.stdout.write(first.out) | ||
| return first.code | ||
| } | ||
| const valid = parsed.filter(x => x.verdict === 'valid' && typeof x.w === 'string').map(x => x.w) | ||
| const typos = parsed.filter(x => x.verdict === 'typo' && typeof x.w === 'string') | ||
| const added = appendWordsToDict(cwd, valid) | ||
| process.stdout.write(`✓ cspell: +${added} валідних слів у .cspell.json (з ${valid.length} класифікованих)\n`) | ||
| if (typos.length > 0) { | ||
| process.stdout.write("⚠️ cspell: ймовірні одруки на рев'ю (НЕ виправлено авто):\n") | ||
| for (const t of typos) { | ||
| const arrow = t.fix ? ` → ${t.fix}` : '' | ||
| process.stdout.write(` - ${t.w}${arrow}\n`) | ||
| } | ||
| } | ||
| // Re-detect: валідні тепер у словнику → лишаються одруки/нерозкласифіковане → exit 1. | ||
| const second = detectCspell(cwd, bin) | ||
@@ -104,0 +179,0 @@ if (second.code !== 0) process.stdout.write(second.out) |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
4573233
0.2%37123
0.22%29
3.57%