@nitra/cursor 3.21.1 → 3.23.0

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 (231) hide show
  1. package/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
  2. package/CHANGELOG.md +37 -3
  3. package/bin/docs/n-cursor.md +636 -0
  4. package/bin/docs/rename-yaml-extensions.md +207 -0
  5. package/bin/n-cursor.js +30 -3
  6. package/package.json +1 -1
  7. package/rules/abie/docs/fix.md +18 -0
  8. package/rules/abie/js/docs/applies.md +26 -0
  9. package/rules/abie/js/docs/env_dns.md +32 -0
  10. package/rules/abie/js/docs/firebase_hosting.md +23 -0
  11. package/rules/abie/js/docs/hc_pairing.md +35 -0
  12. package/rules/abie/js/docs/ua_http_route.md +28 -0
  13. package/rules/abie/js/docs/ua_node_selector.md +28 -0
  14. package/rules/abie/lib/docs/enabled.md +29 -0
  15. package/rules/abie/lib/docs/env-dns.md +35 -0
  16. package/rules/abie/lib/docs/hc-yaml.md +33 -0
  17. package/rules/abie/lib/docs/http-route.md +44 -0
  18. package/rules/abie/lib/docs/k8s-tree.md +40 -0
  19. package/rules/abie/lib/docs/kustomization-patches.md +47 -0
  20. package/rules/abie/lib/docs/overlay-paths.md +38 -0
  21. package/rules/abie/lib/docs/yaml.md +29 -0
  22. package/rules/adr/docs/fix.md +148 -0
  23. package/rules/adr/js/docs/hooks.md +259 -0
  24. package/rules/bun/docs/fix.md +156 -0
  25. package/rules/bun/js/docs/layout.md +393 -0
  26. package/rules/capacitor/docs/fix.md +121 -0
  27. package/rules/capacitor/js/docs/platforms.md +295 -0
  28. package/rules/changelog/changelog.mdc +2 -2
  29. package/rules/changelog/docs/fix.md +174 -0
  30. package/rules/changelog/js/consistency.mjs +114 -13
  31. package/rules/changelog/js/docs/consistency.md +387 -0
  32. package/rules/changelog/lib/docs/package-manifest.md +210 -0
  33. package/rules/ci4/docs/fix.md +179 -0
  34. package/rules/ci4/js/docs/marksman_config.md +128 -0
  35. package/rules/docker/docker.mdc +8 -3
  36. package/rules/docker/docs/fix.md +171 -0
  37. package/rules/docker/js/docs/lint.md +258 -0
  38. package/rules/docker/lib/docs/docker-hadolint.md +184 -0
  39. package/rules/docker/lib/docs/docker-mirror.md +247 -0
  40. package/rules/docker/lib/docs/docker-native-addon.md +170 -0
  41. package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
  42. package/rules/docker/lint/docs/lint.md +193 -0
  43. package/rules/efes/docs/fix.md +203 -0
  44. package/rules/feedback/docs/fix.md +140 -0
  45. package/rules/flow/docs/fix.md +152 -0
  46. package/rules/ga/docs/fix.md +158 -0
  47. package/rules/ga/js/docs/lint.md +100 -0
  48. package/rules/ga/js/docs/workflows.md +217 -0
  49. package/rules/ga/lint/docs/lint.md +209 -0
  50. package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
  51. package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
  52. package/rules/graphql/docs/fix.md +126 -0
  53. package/rules/graphql/js/docs/tooling.md +264 -0
  54. package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
  55. package/rules/hasura/docs/fix.md +120 -0
  56. package/rules/hasura/hasura.mdc +14 -0
  57. package/rules/hasura/js/docs/internal_urls.md +326 -0
  58. package/rules/image-avif/docs/fix.md +132 -0
  59. package/rules/image-avif/js/docs/avif_generation.md +241 -0
  60. package/rules/image-compress/docs/fix.md +150 -0
  61. package/rules/image-compress/js/docs/package_setup.md +191 -0
  62. package/rules/js-bun-db/docs/fix.md +148 -0
  63. package/rules/js-bun-db/js/docs/safety.md +231 -0
  64. package/rules/js-bun-db/js-bun-db.mdc +42 -13
  65. package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
  66. package/rules/js-bun-redis/docs/fix.md +123 -0
  67. package/rules/js-bun-redis/js/docs/imports.md +176 -0
  68. package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
  69. package/rules/js-lint/docs/fix.md +117 -0
  70. package/rules/js-lint/js/docs/lint.md +250 -0
  71. package/rules/js-lint/js/docs/tooling.md +348 -0
  72. package/rules/js-lint/js/docs/utils_imports.md +207 -0
  73. package/rules/js-lint/js/lint-findings.mjs +110 -0
  74. package/rules/js-lint/js/lint.mjs +86 -15
  75. package/rules/js-lint-ci/docs/fix.md +154 -0
  76. package/rules/js-lint-ci/js/docs/lint.md +144 -0
  77. package/rules/js-mssql/docs/fix.md +128 -0
  78. package/rules/js-mssql/js/docs/deps.md +263 -0
  79. package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
  80. package/rules/js-run/docs/fix.md +144 -0
  81. package/rules/js-run/js/docs/runtime.md +388 -0
  82. package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
  83. package/rules/js-run/lib/docs/check-env-scan.md +433 -0
  84. package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
  85. package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
  86. package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
  87. package/rules/k8s/docs/fix.md +129 -0
  88. package/rules/k8s/js/docs/manifests.md +344 -0
  89. package/rules/k8s/js/manifests.mjs +6 -2
  90. package/rules/k8s/k8s.mdc +4 -2
  91. package/rules/k8s/lint/docs/lint.md +411 -0
  92. package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
  93. package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
  94. package/rules/nginx-default-tpl/docs/fix.md +124 -0
  95. package/rules/nginx-default-tpl/js/docs/template.md +378 -0
  96. package/rules/npm-module/docs/fix.md +98 -0
  97. package/rules/npm-module/js/docs/package_structure.md +274 -0
  98. package/rules/npm-module/js/docs/rule_meta.md +137 -0
  99. package/rules/npm-module/js/docs/skill_meta.md +190 -0
  100. package/rules/php/docs/fix.md +107 -0
  101. package/rules/php/js/docs/tooling.md +152 -0
  102. package/rules/php/lint/docs/lint.md +215 -0
  103. package/rules/python/docs/fix.md +163 -0
  104. package/rules/python/js/docs/applies.md +108 -0
  105. package/rules/python/js/docs/tooling.md +153 -0
  106. package/rules/python/lint/docs/lint.md +322 -0
  107. package/rules/rego/docs/fix.md +121 -0
  108. package/rules/rego/js/docs/applies.md +174 -0
  109. package/rules/rego/js/docs/lint.md +118 -0
  110. package/rules/rego/lint/docs/lint.md +204 -0
  111. package/rules/release/docs/change.md +185 -0
  112. package/rules/release/docs/fix.md +119 -0
  113. package/rules/release/docs/release.md +222 -0
  114. package/rules/release/lib/docs/aggregate.md +246 -0
  115. package/rules/release/lib/docs/change-file.md +200 -0
  116. package/rules/release/lib/docs/fallback.md +203 -0
  117. package/rules/rust/docs/fix.md +129 -0
  118. package/rules/rust/js/docs/applies.md +140 -0
  119. package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
  120. package/rules/security/docs/fix.md +86 -0
  121. package/rules/security/js/docs/lint.md +171 -0
  122. package/rules/security/js/docs/sample_secret.md +190 -0
  123. package/rules/security/js/docs/trufflehog.md +137 -0
  124. package/rules/security/js/lint.mjs +9 -1
  125. package/rules/style-lint/docs/fix.md +155 -0
  126. package/rules/style-lint/js/docs/lint.md +184 -0
  127. package/rules/style-lint/js/docs/tooling.md +194 -0
  128. package/rules/tauri/docs/fix.md +158 -0
  129. package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
  130. package/rules/tauri/js/docs/tooling.md +228 -0
  131. package/rules/test/coverage/coverage.mjs +15 -3
  132. package/rules/test/docs/fix.md +132 -0
  133. package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
  134. package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
  135. package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
  136. package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
  137. package/rules/test/js/docs/cargo_mutants_config.md +173 -0
  138. package/rules/test/js/docs/location.md +136 -0
  139. package/rules/test/js/docs/no-process-chdir.md +160 -0
  140. package/rules/test/js/docs/no-relative-fs-path.md +271 -0
  141. package/rules/test/js/docs/stryker_config.md +152 -0
  142. package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
  143. package/rules/text/docs/fix.md +118 -0
  144. package/rules/text/js/docs/forbidden-prettier.md +143 -0
  145. package/rules/text/js/docs/formatting.md +256 -0
  146. package/rules/text/js/docs/lint.md +122 -0
  147. package/rules/text/lint/docs/lint.md +220 -0
  148. package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
  149. package/rules/text/lint/docs/run-shellcheck.md +212 -0
  150. package/rules/text/lint/docs/run-v8r.md +197 -0
  151. package/rules/vue/docs/fix.md +127 -0
  152. package/rules/vue/js/docs/packages.md +335 -0
  153. package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
  154. package/rules/worktree/docs/fix.md +161 -0
  155. package/schemas/rule-meta.json +5 -1
  156. package/scripts/auto-rules.mjs +7 -4
  157. package/scripts/coverage-classify/docs/apply.md +202 -0
  158. package/scripts/coverage-classify/docs/cache.md +203 -0
  159. package/scripts/coverage-classify/docs/index.md +218 -0
  160. package/scripts/coverage-classify/docs/prompt.md +132 -0
  161. package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
  162. package/scripts/coverage-fix-extract.mjs +122 -0
  163. package/scripts/coverage-fix.mjs +1 -1
  164. package/scripts/dispatcher/docs/graph.md +346 -0
  165. package/scripts/dispatcher/docs/index.md +236 -0
  166. package/scripts/dispatcher/docs/trace.md +296 -0
  167. package/scripts/dispatcher/index.mjs +1 -1
  168. package/scripts/dispatcher/lib/active.mjs +4 -8
  169. package/scripts/dispatcher/lib/commands.mjs +7 -11
  170. package/scripts/dispatcher/lib/docs/active.md +348 -0
  171. package/scripts/dispatcher/lib/docs/artifact.md +232 -0
  172. package/scripts/dispatcher/lib/docs/budget.md +167 -0
  173. package/scripts/dispatcher/lib/docs/capability.md +196 -0
  174. package/scripts/dispatcher/lib/docs/commands.md +210 -0
  175. package/scripts/dispatcher/lib/docs/events.md +182 -0
  176. package/scripts/dispatcher/lib/docs/executor.md +190 -0
  177. package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
  178. package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
  179. package/scripts/dispatcher/lib/docs/gate.md +231 -0
  180. package/scripts/dispatcher/lib/docs/level.md +335 -0
  181. package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
  182. package/scripts/dispatcher/lib/docs/plan.md +200 -0
  183. package/scripts/dispatcher/lib/docs/planner.md +269 -0
  184. package/scripts/dispatcher/lib/docs/review.md +255 -0
  185. package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
  186. package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
  187. package/scripts/dispatcher/lib/docs/spec.md +203 -0
  188. package/scripts/dispatcher/lib/docs/state-store.md +303 -0
  189. package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
  190. package/scripts/dispatcher/lib/executor.mjs +6 -1
  191. package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
  192. package/scripts/dispatcher/lib/level.mjs +29 -3
  193. package/scripts/dispatcher/lib/review.mjs +1 -1
  194. package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
  195. package/scripts/docs/auto-rules.md +376 -0
  196. package/scripts/docs/auto-skills.md +173 -0
  197. package/scripts/docs/build-agents-commands.md +183 -0
  198. package/scripts/docs/cli-entry.md +153 -0
  199. package/scripts/docs/coverage-fix.md +177 -0
  200. package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
  201. package/scripts/lib/changed-files.mjs +4 -1
  202. package/scripts/lib/diff-added-lines.mjs +85 -0
  203. package/scripts/lib/docs/changed-files.md +149 -0
  204. package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
  205. package/scripts/lib/docs/check-reporter.md +175 -0
  206. package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
  207. package/scripts/lib/docs/discover-checkable-rules.md +165 -0
  208. package/scripts/lib/docs/ensure-tool.md +254 -0
  209. package/scripts/lib/docs/generated-markdown.md +275 -0
  210. package/scripts/lib/docs/gha-workflow.md +326 -0
  211. package/scripts/lib/docs/inline-template-links.md +303 -0
  212. package/scripts/lib/docs/list-rule-ids.md +156 -0
  213. package/scripts/lib/docs/load-cursor-config.md +147 -0
  214. package/scripts/lib/docs/mirror-parity.md +167 -0
  215. package/scripts/lib/worktree.mjs +26 -0
  216. package/scripts/worktree-cli.mjs +12 -2
  217. package/skills/coverage-fix/SKILL.md +34 -45
  218. package/skills/docgen/SKILL.md +44 -23
  219. package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
  220. package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
  221. package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
  222. package/skills/docgen/js/docgen-ignore.mjs +54 -0
  223. package/skills/docgen/js/docgen-scan.mjs +37 -21
  224. package/skills/llm-patch/SKILL.md +23 -2
  225. package/skills/start-check/SKILL.md +26 -53
  226. package/skills/start-check/js/check.mjs +211 -0
  227. package/skills/taze/SKILL.md +9 -3
  228. package/skills/taze/js/diff.mjs +154 -0
  229. package/types/bin/n-cursor.d.ts +1 -1
  230. package/skills/fix-tests/SKILL.md +0 -119
  231. package/skills/fix-tests/meta.json +0 -1
