@nitra/cursor 12.11.1 → 12.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +1 -1
  3. package/rules/adr/js/docs/hooks.md +0 -2
  4. package/rules/bun/js/docs/fix-layout.md +25 -0
  5. package/rules/bun/js/fix-layout.mjs +55 -0
  6. package/rules/changelog/js/docs/fix-consistency.md +27 -0
  7. package/rules/changelog/js/docs/index.md +2 -2
  8. package/rules/changelog/js/fix-consistency.mjs +50 -0
  9. package/rules/ci4/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  10. package/rules/ci4/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  11. package/rules/ga/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  12. package/rules/ga/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  13. package/rules/graphql/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  14. package/rules/graphql/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  15. package/rules/js/js/docs/dep-policy.md +12 -10
  16. package/rules/js/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  17. package/rules/js/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  18. package/rules/js-run/js/docs/fix-runtime.md +25 -0
  19. package/rules/js-run/js/fix-runtime.mjs +41 -0
  20. package/rules/nginx-default-tpl/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  21. package/rules/nginx-default-tpl/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  22. package/rules/rego/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  23. package/rules/rego/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  24. package/rules/rust/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  25. package/rules/rust/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  26. package/rules/style/js/docs/fix-tooling.md +29 -0
  27. package/rules/style/js/fix-tooling.mjs +46 -0
  28. package/rules/style/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  29. package/rules/style/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  30. package/rules/tauri/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  31. package/rules/tauri/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  32. package/rules/text/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  33. package/rules/text/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  34. package/rules/vue/js/docs/packages.md +0 -2
  35. package/scripts/docs/index.md +0 -2
  36. package/scripts/lib/discover-checkable-rules.mjs +1 -0
  37. package/scripts/lib/docs/discover-checkable-rules.md +13 -155
  38. package/scripts/lib/fix/discover-t0-patterns.mjs +83 -0
  39. package/scripts/lib/fix/docs/discover-t0-patterns.md +37 -0
  40. package/scripts/lib/fix/docs/llm-fix-apply.md +12 -10
  41. package/scripts/lib/fix/docs/llm-worker.md +6 -14
  42. package/scripts/lib/fix/docs/orchestrator.md +0 -2
  43. package/scripts/lib/fix/docs/t0.md +11 -10
  44. package/scripts/lib/fix/docs/vscode-ext-add.md +29 -0
  45. package/scripts/lib/fix/t0.mjs +8 -234
  46. package/scripts/lib/fix/vscode-ext-add.mjs +45 -0
  47. package/rules/test/coverage/coverage.mjs +0 -317
  48. package/scripts/coverage-classify/apply.mjs +0 -67
  49. package/scripts/coverage-classify/cache.mjs +0 -77
  50. package/scripts/coverage-classify/docs/apply.md +0 -206
  51. package/scripts/coverage-classify/docs/cache.md +0 -207
  52. package/scripts/coverage-classify/docs/index.md +0 -14
  53. package/scripts/coverage-classify/docs/prompt.md +0 -136
  54. package/scripts/coverage-classify/docs/verdict-schema.md +0 -28
  55. package/scripts/coverage-classify/index.mjs +0 -114
  56. package/scripts/coverage-classify/prompt.mjs +0 -126
  57. package/scripts/coverage-classify/verdict-schema.mjs +0 -35
  58. package/scripts/coverage-fix-extract.mjs +0 -122
  59. package/scripts/coverage-fix.mjs +0 -119
  60. package/scripts/docs/coverage-fix-extract.md +0 -36
  61. package/scripts/docs/coverage-fix.md +0 -181
  62. package/skills/coverage-fix/SKILL.md +0 -131
  63. package/skills/coverage-fix/main.json +0 -1
