@nitra/cursor 12.11.0 → 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 (71) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/n-cursor.js +9 -27
  3. package/package.json +1 -1
  4. package/rules/adr/js/docs/hooks.md +0 -2
  5. package/rules/bun/js/docs/fix-layout.md +25 -0
  6. package/rules/bun/js/fix-layout.mjs +55 -0
  7. package/rules/changelog/js/docs/consistency.md +11 -13
  8. package/rules/changelog/js/docs/fix-consistency.md +27 -0
  9. package/rules/changelog/js/docs/index.md +2 -2
  10. package/rules/changelog/js/fix-consistency.mjs +50 -0
  11. package/rules/ci4/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  12. package/rules/ci4/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  13. package/rules/ga/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  14. package/rules/ga/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  15. package/rules/ga/policy/workflow_common/workflow_common.rego +15 -0
  16. package/rules/graphql/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  17. package/rules/graphql/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  18. package/rules/js/js/docs/dep-policy.md +12 -10
  19. package/rules/js/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  20. package/rules/js/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  21. package/rules/js-run/js/docs/fix-runtime.md +25 -0
  22. package/rules/js-run/js/fix-runtime.mjs +41 -0
  23. package/rules/k8s/policy/lint_k8s_yml/lint_k8s_yml.rego +57 -0
  24. package/rules/k8s/policy/lint_k8s_yml/target.json +4 -0
  25. package/rules/k8s/policy/lint_k8s_yml/template/lint-k8s.yml.snippet.yml +43 -0
  26. package/rules/nginx-default-tpl/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  27. package/rules/nginx-default-tpl/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  28. package/rules/rego/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  29. package/rules/rego/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  30. package/rules/rust/policy/vscode_extensions/docs/fix-vscode_extensions.md +22 -0
  31. package/rules/rust/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  32. package/rules/style/js/docs/fix-tooling.md +29 -0
  33. package/rules/style/js/fix-tooling.mjs +46 -0
  34. package/rules/style/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  35. package/rules/style/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  36. package/rules/tauri/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  37. package/rules/tauri/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  38. package/rules/text/policy/vscode_extensions/docs/fix-vscode_extensions.md +21 -0
  39. package/rules/text/policy/vscode_extensions/fix-vscode_extensions.mjs +1 -0
  40. package/rules/vue/js/docs/packages.md +0 -2
  41. package/scripts/docs/index.md +0 -2
  42. package/scripts/lib/discover-checkable-rules.mjs +1 -0
  43. package/scripts/lib/docs/discover-checkable-rules.md +13 -155
  44. package/scripts/lib/fix/discover-t0-patterns.mjs +83 -0
  45. package/scripts/lib/fix/docs/discover-t0-patterns.md +37 -0
  46. package/scripts/lib/fix/docs/llm-fix-apply.md +12 -10
  47. package/scripts/lib/fix/docs/llm-worker.md +6 -14
  48. package/scripts/lib/fix/docs/orchestrator.md +0 -2
  49. package/scripts/lib/fix/docs/t0.md +11 -10
  50. package/scripts/lib/fix/docs/vscode-ext-add.md +29 -0
  51. package/scripts/lib/fix/llm-fix-apply.mjs +34 -3
  52. package/scripts/lib/fix/llm-worker.mjs +24 -15
  53. package/scripts/lib/fix/t0.mjs +8 -119
  54. package/scripts/lib/fix/vscode-ext-add.mjs +45 -0
  55. package/rules/test/coverage/coverage.mjs +0 -317
  56. package/scripts/coverage-classify/apply.mjs +0 -67
  57. package/scripts/coverage-classify/cache.mjs +0 -77
  58. package/scripts/coverage-classify/docs/apply.md +0 -206
  59. package/scripts/coverage-classify/docs/cache.md +0 -207
  60. package/scripts/coverage-classify/docs/index.md +0 -14
  61. package/scripts/coverage-classify/docs/prompt.md +0 -136
  62. package/scripts/coverage-classify/docs/verdict-schema.md +0 -28
  63. package/scripts/coverage-classify/index.mjs +0 -114
  64. package/scripts/coverage-classify/prompt.mjs +0 -126
  65. package/scripts/coverage-classify/verdict-schema.mjs +0 -35
  66. package/scripts/coverage-fix-extract.mjs +0 -122
  67. package/scripts/coverage-fix.mjs +0 -119
  68. package/scripts/docs/coverage-fix-extract.md +0 -36
  69. package/scripts/docs/coverage-fix.md +0 -181
  70. package/skills/coverage-fix/SKILL.md +0 -131
  71. package/skills/coverage-fix/main.json +0 -1