@@ -0,0 +1,202 @@
1
+ # apply.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `apply.mjs` із пакета `coverage-classify` відповідає за **застосування вердиктів класифікатора мутаційних розривів** до табличних coverage-рядків. Він фільтрує список «вижилих» мутантів (`survived`) у кожному рядку, ділячи їх на дві категорії:
6
+
7
+ 1. **Allowed gaps** — мутанти, які класифікатор позначив як `equivalent`, `defensive`, `glue` або `wrapper` з рівнем впевненості (`confidence`) не нижче встановленого порогу. Такі мутанти виключаються з підрахунку «killable» (зменшують `mutation.total`) та виносяться в окремий список для подальшого рендеру в `COVERAGE.md`.
8
+ 2. **Залишок (remaining survived)** — все, що варто тестувати (`worth-testing`), а також низько-впевнені `skip`-вердикти. Ці мутанти залишаються у вихідному `row.survived` і впливають на mutation score.
9
+
10
+ Модуль **не мутує вхідні дані** (`rows`, `verdicts`) — повертає нові об'єкти. Це дозволяє безпечно використовувати його у пайплайнах, де ті ж самі рядки можуть оброблятись паралельно або кешуватись.
11
+
12
+ Логіка віднімання `skippedCount` з `mutation.total` зумовлена тим, що allowed-gap-мутанти не є реальною прогалиною в покритті: вони або еквівалентні оригіналу (нічого не ламають), або захисні (стосуються неможливих гілок), або клейові/обгорткові — тобто, тестувати їх економічно невиправдано.
13
+
14
+ ## Експорти / API
15
+
16
+ | Експорт | Тип | Призначення |
17
+ | ------------------------------------------ | -------------- | ---------------------------------------------------------------------------------------------------- |
18
+ | `isAllowedGap(verdict, threshold)` | named function | Перевіряє, чи окремий verdict-об'єкт кваліфікує мутанта як allowed-gap. |
19
+ | `applyVerdicts(rows, verdicts, threshold)` | named function | Застосовує мапу вердиктів до набору coverage-рядків і повертає augmented rows + список allowed-gaps. |
20
+
21
+ Default-експорту немає. Внутрішня константа `SKIP_VERDICTS` не експортується.
22
+
23
+ ## Функції
24
+
25
+ ### `isAllowedGap(verdict, threshold)`
26
+
27
+ **Сигнатура:**
28
+
29
+ ```js
30
+ isAllowedGap(verdict: { verdict: string, confidence: number }, threshold: number): boolean
31
+ ```
32
+
33
+ **Параметри:**
34
+
35
+ - `verdict` — об'єкт із полями:
36
+ - `verdict` (`string`) — категорія вердикту класифікатора. Очікувані значення: `'equivalent' | 'defensive' | 'glue' | 'wrapper' | 'worth-testing'` (та потенційно інші).
37
+ - `confidence` (`number`) — рівень впевненості класифікатора в діапазоні `[0, 1]`.
38
+ - `threshold` (`number`) — мінімальна впевненість, починаючи з якої skip-вердикт визнається allowed-gap (наприклад, `0.7`).
39
+
40
+ **Повертає:** `boolean`. `true` — якщо `verdict.verdict` належить до `SKIP_VERDICTS` (`equivalent`, `defensive`, `glue`, `wrapper`) **і** `verdict.confidence >= threshold`. Інакше — `false`.
41
+
42
+ **Side effects:** немає. Чиста функція.
43
+
44
+ **Гранична поведінка:**
45
+
46
+ - Низько-впевнений skip-verdict (`confidence < threshold`) → `false`, мутант залишиться як survived.
47
+ - `worth-testing` із будь-якою впевненістю → `false`.
48
+ - Невідома категорія, відсутня в `SKIP_VERDICTS` → `false`.
49
+
50
+ ---
51
+
52
+ ### `applyVerdicts(rows, verdicts, threshold)`
53
+
54
+ **Сигнатура:**
55
+
56
+ ```js
57
+ applyVerdicts(
58
+ rows: Array<Row>,
59
+ verdicts: Array<{ key: string, verdict: VerdictObj }>,
60
+ threshold: number
61
+ ): { rows: Array<Row>, allowedGaps: Array<AllowedGap> }
62
+ ```
63
+
64
+ Де:
65
+
66
+ ```ts
67
+ Row = {
68
+ area: string,
69
+ coverage: object,
70
+ mutation: { caught: number, total: number },
71
+ survived?: Array<{
72
+ file: string,
73
+ mutants: Array<Mutant>,
74
+ exampleTest?: object | null,
75
+ recommendationText?: string | null
76
+ }>
77
+ }
78
+
79
+ Mutant = { line: number, col: number, replacement: string, ...rest }
80
+ VerdictObj = { verdict: string, confidence: number, reason: string }
81
+ AllowedGap = { file: string, mutant: Mutant, verdict: VerdictObj }
82
+ ```
83
+
84
+ **Параметри:**
85
+
86
+ - `rows` — масив coverage-рядків (один рядок на «area», тобто workspace/директорію). Кожен рядок містить агреговану статистику мутацій та опційний список `survived`-груп (згрупованих по файлу).
87
+ - `verdicts` — масив об'єктів `{ key, verdict }`, де `key` має формат `${file}:${line}:${col}:${replacement}` і однозначно ідентифікує мутанта.
88
+ - `threshold` — поріг впевненості (передається в `isAllowedGap`).
89
+
90
+ **Повертає:** об'єкт з двома полями:
91
+
92
+ - `rows` (`Array<Row>`) — нові рядки, де:
93
+ - `survived` містить лише ті групи й тих мутантів, які **не** є allowed-gap; порожні групи (де всі мутанти стали allowed-gap) виключаються.
94
+ - `mutation.total` зменшено на сумарну кількість allowed-gap-мутантів у цьому рядку (`skippedCount`).
95
+ - `mutation.caught`, `coverage`, `area` залишаються без змін.
96
+ - Усі інші поля рядка зберігаються через spread (`...row`).
97
+ - `allowedGaps` (`Array<AllowedGap>`) — плоский список (без групування по area/file) усіх мутантів, які класифіковано як allowed-gap, разом із посиланням на файл та оригінальним verdict-об'єктом. Призначений для рендеру окремої секції в `COVERAGE.md`.
98
+
99
+ **Side effects:** немає. Не мутує `rows`, `verdicts`, `verdict`-об'єкти, групи `survived`, окремих мутантів. Кожен новий об'єкт створюється через `{...row}` / `{...group, mutants: remainingMutants}`.
100
+
101
+ **Алгоритм:**
102
+
103
+ 1. Побудувати `Map<key, verdict>` із масиву `verdicts` для O(1)-пошуку.
104
+ 2. Ініціалізувати порожній акумулятор `allowedGaps`.
105
+ 3. Для кожного `row` (через `rows.map(...)`):
106
+ - Прочитати `survived ?? []` (підтримка рядків без поля `survived`).
107
+ - Завести лічильник `skippedCount = 0` і масив `remainingSurvived`.
108
+ - Для кожної `group` із `survived`:
109
+ - Завести `remainingMutants`.
110
+ - Для кожного `mutant` зібрати ключ `${group.file}:${mutant.line}:${mutant.col}:${mutant.replacement}`.
111
+ - Знайти verdict у мапі. Якщо знайдено й `isAllowedGap(verdict, threshold)` — додати `{ file: group.file, mutant, verdict }` у `allowedGaps` та інкрементувати `skippedCount`. Інакше — додати мутанта в `remainingMutants`.
112
+ - Якщо `remainingMutants` непорожній — додати в `remainingSurvived` об'єкт-копію групи з оновленим списком мутантів. Порожні групи відсіюються.
113
+ - Повернути новий рядок: `{ ...row, survived: remainingSurvived, mutation: { ...row.mutation, total: row.mutation.total - skippedCount } }`.
114
+ 4. Повернути `{ rows: augmentedRows, allowedGaps }`.
115
+
116
+ **Гранична поведінка:**
117
+
118
+ - Мутант без відповідного запису у `verdicts` (verdict не знайдено в мапі) → залишається в `remainingMutants` (вважаємо «не класифіковано» → не allowed-gap).
119
+ - `row.survived` відсутній / `undefined` → `survived` у вихідному рядку буде `[]` (порожній масив), `mutation.total` без змін.
120
+ - Усі мутанти однієї групи стали allowed-gap → група не з'являється в `remainingSurvived`.
121
+ - Жоден мутант не визнано allowed-gap → `allowedGaps` буде порожнім, `mutation.total` без змін.
122
+ - `mutation.total - skippedCount` може теоретично стати від'ємним, якщо `total` був неконсистентним із кількістю survived (модуль не валідує цей інваріант, надія на коректність вхідних даних).
123
+
124
+ ## Залежності
125
+
126
+ **Зовнішні (npm):** немає. Файл — чистий ES-модуль без імпортів.
127
+
128
+ **Внутрішні:** немає. Модуль є самодостатнім listener-free helper'ом без побічних залежностей.
129
+
130
+ **Runtime:** Node.js / Bun (ESM, синтаксис `export function`). Використовує стандартні структури даних `Map` та `Set` без полі­філів.
131
+
132
+ **Тип-формат:** усі типи описані JSDoc-блоками (без TypeScript-файлу типів). Структури `Row`, `Mutant`, `VerdictObj` визначені неявно через JSDoc у сигнатурах.
133
+
134
+ ## Потік виконання / Використання
135
+
136
+ Модуль є проміжною ланкою в пайплайні класифікації мутаційних прогалин у coverage-звіті:
137
+
138
+ 1. **Збір coverage-рядків.** Інший етап пайплайну агрегує статистику покриття й мутаційного тестування по кожній area (workspace) та формує `rows` із полями `mutation.{caught,total}` та `survived` (групи `{file, mutants[]}`).
139
+ 2. **Класифікація через LLM (або іншого класифікатора).** Для кожного survived-мутанта будується ключ `${file}:${line}:${col}:${replacement}` і отримується `verdict = { verdict, confidence, reason }`. Результат — масив `{key, verdict}`.
140
+ 3. **Виклик `applyVerdicts(rows, verdicts, threshold)`.** На цьому етапі мутанти діляться на allowed-gaps та залишок, а `mutation.total` коригується.
141
+ 4. **Рендер у `COVERAGE.md`.** Поверне́ні `rows` рендеряться у таблицю покриття; `allowedGaps` — в окрему секцію «Allowed gaps» (з причинами вердиктів).
142
+
143
+ **Типовий приклад використання:**
144
+
145
+ ```js
146
+ import { applyVerdicts, isAllowedGap } from './apply.mjs'
147
+
148
+ const threshold = 0.7
149
+
150
+ const rows = [
151
+ {
152
+ area: 'npm/foo',
153
+ coverage: { lines: 95.2 },
154
+ mutation: { caught: 18, total: 20 },
155
+ survived: [
156
+ {
157
+ file: 'npm/foo/src/index.mjs',
158
+ mutants: [
159
+ { line: 10, col: 5, replacement: '!=' },
160
+ { line: 22, col: 9, replacement: '+' }
161
+ ]
162
+ }
163
+ ]
164
+ }
165
+ ]
166
+
167
+ const verdicts = [
168
+ {
169
+ key: 'npm/foo/src/index.mjs:10:5:!=',
170
+ verdict: { verdict: 'equivalent', confidence: 0.9, reason: 'no behavioral diff' }
171
+ },
172
+ {
173
+ key: 'npm/foo/src/index.mjs:22:9:+',
174
+ verdict: { verdict: 'worth-testing', confidence: 0.85, reason: 'real gap' }
175
+ }
176
+ ]
177
+
178
+ const { rows: augmented, allowedGaps } = applyVerdicts(rows, verdicts, threshold)
179
+
180
+ // augmented[0].mutation.total === 19 (20 - 1 allowed-gap)
181
+ // augmented[0].survived[0].mutants має 1 елемент (другий мутант)
182
+ // allowedGaps має 1 елемент із file: 'npm/foo/src/index.mjs'
183
+ ```
184
+
185
+ **Інваріанти, на які слід зважати при змінах:**
186
+
187
+ - Ключ мутанта **має точно збігатися** з ключем у `verdicts` (формат `${file}:${line}:${col}:${replacement}`). Зміна формату в одному місці ламає матчинг.
188
+ - `mutation.caught` ніколи не змінюється — allowed-gaps вилучаються тільки з `total` (бо вони і так не були caught).
189
+ - Не покладайтесь на стабільний порядок `allowedGaps`: він залежить від порядку `rows` і всередині — порядку груп та мутантів. Якщо потрібен детермінований ордер, сортуйте у викликачі.
190
+
191
+ ## Rebuild Test
192
+
193
+ За цією документацією має бути можливо повністю відтворити поведінку `apply.mjs`:
194
+
195
+ - Скласти `Set` SKIP_VERDICTS = `{equivalent, defensive, glue, wrapper}`.
196
+ - Реалізувати `isAllowedGap(verdict, threshold)` як `SKIP_VERDICTS.has(verdict.verdict) && verdict.confidence >= threshold`.
197
+ - Реалізувати `applyVerdicts(rows, verdicts, threshold)`:
198
+ - побудувати `Map` з `verdicts`,
199
+ - пройти `rows.map` з immutable-оновленням,
200
+ - для кожного survived-мутанта зібрати ключ за фіксованим форматом, перевірити через `isAllowedGap`, зібрати окремий список allowedGaps та зменшити `mutation.total`,
201
+ - відсіяти порожні групи, повернути `{rows, allowedGaps}`.
202
+ - Не імпортувати нічого; не мутувати входи.
@@ -0,0 +1,203 @@
1
+ # cache.mjs
2
+
3
+ ## Огляд
4
+
5
+ Модуль `cache.mjs` реалізує **file-hash-keyed cache** для вердиктів класифікатора покриття мутаційного тестування (`coverage-classify`). Призначення — уникати повторної (зазвичай дорогої — через LLM) класифікації того ж самого мутанта в незмінному файлі.
6
+
7
+ Ключова ідея кешу:
8
+
9
+ - Ключ кешу формується з трьох компонентів:
10
+ 1. `blob-hash` — sha1-хеш контенту source-файла (отримується через `git hash-object` з fallback на власне sha1 від `readFileSync`).
11
+ 2. Координати мутанта в файлі: `line:col`.
12
+ 3. `base64url` від рядка-replacement мутанта (щоб ключ був безпечним для будь-яких символів).
13
+ - Формат ключа: `` `<blob-hash>:<line>:<col>:<base64url(replacement)>` ``.
14
+
15
+ Логіка інвалідації:
16
+
17
+ - Будь-яка зміна source-файла → новий `blob-hash` → старий ключ більше ніколи не співпадає → `cache miss` → мутант буде перекласифіковано.
18
+ - Інвалідація автоматична: жодного TTL, версіонування user-input або ручного очищення не потрібно.
19
+
20
+ Схема кешу на диску:
21
+
22
+ ```json
23
+ {
24
+ "version": 1,
25
+ "model": "string|null",
26
+ "entries": {
27
+ "<key>": {
28
+ "verdict": "...",
29
+ "confidence": "...",
30
+ "reason": "...",
31
+ "suggestedTest": "...",
32
+ "classifiedAt": "..."
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ Поле `version` використовується як schema-guard: при зміні константи `CACHE_VERSION` всі старі файли кешу автоматично трактуються як порожні (без помилок), що дозволяє безпечно еволюціонувати схему.
39
+
40
+ ## Експорти / API
41
+
42
+ Модуль експортує три іменовані функції (ESM):
43
+
44
+ | Функція | Призначення |
45
+ | ---------------------------------- | ------------------------------------------------------------------------------ |
46
+ | `deriveBlobHash(filePath)` | Обчислити sha1-хеш контенту файла. |
47
+ | `deriveCacheKey(filePath, mutant)` | Сформувати повний ключ кешу для конкретного мутанта в конкретному стані файла. |
48
+ | `readCache(cachePath)` | Безпечно прочитати cache з диска з порожнім fallback. |
49
+ | `writeCache(cachePath, cache)` | Записати cache на диск (з автостворенням батьківських директорій). |
50
+
51
+ Внутрішня (не експортована) константа:
52
+
53
+ - `CACHE_VERSION = 1` — поточна версія schema.
54
+
55
+ ## Функції
56
+
57
+ ### `deriveBlobHash(filePath)`
58
+
59
+ Обчислює 40-символьний hex sha1-хеш контенту файла.
60
+
61
+ - **Сигнатура:** `deriveBlobHash(filePath: string): string | null`
62
+ - **Параметри:**
63
+ - `filePath` — абсолютний шлях до source-файла.
64
+ - **Повертає:**
65
+ - 40-символьний hex-рядок (sha1) — якщо файл прочитано.
66
+ - `null` — якщо файл не існує (`existsSync` повернув `false`).
67
+ - **Алгоритм:**
68
+ 1. Якщо файл відсутній — повернути `null`.
69
+ 2. Спробувати викликати `git hash-object <file>` через `execFileSync` з кодуванням `utf8` і обрізати whitespace (`.trim()`). Це детерміністичний хеш в межах working tree; точно такий же хеш Git використовує внутрішньо для blob-ів.
70
+ 3. Якщо `git` недоступний або кинув помилку — fallback: прочитати файл (`readFileSync`) і обчислити `createHash('sha1').update(content).digest('hex')`.
71
+ - **Side effects:** виконує зовнішній процес `git`; читає файл (у fallback-гілці).
72
+ - **Чому два шляхи:** `git hash-object` працює швидше для великих репозиторіїв і додає до хешу префікс `blob <size>\0` як справжній Git. Однак скрипт може бути запущений у середовищі без `git` (CI worker, контейнер) — звідси sha1-fallback. Для двох однакових файлів обидва методи дають **різні** хеші, але це **не критично**, бо ключ кешу самосумісний у межах одного запуску — головне, щоб хеш був детерміністичним для тих самих байтів.
73
+
74
+ ### `deriveCacheKey(filePath, mutant)`
75
+
76
+ Формує ключ кешу для конкретного мутанта в конкретному стані файла.
77
+
78
+ - **Сигнатура:** `deriveCacheKey(filePath: string, mutant: { line: number, col: number, replacement: string }): string | null`
79
+ - **Параметри:**
80
+ - `filePath` — абсолютний шлях до source-файла, в якому міститься мутант.
81
+ - `mutant` — об'єкт-опис мутанта:
82
+ - `line: number` — номер рядка (1-based, як прийнято в Stryker/інших мутаторах).
83
+ - `col: number` — номер колонки.
84
+ - `replacement: string` — рядок-заміна, який мутатор вставляє замість оригінального коду.
85
+ - **Повертає:**
86
+ - Рядок виду `` `<sha1>:<line>:<col>:<base64url>` ``.
87
+ - `null` — якщо `deriveBlobHash` повернув `null` (файл недоступний → ключ створити неможливо).
88
+ - **Алгоритм:**
89
+ 1. Отримати `blobHash` через `deriveBlobHash(filePath)`.
90
+ 2. Якщо `null` — повернути `null` (попереджаючий контракт: caller має обробити cache-miss).
91
+ 3. Закодувати `mutant.replacement` як `base64url` (без `=` padding і без `/`/`+`, що робить його безпечним і для filename, і для key).
92
+ 4. Зібрати ключ template-literal-ом.
93
+ - **Side effects:** ті ж, що й у `deriveBlobHash` (виклик `git`/`readFileSync`).
94
+ - **Чому `base64url`:** `replacement` може містити будь-які символи (двокрапки, переноси рядків, юнікод). `base64url` гарантує однорядковий ASCII-ключ без колізій по роздільнику `:`.
95
+
96
+ ### `readCache(cachePath)`
97
+
98
+ Безпечно читає cache з диска. Будь-яка аномалія (відсутність файла, битий JSON, чужа схема) **не кидає** помилку, а повертає порожній cache.
99
+
100
+ - **Сигнатура:** `readCache(cachePath: string): { version: number, model: string | null, entries: Record<string, object> }`
101
+ - **Параметри:**
102
+ - `cachePath` — абсолютний шлях до файла `cache.json`.
103
+ - **Повертає:** об'єкт cache-схеми. Або реальні дані з диска, або **empty cache**:
104
+
105
+ ```js
106
+ { version: CACHE_VERSION, model: null, entries: {} }
107
+ ```
108
+
109
+ - **Алгоритм (5 умов повернення empty):**
110
+ 1. Файл не існує (`!existsSync(cachePath)`) → empty.
111
+ 2. `JSON.parse` кинув виняток → empty (catch).
112
+ 3. `data?.version !== CACHE_VERSION` (включно з `data === null`/`undefined`) → empty.
113
+ 4. `!data.entries` → empty.
114
+ 5. `typeof data.entries !== 'object'` або `Array.isArray(data.entries)` → empty.
115
+ 6. Інакше повернути `data` як є.
116
+ - **Side effects:** читає файл з диска (`readFileSync` з кодуванням `utf8`).
117
+ - **Інваріант:** ніколи не кидає винятки — гарантовано повертає валідний cache-об'єкт.
118
+
119
+ ### `writeCache(cachePath, cache)`
120
+
121
+ Серіалізує cache в JSON і записує на диск.
122
+
123
+ - **Сигнатура:** `writeCache(cachePath: string, cache: { version: number, model: string | null, entries: Record<string, object> }): void`
124
+ - **Параметри:**
125
+ - `cachePath` — абсолютний шлях, куди писати (наприклад `<repo>/.cache/coverage-classify/cache.json`).
126
+ - `cache` — cache-об'єкт у відповідності до schema.
127
+ - **Повертає:** `void`.
128
+ - **Алгоритм:**
129
+ 1. `mkdirSync(dirname(cachePath), { recursive: true })` — гарантовано створює всі батьківські директорії; не падає, якщо вони вже існують.
130
+ 2. `writeFileSync(cachePath, JSON.stringify(cache, null, 2) + '\n', 'utf8')` — двопробільний indent для людиночитності + trailing newline (POSIX-конвенція).
131
+ - **Side effects:** створює директорії, перезаписує файл повністю (атомарність не гарантується — це звичайний `writeFileSync`).
132
+
133
+ ## Залежності
134
+
135
+ Лише модулі стандартної бібліотеки Node.js (ESM):
136
+
137
+ | Імпорт | Звідки | Призначення |
138
+ | ---------------------------------------------------------- | -------------------- | ------------------------------------------------------------------ |
139
+ | `execFileSync` | `node:child_process` | Виклик `git hash-object` як зовнішнього процесу. |
140
+ | `createHash` | `node:crypto` | sha1-fallback, якщо `git` недоступний. |
141
+ | `existsSync`, `mkdirSync`, `readFileSync`, `writeFileSync` | `node:fs` | Sync-FS-операції для cache та source-файлів. |
142
+ | `dirname` | `node:path` | Виокремити батьківську директорію з cache-шляху перед `mkdirSync`. |
143
+
144
+ Зовнішніх npm-залежностей **немає**. Усі імпорти — з prefix `node:` (рекомендований формат для Node.js core-модулів).
145
+
146
+ Опціональна зовнішня залежність: бінарник `git` у `$PATH`. Якщо його немає — модуль автоматично переходить на sha1-fallback.
147
+
148
+ ## Потік виконання / Використання
149
+
150
+ Типовий sequence використання у класифікаторі:
151
+
152
+ 1. На старті прогону: `cache = readCache(cachePath)`.
153
+ 2. Для кожного мутанта:
154
+ 1. `key = deriveCacheKey(filePath, mutant)`.
155
+ 2. Якщо `key === null` — пропустити (файл-джерело недоступний) або повторити пізніше.
156
+ 3. Якщо `cache.entries[key]` існує — використати збережений verdict, **не** викликати LLM.
157
+ 4. Інакше — викликати класифікатор (LLM/heuristic), отримати `verdict` і записати:
158
+ ```js
159
+ cache.entries[key] = { verdict, confidence, reason, suggestedTest, classifiedAt: new Date().toISOString() }
160
+ ```
161
+ 3. На завершенні прогону: `writeCache(cachePath, cache)`.
162
+
163
+ Граничні випадки та їх обробка:
164
+
165
+ - **Source файл видалили** → `deriveBlobHash` повертає `null` → `deriveCacheKey` повертає `null`. Caller повинен пропустити кешування.
166
+ - **Git недоступний** → автоматичний fallback на sha1. Ключі будуть відрізнятися від ключів, отриманих через `git hash-object` для того ж файла. Тому при міграції оточення можливий повний cache-miss — але це безпечно (просто повторна класифікація).
167
+ - **Cache-файл пошкоджений** → `readCache` повертає empty cache. Жодних винятків. Старий битий вміст буде перезаписаний при наступному `writeCache`.
168
+ - **Зміна `CACHE_VERSION`** → всі попередні файли кешу мовчки трактуються як empty. Безпечний шлях для еволюції schema.
169
+
170
+ Приклад мінімального використання:
171
+
172
+ ```js
173
+ import { readCache, writeCache, deriveCacheKey } from './cache.mjs'
174
+
175
+ const cachePath = '/abs/path/to/cache.json'
176
+ const cache = readCache(cachePath)
177
+
178
+ const key = deriveCacheKey('/abs/path/to/src/foo.js', { line: 10, col: 5, replacement: '' })
179
+ if (key && cache.entries[key]) {
180
+ // Cache hit — використати готовий verdict.
181
+ const verdict = cache.entries[key]
182
+ console.log(verdict)
183
+ } else if (key) {
184
+ // Cache miss — викликати класифікатор і зберегти.
185
+ const verdict = await classify(/* ... */)
186
+ cache.entries[key] = { ...verdict, classifiedAt: new Date().toISOString() }
187
+ }
188
+
189
+ writeCache(cachePath, cache)
190
+ ```
191
+
192
+ ## Rebuild Test
193
+
194
+ Розумова перевірка: чи можна за цією документацією відтворити модуль без читання source? Так:
195
+
196
+ - Заявлені 4 експорти (`deriveBlobHash`, `deriveCacheKey`, `readCache`, `writeCache`) — всі описані з сигнатурами, типами параметрів, повертанням, side effects і алгоритмом.
197
+ - Внутрішня константа `CACHE_VERSION = 1` згадана.
198
+ - Schema cache-файла наведена.
199
+ - Алгоритм формування ключа (3 компоненти + base64url для replacement) пояснений однозначно.
200
+ - Fallback-логіка `git hash-object` → `sha1(readFileSync)` описана.
201
+ - Поведінка `readCache` при кожному типі помилки (5 пунктів) перерахована.
202
+ - Імпорти node-core модулів перелічені з їх роллю.
203
+ - Жодного зовнішнього npm-пакета не використано.
@@ -0,0 +1,218 @@
1
+ # `coverage-classify/index.mjs`
2
+
3
+ ## Огляд
4
+
5
+ Модуль `coverage-classify/index.mjs` — це **публічна точка входу** (Public API) класифікатора survived-мутантів за допомогою Claude API від Anthropic. Він відповідає за оркестрацію LLM-класифікації мутантів, які пережили mutation-testing раунд (тобто не були вбиті жодним тестом), і повертає для кожного з них вердикт у форматі `{ verdict, confidence, reason, suggestedTest? }`.
6
+
7
+ Призначення модуля — допомогти автоматичним інструментам типу `/n-coverage-fix` ухвалювати рішення, чи варто дописувати тест на конкретний survived-мутант, чи можна його ігнорувати як equivalent / not-worth-testing.
8
+
9
+ Ключові архітектурні характеристики:
10
+
11
+ - **Graceful degradation:** відсутність змінної `ANTHROPIC_API_KEY` або непрацездатний dynamic import пакета `@anthropic-ai/sdk` не приводить до краху — функція мовчки повертає порожній масив `[]`, попередньо вивівши попередження у `console.warn`.
12
+ - **Persistent cache:** результати класифікації кешуються на диск через утиліти з `./cache.mjs`. При зміні `MODEL` кеш повністю інвалідується (entries чистяться, поле `model` оновлюється). Це гарантує консистентність вердиктів між запусками за умови незмінного контексту мутанта.
13
+ - **Prompt caching на стороні API:** системний промпт передається з `cache_control: { type: 'ephemeral' }` — усі мутанти одного прогону reuse кешований префікс на стороні Anthropic API (економія токенів).
14
+ - **Retry з експоненційним backoff:** кожен мутант отримує до `MAX_RETRIES + 1 = 3` спроб; між спробами — затримка `retryDelayMs * 2 ** attempt`. Після вичерпання спроб — повертається **conservative fallback** `worth-testing/confidence=0`, щоб не втратити мутант з виду.
15
+
16
+ Модуль не виконує мережевих викликів самостійно — він делегує їх в `client.messages.create(...)` (за замовчуванням це інстанс `new Anthropic()` з SDK).
17
+
18
+ ## Експорти / API
19
+
20
+ | Експорт | Тип | Опис |
21
+ | ---------- | -------- | ------------------------------------------------------------------------------------------------ |
22
+ | `classify` | function | Іменований async-експорт. Класифікує всіх survived-мутантів і повертає масив `{ key, verdict }`. |
23
+
24
+ Інші ідентифікатори файлу (`MODEL`, `MAX_RETRIES`, `DEFAULT_RETRY_DELAY_MS`, `FALLBACK_VERDICT`, `classifyOne`) — **внутрішні**, не експортуються.
25
+
26
+ ### Сигнатура `classify`
27
+
28
+ ```js
29
+ export async function classify(survived, cwd, opts = {})
30
+ ```
31
+
32
+ **Параметри:**
33
+
34
+ - `survived` — `Array<{ file: string, mutants: Array<object>, exampleTest?: object|null, recommendationText?: string|null }>` — список survived-груп. Структура аналогічна до використовуваної в `COVERAGE.md` (звіт mutation-testing). Кожна група відповідає одному файлу та містить масив мутантів.
35
+ - `cwd` — `string` — абсолютний шлях кореня проєкту. Використовується для:
36
+ - формування дефолтного шляху кешу (`<cwd>/npm/reports/coverage-classify.cache.json`);
37
+ - резолвінгу шляхів до файлів-джерел мутантів (для побудови юзер-промпта та cache key).
38
+ - `opts` — `{ cachePath?: string, client?: object, retryDelayMs?: number }` — опціональні ін'єкції для тестування:
39
+ - `cachePath` — кастомний шлях до файлу кешу.
40
+ - `client` — підставний Anthropic SDK client (для unit-тестів без реальних мережевих викликів). Має мати метод `messages.create(...)`.
41
+ - `retryDelayMs` — базова затримка для exp-backoff у мс. `0` фактично вимикає sleep між retries (зручно для тестів).
42
+
43
+ **Повертає:** `Promise<Array<{ key: string, verdict: object }>>`
44
+
45
+ - `key` — рядок формату `<file>:<line>:<col>:<replacement>`, який однозначно ідентифікує мутант для зовнішнього коду.
46
+ - `verdict` — об'єкт з полями `{ verdict, confidence, reason, suggestedTest? }` (формат описаний у `./verdict-schema.mjs`).
47
+
48
+ При відсутності API-ключа або SDK — повертає `[]`.
49
+
50
+ ## Функції
51
+
52
+ ### `classify(survived, cwd, opts)` — публічна
53
+
54
+ **Сигнатура:** `async function classify(survived, cwd, opts = {}) -> Promise<Array<{key, verdict}>>`
55
+
56
+ **Параметри:** див. розділ «Експорти / API» вище.
57
+
58
+ **Повертає:** `Promise<Array<{ key: string, verdict: object }>>` — плаский список вердиктів по всіх мутантах усіх груп (порядок: group-by-group, mutant-by-mutant у межах групи).
59
+
60
+ **Кроки виконання (orchestration):**
61
+
62
+ 1. Резолвить `cachePath` (дефолт: `<cwd>/npm/reports/coverage-classify.cache.json`) та `retryDelayMs` (дефолт: `DEFAULT_RETRY_DELAY_MS = 1000`).
63
+ 2. Перевіряє `env.ANTHROPIC_API_KEY`. Якщо відсутній — `console.warn` + `return []`.
64
+ 3. Виконує `await import('@anthropic-ai/sdk')`. Якщо пакет не встановлено — `console.warn` + `return []`.
65
+ 4. Створює клієнт: `opts.client ?? new Anthropic()`.
66
+ 5. Завантажує кеш через `readCache(cachePath)`. Якщо `cache.model !== MODEL` — повністю чистить `cache.entries` і виставляє `cache.model = MODEL` (інвалідація при зміні моделі).
67
+ 6. Ітерує `survived.mutants`:
68
+ - Будує `lookupKey = "<group.file>:<line>:<col>:<replacement>"`.
69
+ - Обчислює `cacheKey = deriveCacheKey(join(cwd, group.file), mutant)`.
70
+ - Якщо в кеші є запис — повертає його (нормалізуючи поля, опціонально розгортаючи `suggestedTest`).
71
+ - Інакше викликає `classifyOne(client, group, mutant, cwd, retryDelayMs)` і записує результат у `cache.entries[cacheKey]` з полем `classifiedAt: new Date().toISOString()` (якщо `cacheKey` truthy).
72
+ 7. Зберігає кеш на диск через `writeCache(cachePath, cache)`.
73
+ 8. Повертає накопичений `verdicts`.
74
+
75
+ **Side effects:**
76
+
77
+ - **Disk I/O:** читання і запис файлу кешу (`<cwd>/npm/reports/coverage-classify.cache.json` за замовчуванням).
78
+ - **Мережа:** виклики `client.messages.create(...)` до Anthropic API (через делегування в `classifyOne`).
79
+ - **stdout/stderr:** `console.warn(...)` при відсутності ключа / SDK / при фатальних retry-фейлах окремих мутантів.
80
+ - **Час:** функція **очікує** мережеві запити послідовно (no parallelism між мутантами) — у гіршому випадку загальна тривалість = `N × (MAX_RETRIES+1) × API_latency + sum(backoff)`.
81
+ - **Стан системи:** мутації `cache` на місці (об'єкт переписується), потім дамп на диск.
82
+
83
+ ### `classifyOne(client, group, mutant, cwd, retryDelayMs)` — внутрішня
84
+
85
+ **Сигнатура:** `async function classifyOne(client, group, mutant, cwd, retryDelayMs) -> Promise<object>`
86
+
87
+ **Параметри:**
88
+
89
+ - `client` — `{ messages: { create: Function } }` — Anthropic SDK client (реальний або mock).
90
+ - `group` — `{ file: string }` — група для контексту (з неї потрібно лише `.file` — повний шлях до файлу-джерела).
91
+ - `mutant` — `object` — дані мутанта (передаються в `buildUserPrompt` як `{ ...mutant, file: group.file }`).
92
+ - `cwd` — `string` — корінь проєкту, потрібний для resolving шляхів у `buildUserPrompt`.
93
+ - `retryDelayMs` — `number` — базова затримка для exp-backoff (`0` у тестах вимикає sleep).
94
+
95
+ **Повертає:** `Promise<object>` — розпарсений вердикт (через `parseVerdict(text)`) або копія `FALLBACK_VERDICT`.
96
+
97
+ **Кроки виконання:**
98
+
99
+ 1. Будує юзер-промпт: `userPrompt = buildUserPrompt({ ...mutant, file: group.file }, cwd)`.
100
+ 2. Цикл `for (attempt = 0; attempt <= MAX_RETRIES; attempt++)`:
101
+ - Виконує `client.messages.create({ model: MODEL, max_tokens: 1024, system: [{ type:'text', text: SYSTEM_PROMPT, cache_control:{ type:'ephemeral' } }], messages: [{ role:'user', content: userPrompt }] })`.
102
+ - Дістає текст з `response?.content?.[0]?.text ?? ''`.
103
+ - Повертає `parseVerdict(text)`. Якщо `parseVerdict` кидає — це впаде як виключення і потрапить в `catch` цієї ж ітерації (тобто буде ще одна retry-спроба).
104
+ 3. На `catch`: запам'ятовує `lastError`. Якщо `attempt < MAX_RETRIES && retryDelayMs > 0` — `await setTimeout(retryDelayMs * 2 ** attempt)`.
105
+ 4. Після вичерпання спроб: `console.warn` з деталями (file:line:col, кількість спроб, повідомлення останньої помилки) і `return { ...FALLBACK_VERDICT }` (копія, щоб уникнути shared mutation на константі).
106
+
107
+ **Side effects:**
108
+
109
+ - Мережевий виклик до Anthropic API (1..MAX_RETRIES+1 разів).
110
+ - `setTimeout` з `node:timers/promises` (async sleep).
111
+ - `console.warn` при фатальному фейлі.
112
+
113
+ ## Залежності
114
+
115
+ ### Внутрішні (relative imports)
116
+
117
+ | Шлях | Що використовується | Призначення |
118
+ | ---------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
119
+ | `./cache.mjs` | `deriveCacheKey`, `readCache`, `writeCache` | Робота з персистентним кешем класифікацій (deriving детермінованого ключа з мутанта + читання/запис JSON). |
120
+ | `./prompt.mjs` | `buildUserPrompt`, `SYSTEM_PROMPT` | Побудова промптів для LLM: системний (статичний, кешується) та юзер-промпт (динамічний, per-mutant). |
121
+ | `./verdict-schema.mjs` | `parseVerdict` | Парсинг та валідація JSON-відповіді LLM у структурований об'єкт вердикту. |
122
+
123
+ ### Зовнішні
124
+
125
+ | Пакет/модуль | Що використовується | Як підключено |
126
+ | ---------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------- |
127
+ | `node:path` | `join` | Статичний `import`. Збирання шляхів до cache file та source file. |
128
+ | `node:process` | `env` | Статичний `import`. Читання `ANTHROPIC_API_KEY`. |
129
+ | `node:timers/promises` | `setTimeout` | Статичний `import`. Async sleep для exp-backoff між retries. |
130
+ | `@anthropic-ai/sdk` | `Anthropic` (default export) | **Dynamic** `await import(...)` всередині `classify` — graceful degradation, якщо пакет не встановлено. |
131
+
132
+ ### Зовнішні артефакти середовища
133
+
134
+ - **Змінна оточення `ANTHROPIC_API_KEY`** — обов'язкова. Без неї модуль не робить мережевих викликів.
135
+ - **Файл кешу** — за замовчуванням `<cwd>/npm/reports/coverage-classify.cache.json`. Структура: `{ model: string, entries: { [cacheKey]: { verdict, confidence, reason, suggestedTest?, classifiedAt } } }`.
136
+
137
+ ### Константи модуля
138
+
139
+ | Константа | Значення | Призначення |
140
+ | ------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------- |
141
+ | `MODEL` | `'claude-sonnet-4-6'` | ID моделі Anthropic для класифікації. Зміна → автоматична інвалідація кешу. |
142
+ | `MAX_RETRIES` | `2` | Кількість **повторних** спроб (всього спроб: `MAX_RETRIES + 1 = 3`). |
143
+ | `DEFAULT_RETRY_DELAY_MS` | `1000` | Базова затримка для exp-backoff (мс). Реальні delays: 1000, 2000, 4000. |
144
+ | `FALLBACK_VERDICT` | `{ verdict: 'worth-testing', confidence: 0, reason: '...' }` | Консервативний вердикт при фатальному фейлі (мутант ще не відкидається). |
145
+
146
+ ## Потік виконання / Використання
147
+
148
+ ### Типовий сценарій виклику
149
+
150
+ Модуль викликається з вищерівневого pipeline (наприклад, `/n-coverage-fix` або інший CLI поверх mutation-testing звіту):
151
+
152
+ ```js
153
+ import { classify } from '<repo>/npm/scripts/coverage-classify/index.mjs'
154
+
155
+ // survived зазвичай парситься з COVERAGE.md
156
+ const survived = [
157
+ {
158
+ file: 'src/utils/foo.mjs',
159
+ mutants: [
160
+ { line: 10, col: 5, replacement: '=== 0', original: '!== 0', mutator: 'EqualityOperator' }
161
+ // ...
162
+ ],
163
+ exampleTest: null,
164
+ recommendationText: null
165
+ }
166
+ // ...
167
+ ]
168
+
169
+ const verdicts = await classify(survived, process.cwd())
170
+
171
+ for (const { key, verdict } of verdicts) {
172
+ if (verdict.verdict === 'worth-testing' && verdict.confidence > 0.5) {
173
+ // дописати тест
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Стани виходу та граничні випадки
179
+
180
+ | Стан | Поведінка |
181
+ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------ |
182
+ | `ANTHROPIC_API_KEY` не виставлений | `console.warn` + `return []` (порожній масив, не помилка). |
183
+ | Пакет `@anthropic-ai/sdk` не встановлено | `console.warn` + `return []`. |
184
+ | Кеш hit для мутанта | Мережевий виклик не виконується, повертається cached verdict. |
185
+ | Кеш miss, успішна класифікація | Виклик API, парсинг, запис у кеш, push у verdicts. |
186
+ | Кеш miss, всі retry-спроби впали | `console.warn` з деталями + `FALLBACK_VERDICT` push у verdicts (мутант не пропадає). |
187
+ | Зміна `MODEL` у коді | На наступному запуску `cache.entries` повністю обнуляється. |
188
+ | `cacheKey === null/undefined` (наприклад, неможливо derive) | Класифікація виконується, але **не** кешується (запис у кеш пропускається). |
189
+
190
+ ### Послідовність всередині одного прогону (timeline)
191
+
192
+ 1. Читання `survived` (відповідальність caller'а).
193
+ 2. `classify(...)` запускається → preflight перевірки (API key, SDK).
194
+ 3. Завантаження дискового кешу → можливе очищення при зміні моделі.
195
+ 4. **Послідовно** (не паралельно) для кожного `(group, mutant)`:
196
+ - Cache lookup (за `cacheKey`).
197
+ - Якщо miss — `classifyOne` з retry-логікою.
198
+ - Запис у кеш-об'єкт у пам'яті.
199
+ 5. Після обходу всіх мутантів — атомарний `writeCache` на диск.
200
+ 6. Повернення `verdicts` caller'у.
201
+
202
+ ### Особливості та інваріанти
203
+
204
+ - **Послідовність викликів API:** немає паралелізму між мутантами. Це навмисно — обмеження по rate-limits Anthropic API та для стабільної взаємодії з prompt cache.
205
+ - **Prompt cache reuse:** оскільки системний промпт ідентичний для всіх мутантів і помічений `cache_control: ephemeral`, Anthropic API повторно використовує кешований префікс — суттєва економія input-токенів на великих прогонах.
206
+ - **Ідемпотентність:** повторний запуск з тим же `cachePath` і незмінним `survived` дає той же `verdicts` без додаткових мережевих викликів (повний cache hit).
207
+ - **Conservative fallback:** при фейлі класифікації мутант **не** відкидається — він отримує `worth-testing/confidence=0`, що змушує caller свідомо вирішувати, чи приймати такий вердикт. Це усуває ризик «непомітної втрати» мутанта через мережеву помилку.
208
+ - **Defensive copy fallback:** повертається `{ ...FALLBACK_VERDICT }`, а не сам об'єкт — щоб caller'и не могли випадково замутувати константу.
209
+
210
+ ### Тестування
211
+
212
+ Для unit-тестів зручно ін'єктувати:
213
+
214
+ - `opts.client = { messages: { create: async () => ({ content: [{ text: '...' }] }) } }` — підставний клієнт.
215
+ - `opts.cachePath = '/tmp/test-cache.json'` — ізольований кеш.
216
+ - `opts.retryDelayMs = 0` — миттєві retries без `setTimeout`-блокувань.
217
+
218
+ Файл `index.mjs` спроєктований так, що жодних інших залежностей мокати **не потрібно** — `cache.mjs`/`prompt.mjs`/`verdict-schema.mjs` мають детерміновану поведінку для тестів.