@nitra/cursor 12.11.1 → 12.11.3
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.
- package/CHANGELOG.md +12 -0
- package/bin/n-cursor.js +0 -15
- package/package.json +1 -1
- package/rules/adr/js/docs/hooks.md +0 -2
- package/rules/bun/docs/index.md +2 -2
- package/rules/bun/docs/main.md +9 -9
- package/rules/bun/js/docs/fix-layout.md +25 -0
- package/rules/bun/js/docs/index.md +3 -2
- package/rules/bun/js/fix-layout.mjs +55 -0
- package/rules/bun/main.json +1 -1
- package/rules/bun/main.mjs +53 -3
- package/rules/changelog/js/docs/fix-consistency.md +27 -0
- package/rules/changelog/js/docs/index.md +1 -0
- package/rules/changelog/js/fix-consistency.mjs +50 -0
- package/rules/ci4/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/ci4/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/ci4/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/ga/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
- package/rules/ga/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/ga/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/graphql/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/graphql/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/graphql/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/js/js/dep-policy.mdc +19 -0
- package/rules/js/js/dep-policy.mjs +14 -6
- package/rules/js/js/docs/dep-policy.md +12 -10
- package/rules/js/js/docs/index.md +5 -5
- package/rules/js/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
- package/rules/js/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/js/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/js-run/js/docs/fix-runtime.md +25 -0
- package/rules/js-run/js/docs/index.md +3 -2
- package/rules/js-run/js/fix-runtime.mjs +41 -0
- package/rules/nginx-default-tpl/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/nginx-default-tpl/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/nginx-default-tpl/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/python/docs/index.md +2 -2
- package/rules/python/docs/main.md +12 -10
- package/rules/python/main.mjs +1 -0
- package/rules/rego/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
- package/rules/rego/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/rego/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/rust/docs/index.md +2 -2
- package/rules/rust/docs/main.md +8 -6
- package/rules/rust/main.mjs +11 -0
- package/rules/rust/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
- package/rules/rust/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/rust/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/style/js/docs/fix-tooling.md +29 -0
- package/rules/style/js/docs/index.md +3 -2
- package/rules/style/js/fix-tooling.mjs +46 -0
- package/rules/style/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/style/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/style/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/tauri/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/tauri/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/tauri/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/text/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
- package/rules/text/policy/vscode_extensions/docs/index.md +11 -0
- package/rules/text/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
- package/rules/vue/js/docs/packages.md +0 -2
- package/scripts/docs/index.md +0 -2
- package/scripts/lib/discover-checkable-rules.mjs +1 -0
- package/scripts/lib/docs/discover-checkable-rules.md +13 -155
- package/scripts/lib/docs/index.md +35 -35
- package/scripts/lib/fix/discover-t0-patterns.mjs +83 -0
- package/scripts/lib/fix/docs/discover-t0-patterns.md +37 -0
- package/scripts/lib/fix/docs/index.md +12 -10
- package/scripts/lib/fix/docs/llm-fix-apply.md +12 -10
- package/scripts/lib/fix/docs/llm-worker.md +6 -14
- package/scripts/lib/fix/docs/orchestrator.md +0 -2
- package/scripts/lib/fix/docs/t0.md +11 -10
- package/scripts/lib/fix/docs/vscode-ext-add.md +29 -0
- package/scripts/lib/fix/t0.mjs +8 -234
- package/scripts/lib/fix/vscode-ext-add.mjs +45 -0
- package/skills/doc-aggregate/SKILL.md +8 -18
- package/skills/doc-aggregate/js/docs/index.md +0 -1
- package/rules/test/coverage/coverage.mjs +0 -317
- package/scripts/coverage-classify/apply.mjs +0 -67
- package/scripts/coverage-classify/cache.mjs +0 -77
- package/scripts/coverage-classify/docs/apply.md +0 -206
- package/scripts/coverage-classify/docs/cache.md +0 -207
- package/scripts/coverage-classify/docs/index.md +0 -14
- package/scripts/coverage-classify/docs/prompt.md +0 -136
- package/scripts/coverage-classify/docs/verdict-schema.md +0 -28
- package/scripts/coverage-classify/index.mjs +0 -114
- package/scripts/coverage-classify/prompt.mjs +0 -126
- package/scripts/coverage-classify/verdict-schema.mjs +0 -35
- package/scripts/coverage-fix-extract.mjs +0 -122
- package/scripts/coverage-fix.mjs +0 -119
- package/scripts/docs/coverage-fix-extract.md +0 -36
- package/scripts/docs/coverage-fix.md +0 -181
- package/skills/coverage-fix/SKILL.md +0 -131
- package/skills/coverage-fix/main.json +0 -1
- package/skills/doc-aggregate/js/docgen-scan.mjs +0 -195
- package/skills/doc-aggregate/js/docs/docgen-scan.md +0 -76
|
@@ -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
|
-
}
|