@@ -1,207 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: cache.mjs
4
- resource: npm/scripts/coverage-classify/cache.mjs
5
- docgen:
6
- crc: 53b251b1
7
- ---
8
-
9
- Модуль `cache.mjs` реалізує **file-hash-keyed cache** для вердиктів класифікатора покриття мутаційного тестування (`coverage-classify`). Призначення — уникати повторної (зазвичай дорогої — через LLM) класифікації того ж самого мутанта в незмінному файлі.
10
-
11
- Ключова ідея кешу:
12
-
13
- - Ключ кешу формується з трьох компонентів:
14
- 1. `blob-hash` — sha1-хеш контенту source-файла (отримується через `git hash-object` з fallback на власне sha1 від `readFileSync`).
15
- 2. Координати мутанта в файлі: `line:col`.
16
- 3. `base64url` від рядка-replacement мутанта (щоб ключ був безпечним для будь-яких символів).
17
- - Формат ключа: `` `<blob-hash>:<line>:<col>:<base64url(replacement)>` ``.
18
-
19
- Логіка інвалідації:
20
-
21
- - Будь-яка зміна source-файла → новий `blob-hash` → старий ключ більше ніколи не співпадає → `cache miss` → мутант буде перекласифіковано.
22
- - Інвалідація автоматична: жодного TTL, версіонування user-input або ручного очищення не потрібно.
23
-
24
- Схема кешу на диску:
25
-
26
- ```json
27
- {
28
- "version": 1,
29
- "model": "string|null",
30
- "entries": {
31
- "<key>": {
32
- "verdict": "...",
33
- "confidence": "...",
34
- "reason": "...",
35
- "suggestedTest": "...",
36
- "classifiedAt": "..."
37
- }
38
- }
39
- }
40
- ```
41
-
42
- Поле `version` використовується як schema-guard: при зміні константи `CACHE_VERSION` всі старі файли кешу автоматично трактуються як порожні (без помилок), що дозволяє безпечно еволюціонувати схему.
43
-
44
- ## Експорти / API
45
-
46
- Модуль експортує три іменовані функції (ESM):
47
-
48
- | Функція | Призначення |
49
- | ---------------------------------- | ------------------------------------------------------------------------------ |
50
- | `deriveBlobHash(filePath)` | Обчислити sha1-хеш контенту файла. |
51
- | `deriveCacheKey(filePath, mutant)` | Сформувати повний ключ кешу для конкретного мутанта в конкретному стані файла. |
52
- | `readCache(cachePath)` | Безпечно прочитати cache з диска з порожнім fallback. |
53
- | `writeCache(cachePath, cache)` | Записати cache на диск (з автостворенням батьківських директорій). |
54
-
55
- Внутрішня (не експортована) константа:
56
-
57
- - `CACHE_VERSION = 1` — поточна версія schema.
58
-
59
- ## Функції
60
-
61
- ### `deriveBlobHash(filePath)`
62
-
63
- Обчислює 40-символьний hex sha1-хеш контенту файла.
64
-
65
- - **Сигнатура:** `deriveBlobHash(filePath: string): string | null`
66
- - **Параметри:**
67
- - `filePath` — абсолютний шлях до source-файла.
68
- - **Повертає:**
69
- - 40-символьний hex-рядок (sha1) — якщо файл прочитано.
70
- - `null` — якщо файл не існує (`existsSync` повернув `false`).
71
- - **Алгоритм:**
72
- 1. Якщо файл відсутній — повернути `null`.
73
- 2. Спробувати викликати `git hash-object <file>` через `execFileSync` з кодуванням `utf8` і обрізати whitespace (`.trim()`). Це детерміністичний хеш в межах working tree; точно такий же хеш Git використовує внутрішньо для blob-ів.
74
- 3. Якщо `git` недоступний або кинув помилку — fallback: прочитати файл (`readFileSync`) і обчислити `createHash('sha1').update(content).digest('hex')`.
75
- - **Side effects:** виконує зовнішній процес `git`; читає файл (у fallback-гілці).
76
- - **Чому два шляхи:** `git hash-object` працює швидше для великих репозиторіїв і додає до хешу префікс `blob <size>\0` як справжній Git. Однак скрипт може бути запущений у середовищі без `git` (CI worker, контейнер) — звідси sha1-fallback. Для двох однакових файлів обидва методи дають **різні** хеші, але це **не критично**, бо ключ кешу самосумісний у межах одного запуску — головне, щоб хеш був детерміністичним для тих самих байтів.
77
-
78
- ### `deriveCacheKey(filePath, mutant)`
79
-
80
- Формує ключ кешу для конкретного мутанта в конкретному стані файла.
81
-
82
- - **Сигнатура:** `deriveCacheKey(filePath: string, mutant: { line: number, col: number, replacement: string }): string | null`
83
- - **Параметри:**
84
- - `filePath` — абсолютний шлях до source-файла, в якому міститься мутант.
85
- - `mutant` — об'єкт-опис мутанта:
86
- - `line: number` — номер рядка (1-based, як прийнято в Stryker/інших мутаторах).
87
- - `col: number` — номер колонки.
88
- - `replacement: string` — рядок-заміна, який мутатор вставляє замість оригінального коду.
89
- - **Повертає:**
90
- - Рядок виду `` `<sha1>:<line>:<col>:<base64url>` ``.
91
- - `null` — якщо `deriveBlobHash` повернув `null` (файл недоступний → ключ створити неможливо).
92
- - **Алгоритм:**
93
- 1. Отримати `blobHash` через `deriveBlobHash(filePath)`.
94
- 2. Якщо `null` — повернути `null` (попереджаючий контракт: caller має обробити cache-miss).
95
- 3. Закодувати `mutant.replacement` як `base64url` (без `=` padding і без `/`/`+`, що робить його безпечним і для filename, і для key).
96
- 4. Зібрати ключ template-literal-ом.
97
- - **Side effects:** ті ж, що й у `deriveBlobHash` (виклик `git`/`readFileSync`).
98
- - **Чому `base64url`:** `replacement` може містити будь-які символи (двокрапки, переноси рядків, юнікод). `base64url` гарантує однорядковий ASCII-ключ без колізій по роздільнику `:`.
99
-
100
- ### `readCache(cachePath)`
101
-
102
- Безпечно читає cache з диска. Будь-яка аномалія (відсутність файла, битий JSON, чужа схема) **не кидає** помилку, а повертає порожній cache.
103
-
104
- - **Сигнатура:** `readCache(cachePath: string): { version: number, model: string | null, entries: Record<string, object> }`
105
- - **Параметри:**
106
- - `cachePath` — абсолютний шлях до файла `cache.json`.
107
- - **Повертає:** об'єкт cache-схеми. Або реальні дані з диска, або **empty cache**:
108
-
109
- ```js
110
- { version: CACHE_VERSION, model: null, entries: {} }
111
- ```
112
-
113
- - **Алгоритм (5 умов повернення empty):**
114
- 1. Файл не існує (`!existsSync(cachePath)`) → empty.
115
- 2. `JSON.parse` кинув виняток → empty (catch).
116
- 3. `data?.version !== CACHE_VERSION` (включно з `data === null`/`undefined`) → empty.
117
- 4. `!data.entries` → empty.
118
- 5. `typeof data.entries !== 'object'` або `Array.isArray(data.entries)` → empty.
119
- 6. Інакше повернути `data` як є.
120
- - **Side effects:** читає файл з диска (`readFileSync` з кодуванням `utf8`).
121
- - **Інваріант:** ніколи не кидає винятки — гарантовано повертає валідний cache-об'єкт.
122
-
123
- ### `writeCache(cachePath, cache)`
124
-
125
- Серіалізує cache в JSON і записує на диск.
126
-
127
- - **Сигнатура:** `writeCache(cachePath: string, cache: { version: number, model: string | null, entries: Record<string, object> }): void`
128
- - **Параметри:**
129
- - `cachePath` — абсолютний шлях, куди писати (наприклад `<repo>/.cache/coverage-classify/cache.json`).
130
- - `cache` — cache-об'єкт у відповідності до schema.
131
- - **Повертає:** `void`.
132
- - **Алгоритм:**
133
- 1. `mkdirSync(dirname(cachePath), { recursive: true })` — гарантовано створює всі батьківські директорії; не падає, якщо вони вже існують.
134
- 2. `writeFileSync(cachePath, JSON.stringify(cache, null, 2) + '\n', 'utf8')` — двопробільний indent для людиночитності + trailing newline (POSIX-конвенція).
135
- - **Side effects:** створює директорії, перезаписує файл повністю (атомарність не гарантується — це звичайний `writeFileSync`).
136
-
137
- ## Залежності
138
-
139
- Лише модулі стандартної бібліотеки Node.js (ESM):
140
-
141
- | Імпорт | Звідки | Призначення |
142
- | ---------------------------------------------------------- | -------------------- | ------------------------------------------------------------------ |
143
- | `execFileSync` | `node:child_process` | Виклик `git hash-object` як зовнішнього процесу. |
144
- | `createHash` | `node:crypto` | sha1-fallback, якщо `git` недоступний. |
145
- | `existsSync`, `mkdirSync`, `readFileSync`, `writeFileSync` | `node:fs` | Sync-FS-операції для cache та source-файлів. |
146
- | `dirname` | `node:path` | Виокремити батьківську директорію з cache-шляху перед `mkdirSync`. |
147
-
148
- Зовнішніх npm-залежностей **немає**. Усі імпорти — з prefix `node:` (рекомендований формат для Node.js core-модулів).
149
-
150
- Опціональна зовнішня залежність: бінарник `git` у `$PATH`. Якщо його немає — модуль автоматично переходить на sha1-fallback.
151
-
152
- ## Потік виконання / Використання
153
-
154
- Типовий sequence використання у класифікаторі:
155
-
156
- 1. На старті прогону: `cache = readCache(cachePath)`.
157
- 2. Для кожного мутанта:
158
- 1. `key = deriveCacheKey(filePath, mutant)`.
159
- 2. Якщо `key === null` — пропустити (файл-джерело недоступний) або повторити пізніше.
160
- 3. Якщо `cache.entries[key]` існує — використати збережений verdict, **не** викликати LLM.
161
- 4. Інакше — викликати класифікатор (LLM/heuristic), отримати `verdict` і записати:
162
- ```js
163
- cache.entries[key] = { verdict, confidence, reason, suggestedTest, classifiedAt: new Date().toISOString() }
164
- ```
165
- 3. На завершенні прогону: `writeCache(cachePath, cache)`.
166
-
167
- Граничні випадки та їх обробка:
168
-
169
- - **Source файл видалили** → `deriveBlobHash` повертає `null` → `deriveCacheKey` повертає `null`. Caller повинен пропустити кешування.
170
- - **Git недоступний** → автоматичний fallback на sha1. Ключі будуть відрізнятися від ключів, отриманих через `git hash-object` для того ж файла. Тому при міграції оточення можливий повний cache-miss — але це безпечно (просто повторна класифікація).
171
- - **Cache-файл пошкоджений** → `readCache` повертає empty cache. Жодних винятків. Старий битий вміст буде перезаписаний при наступному `writeCache`.
172
- - **Зміна `CACHE_VERSION`** → всі попередні файли кешу мовчки трактуються як empty. Безпечний шлях для еволюції schema.
173
-
174
- Приклад мінімального використання:
175
-
176
- ```js
177
- import { readCache, writeCache, deriveCacheKey } from './cache.mjs'
178
-
179
- const cachePath = '/abs/path/to/cache.json'
180
- const cache = readCache(cachePath)
181
-
182
- const key = deriveCacheKey('/abs/path/to/src/foo.js', { line: 10, col: 5, replacement: '' })
183
- if (key && cache.entries[key]) {
184
- // Cache hit — використати готовий verdict.
185
- const verdict = cache.entries[key]
186
- console.log(verdict)
187
- } else if (key) {
188
- // Cache miss — викликати класифікатор і зберегти.
189
- const verdict = await classify(/* ... */)
190
- cache.entries[key] = { ...verdict, classifiedAt: new Date().toISOString() }
191
- }
192
-
193
- writeCache(cachePath, cache)
194
- ```
195
-
196
- ## Rebuild Test
197
-
198
- Розумова перевірка: чи можна за цією документацією відтворити модуль без читання source? Так:
199
-
200
- - Заявлені 4 експорти (`deriveBlobHash`, `deriveCacheKey`, `readCache`, `writeCache`) — всі описані з сигнатурами, типами параметрів, повертанням, side effects і алгоритмом.
201
- - Внутрішня константа `CACHE_VERSION = 1` згадана.
202
- - Schema cache-файла наведена.
203
- - Алгоритм формування ключа (3 компоненти + base64url для replacement) пояснений однозначно.
204
- - Fallback-логіка `git hash-object` → `sha1(readFileSync)` описана.
205
- - Поведінка `readCache` при кожному типі помилки (5 пунктів) перерахована.
206
- - Імпорти node-core модулів перелічені з їх роллю.
207
- - Жодного зовнішнього npm-пакета не використано.
@@ -1,14 +0,0 @@
1
- ---
2
- type: JS Module
3
- title: index.mjs
4
- resource: npm/scripts/coverage-classify/index.mjs
5
- docgen:
6
- crc: 06249ac8
7
- ---
8
-
9
- | Файл | Тип | Опис |
10
- | --------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------ |
11
- | [apply.mjs](apply.md) | JS Module | Модуль apply.mjs із пакета coverage-classify відповідає за \*\*застосування вердиктів класифікатора му |
12
- | [cache.mjs](cache.md) | JS Module | Модуль cache.mjs реалізує **file-hash-keyed cache** для вердиктів класифікатора покриття мутаційного |
13
- | [prompt.mjs](prompt.md) | JS Module | Модуль prompt.mjs — це prompt-builder для скрипта coverage-classify, що класифікує вцілілих мутантів |
14
- | [verdict-schema.mjs](verdict-schema.md) | JS Module | Файл надає схему VerdictSchema для валідації вердиктів LLM-класифікатора. |
@@ -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
- }