@nitra/cursor
Advanced tools
| # 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')}` | ||
| } |
@@ -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 |
+1
-1
| { | ||
| "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
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
771
0.39%33807
0.98%5463474
0