@@ -1,136 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: prompt.mjs
4
- resource: npm/scripts/coverage-classify/prompt.mjs
5
- docgen:
6
- crc: 12bfb99a
7
- ---
8
-
9
- Модуль `prompt.mjs` — це prompt-builder для скрипта `coverage-classify`, що класифікує вцілілих мутантів зі звіту Stryker через LLM. Файл експонує дві сутності:
10
-
11
- - статичний рядок `SYSTEM_PROMPT`, який описує LLM правила класифікації та формат JSON-відповіді;
12
- - функцію `buildUserPrompt(mutant, cwd)`, яка для кожного конкретного мутанта збирає контекстний user-prompt: фрагмент вихідного коду навколо мутації, вміст відповідного тестового файла та дату останньої git-активності.
13
-
14
- Розділення на статичний `SYSTEM_PROMPT` і динамічний `buildUserPrompt` обґрунтоване стратегією кешування промптів через Anthropic API (`cache_control: ephemeral`) — незмінна частина (системний промпт) кешується між викликами, а змінна (контекст мутанта) формується щоразу заново.
15
-
16
- Модуль не виконує жодних мережевих викликів — він лише будує текст. Виклик LLM відбувається у викликальному коді (інший модуль `coverage-classify`).
17
-
18
- ## Експорти / API
19
-
20
- | Експорт | Тип | Призначення |
21
- | ------------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
- | `SYSTEM_PROMPT` | `string` (named export) | Статичний англомовний системний промпт для LLM-класифікатора мутантів. Містить опис п'яти можливих verdict-категорій (`worth-testing`, `equivalent`, `defensive`, `glue`, `wrapper`), JSON-схему відповіді та інструкції щодо рівня впевненості (`confidence`). |
23
- | `buildUserPrompt(mutant, cwd)` | `function` (named export) | Будує user-prompt для одного мутанта. |
24
-
25
- Внутрішня (не експортована) функція:
26
-
27
- - `extractTestTitles(content)` — допоміжна, витягує заголовки `describe/test/it` з тексту test-файла.
28
-
29
- Внутрішні константи (не експортуються):
30
-
31
- - `CONTEXT_LINES = 10` — кількість рядків контексту вище й нижче рядка мутанта при формуванні фрагмента вихідного коду.
32
- - `TEST_FILE_MAX_LINES = 2000` — поріг розміру test-файла у рядках; якщо більше — у промпт іде лише список title-ів, а не повний текст.
33
-
34
- ## Функції
35
-
36
- ### `extractTestTitles(content)`
37
-
38
- Внутрішня helper-функція для редукування довгих тестових файлів до списку заголовків.
39
-
40
- - **Сигнатура**: `extractTestTitles(content: string) => string`
41
- - **Параметри**:
42
- - `content` — повний текст test-файла як рядок.
43
- - **Повертає**: рядок, де кожен запис має формат `describe: <title>` або `test: <title>` / `it: <title>`, з'єднаний через `\n`. Якщо у файлі не знайдено жодного блоку `describe`/`test`/`it` — повертає літерал `(no describe/test blocks found)`.
44
- - **Алгоритм**: проганяє по `content` глобальний `unicode/multiline` regex `^\s*(describe|test|it)\(['"\`](.+?)['"\`]`, що ловить початки тестових блоків з аргументом у одинарних, подвійних або зворотних лапках. Для кожного match-у формує рядок `<kind>: <title>` і додає до масиву, який в кінці зливається в один рядок.
45
- - **Side effects**: відсутні (чиста функція).
46
- - **Обмеження**: regex не аналізує AST, тому коментарі типу `// describe('foo'`) також можуть бути захоплені; для шаблонів промпту це прийнятна апроксимація.
47
-
48
- ### `buildUserPrompt(mutant, cwd)`
49
-
50
- Основна публічна функція модуля. Збирає markdown-розмічений user-prompt з чотирма секціями: метаінформація про мутант, фрагмент вихідного коду, існуючі тести, дата останньої git-активності файла.
51
-
52
- - **Сигнатура**: `buildUserPrompt(mutant, cwd: string) => string`, де `mutant` має форму:
53
- ```
54
- {
55
- file: string, // шлях до файла відносно cwd
56
- line: number, // рядок мутації (1-based)
57
- col: number, // колонка мутації
58
- mutantType: string, // тип мутанта зі Stryker (наприклад "ConditionalExpression")
59
- original: string, // оригінальний фрагмент коду
60
- replacement: string // мутований фрагмент
61
- }
62
- ```
63
- - **Параметри**:
64
- - `mutant` — об'єкт-опис мутанта, як зазначено вище.
65
- - `cwd` — абсолютний шлях до кореня проєкту; використовується для побудови абсолютного шляху до файла й як `cwd` для виклику `git`.
66
- - **Повертає**: markdown-рядок із секціями `# Mutant`, `# Source context (±10 lines)`, `# Existing tests`, `# Recent activity`. Готовий бути переданим у поле `messages[].content` LLM-запиту.
67
- - **Side effects**:
68
- - синхронні read-only file system операції: `existsSync`, `readFileSync` для джерельного файла й тестового файла;
69
- - синхронний виклик процесу `git log -1 --format=%ar -- <absPath>` через `execFileSync` (read-only щодо git-репозиторію).
70
- - **Обробка помилок / graceful fallback**:
71
- - якщо джерельний файл не існує — `srcContext = '(source file unavailable)'`;
72
- - якщо тестовий файл не існує — `existingTests = '(no test file)'`;
73
- - якщо `git` недоступний, файл untracked або команда падає — `recentActivity = '(no git history)'`. `catch`-блок мовчазний, помилка проковтується (за коментарем `git unavailable or file untracked — keep placeholder`).
74
-
75
- #### Алгоритм формування секцій
76
-
77
- 1. **Абсолютний шлях**: `absPath = join(cwd, mutant.file)`.
78
- 2. **Source context**:
79
- - читає файл, розбиває на рядки;
80
- - обчислює діапазон `[start, end)`, де `start = max(0, mutant.line - 1 - 10)`, `end = min(lines.length, mutant.line + 10)`;
81
- - вирізає slice, додає до кожного рядка префікс `<absoluteLineNumber>: ` (1-based) і об'єднує через `\n`.
82
- 3. **Existing tests** — шукає файл за конвенцією `dirname(absPath)/tests/<basename без .mjs>.test.mjs`:
83
- - якщо файл існує і має <= 2000 рядків — вставляє повний вміст;
84
- - якщо більше — викликає `extractTestTitles(content)` і вставляє лише заголовки тест-блоків.
85
- 4. **Recent activity**: викликає `git log -1 --format=%ar -- <absPath>` з опціями `cwd`, `encoding: 'utf8'`, `stdio: ['ignore', 'pipe', 'ignore']` (stderr глушиться). Trim-ить результат; якщо він непорожній — підставляє у плейсхолдер.
86
- 5. Повертає шаблонний рядок із усіма зібраними секціями.
87
-
88
- ## Залежності
89
-
90
- ### Зовнішні (Node.js builtins)
91
-
92
- - `node:child_process` — `execFileSync` для виклику `git log`. Прямий виклик бінарника `git` без shell-інтерпретації (аргументи — масив).
93
- - `node:fs` — `existsSync`, `readFileSync` для синхронного читання джерельних і тестових файлів.
94
- - `node:path` — `basename`, `dirname`, `join` для роботи зі шляхами.
95
-
96
- ### Внутрішньопроєктні
97
-
98
- Модуль не імпортує жодних інших модулів проєкту і не має внутрішньопроєктних залежностей.
99
-
100
- ### Очікувані виклики ззовні
101
-
102
- Файл є частиною комплекту `npm/scripts/coverage-classify/`. Сусідні модулі тієї ж теки (`index.mjs`, `apply.mjs`, `cache.mjs`, `verdict-schema.mjs`) ймовірно імпортують `SYSTEM_PROMPT` і `buildUserPrompt` для побудови запитів до LLM і подальшої обробки verdict-ів. Цей файл сам по собі нічого не виконує — він суто «бібліотечний».
103
-
104
- ## Потік виконання / Використання
105
-
106
- Типовий сценарій споживання:
107
-
108
- 1. Споживач (наприклад `index.mjs` у тій самій теці) імпортує:
109
- ```
110
- import { SYSTEM_PROMPT, buildUserPrompt } from './prompt.mjs'
111
- ```
112
- 2. Для кожного survived-мутанта зі Stryker-звіту:
113
- - формує об'єкт `mutant` із полів звіту;
114
- - викликає `const userPrompt = buildUserPrompt(mutant, process.cwd())`;
115
- - відправляє у LLM-API два повідомлення:
116
- - system: `SYSTEM_PROMPT` (з `cache_control: { type: 'ephemeral' }`),
117
- - user: `userPrompt`.
118
- 3. LLM повертає JSON-об'єкт за схемою, описаною у `SYSTEM_PROMPT`:
119
- ```
120
- {
121
- "verdict": "worth-testing" | "equivalent" | "defensive" | "glue" | "wrapper",
122
- "confidence": number,
123
- "reason": string,
124
- "suggestedTest": string // тільки якщо verdict === "worth-testing"
125
- }
126
- ```
127
- 4. Викликальний код парсить відповідь (валідація схеми ймовірно в `verdict-schema.mjs`) і застосовує її (`apply.mjs`).
128
-
129
- ### Інваріанти й обмеження
130
-
131
- - `mutant.file` має бути шляхом відносно `cwd` — інакше абсолютний шлях буде некоректний і обидва файли (джерело й тест) випадуть у fallback-плейсхолдери.
132
- - Конвенція розташування тестів є жорсткою: `tests/<basename без .mjs>.test.mjs` поряд з джерельним файлом. Інші конвенції (наприклад `__tests__/` або `.spec.mjs`) не підтримуються — для них `existingTests` буде `(no test file)`.
133
- - Розширення `.mjs` зашите у `basename(absPath, '.mjs')`. Для файлів інших розширень (`.js`, `.ts`, `.vue`) `basename` залишить розширення в результаті, тому шлях до тестів стане некоректним.
134
- - `CONTEXT_LINES = 10` і `TEST_FILE_MAX_LINES = 2000` — внутрішні константи, не конфігуруються параметрами.
135
- - `git log` запускається синхронно — для великої кількості мутантів це може бути bottleneck.
136
- - Усі операції синхронні; модуль безпечний для послідовного використання, але не оптимізований під concurrent-доступ (хоча сам по собі stateless).
@@ -1,28 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: verdict-schema.mjs
4
- resource: npm/scripts/coverage-classify/verdict-schema.mjs
5
- docgen:
6
- crc: ecf5dfe1
7
- score: 100
8
- ---
9
-
10
- Файл надає схему `VerdictSchema` для валідації вердиктів LLM-класифікатора. Функція `parseVerdict` витягує JSON-об'єкт з сирої текстової відповіді моделі та перевіряє його відповідність визначеній схемі.
11
-
12
- ## Поведінка
13
-
14
- VerdictSchema
15
- Визначає схему для валідації вердикту LLM-класифікатора
16
-
17
- parseVerdict
18
- Витягує JSON-об'єкт з текстової відповіді LLM і валідує його за схемою
19
-
20
- ## Публічний API
21
-
22
- VerdictSchema — Схема для структури вердикту.
23
- parseVerdict — Витягує JSON з тексту LLM і перевіряє його за схемою VerdictSchema.
24
-
25
- ## Гарантії поведінки
26
-
27
- - Read-only: файл не виконує операцій запису у файлову систему.
28
- - Не звертається до мережі.
@@ -1,114 +0,0 @@
1
- /**
2
- * Public API класифікатора: classify(survived, cwd, opts) → verdicts[]
3
- *
4
- * Routing:
5
- * 1. Cache lookup → hit → використати збережений verdict.
6
- * 2. Cache miss → Tier 1 (resolveModel('min')) → parseVerdict.
7
- * 3. Tier 1 fail (model error / bad JSON / Zod) → Tier 2 (CLOUD_MIN через pi).
8
- * 4. Tier 2 fail → conservative fallback worth-testing/confidence=0.
9
- *
10
- * Бекенд обирається за model-id: `omlx/...` → прямий HTTP до omlx (локально),
11
- * решта → pi CLI. Якщо omlx-Tier 1 недоступний, помилка падає в той самий catch
12
- * і класифікація відкочується на хмарний Tier 2 через pi.
13
- */
14
- import { join } from 'node:path'
15
-
16
- import { CLOUD_MIN, resolveModel } from '../../lib/models.mjs'
17
- import { callLlm } from '../../lib/llm.mjs'
18
- import { deriveCacheKey, readCache, writeCache } from './cache.mjs'
19
- import { buildUserPrompt, SYSTEM_PROMPT } from './prompt.mjs'
20
- import { parseVerdict } from './verdict-schema.mjs'
21
-
22
- const FALLBACK_VERDICT = {
23
- verdict: 'worth-testing',
24
- confidence: 0,
25
- reason: 'LLM-classification unavailable, conservative fallback (treat as worth-testing)'
26
- }
27
-
28
- /**
29
- * Викликає LLM через спільний `callLlm` (маршрут за префіксом model-id; wire-trace).
30
- * @param {string} prompt текст промпта
31
- * @param {string} model provider/model-id, `omlx/...` або '' для pi-дефолту
32
- * @returns {string} текст відповіді моделі
33
- * @throws {Error} якщо backend недоступний або повертає помилку
34
- */
35
- function callModel(prompt, model) {
36
- return callLlm([{ role: 'user', content: prompt }], model, { timeoutMs: 60_000, caller: 'coverage' })
37
- }
38
-
39
- /**
40
- * Два тири: LOCAL_MIN → Tier 2 CLOUD_MIN → FALLBACK_VERDICT.
41
- * @param {{file: string, mutants: object[]}} group група мутантів одного файлу
42
- * @param {object} mutant конкретний мутант
43
- * @param {string} cwd корінь проєкту
44
- * @param {(prompt: string, model: string) => string} callModelFn ін'єкція для тестів
45
- * @returns {object} verdict класифікації
46
- */
47
- function classifyOne(group, mutant, cwd, callModelFn) {
48
- const prompt = `${SYSTEM_PROMPT}\n\n${buildUserPrompt({ ...mutant, file: group.file }, cwd)}`
49
- const loc = `${group.file}:${mutant.line}:${mutant.col}`
50
-
51
- // Tier 1: resolveModel('min') — каскад local→cloud якщо локалі нема
52
- try {
53
- const text = callModelFn(prompt, resolveModel('min'))
54
- return parseVerdict(text)
55
- } catch {
56
- // Tier 2: CLOUD_MIN
57
- try {
58
- const text = callModelFn(prompt, CLOUD_MIN)
59
- return parseVerdict(text)
60
- } catch (error) {
61
- console.warn(`⚠ coverage classify: ${loc} both tiers failed: ${error.message}`)
62
- return { ...FALLBACK_VERDICT }
63
- }
64
- }
65
- }
66
-
67
- /**
68
- * Класифікує survived мутантів (resolveModel('min') → CLOUD_MIN → fallback).
69
- * @param {Array<{file: string, mutants: object[], exampleTest?: object|null, recommendationText?: string|null}>} survived список вцілілих мутантів
70
- * @param {string} cwd корінь проєкту
71
- * @param {{cachePath?: string, callModel?: (prompt: string, model: string) => string}} [opts] ін'єкції для тестів
72
- * @returns {Promise<Array<{key: string, verdict: object}>>} verdicts
73
- */
74
- export function classify(survived, cwd, opts = {}) {
75
- const cachePath = opts.cachePath ?? join(cwd, 'npm/reports/coverage-classify.cache.json')
76
- const callModelFn = opts.callModel ?? callModel
77
- const cacheModel = `${resolveModel('min') || 'default'}+${CLOUD_MIN || 'cloud'}`
78
-
79
- const cache = readCache(cachePath)
80
- if (cache.model !== cacheModel) {
81
- cache.entries = {}
82
- cache.model = cacheModel
83
- }
84
-
85
- const verdicts = []
86
- for (const group of survived) {
87
- for (const mutant of group.mutants) {
88
- const lookupKey = `${group.file}:${mutant.line}:${mutant.col}:${mutant.replacement}`
89
- const cacheKey = deriveCacheKey(join(cwd, group.file), mutant)
90
-
91
- let verdict = null
92
- if (cacheKey && cache.entries[cacheKey]) {
93
- const cached = cache.entries[cacheKey]
94
- verdict = {
95
- verdict: cached.verdict,
96
- confidence: cached.confidence,
97
- reason: cached.reason,
98
- ...(cached.suggestedTest ? { suggestedTest: cached.suggestedTest } : {})
99
- }
100
- }
101
- if (!verdict) {
102
- verdict = classifyOne(group, mutant, cwd, callModelFn)
103
- if (cacheKey) {
104
- cache.entries[cacheKey] = { ...verdict, classifiedAt: new Date().toISOString() }
105
- }
106
- }
107
-
108
- verdicts.push({ key: lookupKey, verdict })
109
- }
110
- }
111
-
112
- writeCache(cachePath, cache)
113
- return verdicts
114
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * Промпт-builder для coverage-classify.
3
- * SYSTEM_PROMPT — статичний, кешується через cache_control: ephemeral у API call.
4
- * buildUserPrompt — асемблює per-mutant контекст (location, source ±10, tests, git).
5
- */
6
- import { execFileSync } from 'node:child_process'
7
- import { existsSync, readFileSync } from 'node:fs'
8
- import { basename, dirname, join } from 'node:path'
9
-
10
- const CONTEXT_LINES = 10
11
- const TEST_FILE_MAX_LINES = 2000
12
-
13
- export const SYSTEM_PROMPT = `You are a mutation testing classifier.
14
-
15
- For each survived Stryker mutant, classify it into exactly one verdict:
16
-
17
- - **worth-testing**: pure logic with real branches that should be tested. The mutant
18
- exposes a missing assertion in a unit test. Recommend a test approach.
19
- - **equivalent**: the mutated code is behaviorally indistinguishable from the original
20
- (e.g., both branches produce the same observable output, or the mutant lies on dead
21
- code). You MUST cite a concrete reason referencing input flow or output equivalence.
22
- - **defensive**: the branch guards against an impossible state given input contracts
23
- or type system. You MUST identify the invariant that makes the state unreachable.
24
- - **glue**: thin CLI entrypoint, factory, or boilerplate (e.g., runStandardRule
25
- wrapper, fix.mjs stubs). Integration tests via subprocess cover the behavior.
26
- Name the integration test or pattern.
27
- - **wrapper**: thin shell around an external tool (spawnSync, fetch, dynamic import).
28
- The wrapper has no logic worth unit-testing in isolation; behavior comes from the
29
- wrapped tool. Name the integration test or pattern.
30
-
31
- Output ONLY a single JSON object matching this schema:
32
-
33
- \`\`\`
34
- {
35
- "verdict": "worth-testing" | "equivalent" | "defensive" | "glue" | "wrapper",
36
- "confidence": number 0-1,
37
- "reason": string (20-500 chars; concrete code-level reference, not "seems like"),
38
- "suggestedTest": string (max 300 chars; required only when verdict is worth-testing)
39
- }
40
- \`\`\`
41
-
42
- Confidence guidance:
43
- - 0.9+: cite specific code fragment, identifier, or input contract proving the verdict.
44
- - 0.7-0.9: strong inference from visible code structure.
45
- - <0.7: ambiguity, lacking context, or unfamiliar pattern. Be honest.
46
-
47
- Never invent integration test names. If you cannot identify a covering test, use
48
- worth-testing with low confidence instead of glue/wrapper.
49
- `
50
-
51
- /**
52
- * Витягує describe/test/it title з рядка тексту.
53
- * @param {string} content повний текст test-файла
54
- * @returns {string} список "describe: <title>" / "test: <title>" або порожній
55
- */
56
- function extractTestTitles(content) {
57
- const titles = []
58
- for (const match of content.matchAll(/^[ \t]{0,16}(describe|test|it)\(['"`](.{1,300}?)['"`]/gmu)) {
59
- titles.push(`${match[1]}: ${match[2]}`)
60
- }
61
- return titles.join('\n') || '(no describe/test blocks found)'
62
- }
63
-
64
- /**
65
- * Будує користувацький промпт для класифікації одного мутанта.
66
- * @param {{file: string, line: number, col: number, mutantType: string, original: string, replacement: string}} mutant параметри мутанта (file — відносний до cwd)
67
- * @param {string} cwd корінь проєкту
68
- * @returns {string} user prompt
69
- */
70
- export function buildUserPrompt(mutant, cwd) {
71
- const absPath = join(cwd, mutant.file)
72
-
73
- // Source context
74
- let srcContext = '(source file unavailable)'
75
- if (existsSync(absPath)) {
76
- const lines = readFileSync(absPath, 'utf8').split('\n')
77
- const start = Math.max(0, mutant.line - 1 - CONTEXT_LINES)
78
- const end = Math.min(lines.length, mutant.line + CONTEXT_LINES)
79
- srcContext = lines
80
- .slice(start, end)
81
- .map((l, i) => `${start + i + 1}: ${l}`)
82
- .join('\n')
83
- }
84
-
85
- // Existing tests
86
- const testPath = join(dirname(absPath), 'tests', `${basename(absPath, '.mjs')}.test.mjs`)
87
- let existingTests = '(no test file)'
88
- if (existsSync(testPath)) {
89
- const content = readFileSync(testPath, 'utf8')
90
- if (content.split('\n').length > TEST_FILE_MAX_LINES) {
91
- existingTests = extractTestTitles(content)
92
- } else {
93
- existingTests = content
94
- }
95
- }
96
-
97
- // Recent git activity (graceful если нет git або untracked)
98
- let recentActivity = '(no git history)'
99
- try {
100
- const out = execFileSync('git', ['log', '-1', '--format=%ar', '--', absPath], {
101
- cwd,
102
- encoding: 'utf8',
103
- stdio: ['ignore', 'pipe', 'ignore']
104
- }).trim()
105
- if (out) recentActivity = out
106
- } catch {
107
- // git unavailable or file untracked — keep placeholder
108
- }
109
-
110
- return `# Mutant
111
- File: ${mutant.file}
112
- Line: ${mutant.line}:${mutant.col}
113
- Type: ${mutant.mutantType}
114
- Original code: \`${mutant.original}\`
115
- Mutated to: \`${mutant.replacement}\`
116
-
117
- # Source context (±${CONTEXT_LINES} lines)
118
- ${srcContext}
119
-
120
- # Existing tests
121
- ${existingTests}
122
-
123
- # Recent activity
124
- File last modified: ${recentActivity}
125
- `
126
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * Zod-схема для verdict-відповіді LLM-класифікатора (coverage-classify).
3
- * parseVerdict — витяг JSON з raw-text LLM-відповіді + validate.
4
- *
5
- * Категорії:
6
- * - worth-testing: pure logic, real branches — пиши тест
7
- * - equivalent: мутант поведінково еквівалентний (не killable)
8
- * - defensive: гілка для impossible state (не killable)
9
- * - glue: CLI entry / runStandardRule wrapper (integration covers)
10
- * - wrapper: тонкий spawn/fetch wrapper (integration covers)
11
- */
12
- import { z } from 'zod'
13
-
14
- export const VerdictSchema = z.object({
15
- verdict: z.enum(['worth-testing', 'equivalent', 'defensive', 'glue', 'wrapper']),
16
- confidence: z.number().min(0).max(1),
17
- reason: z.string().min(20).max(500),
18
- suggestedTest: z.string().max(300).optional()
19
- })
20
-
21
- /**
22
- * Витягує JSON-об'єкт з raw-text LLM-відповіді і валідує через VerdictSchema.
23
- * @param {string} rawText raw-text відповідь LLM
24
- * @returns {{verdict: string, confidence: number, reason: string, suggestedTest?: string}} verdict
25
- * @throws {Error} якщо JSON не знайдено, не парситься, або не відповідає схемі
26
- */
27
- export function parseVerdict(rawText) {
28
- const jsonStart = rawText.indexOf('{')
29
- const jsonEnd = rawText.lastIndexOf('}')
30
- if (jsonStart === -1 || jsonEnd === -1) {
31
- throw new Error('No JSON object found in LLM response')
32
- }
33
- const json = JSON.parse(rawText.slice(jsonStart, jsonEnd + 1))
34
- return VerdictSchema.parse(json)
35
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * `n-cursor coverage-fix index|slice` — read-only витяг вцілілих мутантів із
3
- * `COVERAGE.md` для скілу `n-coverage-fix`.
4
- *
5
- * Мотивація: `COVERAGE.md` може важити мегабайти (секція `## Вцілілі мутанти`
6
- * з JSON-блоком на сотні файлів). Якщо цей файл читає LLM-оркестратор, він
7
- * спалює сотні тисяч токенів лише на парсинг. Натомість важкий парсинг несе цей
8
- * скрипт (для JS — мілісекунди, 0 токенів), а агенту віддається рівно потрібна
9
- * порція:
10
- * - `index` — крихітний `[{file, mutants}]` для рішення про фан-аут;
11
- * - `slice --file <path>` — промпт лише для одного файлу (контекст ±3 рядки),
12
- * рівно під когнітивне навантаження одного субагента.
13
- *
14
- * Команда read-only: лише парсить наявний `COVERAGE.md`, нічого не мутує і не
15
- * перезапускає Stryker (тож не входить у root-guard).
16
- */
17
- import { readFile } from 'node:fs/promises'
18
- import { join } from 'node:path'
19
-
20
- import { buildFixPrompt } from './coverage-fix.mjs'
21
-
22
- /** Заголовок секції вцілілих мутантів у COVERAGE.md (контракт із renderMarkdown). */
23
- const SURVIVED_SECTION = '## Вцілілі мутанти'
24
-
25
- /**
26
- * Огорожа json-блоку: ≥3 бектики, далі `json` і решта рядка до `\n`. Довжина
27
- * захоплюється в групу 1 — renderMarkdown пише 3, але oxfmt підвищує до 4+, коли
28
- * сам JSON-вміст містить ``` (типово для original/replacement мутантів).
29
- */
30
- const FENCE_OPEN_RE = /(`{3,8})json[^\n]{0,200}\n/
31
-
32
- /**
33
- * Витягує JSON-масив вцілілих мутантів із тексту COVERAGE.md: знаходить секцію
34
- * `## Вцілілі мутанти`, перший огороджений ` ```json ` блок під нею і парсить.
35
- * @param {string} md повний текст COVERAGE.md
36
- * @returns {import('./coverage-fix.mjs').SurvivedFileGroup[]} групи вцілілих по файлах (порожньо, якщо секції/блоку немає або JSON невалідний)
37
- */
38
- export function parseSurvivedBlock(md) {
39
- const sectionAt = md.indexOf(SURVIVED_SECTION)
40
- if (sectionAt === -1) return []
41
- const after = md.slice(sectionAt)
42
- const open = after.match(FENCE_OPEN_RE)
43
- if (!open) return []
44
- const fence = open[1]
45
- const bodyStart = open.index + open[0].length
46
- const rest = after.slice(bodyStart)
47
- // Закриття — рядок із тих самих бектиків. Усередині JSON реальних переводів
48
- // рядка немає (JSON.stringify екранує їх як `\n`), тож `\n<fence>` унікально
49
- // позначає кінець блоку навіть якщо значення містять бектики.
50
- const closeAt = rest.indexOf(`\n${fence}`)
51
- const json = closeAt === -1 ? rest : rest.slice(0, closeAt)
52
- try {
53
- const parsed = JSON.parse(json)
54
- return Array.isArray(parsed) ? parsed : []
55
- } catch {
56
- return []
57
- }
58
- }
59
-
60
- /**
61
- * Читає `COVERAGE.md` із кореня проєкту і повертає структуровані групи вцілілих.
62
- * @param {string} cwd корінь проєкту
63
- * @returns {Promise<import('./coverage-fix.mjs').SurvivedFileGroup[]>} групи вцілілих по файлах
64
- */
65
- export async function readSurvived(cwd) {
66
- let md
67
- try {
68
- md = await readFile(join(cwd, 'COVERAGE.md'), 'utf8')
69
- } catch {
70
- return []
71
- }
72
- return parseSurvivedBlock(md)
73
- }
74
-
75
- /**
76
- * Згортає групи вцілілих у компактний index `[{file, mutants}]`.
77
- * @param {import('./coverage-fix.mjs').SurvivedFileGroup[]} survived групи вцілілих
78
- * @returns {Array<{file:string, mutants:number}>} файл → кількість вцілілих мутантів
79
- */
80
- export function buildIndex(survived) {
81
- return survived
82
- .filter(group => group && typeof group.file === 'string' && Array.isArray(group.mutants))
83
- .map(group => ({ file: group.file, mutants: group.mutants.length }))
84
- }
85
-
86
- const USAGE = 'Usage: n-cursor coverage-fix <index | slice --file <path>>'
87
-
88
- /**
89
- * CLI: `index` друкує компактний JSON-масив, `slice --file <path>` — промпт для
90
- * одного файлу. Обидва read-only (читають лише COVERAGE.md).
91
- * @param {string[]} args аргументи після `coverage-fix`
92
- * @param {string} [cwd] корінь проєкту (ін'єкція для тестів)
93
- * @returns {Promise<number>} exit code
94
- */
95
- export async function runCoverageFixCli(args, cwd = process.cwd()) {
96
- const sub = args[0]
97
- const survived = await readSurvived(cwd)
98
-
99
- if (sub === 'index') {
100
- process.stdout.write(`${JSON.stringify(buildIndex(survived))}\n`)
101
- return 0
102
- }
103
-
104
- if (sub === 'slice') {
105
- const flagAt = args.indexOf('--file')
106
- const file = flagAt === -1 ? undefined : args[flagAt + 1]
107
- if (!file) {
108
- console.error(USAGE)
109
- return 1
110
- }
111
- const group = survived.find(g => g && g.file === file)
112
- if (!group) {
113
- console.error(`✗ Файл не знайдено серед вцілілих мутантів: ${file}`)
114
- return 1
115
- }
116
- process.stdout.write(`${await buildFixPrompt([group], cwd)}\n`)
117
- return 0
118
- }
119
-
120
- console.error(USAGE)
121
- return 1
122
- }