@nitra/cursor 1.25.0 → 1.25.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/.pi-template/extensions/n-cursor-adr/index.ts +2 -8
- package/CHANGELOG.md +20 -63
- package/package.json +5 -5
- package/rules/js-lint/coverage/coverage.mjs +14 -9
- package/rules/k8s/js/manifests.mjs +28 -20
- package/rules/k8s/policy/network_policy/network_policy.rego +21 -23
- package/rules/test/coverage/coverage.mjs +5 -10
- package/rules/test/js/cargo_mutants_config.mjs +1 -3
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +2 -2
- package/rules/test/js/stryker_config.mjs +1 -3
- package/scripts/coverage-fix.mjs +24 -19
- package/scripts/lib/timing-summary.mjs +1 -4
- package/scripts/post-tool-use-fix.mjs +1 -1
- package/scripts/sync-claude-config.mjs +0 -1
- package/skills/coverage-fix/SKILL.md +82 -58
- package/skills/fix-tests/SKILL.md +17 -7
|
@@ -36,10 +36,7 @@ interface PiExec {
|
|
|
36
36
|
timeout?: number
|
|
37
37
|
}
|
|
38
38
|
) => Promise<{ code: number; stdout: string; stderr: string }>
|
|
39
|
-
on: (
|
|
40
|
-
event: string,
|
|
41
|
-
handler: (event: unknown, ctx: PiContext) => Promise<void> | void
|
|
42
|
-
) => void
|
|
39
|
+
on: (event: string, handler: (event: unknown, ctx: PiContext) => Promise<void> | void) => void
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
const CAPTURE_HOOK = '.claude/hooks/capture-decisions.sh'
|
|
@@ -68,10 +65,7 @@ export default function (pi: PiExec): void {
|
|
|
68
65
|
jsonlPath = join(tmpdir(), `n-cursor-pi-transcript-${Date.now()}-${randomUUID()}.jsonl`)
|
|
69
66
|
writeFileSync(jsonlPath, lines + '\n', 'utf8')
|
|
70
67
|
} catch (error) {
|
|
71
|
-
ctx.ui?.notify?.(
|
|
72
|
-
`@nitra/cursor: transcript serialization failed — ${(error as Error).message}`,
|
|
73
|
-
'error'
|
|
74
|
-
)
|
|
68
|
+
ctx.ui?.notify?.(`@nitra/cursor: transcript serialization failed — ${(error as Error).message}`, 'error')
|
|
75
69
|
return
|
|
76
70
|
}
|
|
77
71
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,86 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
-
## [1.25.
|
|
8
|
-
|
|
9
|
-
### Added
|
|
10
|
-
|
|
11
|
-
- **`.claude-template/hooks/lib/tooling-only.sh`** — спільний bash-helper із функціями `is_tooling_only_change` і `git_diff_only_version_field`. Source'ається з `capture-decisions.sh` і `normalize-decisions.sh` через `. "$SCRIPT_DIR/lib/tooling-only.sh"` — caller'и більше не дублюють ~27 рядків логіки розпізнавання "tooling-only" сесій. Bash 3.2 (macOS `/bin/bash`) сумісний.
|
|
12
|
-
- **`syncAdrHookLibScripts(projectRoot, templateDir)`** у `npm/scripts/sync-claude-config.mjs` — копіює всі `*.sh` із `.claude-template/hooks/lib/` у `.claude/hooks/lib/` проєкту-споживача без exec-біта (source-only). Експортується разом із новою `removeOrphanAdrHookLib(projectRoot)` для cleanup-у при вимкненому `adr` правилі.
|
|
13
|
-
- Поле `adrHookLib: string[]` у відповіді `syncClaudeConfig` — список синкнутих lib-файлів. `npm/bin/n-cursor.js` виводить їх у `🤖 Claude-конфіг` логу окремими записами.
|
|
14
|
-
|
|
15
|
-
### Changed
|
|
16
|
-
|
|
17
|
-
- **`.claude-template/hooks/{capture,normalize}-decisions.sh`** — обидва скрипти `source`-ять спільний `lib/tooling-only.sh` замість локальних копій функцій. Заголовок `# shellcheck source=lib/tooling-only.sh` тримає shellcheck-аналіз чистим.
|
|
7
|
+
## [1.25.3] - 2026-05-26
|
|
18
8
|
|
|
19
9
|
### Fixed
|
|
20
10
|
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
- **`"pi-package"` keyword** у `npm/package.json` — пакет з’являється у pi-gallery для discoverability.
|
|
11
|
+
- **JSDoc**: дописано опис `@returns`/`@param`-описи й типи в `rules/js-lint/coverage/coverage.mjs`, `rules/k8s/js/manifests.mjs`, `rules/adr/js/tests/*.test.mjs`, `rules/test/js/tests/*.test.mjs`, `scripts/coverage-fix.mjs`, `scripts/post-tool-use-fix.mjs`, `scripts/utils/tests/resolve-*.test.mjs` (oxlint/eslint jsdoc-правила).
|
|
12
|
+
- **`k8s/js/manifests.mjs`**: `JSON.parse(JSON.stringify(...))` → `structuredClone(...)` (unicorn `prefer-structured-clone`); інверсія негованої умови в `validateNetworkPolicyForWorkload` (eslint `no-negated-condition`).
|
|
13
|
+
- **`k8s/policy/network_policy/network_policy.rego`**: `list_contains` → `contains_item` (regal `avoid-get-and-list-prefix`); `items[i] == item` → `some candidate in items` (`prefer-some-in-iteration`); `workload_kind` без зайвого `if {}` (`unconditional-assignment`); helper-правила переміщено після всіх `deny`, щоб задовольнити `messy-rule`. `network_policy_test.rego` переформатовано через `opa fmt`.
|
|
14
|
+
- **`scripts/tests/post-tool-use-fix.test.mjs`**: fake-child перероблено з `EventEmitter` на duck-typed `addListener`/`removeListener` (unicorn `prefer-event-target`).
|
|
15
|
+
- **`scripts/tests/cli-entry.test.mjs`**: symlink-тест /tmp ↔ /private/tmp використовує `mkdtempSync` з префіксом, зібраним з частин (sonarjs `publicly-writable-directories`).
|
|
16
|
+
- **`rules/test/coverage/coverage.mjs`**: множинні `push()` об’єднано в один виклик (unicorn `prefer-single-call`).
|
|
17
|
+
- Винесено повторно-компільовані regex у module scope (`e18e/prefer-static-regex`) у `coverage.mjs`, `test/coverage/tests/coverage.test.mjs`, `k8s/tests/manifests/tests/check-schema.test.mjs`.
|
|
18
|
+
- Видалено невикористаний `npm/lib/x.js` (unicorn `no-empty-file`).
|
|
30
19
|
|
|
31
20
|
### Changed
|
|
32
21
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
22
|
+
- **`.claude/hooks/{capture,normalize}-decisions.sh`** синхронізовано з `npm/.claude-template/hooks/` (включно з новим `lib/tooling-only.sh`).
|
|
23
|
+
- **`knip.json`**: додано entry-патерни для динамічно імпортованих/зовнішніх скриптів (stryker configs, pi extensions, fixtures, `coverage-fix.mjs`); `@anthropic-ai/claude-code`, `@anthropic-ai/sdk`, `@stryker-mutator/core` додано в `ignoreDependencies` (тип-only або dynamic import).
|
|
24
|
+
- **`.jscpd.json`**: ігнор-патерни розширено для template/canonical пар тієї ж природи, що вже були в винятках (`npm/.pi-template/**`, `knip-canonical.json`, `*.snippet.yaml`).
|
|
25
|
+
- **`.cspell.json`**: до `ignorePaths` додано `**/reports/stryker/**` (генеровані Stryker-репорти, вже в .gitignore).
|
|
26
|
+
- **Кореневий `package.json#scripts.lint`** — чейн `bun run lint-ga && lint-js && lint-rego && lint-security && lint-style && lint-text && oxfmt .` замість делегування до CLI (bun.mdc + security.mdc).
|
|
35
27
|
|
|
36
|
-
## [1.
|
|
28
|
+
## [1.25.2] - 2026-05-26
|
|
37
29
|
|
|
38
30
|
### Added
|
|
39
31
|
|
|
40
|
-
-
|
|
41
|
-
- `npm/scripts/sync-claude-config.mjs`: експорт `PI_DIR`, `PI_EXTENSIONS_DIR`, `PI_TEMPLATE_DIR_NAME`, `PI_EXTENSION_NAME`; нова функція `syncPiExtensions(projectRoot, bundledPackageRoot)` (copy) і `removeOrphanPiExtension(projectRoot)` (cleanup); поле `piExtension: boolean` у відповіді `syncClaudeConfig` (gated на `adr` ∈ rules).
|
|
42
|
-
- `npm/package.json` `files` array: додано `.pi-template` — bundled-директорія шипиться разом із пакетом.
|
|
43
|
-
- `npm/bin/n-cursor.js`: у `🤖 Claude-конфіг`-логу після sync додається `.pi/extensions/n-cursor-adr/index.ts` коли pi-extension згенерована.
|
|
32
|
+
- **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (SIGURG). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
|
|
44
33
|
|
|
45
|
-
## [1.
|
|
34
|
+
## [1.25.1] - 2026-05-26
|
|
46
35
|
|
|
47
36
|
### Added
|
|
48
37
|
|
|
49
|
-
- **`
|
|
50
|
-
- **`runFixCommand` тепер друкує `⏱ Fix timing`** після прогону всіх `rules/<id>/fix.mjs` — per-rule час + сума. Маркер `❌` на впалих рядках.
|
|
51
|
-
- `npm/scripts/lib/timing-summary.mjs` — чистий форматер `formatTimingSummary(title, entries)` (спільний для fix і lint). 9 тестів у `tests/timing-summary.test.mjs`.
|
|
52
|
-
- `npm/scripts/lib/run-lint-cli.mjs` — `runLintCli({ cwd, spawnSyncFn, now, log, logError })` з DI для юніт-тестів. 7 тестів у `tests/run-lint-cli.test.mjs`.
|
|
53
|
-
|
|
54
|
-
### Changed
|
|
55
|
-
|
|
56
|
-
- Кореневий `package.json` цього монорепо: `lint` → `n-cursor lint`; додано окремий скрипт `oxfmt: "oxfmt ."`, який раніше йшов у хвості ланцюжка прямою командою.
|
|
57
|
-
- Скіли `/n-fix` і `/n-lint`: додано вимогу копіювати таблицю `⏱` з виводу інструмента у фінальне резюме відповіді користувачу.
|
|
58
|
-
|
|
59
|
-
## [1.21.0] - 2026-05-25
|
|
38
|
+
- **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вижилих мутантів із секції `## Вижилі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
|
|
60
39
|
|
|
61
40
|
### Changed
|
|
62
41
|
|
|
63
|
-
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
### Added
|
|
67
|
-
|
|
68
|
-
- `npm/scripts/post-tool-use-fix.mjs` — реалізація `routeFilePathToRules(filePath)` (чиста функція, picomatch) і `runPostToolUseFixCli({ stdinJson, spawnFn })` (DI-friendly для тестів). 21 тест у `npm/scripts/tests/post-tool-use-fix.test.mjs`.
|
|
69
|
-
- `LEGACY_STOP_HOOK_COMMAND_MARKER` — публічний export для тестів і потенційних консьюмерів, які перевіряють відсутність застарілого хука.
|
|
70
|
-
|
|
71
|
-
### Removed
|
|
72
|
-
|
|
73
|
-
- `npm/scripts/claude-stop-hook.mjs` — більше не потрібен.
|
|
74
|
-
|
|
75
|
-
## [1.20.0] - 2026-05-25
|
|
76
|
-
|
|
77
|
-
### Added
|
|
78
|
-
|
|
79
|
-
- **NetworkPolicy: два повних канон-snippets**: `deployment.snippet.yaml` (для `Deployment`/`Job`/`CronJob`/`DaemonSet`) і `statefulset.snippet.yaml` (повний канон для `StatefulSet` з intra-replica правилами). Жодного runtime-merge — JS-генератор/rego обирають один за `kind` workload-у через анотацію `metadata.annotations['nitra.dev/workload-kind']`. Нові publiс exports: `loadSnippetSpec('deployment'|'statefulset')`, `KIND_TO_SNIPPET`, `snippetNameForKind(kind)`. `buildNetworkPolicyYaml(deployName, appLabel, kind)` — `kind` тепер обовʼязковий (throws на невідомий). Rego (`network_policy.rego`) робить superset-перевірку проти обраного канону; safety-net deny на allow-all `egress: [{}]`. GKE NodeLocal DNSCache: link-local `169.254.0.0/16` UDP/TCP 53 — у обох канонах. **Breaking** з v1.19.x: видалено `networkPolicyManifestViolations` (структуру тримає rego); `buildNetworkPolicyYaml` без `kind` тепер throws. Перейменування `common.snippet.yaml` → `deployment.snippet.yaml`; `data.template.snippet` → `data.template.deployment_snippet` у rego.
|
|
80
|
-
- **`rules/js-lint/coverage`**: `parseStrykerReport` тепер зчитує оригінальний код вижилих мутантів (`extractOriginal`), групує по файлах і повертає `survived: [{file, mutants: [{line,col,mutantType,original,replacement}], exampleTest, recommendationText}]`; `findExampleTest` + `extractFirstTestBlock` знаходять і витягують перший тест-блок із тест-файлу поруч — для стилю.
|
|
81
|
-
- **LLM-рекомендації у COVERAGE.md**: коли встановлено `ANTHROPIC_API_KEY`, `n-cursor coverage` робить один Anthropic API-виклик на кожен файл з вижилими мутантами та записує рекомендацію «Що треба протестувати» у секцію `## Recommendations`. Модель: `claude-haiku-4-5-20251001` з prompt caching (`ephemeral`). Без ключа — секція генерується без LLM-тексту.
|
|
82
|
-
- **`rules/js-lint/coverage/lib/generate-recommendation.mjs`**: `generateMutantRecommendation(client, sourceContent, mutants)` — ізольований модуль LLM-виклику.
|
|
83
|
-
- **`@anthropic-ai/sdk`** у dependencies — потрібен для LLM-рекомендацій (опціонально: якщо `ANTHROPIC_API_KEY` не задано, sdk не викликається).
|
|
84
|
-
- **`rules/test/coverage`**: `renderMarkdown` генерує секцію `## Recommendations` з per-file підрозділами (`### <file>`) — таблиця мутантів + приклад тесту + LLM-текст (якщо є).
|
|
85
|
-
- **Stryker incremental mode** у `stryker.config.baseline.mjs`: `incremental: true` + `incrementalFile: 'reports/stryker/stryker-incremental.json'` — Stryker зберігає прогрес між прогонами, відновлює стан після переривання (SIGURG, OOM тощо).
|
|
86
|
-
- **`skills/coverage-fix`**: новий скіл `/n-coverage-fix` — читає `## Recommendations` з COVERAGE.md і ітеративно дописує тести до конвергенції mutation score, включаючи LLM-рекомендації та приклади тестів у промпт агента.
|
|
42
|
+
- **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вижилих мутантів перейменована `## Recommendations` → `## Вижилі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься скілами `/n-fix-tests` і `/n-coverage-fix`.
|
|
43
|
+
- **`skills/fix-tests/SKILL.md`**: конвенція test-файлів оновлена — цільовий файл завжди `<dir>/tests/<basename>.test.mjs`; якщо знайдено co-located тест (`.test.js`/`.test.mjs`) — переноситься в `tests/` з оновленням imports.
|
|
87
44
|
|
|
88
45
|
## [1.19.2] - 2026-05-25
|
|
89
46
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.3",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"n",
|
|
11
11
|
"pi-package"
|
|
12
12
|
],
|
|
13
|
-
"pi": {
|
|
14
|
-
"skills": "skills",
|
|
15
|
-
"extensions": ".pi-template/extensions"
|
|
16
|
-
},
|
|
17
13
|
"homepage": "https://github.com/n/cursor#readme",
|
|
18
14
|
"bugs": {
|
|
19
15
|
"url": "https://github.com/n/cursor/issues"
|
|
@@ -64,5 +60,9 @@
|
|
|
64
60
|
"engines": {
|
|
65
61
|
"bun": ">=1.3",
|
|
66
62
|
"node": ">=25"
|
|
63
|
+
},
|
|
64
|
+
"pi": {
|
|
65
|
+
"skills": "skills",
|
|
66
|
+
"extensions": ".pi-template/extensions"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -13,6 +13,9 @@ import { join } from 'node:path'
|
|
|
13
13
|
|
|
14
14
|
import { resolveJsRoot } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
15
15
|
|
|
16
|
+
const TEST_BLOCK_START = /^\s*(it|test)\(/
|
|
17
|
+
const FILE_EXTENSION = /\.[^.]+$/
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Чи `scripts` містить coverage-сумісну команду.
|
|
18
21
|
* @param {Record<string, string> | undefined} scripts секція scripts з package.json
|
|
@@ -89,13 +92,15 @@ export function extractFirstTestBlock(content) {
|
|
|
89
92
|
let depth = 0
|
|
90
93
|
let inBlock = false
|
|
91
94
|
const result = []
|
|
92
|
-
for (
|
|
93
|
-
if (startLine === -1 &&
|
|
95
|
+
for (const [i, line] of lines.entries()) {
|
|
96
|
+
if (startLine === -1 && TEST_BLOCK_START.test(line)) startLine = i
|
|
94
97
|
if (startLine === -1) continue
|
|
95
|
-
result.push(
|
|
96
|
-
for (const ch of
|
|
97
|
-
if (ch === '{') {
|
|
98
|
-
|
|
98
|
+
result.push(line)
|
|
99
|
+
for (const ch of line) {
|
|
100
|
+
if (ch === '{') {
|
|
101
|
+
depth++
|
|
102
|
+
inBlock = true
|
|
103
|
+
} else if (ch === '}') depth--
|
|
99
104
|
}
|
|
100
105
|
if (inBlock && depth === 0) break
|
|
101
106
|
}
|
|
@@ -110,7 +115,7 @@ export function extractFirstTestBlock(content) {
|
|
|
110
115
|
* @returns {{testFile:string, code:string|null} | null} null — якщо тест-файл не знайдено
|
|
111
116
|
*/
|
|
112
117
|
export function findExampleTest(jsRoot, filename) {
|
|
113
|
-
const base = filename.replace(
|
|
118
|
+
const base = filename.replace(FILE_EXTENSION, '')
|
|
114
119
|
const candidates = [`${base}.test.js`, `${base}.test.mjs`, `${base}.test.ts`]
|
|
115
120
|
const lastSlash = base.lastIndexOf('/')
|
|
116
121
|
if (lastSlash !== -1) {
|
|
@@ -131,9 +136,9 @@ export function findExampleTest(jsRoot, filename) {
|
|
|
131
136
|
* Парс Stryker mutation.json: Killed+Timeout → caught; Survived+NoCoverage → до total.
|
|
132
137
|
* Compile/Runtime errors виключаються з total.
|
|
133
138
|
* Survived мутанти групуються по файлах з exampleTest.
|
|
134
|
-
* @param {{files:Record<string,{mutants:Array<{status:string,mutatorName?:string,replacement?:string,location?:{start:{line:number,column:number},end:{line:number,column:number}}}>}>}} report
|
|
139
|
+
* @param {{files:Record<string,{mutants:Array<{status:string,mutatorName?:string,replacement?:string,location?:{start:{line:number,column:number},end:{line:number,column:number}}}>}>}} report Stryker mutation.json
|
|
135
140
|
* @param {string|null} [jsRoot] корінь для читання source-рядків і пошуку тест-файлів
|
|
136
|
-
* @returns {{caught:number,total:number,survived:Array<{file:string,mutants:Array<{line:number,col:number,mutantType:string,original:string,replacement:string}>,exampleTest:{testFile:string,code:string|null}|null,recommendationText:string|null}>}}
|
|
141
|
+
* @returns {{caught:number,total:number,survived:Array<{file:string,mutants:Array<{line:number,col:number,mutantType:string,original:string,replacement:string}>,exampleTest:{testFile:string,code:string|null}|null,recommendationText:string|null}>}} результат парсу: caught/total та згруповані survived мутанти
|
|
137
142
|
*/
|
|
138
143
|
export function parseStrykerReport(report, jsRoot) {
|
|
139
144
|
let caught = 0
|
|
@@ -4245,7 +4245,7 @@ export function pdbManifestViolations(manifest, expectedAppLabel, isDevLike) {
|
|
|
4245
4245
|
|
|
4246
4246
|
const NETWORK_POLICY_SNIPPET_URLS = {
|
|
4247
4247
|
deployment: new URL('../policy/network_policy/template/deployment.snippet.yaml', import.meta.url),
|
|
4248
|
-
statefulset: new URL('../policy/network_policy/template/statefulset.snippet.yaml', import.meta.url)
|
|
4248
|
+
statefulset: new URL('../policy/network_policy/template/statefulset.snippet.yaml', import.meta.url)
|
|
4249
4249
|
}
|
|
4250
4250
|
|
|
4251
4251
|
/** @type {Record<string, Record<string, unknown>>} */
|
|
@@ -4256,7 +4256,7 @@ const _snippetCache = {}
|
|
|
4256
4256
|
* Кожен snippet — повний самодостатній канон NetworkPolicy для своєї групи workload-типів
|
|
4257
4257
|
* (без merge між snippets у runtime).
|
|
4258
4258
|
* @param {'deployment' | 'statefulset'} snippetName ім'я сніпету
|
|
4259
|
-
* @returns {{ podSelector?: Record<string, unknown>, policyTypes?: string[], ingress?: unknown[], egress?: unknown[] }}
|
|
4259
|
+
* @returns {{ podSelector?: Record<string, unknown>, policyTypes?: string[], ingress?: unknown[], egress?: unknown[] }} розпарсений spec
|
|
4260
4260
|
*/
|
|
4261
4261
|
export function loadSnippetSpec(snippetName) {
|
|
4262
4262
|
if (_snippetCache[snippetName]) return _snippetCache[snippetName]
|
|
@@ -4277,13 +4277,13 @@ export const KIND_TO_SNIPPET = {
|
|
|
4277
4277
|
Job: 'deployment',
|
|
4278
4278
|
CronJob: 'deployment',
|
|
4279
4279
|
DaemonSet: 'deployment',
|
|
4280
|
-
StatefulSet: 'statefulset'
|
|
4280
|
+
StatefulSet: 'statefulset'
|
|
4281
4281
|
}
|
|
4282
4282
|
|
|
4283
4283
|
/**
|
|
4284
4284
|
* Обирає snippet name для конкретного workload-kind; throws на невідомий.
|
|
4285
4285
|
* @param {string} kind workload-kind
|
|
4286
|
-
* @returns {'deployment' | 'statefulset'}
|
|
4286
|
+
* @returns {'deployment' | 'statefulset'} snippet name
|
|
4287
4287
|
*/
|
|
4288
4288
|
export function snippetNameForKind(kind) {
|
|
4289
4289
|
const name = KIND_TO_SNIPPET[kind]
|
|
@@ -4294,7 +4294,7 @@ export function snippetNameForKind(kind) {
|
|
|
4294
4294
|
/**
|
|
4295
4295
|
* Читає deployment.snippet.yaml і повертає розпарсений spec.
|
|
4296
4296
|
* @deprecated Використовуй loadSnippetSpec('deployment')
|
|
4297
|
-
* @returns {{ podSelector: Record<string, unknown>, policyTypes: string[], ingress: unknown[], egress: unknown[] }}
|
|
4297
|
+
* @returns {{ podSelector: Record<string, unknown>, policyTypes: string[], ingress: unknown[], egress: unknown[] }} розпарсений spec deployment snippet
|
|
4298
4298
|
*/
|
|
4299
4299
|
export function readNetworkPolicySnippet() {
|
|
4300
4300
|
return /** @type {any} */ (loadSnippetSpec('deployment'))
|
|
@@ -4312,9 +4312,11 @@ export function readNetworkPolicySnippet() {
|
|
|
4312
4312
|
export function buildNetworkPolicyYaml(deployName, appLabel, kind) {
|
|
4313
4313
|
const schemaUrl = `${YANNH_BASE}networkpolicy-networking-v1.json`
|
|
4314
4314
|
const snippetName = snippetNameForKind(kind)
|
|
4315
|
-
const spec =
|
|
4315
|
+
const spec = structuredClone(loadSnippetSpec(snippetName))
|
|
4316
4316
|
spec.podSelector.matchLabels = { app: appLabel }
|
|
4317
|
-
const specYaml = stringify(spec, { indent: 2 })
|
|
4317
|
+
const specYaml = stringify(spec, { indent: 2 })
|
|
4318
|
+
.replaceAll(/^(?!$)/gm, ' ')
|
|
4319
|
+
.trimEnd()
|
|
4318
4320
|
return `# yaml-language-server: $schema=${schemaUrl}
|
|
4319
4321
|
apiVersion: networking.k8s.io/v1
|
|
4320
4322
|
kind: NetworkPolicy
|
|
@@ -4326,7 +4328,6 @@ spec:
|
|
|
4326
4328
|
${specYaml}`
|
|
4327
4329
|
}
|
|
4328
4330
|
|
|
4329
|
-
|
|
4330
4331
|
/**
|
|
4331
4332
|
* Додає `resourceName` у `resources:` kustomization/Component YAML, якщо ще немає; сортує за алфавітом (en).
|
|
4332
4333
|
* @param {string} raw вміст `kustomization.yaml`
|
|
@@ -5094,12 +5095,12 @@ function validateNetworkPolicyForWorkload(npDocs, workloadName, appLabel, worklo
|
|
|
5094
5095
|
}
|
|
5095
5096
|
const spec = /** @type {Record<string, unknown>} */ (matchedNp).spec
|
|
5096
5097
|
const foundLabel = networkPolicyPodSelectorAppLabel(spec)
|
|
5097
|
-
if (foundLabel
|
|
5098
|
+
if (foundLabel === appLabel) {
|
|
5099
|
+
passFn(`${npRel}: NetworkPolicy для ${workloadKind} '${workloadName}' валідний (k8s.mdc)`)
|
|
5100
|
+
} else {
|
|
5098
5101
|
fail(
|
|
5099
5102
|
`${npRel}: NetworkPolicy '${workloadName}' spec.podSelector.matchLabels.app='${foundLabel}' не відповідає мітці workload '${appLabel}' (k8s.mdc)`
|
|
5100
5103
|
)
|
|
5101
|
-
} else {
|
|
5102
|
-
passFn(`${npRel}: NetworkPolicy для ${workloadKind} '${workloadName}' валідний (k8s.mdc)`)
|
|
5103
5104
|
}
|
|
5104
5105
|
}
|
|
5105
5106
|
|
|
@@ -6379,12 +6380,14 @@ export async function regenerateLegacyNetworkPolicyDocsInFile(npAbs) {
|
|
|
6379
6380
|
const spec = docRec.spec
|
|
6380
6381
|
const appLabel = networkPolicyPodSelectorAppLabel(spec)
|
|
6381
6382
|
const meta = docRec.metadata
|
|
6382
|
-
const annotations =
|
|
6383
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6383
|
+
const annotations =
|
|
6384
|
+
meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
6385
|
+
? /** @type {Record<string, unknown>} */ (meta).annotations
|
|
6386
|
+
: null
|
|
6387
|
+
const rawKind =
|
|
6388
|
+
annotations !== null && typeof annotations === 'object' && !Array.isArray(annotations)
|
|
6389
|
+
? /** @type {Record<string, unknown>} */ (annotations)['nitra.dev/workload-kind']
|
|
6390
|
+
: null
|
|
6388
6391
|
const kind = typeof rawKind === 'string' && rawKind !== '' ? rawKind : 'Deployment'
|
|
6389
6392
|
if (typeof name === 'string' && name !== '' && appLabel !== '') specs.push({ name, appLabel, kind })
|
|
6390
6393
|
}
|
|
@@ -6572,8 +6575,8 @@ function runAllK8sRego(root, yamlFiles, fail) {
|
|
|
6572
6575
|
files: allYaml,
|
|
6573
6576
|
templateData: {
|
|
6574
6577
|
deployment_snippet: loadSnippetSpec('deployment'),
|
|
6575
|
-
statefulset_snippet: loadSnippetSpec('statefulset')
|
|
6576
|
-
}
|
|
6578
|
+
statefulset_snippet: loadSnippetSpec('statefulset')
|
|
6579
|
+
}
|
|
6577
6580
|
},
|
|
6578
6581
|
{ ns: 'k8s.kustomization', dir: 'k8s/kustomization', files: kustYaml },
|
|
6579
6582
|
{ ns: 'k8s.svc_yaml', dir: 'k8s/svc_yaml', files: svcYaml },
|
|
@@ -6584,7 +6587,12 @@ function runAllK8sRego(root, yamlFiles, fail) {
|
|
|
6584
6587
|
|
|
6585
6588
|
for (const t of targets) {
|
|
6586
6589
|
if (t.files.length === 0) continue
|
|
6587
|
-
const violations = runConftestBatch({
|
|
6590
|
+
const violations = runConftestBatch({
|
|
6591
|
+
policyDirRel: t.dir,
|
|
6592
|
+
namespace: t.ns,
|
|
6593
|
+
files: t.files,
|
|
6594
|
+
templateData: t.templateData
|
|
6595
|
+
})
|
|
6588
6596
|
for (const v of violations) {
|
|
6589
6597
|
fail(`${relOf(v.filename)}: ${v.message}`)
|
|
6590
6598
|
}
|
|
@@ -72,31 +72,13 @@ deny contains "spec.ingress має містити from.podSelector (NetworkPolic
|
|
|
72
72
|
not ingress_has_pod_selector_rule(spec)
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
# Dispatch на повний canon-snippet за анотацією nitra.dev/workload-kind.
|
|
76
|
-
# StatefulSet → statefulset_snippet (з intra-replica), решта → deployment_snippet.
|
|
77
|
-
canon_for_kind("StatefulSet") := data.template.statefulset_snippet
|
|
78
|
-
|
|
79
|
-
canon_for_kind(kind) := data.template.deployment_snippet if {
|
|
80
|
-
kind != "StatefulSet"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
snippet_name_for_kind("StatefulSet") := "statefulset"
|
|
84
|
-
|
|
85
|
-
snippet_name_for_kind(kind) := "deployment" if {
|
|
86
|
-
kind != "StatefulSet"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
workload_kind := kind if {
|
|
90
|
-
kind := object.get(object.get(input.metadata, "annotations", {}), "nitra.dev/workload-kind", "")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
75
|
# Superset-check egress: кожне канонічне правило має бути в input.spec.egress.
|
|
94
76
|
deny contains msg if {
|
|
95
77
|
is_np_doc
|
|
96
78
|
is_object(object.get(input, "spec", null))
|
|
97
79
|
canon := canon_for_kind(workload_kind)
|
|
98
80
|
some canon_rule in canon.egress
|
|
99
|
-
not
|
|
81
|
+
not contains_item(object.get(input.spec, "egress", []), canon_rule)
|
|
100
82
|
msg := sprintf(
|
|
101
83
|
"NetworkPolicy %v: відсутнє обовʼязкове egress-правило (%v.snippet.yaml; k8s.mdc): %v",
|
|
102
84
|
[input.metadata.name, snippet_name_for_kind(workload_kind), json.marshal(canon_rule)],
|
|
@@ -109,7 +91,7 @@ deny contains msg if {
|
|
|
109
91
|
is_object(object.get(input, "spec", null))
|
|
110
92
|
canon := canon_for_kind(workload_kind)
|
|
111
93
|
some canon_rule in canon.ingress
|
|
112
|
-
not
|
|
94
|
+
not contains_item(object.get(input.spec, "ingress", []), canon_rule)
|
|
113
95
|
msg := sprintf(
|
|
114
96
|
"NetworkPolicy %v: відсутнє обовʼязкове ingress-правило (%v.snippet.yaml; k8s.mdc): %v",
|
|
115
97
|
[input.metadata.name, snippet_name_for_kind(workload_kind), json.marshal(canon_rule)],
|
|
@@ -124,6 +106,22 @@ deny contains "spec.egress: заборонено allow-all {} — додавай
|
|
|
124
106
|
count(object.keys(rule)) == 0
|
|
125
107
|
}
|
|
126
108
|
|
|
109
|
+
# Dispatch на повний canon-snippet за анотацією nitra.dev/workload-kind.
|
|
110
|
+
# StatefulSet → statefulset_snippet (з intra-replica), решта → deployment_snippet.
|
|
111
|
+
canon_for_kind("StatefulSet") := data.template.statefulset_snippet
|
|
112
|
+
|
|
113
|
+
canon_for_kind(kind) := data.template.deployment_snippet if {
|
|
114
|
+
kind != "StatefulSet"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
snippet_name_for_kind("StatefulSet") := "statefulset"
|
|
118
|
+
|
|
119
|
+
snippet_name_for_kind(kind) := "deployment" if {
|
|
120
|
+
kind != "StatefulSet"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
workload_kind := object.get(object.get(input.metadata, "annotations", {}), "nitra.dev/workload-kind", "")
|
|
124
|
+
|
|
127
125
|
is_np_doc if input.kind == "NetworkPolicy"
|
|
128
126
|
|
|
129
127
|
is_np_doc if startswith(object.get(input, "apiVersion", ""), "networking.k8s.io/")
|
|
@@ -146,8 +144,8 @@ ingress_has_pod_selector_rule(spec) if {
|
|
|
146
144
|
object.get(peer, "podSelector", null) != null
|
|
147
145
|
}
|
|
148
146
|
|
|
149
|
-
|
|
147
|
+
contains_item(items, item) if {
|
|
150
148
|
is_array(items)
|
|
151
|
-
some
|
|
152
|
-
|
|
149
|
+
some candidate in items
|
|
150
|
+
candidate == item
|
|
153
151
|
}
|
|
@@ -92,18 +92,15 @@ export function renderMarkdown(rows) {
|
|
|
92
92
|
|
|
93
93
|
const allSurvived = rows.flatMap(r => r.survived ?? [])
|
|
94
94
|
if (allSurvived.length > 0) {
|
|
95
|
-
lines.push('', '##
|
|
95
|
+
lines.push('', '## Вижилі мутанти', '', '```json', JSON.stringify(allSurvived, null, 2), '```')
|
|
96
|
+
// Людиночитабельна таблиця
|
|
96
97
|
for (const group of allSurvived) {
|
|
97
|
-
lines.push('', `### ${group.file}`, '')
|
|
98
|
-
lines.push('| Рядок | Оригінал | Заміна | Тип |')
|
|
99
|
-
lines.push('| --- | --- | --- | --- |')
|
|
98
|
+
lines.push('', `### ${group.file}`, '', '| Рядок | Оригінал | Заміна | Тип |', '| --- | --- | --- | --- |')
|
|
100
99
|
for (const m of group.mutants) {
|
|
101
100
|
lines.push(`| ${m.line} | \`${m.original}\` | \`${m.replacement}\` | ${m.mutantType} |`)
|
|
102
101
|
}
|
|
103
102
|
if (group.exampleTest) {
|
|
104
|
-
lines.push('', `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`, '', '```js')
|
|
105
|
-
lines.push(group.exampleTest.code ?? '')
|
|
106
|
-
lines.push('```')
|
|
103
|
+
lines.push('', `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`, '', '```js', group.exampleTest.code ?? '', '```')
|
|
107
104
|
}
|
|
108
105
|
if (group.recommendationText) {
|
|
109
106
|
lines.push('', '**Що треба протестувати:**', '', group.recommendationText)
|
|
@@ -184,9 +181,7 @@ export async function runCoverageSteps(opts = {}) {
|
|
|
184
181
|
if (opts.fix) {
|
|
185
182
|
const allSurvived = rows.flatMap(r => r.survived ?? [])
|
|
186
183
|
// eslint-disable-next-line no-unsanitized/method -- шлях відносний до пакету, не user-input
|
|
187
|
-
const { fixSurvivedMutants } = await import(
|
|
188
|
-
new URL('../../scripts/coverage-fix.mjs', import.meta.url).href
|
|
189
|
-
)
|
|
184
|
+
const { fixSurvivedMutants } = await import(new URL('../../scripts/coverage-fix.mjs', import.meta.url).href)
|
|
190
185
|
await fixSurvivedMutants(allSurvived, cwd)
|
|
191
186
|
}
|
|
192
187
|
|
|
@@ -42,9 +42,7 @@ export async function check() {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (!existsSync(BASELINE_PATH)) {
|
|
45
|
-
reporter.fail(
|
|
46
|
-
`.cargo/mutants.toml canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`
|
|
47
|
-
)
|
|
45
|
+
reporter.fail(`.cargo/mutants.toml canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`)
|
|
48
46
|
return reporter.getExitCode()
|
|
49
47
|
}
|
|
50
48
|
|
|
@@ -9,7 +9,7 @@ export default {
|
|
|
9
9
|
reporters: ['json', 'clear-text'],
|
|
10
10
|
jsonReporter: { fileName: 'reports/stryker/mutation.json' },
|
|
11
11
|
coverageAnalysis: 'off',
|
|
12
|
-
// incremental: зберігає
|
|
12
|
+
// incremental: зберігає результати між запусками, відновлює після краш/kill.
|
|
13
13
|
incremental: true,
|
|
14
|
-
incrementalFile: 'reports/stryker/
|
|
14
|
+
incrementalFile: 'reports/stryker/incremental.json'
|
|
15
15
|
}
|
|
@@ -49,9 +49,7 @@ export async function check() {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (!existsSync(BASELINE_PATH)) {
|
|
52
|
-
reporter.fail(
|
|
53
|
-
`stryker.config.mjs canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`
|
|
54
|
-
)
|
|
52
|
+
reporter.fail(`stryker.config.mjs canonical baseline не знайдено (${BASELINE_PATH}) — перевстанови @nitra/cursor`)
|
|
55
53
|
return reporter.getExitCode()
|
|
56
54
|
}
|
|
57
55
|
|
package/scripts/coverage-fix.mjs
CHANGED
|
@@ -39,7 +39,7 @@ export async function fixSurvivedMutants(survived, projectRoot) {
|
|
|
39
39
|
options: {
|
|
40
40
|
cwd: projectRoot,
|
|
41
41
|
maxTurns: 20,
|
|
42
|
-
allowedTools: ['Read', 'Edit', 'Bash']
|
|
42
|
+
allowedTools: ['Read', 'Edit', 'Bash']
|
|
43
43
|
}
|
|
44
44
|
})) {
|
|
45
45
|
if (msg.type === 'text') process.stdout.write(msg.text)
|
|
@@ -50,9 +50,9 @@ export async function fixSurvivedMutants(survived, projectRoot) {
|
|
|
50
50
|
/**
|
|
51
51
|
* Формує rich-промпт для агента: список вижилих мутантів згрупований по файлах,
|
|
52
52
|
* з контекстом ±3 рядки навколо кожного мутанта з source-файлу.
|
|
53
|
-
* @param {SurvivedFileGroup[]} survived
|
|
54
|
-
* @param {string} projectRoot
|
|
55
|
-
* @returns {Promise<string>}
|
|
53
|
+
* @param {SurvivedFileGroup[]} survived групи вижилих мутантів по файлах
|
|
54
|
+
* @param {string} projectRoot корінь проєкту
|
|
55
|
+
* @returns {Promise<string>} текст rich-промпту
|
|
56
56
|
*/
|
|
57
57
|
async function buildFixPrompt(survived, projectRoot) {
|
|
58
58
|
const sections = []
|
|
@@ -60,25 +60,30 @@ async function buildFixPrompt(survived, projectRoot) {
|
|
|
60
60
|
for (const { file, mutants, exampleTest } of survived) {
|
|
61
61
|
let srcLines = []
|
|
62
62
|
try {
|
|
63
|
-
|
|
63
|
+
const src = await readFile(join(projectRoot, file), 'utf8')
|
|
64
|
+
srcLines = src.split('\n')
|
|
64
65
|
} catch {
|
|
65
66
|
// файл може бути недоступним — пропускаємо контекст, але продовжуємо
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const mutantDescs = mutants
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
69
|
+
const mutantDescs = mutants
|
|
70
|
+
.map(m => {
|
|
71
|
+
const ctxStart = Math.max(0, m.line - 4)
|
|
72
|
+
const ctxEnd = Math.min(srcLines.length, m.line + 3)
|
|
73
|
+
const context = srcLines
|
|
74
|
+
.slice(ctxStart, ctxEnd)
|
|
75
|
+
.map((l, i) => `${ctxStart + i + 1}: ${l}`)
|
|
76
|
+
.join('\n')
|
|
77
|
+
return [
|
|
78
|
+
` - Рядок ${m.line}, колонка ${m.col}, тип мутації \`${m.mutantType}\``,
|
|
79
|
+
` Оригінал: \`${m.original}\``,
|
|
80
|
+
` Вижив варіант: \`${m.replacement}\``,
|
|
81
|
+
context ? ` Контекст:\n\`\`\`\n${context}\n\`\`\`` : ''
|
|
82
|
+
]
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.join('\n')
|
|
85
|
+
})
|
|
86
|
+
.join('\n')
|
|
82
87
|
|
|
83
88
|
const exampleSection = exampleTest?.code
|
|
84
89
|
? `\n\nПриклад тесту з \`${exampleTest.testFile}\`:\n\`\`\`js\n${exampleTest.code}\n\`\`\``
|
|
@@ -58,9 +58,6 @@ export function formatTimingSummary(title, timings) {
|
|
|
58
58
|
const failMark = ok ? '' : ' ❌'
|
|
59
59
|
lines.push(` ${id.padEnd(idWidth)} ${formatDurationMs(ms)}${failMark}`)
|
|
60
60
|
}
|
|
61
|
-
lines.push(
|
|
62
|
-
` ${RULER.repeat(idWidth + 2 + 6)}`,
|
|
63
|
-
` ${'total'.padEnd(idWidth)} ${formatDurationMs(totalMs)}`
|
|
64
|
-
)
|
|
61
|
+
lines.push(` ${RULER.repeat(idWidth + 2 + 6)}`, ` ${'total'.padEnd(idWidth)} ${formatDurationMs(totalMs)}`)
|
|
65
62
|
return `${lines.join('\n')}\n`
|
|
66
63
|
}
|
|
@@ -104,7 +104,7 @@ function extractFilePath(stdinJson) {
|
|
|
104
104
|
* Точка входу. Викликається з `bin/n-cursor.js` коли argv[0] === `post-tool-use-fix`.
|
|
105
105
|
* Параметри ін'єктовні для тестів: `stdinJson` обходить read від `process.stdin`,
|
|
106
106
|
* `spawnFn` — заміна `node:child_process.spawn` (повертає EventEmitter-сумісний об'єкт).
|
|
107
|
-
* @param {{ stdinJson?: string, spawnFn?: typeof spawn }} [options]
|
|
107
|
+
* @param {{ stdinJson?: string, spawnFn?: typeof spawn }} [options] параметри для тестів (ін'єкція stdin/spawn)
|
|
108
108
|
* @returns {Promise<number>} exit code (0 — пропущено / fix ОК; інше — exit-код `fix`)
|
|
109
109
|
*/
|
|
110
110
|
export async function runPostToolUseFixCli(options = {}) {
|
|
@@ -502,7 +502,6 @@ export async function syncPiExtensions(projectRoot, bundledPackageRoot) {
|
|
|
502
502
|
* Видаляє `.pi/extensions/n-cursor-adr/` директорію з проєкту-споживача.
|
|
503
503
|
* Викликається коли правило `adr` вимкнено у `.n-cursor.json` (симетрично до
|
|
504
504
|
* cleanup-у `.claude/hooks/{capture,normalize}-decisions.sh`).
|
|
505
|
-
*
|
|
506
505
|
* @param {string} projectRoot корінь проєкту-споживача
|
|
507
506
|
* @returns {Promise<{ removed: boolean, path: string }>} чи було щось видалено та відносний шлях
|
|
508
507
|
*/
|
|
@@ -1,114 +1,138 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: n-coverage-fix
|
|
3
3
|
description: >-
|
|
4
|
-
Автономна команда: запускає coverage
|
|
4
|
+
Автономна команда: запускає n-cursor coverage → читає вижилих мутантів → ітеративно пише тести до конвергенції (max 3 ітерації)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
#
|
|
7
|
+
# n-coverage-fix — підвищення mutation score
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Мета
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Автоматично підвищити mutation score: запускає coverage, знаходить survived mutants, пише тести, повторює до конвергенції.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## ⚠️ Не запускати паралельно
|
|
14
|
+
|
|
15
|
+
Цей скіл **не можна** запускати паралельно в різних агентах або Bash-задачах.
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
`n-cursor coverage` всередині серіалізований через `withLock('coverage')` — другий виклик чекатиме. Але Stryker пише `mutation.json` і `incremental.json` в одну директорію: паралельний запуск **зіпсує обидва файли**. Запускай тільки один `/n-coverage-fix` одночасно.
|
|
16
18
|
|
|
17
|
-
##
|
|
19
|
+
## Передумови
|
|
20
|
+
|
|
21
|
+
- Поточна директорія — корінь проєкту (де `.n-cursor.json` і `COVERAGE.md`)
|
|
22
|
+
- `n-cursor coverage` доступний (`npx @nitra/cursor coverage` або `bun run coverage`)
|
|
23
|
+
- Залежності встановлені (`bun i`)
|
|
24
|
+
|
|
25
|
+
## Workflow
|
|
18
26
|
|
|
19
27
|
### Крок 1: Запусти coverage
|
|
20
28
|
|
|
21
29
|
```bash
|
|
22
|
-
|
|
30
|
+
n-cursor coverage
|
|
23
31
|
```
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
Або якщо є у `package.json#scripts`:
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
```bash
|
|
36
|
+
bun run coverage
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Ця команда генерує `COVERAGE.md`. Якщо є survived mutants — COVERAGE.md матиме секцію `## Вижилі мутанти` з JSON-блоком.
|
|
40
|
+
|
|
41
|
+
### Крок 2: Перевір вижилих
|
|
28
42
|
|
|
29
|
-
|
|
43
|
+
Прочитай `COVERAGE.md`. Знайди секцію `## Вижилі мутанти`. Знайди фенсований блок ` ```json ` і розпарси JSON-масив.
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
Якщо секція відсутня або масив порожній — зупинись:
|
|
32
46
|
|
|
33
|
-
Якщо секції немає або вона порожня:
|
|
34
47
|
```
|
|
35
|
-
✓
|
|
48
|
+
✓ Жодних вижилих мутантів — mutation score повний. Coverage завершено.
|
|
36
49
|
```
|
|
37
|
-
→ DONE
|
|
38
50
|
|
|
39
|
-
Запам'ятай
|
|
51
|
+
Запам'ятай `prevCount = масив.length`.
|
|
40
52
|
|
|
41
|
-
### Крок 3: Для кожного файлу
|
|
53
|
+
### Крок 3: Для кожного файлу — спауни Agent
|
|
42
54
|
|
|
43
|
-
|
|
55
|
+
Згрупуй мутанти по полю `file`. Для кожної групи:
|
|
44
56
|
|
|
45
|
-
**3a.
|
|
46
|
-
- Source-файл (`<file>` від кореня проєкту)
|
|
47
|
-
- Таблицю вижилих мутантів: рядок, оригінал, заміна, тип
|
|
48
|
-
- Блок `**Приклад наявного тесту:**` — style guide для нових тестів
|
|
57
|
+
**3a. Визнач test файл (завжди у `tests/` директорії):**
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
1. `<dir>/<basename>.test.js` — поруч із source
|
|
53
|
-
2. `<dir>/<basename>.spec.js`
|
|
54
|
-
3. `test/<basename>.test.js` від кореня
|
|
55
|
-
4. `tests/<basename>.test.js` від кореня
|
|
59
|
+
Цільовий: `<dir>/tests/<basename>.test.mjs`
|
|
60
|
+
(де `<dir>` — директорія source-файлу, `<basename>` — ім'я source без розширення)
|
|
56
61
|
|
|
57
|
-
Якщо
|
|
62
|
+
1. Якщо `<dir>/tests/<basename>.test.mjs` існує → використай
|
|
63
|
+
2. Якщо `<dir>/<basename>.test.js` або `<dir>/<basename>.test.mjs` існує (co-located) →
|
|
64
|
+
- Перенеси до `<dir>/tests/<basename>.test.mjs`
|
|
65
|
+
- Оновити відносні imports (тепер `../` рівень вгору до source)
|
|
66
|
+
3. Жоден не знайдено → буде створено `<dir>/tests/<basename>.test.mjs`
|
|
58
67
|
|
|
59
|
-
**
|
|
68
|
+
**3b. Сформуй промпт для Agent:**
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Тобі дані вижилі мутанти зі Stryker для файлу `<file>`.
|
|
72
|
+
Ці мутанти вижили бо наявні тести НЕ вловили конкретні зміни коду.
|
|
60
73
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- `StringLiteral` / `EqualityOperator`: перевір точний рядок/значення, а не лише happy-path
|
|
74
|
+
**Вихідний код** (`<file>`):
|
|
75
|
+
\`\`\`
|
|
76
|
+
<зміст source-файлу>
|
|
77
|
+
\`\`\`
|
|
66
78
|
|
|
79
|
+
**Наявні тести** (`<test-file>`):
|
|
80
|
+
\`\`\`
|
|
81
|
+
<зміст test-файлу або "файл ще не існує">
|
|
82
|
+
\`\`\`
|
|
83
|
+
|
|
84
|
+
**Вижилі мутанти** (кожен — зміна коду що НЕ вловлена):
|
|
85
|
+
<для кожного мутанта:>
|
|
86
|
+
- Рядок <line>, колонка <col>: `<original>` → `<replacement>` (тип: <mutantType>)
|
|
87
|
+
|
|
88
|
+
**Завдання:**
|
|
89
|
+
Допиши мінімальні test-cases у файл `<test-file>` які вловлять кожен мутант.
|
|
67
90
|
Правила:
|
|
68
91
|
- НЕ видаляй і НЕ змінюй наявні тести
|
|
69
|
-
-
|
|
70
|
-
- Якщо
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
bun test <testFile>
|
|
92
|
+
- Стиль тестів — відповідно до наявного файлу (той самий фреймворк, describe/test)
|
|
93
|
+
- Якщо файл ще не існує — створи `<dir>/tests/<basename>.test.mjs` з правильними імпортами.
|
|
94
|
+
Приклад: source `src/services/auth-store.js` → import `import { ... } from '../auth-store.js'`
|
|
95
|
+
- Після написання запусти: `bun test <test-file>` і переконайся що тести проходять (виправ якщо падають, 1-2 спроби)
|
|
75
96
|
```
|
|
76
97
|
|
|
77
|
-
|
|
98
|
+
**3c. Запусти Agent** з цим промптом. Дочекайся завершення.
|
|
78
99
|
|
|
79
|
-
### Крок 4: Перевір що
|
|
100
|
+
### Крок 4: Перевір що всі тести проходять
|
|
80
101
|
|
|
81
102
|
```bash
|
|
82
103
|
bun test
|
|
83
104
|
```
|
|
84
105
|
|
|
85
|
-
Якщо
|
|
86
|
-
- Не відкочувати зміни
|
|
87
|
-
- Показати: яка помилка, які файли змінені, що вже покращено
|
|
88
|
-
- Очікувати рішення від user: [виправити вручну → продовжити] / [пропустити файл] / [зупинити]
|
|
106
|
+
Якщо падають — поверни відповідний Agent з помилкою і попроси виправити.
|
|
89
107
|
|
|
90
|
-
### Крок 5: Запусти coverage і
|
|
108
|
+
### Крок 5: Запусти coverage і порівняй
|
|
91
109
|
|
|
92
110
|
```bash
|
|
93
|
-
|
|
111
|
+
n-cursor coverage
|
|
94
112
|
```
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
Прочитай новий COVERAGE.md. Візьми `new_score` з рядка `| **Разом** |`.
|
|
114
|
+
Прочитай новий `COVERAGE.md`. Розпарси JSON-масив вижилих.
|
|
115
|
+
`newCount = новий масив.length`
|
|
99
116
|
|
|
100
117
|
**Рішення:**
|
|
101
|
-
|
|
102
|
-
-
|
|
118
|
+
|
|
119
|
+
- `newCount < prevCount` AND iterations < 3 → повтор з Кроку 2 з оновленим масивом
|
|
120
|
+
- `newCount >= prevCount` → конвергенція:
|
|
103
121
|
```
|
|
104
122
|
✓ Конвергенція: mutation score більше не покращується.
|
|
105
|
-
|
|
123
|
+
Було вижилих: <prevCount>, стало: <newCount>.
|
|
124
|
+
```
|
|
125
|
+
- iterations == 3 → зупинись:
|
|
126
|
+
```
|
|
127
|
+
⚠️ Досягнуто максимум ітерацій (3).
|
|
128
|
+
Вижило: <newCount> мутантів. Деякі можуть бути невбивними (dead code, external state).
|
|
106
129
|
```
|
|
107
|
-
|
|
130
|
+
|
|
131
|
+
## Конвергенція — нормальний результат
|
|
132
|
+
|
|
133
|
+
Деякі мутанти неможливо вбити: захищений зовнішній стан, недетермінована логіка, еквівалентні мутації. Не намагайся виправити те що не змінилось після ітерації.
|
|
108
134
|
|
|
109
135
|
## Нотатки
|
|
110
136
|
|
|
111
137
|
- Stryker `incremental` (`incrementalFile`) зберігає прогрес між запусками — crash ≠ перезапуск з нуля
|
|
112
138
|
- Не комітити зміни автоматично — user вирішує коли комітити
|
|
113
|
-
- Пріоритет файлів: більше вижилих мутантів = важливіший (першим у Recommendations = найважливіший)
|
|
114
|
-
- Якщо `COVERAGE.md` відсутній — запусти `bun coverage` спочатку
|
|
@@ -32,10 +32,12 @@ description: >-
|
|
|
32
32
|
Прочитай `package.json` у кореневій директорії.
|
|
33
33
|
|
|
34
34
|
**test-команда** (перша що існує):
|
|
35
|
+
|
|
35
36
|
1. `scripts["test"]` з `package.json`
|
|
36
37
|
2. fallback: `bun test`
|
|
37
38
|
|
|
38
39
|
**coverage-команда** (перша що існує):
|
|
40
|
+
|
|
39
41
|
1. `scripts["coverage"]` з `package.json` → виклик: `bun run coverage`
|
|
40
42
|
2. fallback: `n-cursor coverage`
|
|
41
43
|
|
|
@@ -43,13 +45,18 @@ description: >-
|
|
|
43
45
|
|
|
44
46
|
Згрупуй мутанти по полю `file`. Для кожної групи виконай:
|
|
45
47
|
|
|
46
|
-
**3a. Знайди
|
|
48
|
+
**3a. Знайди / визнач test файл (завжди у `tests/` директорії):**
|
|
49
|
+
|
|
50
|
+
Цільовий файл завжди: `<dir>/tests/<basename>.test.mjs`
|
|
51
|
+
(де `<dir>` — директорія source-файлу, `<basename>` — ім'я без розширення)
|
|
52
|
+
|
|
47
53
|
- Source: `<cwd>/<file>` (прочитай вміст)
|
|
48
|
-
- Test
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
- Test файл:
|
|
55
|
+
1. Якщо `<dir>/tests/<basename>.test.mjs` існує → використай його
|
|
56
|
+
2. Якщо `<dir>/<basename>.test.js` або `<dir>/<basename>.test.mjs` існує (co-located) →
|
|
57
|
+
- Перенеси файл до `<dir>/tests/<basename>.test.mjs`
|
|
58
|
+
- Оновити відносні `import` шляхи якщо є (тепер треба `../` рівень вгору)
|
|
59
|
+
3. Якщо жоден не знайдено → буде створено `<dir>/tests/<basename>.test.mjs`
|
|
53
60
|
|
|
54
61
|
**3b. Сформуй промпт для Agent:**
|
|
55
62
|
|
|
@@ -76,7 +83,9 @@ description: >-
|
|
|
76
83
|
Правила:
|
|
77
84
|
- НЕ видаляй і НЕ змінюй наявні тести
|
|
78
85
|
- Стиль тестів — відповідно до наявного файлу (той самий фреймворк, той самий стиль describe/test)
|
|
79
|
-
- Якщо файл ще не існує — створи
|
|
86
|
+
- Якщо файл ще не існує — створи `<dir>/tests/<basename>.test.mjs` з правильними імпортами.
|
|
87
|
+
Приклад: source `src/services/auth-store.js` → test `src/services/tests/auth-store.test.mjs`,
|
|
88
|
+
import: `import { ... } from '../auth-store.js'`
|
|
80
89
|
- Після написання запусти: `bun test <test-file>` і переконайся що всі тести проходять (виправ якщо падають)
|
|
81
90
|
```
|
|
82
91
|
|
|
@@ -100,6 +109,7 @@ bun run coverage # або coverage-команда з кроку 2
|
|
|
100
109
|
`newCount = новий масив.length`
|
|
101
110
|
|
|
102
111
|
**Рішення:**
|
|
112
|
+
|
|
103
113
|
- Якщо `newCount < prevCount` → повтор з Кроку 1 з оновленим масивом
|
|
104
114
|
- Якщо `newCount >= prevCount` → зупинись:
|
|
105
115
|
`✓ Конвергенція: mutation score більше не покращується. Вижило: <newCount> мутантів.`
|