🚀. Socket Launch Week Day 3:Socket Firewall Now Blocks Malicious VS Code and Open VSX Extensions.Learn more
Sign In

@nitra/cursor

Package Overview
Dependencies
Maintainers
1
Versions
410
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nitra/cursor - npm Package Compare versions

Comparing version
11.1.0
to
11.2.0
+27
-0
lib/llm.mjs

@@ -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