🚀. 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
5.0.1
to
5.0.2
+32
lib/docs/models.md
# models.mjs
## Огляд
Файл визначає ієрархічну класифікацію моделей для системи pi. Класифікація встановлює зв'язок між локальними та хмарними провайдерами. Функція resolveModel забезпечує маршрутизацію вибору моделі залежно від заданого рівня доступності.
## Поведінка
LOCAL_MIN встановлює мінімальний локальний провайдер
LOCAL_AVG встановлює середній локальний провайдер
LOCAL_MAX встановлює максимальний локальний провайдер
CLOUD_MIN встановлює мінімальний хмарний провайдер
CLOUD_AVG встановлює середній хмарний провайдер
CLOUD_MAX встановлює максимальний хмарний провайдер
resolveModel повертає перший непорожній model-id з каскадного перевірки локальних та хмарних провайдерів
resolveModel приймає тир min avg або max
resolveModel повертає model-id або порожній рядок якщо жоден тир не задано
## Публічний API
LOCAL_MIN — Виконує швидкий локальний inference.
LOCAL_AVG — Виконує середній локальний inference.
LOCAL_MAX — Виконує максимальний локальний inference.
CLOUD_MIN — Виконує мінімальний хмарний inference.
CLOUD_AVG — Виконує середній хмарний inference.
CLOUD_MAX — Виконує максимальний хмарний inference.
resolveModel — Повертає перший непорожній model-id для запиту, перевіряючи спочатку локальні, а потім хмарні варіанти.
## Гарантії поведінки
- Read-only: файл не виконує операцій запису у файлову систему.
- Не звертається до мережі.
/**
* Тимчасовий A/B-batch: docgen Tier 1 через omlx (gemma-4-e2b 4bit на MLX)
* замість pi/ollama. Перезаписує всі docs/<stem>.md для файлів з sym<4,
* НЕ ескалює в cloud. Призначення — порівняння якості omlx vs попередньої версії.
*
* Запуск: node npm/skills/docgen/js/docgen-batch-omlx.mjs [--limit N] [--from N]
* --limit N — обробити перші N файлів зі списку sym<4
* --from N — почати з індексу N (для дозапуску)
*/
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execSync } from 'node:child_process'
import { env } from 'node:process'
import { generateDoc } from './docgen-gen.mjs'
import { extractFacts } from './docgen-extract.mjs'
const ROOT = resolve(fileURLToPath(import.meta.url), '../../../../..')
const args = process.argv.slice(2)
const limitIdx = args.indexOf('--limit')
const limit = limitIdx !== -1 ? Number(args[limitIdx + 1]) : Infinity
const fromIdx = args.indexOf('--from')
const from = fromIdx !== -1 ? Number(args[fromIdx + 1]) : 0
env.N_CURSOR_DOCGEN_BACKEND = 'omlx'
const scanOut = execSync('node npm/bin/n-cursor.js docgen scan', { cwd: ROOT, encoding: 'utf8' })
const all = JSON.parse(scanOut)
const local = []
for (const f of all) {
try {
const src = readFileSync(join(ROOT, f.sourcePath), 'utf8')
const facts = extractFacts(src, join(ROOT, f.sourcePath))
const sym = (facts.internalSymbols ?? []).length
if (sym < 4) local.push({ ...f, sym })
} catch {
/* пропускаємо нечитані */
}
}
const slice = local.slice(from, from + limit)
console.log(`📋 Файлів sym<4 у проєкті: ${local.length}; обробляємо: ${slice.length} (from=${from}, limit=${limit === Infinity ? 'усе' : limit})`)
console.log(`🤖 Бекенд: omlx → ${env.N_CURSOR_DOCGEN_OMLX_URL ?? 'http://127.0.0.1:8000/v1/chat/completions'}`)
const stats = { ok: 0, err: 0, totalMs: 0, scores: [], errors: [] }
for (let i = 0; i < slice.length; i++) {
const f = slice[i]
const t0 = Date.now()
const pct = Math.round(((i + 1) / slice.length) * 100)
process.stdout.write(` [${i + 1}/${slice.length} ${pct}%] sym=${f.sym} ${f.sourcePath} ... `)
try {
const result = await generateDoc(join(ROOT, f.sourcePath), {
symThreshold: 999, // не уходити в cloud за sym
cloudModel: null // повністю вимкнути cloud-fallback навіть при low det-score
})
const docAbs = join(ROOT, f.docPath)
mkdirSync(dirname(docAbs), { recursive: true })
writeFileSync(docAbs, result.md)
const ms = Date.now() - t0
stats.ok++
stats.totalMs += ms
stats.scores.push(result.score ?? 0)
process.stdout.write(`✓ ${Math.round(ms / 1000)}s score=${result.score ?? '?'} tier=${result.tier}\n`)
} catch (error) {
stats.err++
stats.errors.push({ path: f.sourcePath, msg: error.message })
process.stdout.write(`✗ ${error.message}\n`)
}
}
const avgScore = stats.scores.length ? Math.round(stats.scores.reduce((a, b) => a + b, 0) / stats.scores.length) : 0
console.log(`\n${'─'.repeat(60)}`)
console.log(`✓ OK: ${stats.ok} ✗ Err: ${stats.err}`)
console.log(` Сумарний час: ${Math.round(stats.totalMs / 1000)}s; середній на файл: ${stats.ok ? Math.round(stats.totalMs / stats.ok / 1000) : 0}s`)
console.log(` Середній det-score: ${avgScore}`)
if (stats.errors.length) {
console.log('Помилки:')
for (const e of stats.errors) console.log(` - ${e.path}: ${e.msg}`)
}
/**
* E1 (Fact-anchoring): детермінований витяг «анкорів» — конкретних фрагментів
* з коду, які LLM зобовʼязана згадати в документації, щоб не зісковзнути на
* generic-фрази.
*
* Категорії анкорів:
* - urls : усі https?://… у вихідному коді
* - magicStrings : export const X = '…' з непорожнім value (≤120 символів)
* - errorMarkers : суфікси повідомлень про помилки виду `(rule.mdc)`
* - configRefs : посилання на .json-конфіги проєкту (.n-cursor.json, …)
* - examples : ```…```-блоки у file-header JSDoc (першому коментарі файла)
*
* Всі регулярки — на сирому src без AST: дешево, безпечно, без false-positive
* критичної ваги (надмір — менша проблема, ніж пропуск).
*/
const URL_RE = /https?:\/\/[^\s'"`)<>]+/g
const EXPORT_CONST_RE = /export\s+const\s+([A-Z][A-Z0-9_]+)\s*=\s*(['"`])([^'"`]+)\2/g
const ERROR_MARKER_RE = /\(([a-z][\w-]*\.mdc)\)/g
const CONFIG_REF_RE = /\b(\.[a-z][\w.-]*\.json)\b/gi
const FILE_HEADER_RE = /^\s*\/\*\*([\s\S]*?)\*\//
const CODE_BLOCK_RE = /```[a-z]*\n([\s\S]*?)\n\s*\*?\s*```/g
/** Dedup масив, зберігаючи порядок появи. */
function uniq(arr) {
const seen = new Set()
const out = []
for (const x of arr) {
if (!seen.has(x)) {
seen.add(x)
out.push(x)
}
}
return out
}
/**
* Витягує анкори з вихідного коду файла.
* @param {string} src
* @returns {{
* urls: string[],
* magicStrings: Array<{name:string, value:string}>,
* errorMarkers: string[],
* configRefs: string[],
* examples: string[]
* }}
*/
export function extractAnchors(src) {
const urls = uniq([...src.matchAll(URL_RE)].map(m => m[0]))
const magicStrings = []
const seenNames = new Set()
for (const m of src.matchAll(EXPORT_CONST_RE)) {
const name = m[1]
const value = m[3]
if (!seenNames.has(name) && value.length <= 120) {
seenNames.add(name)
magicStrings.push({ name, value })
}
}
const errorMarkers = uniq([...src.matchAll(ERROR_MARKER_RE)].map(m => m[1]))
const configRefs = uniq([...src.matchAll(CONFIG_REF_RE)].map(m => m[1]))
// Витягуємо code-block приклади тільки з file-header — там автор зазвичай показує контракт.
const headerMatch = src.match(FILE_HEADER_RE)
const examples = headerMatch ? uniq([...headerMatch[1].matchAll(CODE_BLOCK_RE)].map(m => m[1].trim())) : []
return { urls, magicStrings, errorMarkers, configRefs, examples }
}
/**
* Форматує анкори у компактний текст для system-промпта.
* Якщо анкорів немає взагалі — повертає порожній рядок (системний блок про
* анкори не додається, щоб не вводити LLM в оману «обовʼязковими» полями).
* @param {ReturnType<typeof extractAnchors>} a
* @returns {string}
*/
export function anchorsToPrompt(a) {
const blocks = []
if (a.urls.length) blocks.push(`URLs (згадай у тексті): ${a.urls.join(', ')}`)
if (a.magicStrings.length) {
blocks.push(
`Експортовані константи-рядки (наведи назву і призначення): ${a.magicStrings.map(s => `${s.name}=${JSON.stringify(s.value)}`).join('; ')}`
)
}
if (a.errorMarkers.length) blocks.push(`Маркери повідомлень (згадай у Поведінці): ${a.errorMarkers.map(m => `(${m})`).join(', ')}`)
if (a.configRefs.length) blocks.push(`Конфіги, на які спирається код: ${a.configRefs.join(', ')}`)
if (a.examples.length) blocks.push(`Приклади з документації автора (наведи дослівно у Поведінці):\n${a.examples.map(e => '```\n' + e + '\n```').join('\n')}`)
if (!blocks.length) return ''
return `АНКОРИ ДО ОБОВ'ЯЗКОВОГО ВКЛЮЧЕННЯ:\n${blocks.join('\n')}`
}
+26
-173

@@ -1,181 +0,34 @@

# `npm/.pi-template/extensions/n-cursor-adr/index.ts`
# index.ts
## Огляд
Файл `npm/.pi-template/extensions/n-cursor-adr/index.ts` — це Pi.dev-розширення (extension), яке реалізує функціональність **ADR capture + normalize** для агентських сесій. Розширення є тонким TypeScript-адаптером від pi-середовища до існуючих bash-скриптів `.claude/hooks/capture-decisions.sh` та `.claude/hooks/normalize-decisions.sh`.
Огляд
На подію `agent_end`, що її емітує Pi.dev runtime, розширення:
Файл слугує механізмом для підготовки даних сесії та ініціації виконання зовнішніх рішень. Він серіалізує поточний стан сесії для формування вхідного JSON і запускає відповідні скрипти для прийняття рішень.
1. Серіалізує entries сесії з `ctx.sessionManager.getEntries()` у Claude-сумісний формат **JSONL** у тимчасову теку (`os.tmpdir()`).
2. Формує stdin JSON payload з шляхом до транскрипту та session id.
3. Спавнить bash-хуки `capture-decisions.sh` і `normalize-decisions.sh` через `pi.exec` з відповідними таймаутами (180 с і 600 с).
## Поведінка
Уся бізнес-логіка skip/throttle і вибір LLM CLI (`claude` чи `cursor-agent`) залишається у bash-скриптах — TS-частина лише транслює подію pi у виклик хуків. Recursion guard реалізовано через перевірку env vars (`CAPTURE_DECISIONS_RUNNING`, `ADR_NORMALIZE_RUNNING`), які bash виставляє перед спавном LLM CLI, тож рекурсивний trigger ловиться у TS до старту хуків.
1. Запуск відбувається при події agent_end.
2. Перевіряється наявність змінних середовища, що вказують на активний запуск логіки. Якщо перевірка позитивна, виконання зупиняється.
3. Зчитуються записи сесії з `sessionManager`.
4. Фільтруються записи, залишаються лише ті, що мають роль 'user' або 'assistant'.
5. Фільтровані записи перетворюються у формат JSON для формування транскрипту.
6. Транскрипт записується у тимчасовий файл у директорії tmpdir у форматі JSONL.
7. Формується об'єкт вхідного потоку, який містить шлях до згенерованого транскрипту та ідентифікатор сесії.
8. Створюється новий набір змінних середовища, де змінна CLAUDE_PROJECT_DIR встановлюється на поточну робочу директорію.
9. Паралельно виконуються два окремі скрипти bash: capture-decisions.sh та normalize-decisions.sh.
10. Обидва скрипти отримують вхідний JSON-пакет і виконуються через адаптер pi.exec.
11. Виконування здійснюється асинхронно. Якщо скрипти відсутні, це може призвести до помилки, яка буде зареєстрована у результаті виконання.
12. Помилки виконання ловляться, щоб забезпечити стабільність системи.
## Експорти / API
## Гарантії поведінки
### Default export
```ts
export default function (pi: PiExec): void
```
Default export — функція-реєстратор pi-розширення. Викликається pi-runtime при завантаженні extension і приймає об'єкт `pi: PiExec` з методами `exec` та `on`. Функція реєструє один listener на подію `agent_end` і нічого не повертає.
### Внутрішні TypeScript-інтерфейси (не експортуються)
#### `PiContext`
Опис контексту, що його pi-runtime передає у handler події `agent_end`:
- `cwd: string` — поточний робочий каталог pi-сесії; передається у bash як `CLAUDE_PROJECT_DIR` і як `cwd` для `pi.exec`.
- `sessionId?: string` — опціональний ідентифікатор pi-сесії; якщо відсутній — генерується через `randomUUID()`.
- `signal?: AbortSignal` — опціональний abort-signal для пропагації скасування у `pi.exec`.
- `sessionManager: { getEntries(): Array<{ message?: { role?: string; content?: unknown } }> }` — реєстр сесії з методом отримання масиву entries; кожен entry має опціональне поле `message` із `role` (`'user' | 'assistant' | ...`) і `content`.
- `ui?: { notify?: (msg: string, level?: 'info' | 'warning' | 'error') => void }` — опціональний UI-канал для повідомлень користувачу; використовується для error-нотифікацій про збій серіалізації.
#### `PiExec`
Pi.dev extension API, що його runtime передає у default export:
- `exec(cmd: string, args: string[], opts?: { cwd?: string; env?: Record<string, string>; input?: string; signal?: AbortSignal; timeout?: number }): Promise<{ code: number; stdout: string; stderr: string }>` — спавнить дочірній процес з опціями cwd/env/stdin/signal/timeout і повертає promise з кодом, stdout і stderr.
- `on(event: string, handler: (event: unknown, ctx: PiContext) => Promise<void> | void): void` — реєструє handler для pi-події (тут — `'agent_end'`).
### Константи-шляхи до хуків
- `CAPTURE_HOOK = '.claude/hooks/capture-decisions.sh'` — відносний шлях до bash-хука захоплення ADR-рішень.
- `NORMALIZE_HOOK = '.claude/hooks/normalize-decisions.sh'` — відносний шлях до bash-хука нормалізації ADR-чернеток через LLM.
Шляхи відносні і використовуються разом з `ctx.cwd` як параметром `cwd` у `pi.exec`.
## Функції
### `export default function (pi: PiExec): void`
**Сигнатура:** `(pi: PiExec) => void`.
**Параметри:**
- `pi: PiExec` — pi.dev extension API (див. інтерфейс `PiExec` вище).
**Що повертає:** `void`. Функція синхронно реєструє обробник через `pi.on('agent_end', ...)` і завершується.
**Side effects:**
1. Реєструє listener на подію `'agent_end'` через `pi.on`.
2. Решта side effects відбуваються асинхронно у listener'і `agent_end` (див. нижче).
### Inline listener `pi.on('agent_end', async (_event, ctx) => { ... })`
**Сигнатура:** `(_event: unknown, ctx: PiContext) => Promise<void>`.
**Параметри:**
- `_event: unknown` — payload події `agent_end`; не використовується (префікс `_` сигналізує умисне ігнорування).
- `ctx: PiContext` — контекст pi-сесії.
**Що повертає:** `Promise<void>`. Резолвиться після завершення `Promise.allSettled` з двох викликів `pi.exec`, або раніше — якщо recursion guard спрацював, або якщо серіалізація транскрипту впала з винятком.
**Покроковий алгоритм:**
1. **Recursion guard:**
- Якщо `env.CAPTURE_DECISIONS_RUNNING` або `env.ADR_NORMALIZE_RUNNING` truthy — `return` без жодних дій. Ці env vars виставляє bash перед спавном LLM CLI, який може запустити вкладену pi-сесію.
2. **Серіалізація транскрипту (у блоці `try/catch`):**
- Викликає `ctx.sessionManager.getEntries()` → масив entries.
- Фільтрує entries, де `e.message?.role === 'user' || e.message?.role === 'assistant'`.
- Map'ить кожен entry у JSON-рядок виду `{ type: <role>, message: <message> }` через `JSON.stringify`.
- Об'єднує рядки через `'\n'`.
- Генерує шлях `jsonlPath = join(tmpdir(), \`n-cursor-pi-transcript-${Date.now()}-${randomUUID()}.jsonl\`)`.
- Пише файл `jsonlPath` через `writeFileSync(jsonlPath, lines + '\n', 'utf8')`.
- У catch-блоці: викликає `ctx.ui?.notify?.(\`@nitra/cursor: transcript serialization failed — ${(error as Error).message}\`, 'error')`і`return` (помилка серіалізації — не critical, але хуки не запускаються).
3. **Підготовка stdin payload:**
- `stdinPayload = JSON.stringify({ transcript_path: jsonlPath, session_id: ctx.sessionId ?? randomUUID() })`.
4. **Підготовка env override:**
- `envOverride = { ...env, CLAUDE_PROJECT_DIR: ctx.cwd }` — копія поточного env з доданим/перевизначеним `CLAUDE_PROJECT_DIR`.
5. **Паралельний спавн bash-хуків через `Promise.allSettled`:**
- `pi.exec('bash', [CAPTURE_HOOK], { cwd: ctx.cwd, env: envOverride, input: stdinPayload, signal: ctx.signal, timeout: 180_000 })` — capture-хук, таймаут 180 секунд (180_000 мс).
- `pi.exec('bash', [NORMALIZE_HOOK], { cwd: ctx.cwd, env: envOverride, input: stdinPayload, signal: ctx.signal, timeout: 600_000 })` — normalize-хук, таймаут 600 секунд (600_000 мс).
- `Promise.allSettled` — обидва промісі завжди резолвляться; ENOENT (наприклад, якщо bash-скриптів немає у pi-only консьюмерах із `claude-config: false`) не пробрасує помилку наверх.
**Side effects:**
- Запис файлу в `os.tmpdir()` через `writeFileSync` (синхронно, всередині async-функції).
- Можливий виклик `ctx.ui?.notify?.` з рівнем `'error'` при збої серіалізації.
- Два дочірні процеси `bash` через `pi.exec` (capture + normalize).
- Передача транскрипту і session id у bash через stdin.
- Перевизначення env var `CLAUDE_PROJECT_DIR` у child-процесах.
- Жодного запису у файли проєкту з самого TS — усі такі операції делеговано bash-скриптам.
## Залежності
### Node.js built-in модулі
- `node:crypto` — імпорт `randomUUID` для генерації унікальної частини імені JSONL-файлу та для fallback session id (`ctx.sessionId ?? randomUUID()`).
- `node:fs` — імпорт `writeFileSync` для синхронного запису JSONL у tmpdir.
- `node:os` — імпорт `tmpdir` для отримання шляху до системної тимчасової теки.
- `node:path` — імпорт `join` для побудови абсолютного шляху до JSONL-файлу.
- `node:process` — імпорт `env` для читання env vars (`CAPTURE_DECISIONS_RUNNING`, `ADR_NORMALIZE_RUNNING`) і успадкування у `envOverride`.
### Зовнішні залежності (runtime)
- **Pi.dev runtime** — постачає аргумент `pi: PiExec` (методи `exec` та `on`) і об'єкт `ctx: PiContext` у listener.
- **Bash-скрипти проєкту:**
- `.claude/hooks/capture-decisions.sh` — приймає stdin JSON `{ transcript_path, session_id }` і env `CLAUDE_PROJECT_DIR`; вирішує capture-логіку ADR.
- `.claude/hooks/normalize-decisions.sh` — той самий stdin/env; запускає LLM CLI (`claude` чи `cursor-agent`) для нормалізації чернеток ADR.
- **Env vars контракту з bash:**
- `CAPTURE_DECISIONS_RUNNING`, `ADR_NORMALIZE_RUNNING` — виставляються bash перед спавном LLM CLI; служать як recursion guard для вкладеного pi-trigger.
- `CLAUDE_PROJECT_DIR` — встановлюється у `ctx.cwd` для bash-хуків.
### TypeScript-залежності
- TypeScript-інтерфейси `PiContext` і `PiExec` — локально оголошені, не імпортовані з зовнішніх типів.
- Жодних NPM-пакетів runtime не імпортується.
## Потік виконання / Використання
### Реєстрація розширення
Pi.dev runtime завантажує файл як ECMAScript-модуль і викликає default export з аргументом `pi: PiExec`. Default export реєструє один listener:
```
pi.on('agent_end', listener)
```
Після реєстрації функція повертає `void`. Сам listener виконується пізніше — на кожну подію `agent_end`.
### Тригер події `agent_end`
Pi-runtime емітує `agent_end`, коли агент завершує сесію. Listener отримує `_event` (ігнорується) і `ctx: PiContext` з полями `cwd`, `sessionId?`, `signal?`, `sessionManager`, `ui?`.
### Гілка recursion guard
Якщо у поточному env-проміжку є truthy `CAPTURE_DECISIONS_RUNNING` або `ADR_NORMALIZE_RUNNING` — listener виходить негайно без запису транскрипту і без спавну хуків. Це захищає від нескінченної рекурсії, коли bash спавнить LLM CLI (`claude` або `cursor-agent`), а той знову стартує pi-сесію.
### Гілка нормальної обробки
1. Виклик `ctx.sessionManager.getEntries()` повертає масив entries сесії.
2. Фільтр залишає лише entries з `role` = `'user'` або `'assistant'`.
3. Map створює JSONL-рядки `{ "type": "<role>", "message": <message> }`.
4. Рядки об'єднуються через `\n`, додається фінальний `\n`, файл записується синхронно у `tmpdir()/n-cursor-pi-transcript-<timestamp>-<uuid>.jsonl`.
5. Якщо серіалізація кинула виняток — `ctx.ui?.notify?.` з рівнем `'error'` і повідомленням `@nitra/cursor: transcript serialization failed — <message>`, потім `return`.
6. Формується stdin payload `{ "transcript_path": "<jsonlPath>", "session_id": "<sessionId|uuid>" }`.
7. Створюється `envOverride = { ...env, CLAUDE_PROJECT_DIR: ctx.cwd }`.
8. Через `Promise.allSettled` паралельно запускаються:
- `bash .claude/hooks/capture-decisions.sh` з cwd=`ctx.cwd`, env=`envOverride`, stdin=`stdinPayload`, signal=`ctx.signal`, timeout=180 секунд.
- `bash .claude/hooks/normalize-decisions.sh` з тими ж параметрами і timeout=600 секунд.
9. `Promise.allSettled` чекає обидва — будь-яка помилка (наприклад, ENOENT для відсутніх хуків у pi-only консьюмерах з `claude-config: false`) проковтується і не падає.
10. Listener резолвиться, pi-runtime продовжує обробку події.
### Контракт з bash
- Бізнес-логіка skip/throttle, мін-інтервалів і вибору LLM CLI (`claude` чи `cursor-agent`) — повністю у `.claude/hooks/capture-decisions.sh` і `.claude/hooks/normalize-decisions.sh`.
- TS-розширення `npm/.pi-template/extensions/n-cursor-adr/index.ts` є **тонким адаптером** pi → bash і не дублює жодної бізнес-логіки.
- Recursion guard через `env.CAPTURE_DECISIONS_RUNNING` і `env.ADR_NORMALIZE_RUNNING` — обов'язкова умова коректності контракту: bash має виставити їх перед спавном LLM CLI.
### Сценарій pi-only консьюмера
Якщо консьюмер pi-template має `claude-config: false` і bash-скриптів `.claude/hooks/capture-decisions.sh` / `.claude/hooks/normalize-decisions.sh` фізично немає — `pi.exec` повертає ENOENT, але `Promise.allSettled` ловить це у `rejected`-результат і listener завершується без помилок. TS-розширення лишається працездатним, capture/normalize просто є no-op.
* Доступ до файлу дозволений
* Операція запису та модифікації даних дозволена
* При виникненні помилок система перехоплює їх
* Система не генерує винятків назовні
* Система не використовує кешування
* Система не виконує операцій з мережею
* Логіка пропуску та обмеження швидкості залишається у бах (bash)
* Логіка вибору LLM-CLI залишається у бах (bash)
* Перевірка рекурсії здійснюється через змінні середовища встановлені бах перед запуском LLM CLI
{
"name": "@nitra/cursor",
"version": "5.0.1",
"version": "5.0.2",
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",

@@ -5,0 +5,0 @@ "keywords": [

@@ -5,15 +5,24 @@ # fix.mjs

Точка входу правила `abie` для перевірки/виправлення проєкту. Файл навмисно тонкий: уся реальна оркестрація делегована спільному стандартному раннеру правил, тож сам `fix.mjs` лише задає, що це правило `abie`, і вмикає його у двох режимах роботи — як бібліотечну функцію та як standalone-команду.
Файл виконує трансформацію вхідного запиту у структурований вивід формату Х. Цей вивід призначений для подальшого використання компонентом [Конфіг_X] або [Приклад_Трансформації_v1.2].
## Поведінка
1. Визначає правило за тим, у якій теці лежить файл (`rules/abie/`), і запускає стандартний прогон правила: послідовно проганяються етапи `applies → JS-concerns → policy → mdc-refs`.
2. У бібліотечному режимі (виклик функції `run` із загальної CLI-оркестрації) приймає опційний контекст прогону — наприклад спільний FS-walk-кеш, щоб не обходити файлову систему повторно між етапами одного прогону.
3. Якщо файл запущено напряму як команду (`bun rules/abie/fix.mjs`), він поводиться як повний еквівалент `npx @nitra/cursor fix abie`: читає конфіг проєкту, перевіряє, чи правило ввімкнене у whitelist, друкує підсумок і завершує процес із відповідним exit-кодом.
4. Результат прогону — числовий код: `0` означає, що порушень немає, `1` — що порушення знайдено.
1. Запуск правила
Викликається для виконання основного процесу перевірки.
2. Режим бібліотеки
Функція повертає результат виконання основного правила.
3. Режим автономного запуску
Якщо виконання відбувається через командний рядок, функція виконує повний цикл роботи, включаючи завантаження конфігурації, перевірку дозволених елементів та формування зведення. У цьому режимі функція завершує роботу з кодом виходу, що використовується для інструментальних середовищ.
## Публічний API
- run: Запускає правило, що переходить від applies до JS-concerns, policy та mdc-refs за допомогою runStandardRule.
- Library mode: Ініціює роботу через CLI оркестрацію, використовуючи import та виклик run.
## Гарантії поведінки
- Локальної логіки перевірки в цьому файлі немає: будь-яка зміна поведінки правила відбувається через спільний раннер чи передані опції контексту, а не через правки тут. Це утримує всі правила однотипними.
- Завершення процесу з exit-кодом відбувається лише у standalone-режимі (запуск напряму); при виклику як бібліотеки процес не завершується — повертається лише код результату, придатний для агрегації в загальному прогоні.
- Standalone-режим коректно обробляє випадок, коли правило не ввімкнене в конфізі: прогін просто пропускається з нейтральним (успішним) результатом замість помилки.
- Read-only: файл не виконує операцій запису у файлову систему.
- Кешує результати в межах одного прогону.
- Не звертається до мережі.

@@ -5,23 +5,21 @@ # applies.mjs

Applies-гейт правила `abie` на рівні всього правила. Визначає, чи варто CLI взагалі застосовувати правило `abie` до поточного репозиторію. Це opt-in механізм: правило працює лише тоді, коли репозиторій явно увімкнув його у конфізі. Якщо гейт повертає `false`, CLI пропускає всі концерни правила — як JS-перевірки, так і policy.
Файл надає інструменти для валідації даних. Він використовується для порівняння об'єкта чи значення з визначеним правилом або набором критеріїв.
## Поведінка
1. Визначити застосовність правила: правило `abie` вважається увімкненим, коли воно явно перелічене у списку `rules` конфіга `.n-cursor.json` у корені репозиторію.
2. Якщо правило увімкнене — CLI продовжує виконувати всі концерни правила; якщо ні — CLI повністю пропускає правило.
3. Окрема перевірка-концерн виконує лише символічний прохід: коли вона взагалі запускається, це означає, що правило вже визнане увімкненим, тож вона рапортує успіх (context-pass) і повертає успішний exit-код. Справжню роботу виконують інші концерни правила, а не цей файл.
applies
Перевіряє наявність увімкнення правила на основі шляху до репозиторію. Повертає булеве значення, що вказує на застосовність правила.
Приклад конфіга, який вмикає правило:
check
Ініціалізує механізм перевірки. Записує повідомлення про успішне виконання. Повертає код виходу, який вказує на результат перевірки.
```json
{
"rules": ["abie"]
}
```
## Публічний API
applies Застосовує визначену бізнес-логіку для обробки вхідних даних відповідно до конфігурації.
check Перевіряє відповідність вхідних даних встановленим критеріям валідації.
## Гарантії поведінки
- Read-only: гейт лише читає конфіг репозиторію і нічого не змінює.
- Fail-safe за замовчуванням: за відсутності файла `.n-cursor.json`, помилки читання, некоректного JSON, відсутнього чи нечислового списку `rules` — правило вважається вимкненим (правило пропускається), а не активується помилково.
- Зіставлення назви правила толерантне до регістру й пробілів навколо значення.
- Символічний прохід-концерн не кидає винятків і завжди повертає успішний exit-код.
- Read-only: файл не виконує операцій запису у файлову систему.
- Не звертається до мережі.

@@ -5,20 +5,25 @@ # firebase_hosting.mjs

Перевірка правила `abie`: забороняє артефакти Firebase Hosting у підкаталогах репозиторію. За `abie.mdc` Firebase Hosting у проєкті не дозволено, тож наявність його конфігураційних файлів вважається порушенням. Перевірка лише читає файлову систему й рапортує — нічого не змінює.
Функція `check` надає механізм валідації стану об'єктів проти визначеного контракту. Використовується для внутрішньої перевірки коректності даних без ініціювання зовнішніх операцій. Функція працює у режимі fail-safe, перехоплюючи помилки для забезпечення стабільності системи (abie.mdc).
## Поведінка
1. Зчитується вміст кореня репозиторію (за замовчуванням — поточний робочий каталог).
2. Якщо корінь не вдалося прочитати — фіксується помилка з поясненням і перевірка завершується невдало.
3. Відбираються лише підкаталоги **першого рівня**, окрім службових `.git` та `node_modules`. Сам корінь репозиторію навмисно не перевіряється: однойменні файли там можуть належати суміжним проєктам і не є порушенням.
4. У кожному відібраному підкаталозі шукаються заборонені артефакти Firebase Hosting:
- файли `.firebaserc` та `firebase.json`;
- директорія `.firebase/`.
5. На кожен знайдений артефакт видається повідомлення про порушення з відносним шляхом і вимогою його видалити (шлях нормалізується до `/`-роздільників). Обхід не переривається на першій знахідці — повідомляються всі порушення.
6. Якщо у жодному підкаталозі артефактів не знайдено — видається підтвердження успіху.
1. Ініціалізація. Створюється механізм збору та звітування результатів.
2. Зчитування. Спроба прочитати вміст директорії, переданої як корінь репозиторію.
3. Фільтрація. Виключаються директорії з назвами `.git` та `node_modules` з перевірки.
4. Перевірка. Проводиться ітерація по відфільтрованих директоріях для пошуку заборонених файлів та директорій.
5. Валідація. Перевіряється наявність файлів `.firebaserc` та `firebase.json` у підкаталогах. Знайдені файли повертають невдачу.
6. Валідація. Перевіряється наявність директорій `.firebase` у підкаталогах. Знайдені директорії повертають невдачу (abie.mdc).
7. Результат. Якщо жодних порушень не виявлено, повертається позитивний результат. Якщо порушення виявлено, повертається негативний результат.
## Гарантії поведінки
- Read-only: перевірка лише читає каталоги й перевіряє наявність файлів, нічого не створює й не видаляє.
- Fail-safe щодо помилок читання кореня: замість винятку повертається ненульовий код виходу з діагностичним повідомленням.
- Повертає `0`, якщо порушень немає, і `1`, якщо знайдено хоча б один заборонений артефакт (або корінь не прочитався).
- Сканується лише перший рівень вкладеності: глибші підкаталоги та сам корінь не перевіряються (рекурсивного обходу немає).
- Read-only: файл не виконує операцій запису у файлову систему.
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
- Свідомо пропускає шляхи: `.git`, `node_modules`, `.firebase`.
- Не звертається до мережі.

@@ -5,26 +5,31 @@ # enabled.mjs

Модуль-предикат, який визначає, чи увімкнено правило **abie** у конфігурації репозиторію. Працює як opt-in гейт: правило застосовується лише тоді, коли користувач явно додав `abie` до списку активних правил у `.n-cursor.json`. За замовчуванням (відсутній конфіг, помилки читання чи парсингу) правило вважається вимкненим.
Файл керує активацією правил на рівні гейта. Він визначає, чи повинні виконуватися конфігураційні правила, викликаючи функцію `isAbieRuleEnabled` для перевірки наявності маркера `abie` у файлі `.n-cursor.json:rules`. Цей механізм використовується для фільтрації виконання правил, запобігаючи їх активації при відсутності необхідного прапорця.
## Поведінка
1. Шукає файл `.n-cursor.json` у корені репозиторію.
2. Якщо файл відсутній — повертає «вимкнено».
3. Читає та парсить вміст як JSON. Будь-яка помилка читання чи невалідний JSON трактується як «вимкнено».
4. Бере поле `rules`. Якщо це не масив — повертає «вимкнено».
5. Повертає «увімкнено», лише якщо масив `rules` містить елемент, що дорівнює `abie` після обрізання пробілів і приведення до нижнього регістру. Записи `"abie"`, `" ABIE "`, `"Abie"` усі вмикають правило. Щоб вимкнути — прибрати ім'я з `rules` (або видалити поле чи файл).
1. Створення шляху до конфігураційного файлу `.n-cursor.json`
Приклад конфігурації, що вмикає правило:
2. Перевірка наявності файлу. Якщо файл відсутній, повертається `false`.
```json
{ "rules": ["abie"] }
```
3. Читання вмісту файлу. У разі помилки читання, повертається `false`.
## Де використовується
4. Парсинг вмісту у формат JSON. У разі помилки парсингу, повертається `false`.
Підключається до applies-механізму правила `abie` (`npm/rules/abie/js/applies.mjs`) як рішення «чи запускати правило». Якщо предикат каже «вимкнено», CLI `n-cursor` повністю пропускає всі концерни `abie` (`lint`, `fix`, `policy`).
5. Витягнення масиву правил з конфігурації.
6. Перевірка типу витягнутого масиву. Якщо дані не є масивом, повертається `false`.
7. Ітерація по масиву правил. Проводиться перевірка кожного елемента на відповідність рядку 'abie' після приведення до нижнього регістру та видалення пробілів.
8. Повернення результату. Якщо знайдено правило 'abie', повертається `true`. У іншому випадку повертається `false`.
## Публічний API
isAbieRuleEnabled — перевіряє статус увімкненості правила abie у файлі `.n-cursor.json:rules`.
## Гарантії поведінки
- Read-only: модуль лише читає конфігураційний файл, нічого не змінює й не звертається до мережі.
- Fail-safe: за будь-якої проблеми (немає файлу, недоступний для читання, битий JSON, `rules` не масив) повертає «вимкнено» замість винятку — правило тихо вимикається, а не ламає прогін CLI.
- Толерантний матчинг назви: зайві пробіли та регістр у назві `abie` не заважають розпізнаванню; нерядкові елементи `rules` безпечно ігноруються.
- Read-only: файл не виконує операцій запису у файлову систему.
- Перехоплює помилки і не пропускає винятків назовні (fail-safe).
- За невдалої перевірки повертає `false`/`null` замість винятку.
- Не звертається до мережі.

@@ -1,35 +0,27 @@

# env-dns
# env-dns.mjs
## Огляд
Бібліотека чистих функцій для перевірки кластерного DNS у env-файлах abie. abie розгорнуто у двох GKE-кластерах (`abie-dev.internal` та `abie-ua.internal`), і внутрішньокластерні URL у кожному env-файлі мусять вказувати на той кластер, якому файл належить за своїм іменем. Модуль дає засоби знайти такі env-файли в репозиторії, визначити їхнє цільове середовище та виявити URL, що посилаються не на той кластер чи namespace.
Файл перевіряє конфігураційні файли середовища (`*.dev.env`, `*.ua.env`) на відповідність внутрішніх URL-адрес ідентифікатору GKE-кластера. Функція `validateAbieEnvInternalUrls` сканує URL-адреси формату `http://<svc>.<ns>.<dns>` та вимагає, щоб компонент `<dns>` відповідав необхідному префіксу DNS, визначеному для відповідного кластера (`abie-dev.internal` або `abie-ua.internal`).
## Поведінка
1. **Класифікація env-файла за іменем.** Файл вважається abie env-файлом, якщо його basename — `dev.env` або `ua.env` (опціонально з провідною крапкою: `.dev.env`, `.ua.env`). Із такого імені витягується середовище — `dev` або `ua`. Будь-який інший файл (наприклад `production.env` чи безіменний `.env`, локальний для розробника) середовища не має й до перевірки не залучається.
abieEnvNameFromBasename
Дістає тип середовища dev або ua з імени файлу. Файл без імені повертає null.
2. **Зіставлення середовища з очікуваннями.** Кожному середовищу відповідає фіксована пара — очікуваний кластерний DNS і обов'язковий префікс namespace:
- `dev` → DNS `abie-dev.internal`, namespace має починатися з `dev-`
- `ua` → DNS `abie-ua.internal`, namespace має починатися з `ua-`
validateAbieEnvInternalUrls
Сканує вміст файлу на наявність внутрішніх URL. Перевіряє, чи відповідає кластерний DNS та префікс простору імен очікуваному для заданого середовища.
3. **Сканування внутрішніх URL.** У вмісті env-файла шукаються всі URL виду `http://<svc>.<ns>.svc.<dns>` (з опціональними портом і шляхом). Те саме URL, що трапляється у двох змінних, обробляється як два окремі входження.
collectAbieEnvFiles
Збирає файли середовища abie, які відповідають правилам іменування. Виключає файли без імені.
4. **Звітування про невідповідності.** Для кожного знайденого URL формується помилка, якщо:
- його кластерний DNS не дорівнює очікуваному для цього середовища;
- його namespace не починається з очікуваного префікса.
Один URL може дати дві окремі помилки (і за DNS, і за namespace). Повідомлення містить повний URL, фактичне й очікуване значення та назву середовища.
## Публічний API
5. **Збір файлів для перевірки.** Обхід дерева репозиторію (з урахуванням каталогів-виключень) збирає всі шляхи, що класифікуються як abie env-файли, і повертає їх відсортованими за алфавітом.
* abieEnvNameFromBasename — Витягує `dev` або `ua` з імені env-файлу.
* validateAbieEnvInternalUrls — Виявляє розбіжності кластерного DNS/namespace у внутрішніх URL-адресах.
* collectAbieEnvFiles — Збирає `.env` файли, що відповідають формату abie env (dev.env, ua.env, з провідною крапкою).
Приклад невідповідності: у файлі `app.dev.env` рядок `http://api.ua-core.svc.abie-ua.internal` дасть дві помилки — DNS `abie-ua.internal` не відповідає env `dev` (очікується `abie-dev.internal`), а namespace `ua-core` не починається з `dev-`.
## Де використовується
Функції модуля викликає чек abie у `npm/rules/abie/js/env_dns.mjs`: він збирає env-файли репозиторію, визначає середовище кожного, читає вміст і прогоняє його через перевірку URL, рапортуючи кожну невідповідність як помилку правила `abie.mdc`. Якщо abie env-файлів у репозиторії немає, чек повідомляє про пропуск.
## Гарантії поведінки
- Функції перевірки вмісту чисті й read-only щодо переданого тексту: вони не звертаються до файлової системи й не мутують вхідні дані.
- Перевірка вмісту не кидає винятків: для невідомого середовища або вмісту без внутрішніх URL повертається порожній список помилок (трактується як «усе гаразд»).
- Класифікація за іменем для будь-якого не-abie файла повертає «немає середовища», тож сторонні env-файли мовчки виключаються з перевірки.
- Збір файлів повертає детермінований, відсортований за алфавітом перелік; читання файлів та обробку помилок доступу виконує сам чек-споживач, а не цей модуль.
- Read-only: файл не виконує операцій запису у файлову систему.
- Не звертається до мережі.

@@ -5,30 +5,26 @@ # hc-yaml.mjs

Модуль валідує перший рядок-modeline у файлах `hc.yaml` для правила abie. Мета — гарантувати, що файл декларує очікувану `$schema` Kubernetes-ресурсу `HealthCheckPolicy`, аби редактор давав коректні автодоповнення й валідацію за CRD-каталогом. Структурна (per-document) перевірка самого вмісту політики виконується окремо в Rego-політиці `policy/health_check_policy/health_check_policy.rego`, не тут.
Файл виконує структурну валідацію конфігураційного файлу `hc.yaml` для перевірки відповідності даних визначенню політики перевірки стану. Валідація здійснюється порівнянням даних з контрактом `HealthCheckPolicy`, який визначений у рего-файлі. Ця функція забезпечує перевірку відповідно до схеми, визначеної за посиланням https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json. Використовується константа ABIE_HC_SCHEMA_URL, яка позначає цей URL. Результат валідації повертається у форматі булевого значення або null.
## Поведінка
1. З вмісту файла прибирається BOM, текст розбивається на рядки.
2. Перевіряється лише перший рядок:
- якщо файл порожній або перший рядок порожній — повертається помилка про відсутній modeline;
- якщо перший рядок не відповідає формату modeline `# yaml-language-server: $schema=…` — повертається помилка про обов'язковий modeline;
- якщо modeline присутній, але URL `$schema` не збігається з очікуваним — повертається помилка з правильним URL для підказки.
3. Якщо перший рядок коректний — повертається `null` (валідація пройдена).
validateAbieHcModeline перевіряє modeline у вхідному контенті.
Очікуваний перший рядок файла `hc.yaml`:
Перевіряє, чи перший рядок не порожній. Якщо рядок порожній, повертає повідомлення про необхідність формату modeline (abie.mdc).
```yaml
# yaml-language-server: $schema=https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json
```
Перевіряє наявність modeline у першому рядку. Якщо modeline відсутній, повертає повідомлення про необхідність формату modeline (abie.mdc).
Кожне повідомлення про помилку починається з відносного шляху до файла й завершується маркером `(abie.mdc)`.
Перевіряє, чи значення $schema відповідає очікуваному URL. Якщо значення не відповідає, повертає повідомлення про необхідність використання URL https://datreeio.github.io/CRDs-catalog/networking.gke.io/healthcheckpolicy_v1.json (abie.mdc).
## Де використовується
Повертає null у разі успішної валідації.
Викликається в `npm/rules/abie/js/hc_pairing.mjs`: для кожного знайденого `hc.yaml` перевіряється modeline і репортиться `modeline OK` при `null` або відповідний текст помилки інакше. Константа з очікуваним URL також слугує єдиним джерелом істини для тестів і повідомлень.
## Публічний API
ABIE_HC_SCHEMA_URL — Вказує на необхідний URL `$schema` для файлу `hc.yaml` (abie.mdc).
validateAbieHcModeline — Перевіряє синтаксис modeline (`# yaml-language-server: $schema=...`) у файлі `hc.yaml`.
## Гарантії поведінки
- Read-only: аналізує лише переданий рядок-вміст, не виконує жодного I/O й не змінює вхідних даних.
- Не кидає винятків на штатних рядкових даних; на порожньому чи некоректному вмісті повертає описову помилку замість збою.
- Працює з сирим текстом без повного YAML-парсингу — перевіряється виключно перший рядок.
- Результат завжди детермінований: або точний текст помилки, або `null`.
- Read-only: файл не виконує операцій запису у файлову систему.
- За невдалої перевірки повертає `false`/`null` замість винятку.
- Не звертається до мережі.

@@ -5,41 +5,29 @@ # http-route.mjs

Cross-документна аналітика abie `HTTPRoute`-маніфестів: рахує посилання (`backendRefs`) на спільні `-hl`-сервіси у base-шарі пакета (поза overlay `ua`) і водночас фіксує порушення `namespace`. Слугує джерелом істини для `ua_http_route`-концерну, який звіряє кількість namespace-patch-ів в overlay із кількістю base-reference.
Файл надає інструмент для порівняльного аналізу конфігурації. Він використовується для підрахунку кількості посилань на спільні бекенди в базових маніфестах пакета. Ця інформація слугує для синхронізації кількості патчів у потоковому (overlay) прошарку з кількістю базових посилань.
## Поведінка
1. Обходить переданий перелік YAML-файлів під k8s-каталогом, лишаючи тільки ті, що належать base-шару abie-пакета й не входять до overlay `ua`.
2. Кожен відібраний файл читається й безпечно парситься як набір YAML-документів; документи з помилками парсингу пропускаються (інші документи в тому ж файлі продовжують аналізуватися).
3. Враховуються лише документи з `kind: HTTPRoute`. Інші види та структурно некоректні корені дають нульовий внесок.
4. Для `HTTPRoute` обхід заходить у `spec.rules[].backendRefs[]` і для кожного посилання перевіряє, чи його `name` належить набору спільних cross-namespace сервісів.
5. Спільними вважаються рівно два сервіси: `auth-run-hl` та `file-link-hl`. Кожне таке посилання збільшує загальний лічильник посилань на 1.
6. Якщо посилання на спільний сервіс не має `namespace: dev`, додається помилка виду `<rel>: HTTPRoute backendRefs до <name> має містити namespace: dev (abie.mdc)`.
7. Підсумок по всьому пакету повертається як агрегований лічильник посилань (`refCount`) і список base-помилок (`baseErrors`).
ABIE_SHARED_CROSS_NS_BACKEND_NAMES визначає список спільних сервісів, які підлягають аналітиці.
Приклад спільного backend-посилання, яке проходить перевірку:
ABIE_SHARED_CROSS_NS_BACKEND_SET створює множину спільних сервісів для швидкої перевірки.
```yaml
kind: HTTPRoute
spec:
rules:
- backendRefs:
- name: auth-run-hl
namespace: dev
```
checkSharedBackendRef перевіряє, чи посилається елемент на спільний сервіс, і перевіряє, чи відповідає його імена та namespace вимогам.
## Публічний API
httpRouteDocSharedCrossNsBackendStats збирає кількість посилань на спільні бекенди та фіксує помилки, якщо виявлено порушення вимог до namespace.
- `analyzeAbieSharedBackendRefsInPackageK8s` — за коренем репозиторію, шляхом каталогу пакета й переліком YAML-файлів повертає сумарну кількість base-посилань на спільні `-hl`-сервіси та список порушень `namespace` у base-шарі.
- `ABIE_SHARED_CROSS_NS_BACKEND_NAMES` — заморожений перелік імен спільних cross-namespace сервісів (`auth-run-hl`, `file-link-hl`), за якими ведеться підрахунок.
analyzeAbieSharedBackendRefsInPackageK8s збирає статистику щодо посилань на спільні бекенди та помилки щодо namespace з базових YAML-документів пакета, виключаючи оверлей `ua`.
## Де використовується
## Публічний API
Споживається `ua_http_route`-концерном (`npm/rules/abie/js/ua_http_route.mjs`) для синхронізації числа namespace-patch-ів в overlay `ua` із кількістю base-reference.
- ABIE_SHARED_CROSS_NS_BACKEND_NAMES — Ідентифікація назв бекендів, спільних між різними просторами імен.
- analyzeAbieSharedBackendRefsInPackageK8s — Аналізує YAML-файли пакета, збираючи кількість спільних посилань на бекенди та виявляючи базові помилки.
## Гарантії поведінки
- Read-only: файли лише читаються, аналіз нічого не пише на диск.
- Парсинг fail-safe: помилки читання/розбору YAML придушуються, а документи з помилками не враховуються — некоректний файл не зриває аналіз пакета.
- Структурна стійкість: `null`, масиви та поля неочікуваного типу на будь-якому рівні (корінь, `spec`, `rules`, окреме посилання) дають нульовий внесок без винятків.
- Лічильник рахує тільки посилання на два визначені спільні сервіси; інші backend-сервіси ігноруються.
- Перелік спільних імен заморожений і не може бути змінений споживачами.
- Шляхи у повідомленнях нормалізовані до `/`, тож вихід однаковий на POSIX і Windows.
* Функція повертає підрахунок `backendRefs` для спільних сервісів.
* Підрахунок здійснюється у base-маніфестах пакета поза overlay `ua`.
* Використовується `ua_http_route_concern` для синхронізації кількості patch-ів namespace у overlay із кількістю base-reference.
* Функція є read-only.
* Функція не виконує операцій з мережею.
* Функція не використовує кешування.
* Функція не змінює стан системи.

@@ -8,3 +8,4 @@ /** @see ./docs/docgen-gen.md */

import { extractFacts } from './docgen-extract.mjs'
import { STYLE, oneShotPromptText, sectionMessages } from './docgen-prompts.mjs'
import { extractAnchors } from './docgen-extract-anchors.mjs'
import { oneShotMessages, sectionMessages, criticMessages, refineMessages, guaranteesFromMarkers } from './docgen-prompts.mjs'

@@ -93,4 +94,58 @@ const QUALITY_THRESHOLD = 70

/** Викликає pi і повертає stdout. Кидає якщо pi повертає ненульовий код. */
function callPi(prompt, model, timeoutMs) {
/**
* omlx-бекенд: справжні OpenAI-сумісні messages (system+user збереженi).
* Вмикається `N_CURSOR_DOCGEN_BACKEND=omlx`.
* URL: `N_CURSOR_DOCGEN_OMLX_URL` або http://127.0.0.1:8000/v1/chat/completions.
* Модель: переданий `model`, потім `N_CURSOR_DOCGEN_OMLX_MODEL`, потім дефолт.
*/
function callOmlxMessages(messages, model, timeoutMs, temperature = 0.2) {
const url = env.N_CURSOR_DOCGEN_OMLX_URL ?? 'http://127.0.0.1:8000/v1/chat/completions'
const m = model || env.N_CURSOR_DOCGEN_OMLX_MODEL || 'mlx-community--gemma-4-e2b-it-4bit'
const body = JSON.stringify({
model: m,
messages,
max_tokens: 4096,
temperature
})
// Ретраїмо лише transient curl-помилки (18 = transfer closed, 56 = recv failure, 52 = empty reply).
const TRANSIENT_CURL_CODES = new Set([18, 52, 56])
let lastErr
for (let attempt = 1; attempt <= 3; attempt++) {
const r = spawnSync(
'curl',
['-sS', '-X', 'POST', url, '-H', 'Content-Type: application/json', '-H', 'Connection: close', '--max-time', String(Math.ceil(timeoutMs / 1000)), '--data-binary', '@-'],
{ input: body, encoding: 'utf8', timeout: timeoutMs + 5000 }
)
if (r.error) {
lastErr = new Error(`omlx curl error: ${r.error.message}`)
break
}
if (r.status !== 0) {
if (TRANSIENT_CURL_CODES.has(r.status) && attempt < 3) {
lastErr = new Error(`omlx curl exit ${r.status} (transient, retry ${attempt})`)
continue
}
throw new Error(`omlx curl exit ${r.status}: ${r.stderr?.slice(0, 300) ?? ''}`)
}
let j
try { j = JSON.parse(r.stdout) } catch { throw new Error(`omlx bad json: ${r.stdout?.slice(0, 200) ?? ''}`) }
if (j.error) throw new Error(`omlx api: ${JSON.stringify(j.error).slice(0, 300)}`)
const content = j.choices?.[0]?.message?.content?.trim() ?? ''
if (!content) {
const finish = j.choices?.[0]?.finish_reason
throw new Error(`omlx empty content (finish=${finish})`)
}
return content
}
throw lastErr ?? new Error('omlx unknown failure')
}
/**
* Універсальний виклик LLM за повним messages-масивом.
* - omlx: шле messages напряму (system збережено)
* - pi: конкатенує message.content (pi приймає лише plain prompt)
*/
function callLlm(messages, model, timeoutMs, temperature = 0.2) {
if (env.N_CURSOR_DOCGEN_BACKEND === 'omlx') return callOmlxMessages(messages, model, timeoutMs, temperature)
const prompt = messages.map(m => m.content).join('\n\n')
const modelArgs = model ? ['--model', model] : []

@@ -106,5 +161,26 @@ const r = spawnSync('pi', ['-p', prompt, ...modelArgs, '--no-session', '--mode', 'text', '--no-tools'], {

/** One-shot: один pi-виклик на весь документ. */
/**
* E2 — один цикл critique→refine на секцію.
* Повертає або уточнену чорнетку, або оригінал якщо критик повідомив NONE.
*/
function critiqueRefineSection(sectionKey, draft, facts, anchors, model, timeoutMs) {
const critique = callLlm(criticMessages(sectionKey, draft, facts, anchors), model, timeoutMs).trim()
if (!critique || /^\s*NONE\s*$/i.test(critique) || critique.length < 12) return draft
const refined = callLlm(refineMessages(sectionKey, draft, critique, facts, anchors), model, timeoutMs).trim()
return stripSignatures(stripSection(refined)) || draft
}
/**
* Чи треба refine для секції API: тільки якщо є >1 експорту і всі desc-и порожні
* (саме там модель схильна писати «застосовує логіку до файлу»).
*/
function apiNeedsRefine(facts) {
const exps = facts.exports ?? []
if (exps.length <= 1) return false
return exps.every(e => !e.desc)
}
/** One-shot: один виклик LLM на весь документ. */
function piOneShot(facts, src, model, timeoutMs = 120_000) {
const text = callPi(`${STYLE}\n\n${oneShotPromptText(facts, src)}`, model, timeoutMs)
const text = callLlm(oneShotMessages(facts, src), model, timeoutMs)
let md = stripSignatures(stripSection(text))

@@ -135,8 +211,15 @@ if (!md.startsWith('#')) md = `# ${basename(facts.relPath)}\n\n${md}`

*/
function piOrchestrated(facts, src, model, timeoutMs) {
function piOrchestrated(facts, src, model, timeoutMs, { anchors = null, temperature = 0.2 } = {}) {
const sections = {}
for (const s of sectionMessages(facts, src)) {
// messages = [{role:'system',content}, {role:'user',content}] → plain text prompt для pi
const prompt = s.messages.map(m => m.content).join('\n\n')
sections[s.key] = stripSignatures(stripSection(callPi(prompt, model, timeoutMs)))
const anc = anchors ?? extractAnchors(src)
// E3: «Гарантії» — детермінований шаблон з markers (0 LLM-запитів, 0 generic-фраз)
sections.guarantees = guaranteesFromMarkers(facts)
for (const s of sectionMessages(facts, src, anc)) {
if (s.key === 'guarantees') continue // вже згенеровано детерміновано
let draft = stripSignatures(stripSection(callLlm(s.messages, model, timeoutMs, temperature)))
// E2 + E3: critique→refine лише для секцій, де gemma-4 зриває на generic
if (s.key === 'overview' || (s.key === 'api' && apiNeedsRefine(facts))) {
draft = critiqueRefineSection(s.key, draft, facts, anc, model, timeoutMs)
}
sections[s.key] = draft
}

@@ -188,6 +271,7 @@ return { md: assemble(basename(facts.relPath), sections), genTok: 0 }

let r
const anchors = facts.unsupported ? null : extractAnchors(src)
try {
r = facts.unsupported
? piOneShot(facts, src, model, LOCAL_TIMEOUT_MS)
: piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS)
: piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS, { anchors })
} catch (error) {

@@ -202,4 +286,22 @@ if (cloudModel) {

// Stage 2.5: детермінований скоринг (0 токенів) — gate перед Tier 2
const { score: detScore, issues: detIssues } = scoreDoc(r.md, facts)
let { score: detScore, issues: detIssues } = scoreDoc(r.md, facts)
// E4: best-of-N. Якщо score нижчий за threshold і немає cloud-fallback — спроба
// ще раз з вищою температурою, керуємо через env (повторні прогони коштовні).
if (detScore < threshold && !cloudModel && !facts.unsupported && env.N_CURSOR_DOCGEN_BEST_OF !== '0') {
try {
const r2 = piOrchestrated(facts, src, model, LOCAL_TIMEOUT_MS, { anchors, temperature: 0.5 })
const s2 = scoreDoc(r2.md, facts)
if (s2.score > detScore) {
r = r2
detScore = s2.score
detIssues = [...s2.issues, 'best-of-2:retry-won']
} else {
detIssues = [...detIssues, 'best-of-2:retry-lost']
}
} catch (error) {
detIssues = [...detIssues, `best-of-2:retry-error: ${error.message}`]
}
}
if (detScore < threshold && cloudModel) {

@@ -206,0 +308,0 @@ const r2 = piOneShot(facts, src, cloudModel)

/** @see ./docs/docgen-prompts.md */
import { anchorsToPrompt } from './docgen-extract-anchors.mjs'
export const STYLE = [

@@ -9,2 +11,9 @@ 'Ти технічний письменник. Пишеш лаконічну ПОВЕДІНКОВУ документацію до коду українською, чистим Markdown.',

/** Окремий блок інструкцій з анкорами — підставляється коли вони є. */
function anchorsBlock(anchors) {
if (!anchors) return ''
const txt = anchorsToPrompt(anchors)
return txt ? `\n\n${txt}` : ''
}
/**

@@ -42,4 +51,5 @@ * Короткий людиночитний витяг фактів (без коду).

*/
export function sectionMessages(facts, src) {
export function sectionMessages(facts, src, anchors = null) {
const factsTxt = factsSummary(facts)
const anch = anchorsBlock(anchors)
const multi = (facts.exports?.length || 0) > 1

@@ -53,4 +63,4 @@ const out = []

messages: msgs(
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій.'
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
'Напиши вміст секції «Огляд»: 1-3 речення — що файл робить і навіщо існує (роль у системі). Без заголовка, без переліку функцій. Заборонені generic-фрази типу «забезпечує перевірку», «виконує валідацію» — пиши КОНКРЕТНО що саме і за яким контрактом.'
)

@@ -64,4 +74,4 @@ })

messages: msgs(
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
`Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка.`
`${STYLE}\n\nФАЙЛ ${facts.relPath}:\n\`\`\`\n${src}\n\`\`\`\n\nВІДОМІ ФАКТИ:\n${factsTxt}${anch}`,
`Напиши вміст секції «Поведінка»: ${multi ? 'для кожної публічної функції — один короткий пункт «що вона робить»' : 'нумерований алгоритм у бізнес-термінах'}. Якщо у фактах є свідомі пропуски шляхів — згадай їх там, де доречно (не вигадуй інших «не перевіряє»). НЕ пиши аргументи функцій у дужках, без regex.${facts.internalSymbols?.length ? ` НЕ згадуй за іменами службові функції: ${facts.internalSymbols.join(', ')}.` : ''} Без заголовка, без додаткових ## чи # підзаголовків усередині секції.`
)

@@ -77,4 +87,4 @@ })

messages: msgs(
STYLE,
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка.`
`${STYLE}${anch}`,
`Перепиши цей список як стислі маркери «назва — що робить», СВОЇМИ словами (не копіюй дослівно), без типів і сигнатур. Використовуй РІВНО ці назви, не додавай і не прибирай:\n${list}\nБез заголовка. Без generic-фраз «застосовує логіку», «перевіряє коректність» — пиши конкретно ЩО саме застосовує/перевіряє.`
)

@@ -84,12 +94,2 @@ })

// Гарантії — лише markers (без коду)
out.push({
key: 'guarantees',
numPredict: 300,
messages: msgs(
`${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsTxt}`,
'Напиши вміст секції «Гарантії поведінки» як маркери-інваріанти СУВОРО на основі ВІДОМИХ ФАКТІВ (read-only, fail-safe, пропуски). Згадуй кеш ЛИШЕ якщо у фактах прямо є «Кешує». Без сигнатур у дужках і без імен внутрішніх структур/Map-ів/кешів. Не вигадуй гарантій, яких немає у фактах. Без заголовка.'
)
})
return out

@@ -99,2 +99,77 @@ }

/**
* E2-step 1 — критик. Перевіряє чорнетку секції на конкретні дефекти.
* Повертає messages для LLM-запиту: вихід має бути СПИСКОМ issues або словом NONE.
* @param {'overview'|'behavior'|'api'} sectionKey
* @param {string} draft вже згенерована чорнетка секції
* @param {object} facts факт-лист
* @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
* @returns {Array<{role:string,content:string}>}
*/
export function criticMessages(sectionKey, draft, facts, anchors) {
const anch = anchorsBlock(anchors)
const criteria = [
'generic-фрази без конкретики («забезпечує перевірку», «виконує валідацію», «застосовує логіку»)',
'пропущені обов\'язкові АНКОРИ з контексту (URLs, magic-string constants, error-маркери, конфіги, code-приклади)',
'граматичні помилки українською («перед їх застосування», «моделіне», англіцизми як «applys», «moduleline»)',
'h1/h2/h3 підзаголовки всередині секції — їх не повинно бути',
'дослівна копія JSDoc-сигнатури або параметрів у дужках',
'вигадані факти, відсутні у ВІДОМИХ ФАКТАХ і АНКОРАХ'
].join('\n - ')
return [
{
role: 'system',
content: `Ти жорсткий редактор технічної документації українською. Знаходиш конкретні дефекти у чорнетці. ВІДОМІ ФАКТИ:\n${factsSummary(facts)}${anch}`
},
{
role: 'user',
content: `Перевір цю чорнетку секції «${sectionKey}» за критеріями:\n - ${criteria}\n\nЧЕРНЕТКА:\n${draft}\n\nВідповідь — короткий нумерований список знайдених issues (1-5 пунктів). Якщо дефектів немає — поверни одне слово: NONE.`
}
]
}
/**
* E2-step 2 — refine. Переписує чорнетку, виправляючи перелічені issues.
* @param {'overview'|'behavior'|'api'} sectionKey
* @param {string} draft
* @param {string} issues список issues від critic
* @param {object} facts
* @param {ReturnType<import('./docgen-extract-anchors.mjs').extractAnchors>} anchors
* @returns {Array<{role:string,content:string}>}
*/
export function refineMessages(sectionKey, draft, issues, facts, anchors) {
const anch = anchorsBlock(anchors)
return [
{
role: 'system',
content: `${STYLE}\n\nВІДОМІ ФАКТИ:\n${factsSummary(facts)}${anch}`
},
{
role: 'user',
content: `Перепиши чорнетку секції «${sectionKey}», прибравши перелічені issues. Збережи мову (українська) і формат (без додаткових ## підзаголовків, без обгортки \`\`\`). Якщо issues вимагають включення АНКОРІВ — додай їх дослівно.\n\nЧЕРНЕТКА:\n${draft}\n\nISSUES ВІД РЕДАКТОРА:\n${issues}\n\nПоверни ЛИШЕ оновлений текст секції без преамбули.`
}
]
}
/**
* E3 — детермінований шаблон секції «Гарантії поведінки» з facts.markers.
* НЕ використовує LLM: 0 запитів, 0 галюцинацій, 0 generic-фраз.
* @param {object} facts
* @returns {string} текст секції (без `## Гарантії` — це додає assemble())
*/
export function guaranteesFromMarkers(facts) {
const m = facts.markers || {}
const lines = []
if (m.readOnly) lines.push('- Read-only: файл не виконує операцій запису у файлову систему.')
if (m.catchesErrors) lines.push('- Перехоплює помилки і не пропускає винятків назовні (fail-safe).')
if (m.returnsFalsyOnFail) lines.push('- За невдалої перевірки повертає `false`/`null` замість винятку.')
if (m.caches) lines.push('- Кешує результати в межах одного прогону.')
if (m.skips?.length) {
lines.push(`- Свідомо пропускає шляхи: ${m.skips.map(s => '`' + s + '`').join(', ')}.`)
}
if (!m.network) lines.push('- Не звертається до мережі.')
if (!lines.length) return '- Поведінка детермінована: результат залежить лише від вхідних даних.'
return lines.join('\n')
}
/**
* One-shot messages (база для порівняння).

@@ -101,0 +176,0 @@ * @param {object} facts факт-лист про файл

Sorry, the diff of this file is too big to display