@nitra/cursor 1.24.0 → 1.25.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-template/hooks/capture-decisions.sh +4 -55
- package/.claude-template/hooks/lib/tooling-only.sh +61 -0
- package/.claude-template/hooks/normalize-decisions.sh +4 -49
- package/CHANGELOG.md +6 -54
- package/bin/n-cursor.js +5 -0
- package/package.json +1 -1
- package/rules/test/coverage/coverage.mjs +6 -1
- package/rules/test/js/data/stryker_config/stryker.config.baseline.mjs +2 -2
- package/scripts/sync-claude-config.mjs +55 -1
- package/skills/coverage-fix/SKILL.md +82 -58
- package/skills/fix-tests/SKILL.md +14 -7
|
@@ -34,61 +34,10 @@ mkdir -p "$ADR_DIR" "$LOG_DIR"
|
|
|
34
34
|
|
|
35
35
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
36
36
|
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
# Bash 3.2: без mapfile/асоц. масивів.
|
|
42
|
-
is_tooling_only_change() {
|
|
43
|
-
local proj="$1"
|
|
44
|
-
local had_file=0
|
|
45
|
-
local f rel
|
|
46
|
-
while IFS= read -r f; do
|
|
47
|
-
[ -z "$f" ] && continue
|
|
48
|
-
had_file=1
|
|
49
|
-
case "$f" in
|
|
50
|
-
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
51
|
-
/*) return 1 ;;
|
|
52
|
-
*) rel="$f" ;;
|
|
53
|
-
esac
|
|
54
|
-
case "$rel" in
|
|
55
|
-
.cspell.json) ;;
|
|
56
|
-
docs/adr/*.md) ;;
|
|
57
|
-
AGENTS.md|CLAUDE.md) ;;
|
|
58
|
-
CHANGELOG.md) ;;
|
|
59
|
-
*/CHANGELOG.md) ;;
|
|
60
|
-
package.json|*/package.json)
|
|
61
|
-
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
62
|
-
return 1
|
|
63
|
-
fi
|
|
64
|
-
;;
|
|
65
|
-
*) return 1 ;;
|
|
66
|
-
esac
|
|
67
|
-
done
|
|
68
|
-
[ "$had_file" = "1" ] && return 0
|
|
69
|
-
return 1
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
73
|
-
# Поза git-репо або при помилці — вертаємо 1 (не tooling).
|
|
74
|
-
git_diff_only_version_field() {
|
|
75
|
-
local proj="$1" path="$2"
|
|
76
|
-
[ -d "$proj/.git" ] || return 1
|
|
77
|
-
local diff
|
|
78
|
-
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
79
|
-
[ -z "$diff" ] && return 1
|
|
80
|
-
local line
|
|
81
|
-
while IFS= read -r line; do
|
|
82
|
-
case "$line" in
|
|
83
|
-
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
84
|
-
[+-]*'"version":'*) continue ;;
|
|
85
|
-
[+-]*) return 1 ;;
|
|
86
|
-
esac
|
|
87
|
-
done <<EOF
|
|
88
|
-
$diff
|
|
89
|
-
EOF
|
|
90
|
-
return 0
|
|
91
|
-
}
|
|
37
|
+
# Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
|
|
38
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
39
|
+
# shellcheck source=lib/tooling-only.sh
|
|
40
|
+
. "$SCRIPT_DIR/lib/tooling-only.sh"
|
|
92
41
|
|
|
93
42
|
log "fired: $SESSION_ID"
|
|
94
43
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Спільний helper для ADR Stop-hook'ів: розпізнавання "tooling-only" сесій.
|
|
3
|
+
# Source'ається з capture-decisions.sh і normalize-decisions.sh — функції стають
|
|
4
|
+
# видимими caller'у, який успадковує `set` опції.
|
|
5
|
+
# Bash 3.2 (macOS /bin/bash) сумісний: без mapfile, без асоц. масивів,
|
|
6
|
+
# без process substitution.
|
|
7
|
+
|
|
8
|
+
# Структурний скіп ADR-генерації для "tooling-only" сесій.
|
|
9
|
+
# Вхід: рядки-шляхи у stdin (один шлях на лінію), відносні до $PROJECT_ROOT
|
|
10
|
+
# або абсолютні з префіксом $PROJECT_ROOT (нормалізуємо тут).
|
|
11
|
+
# Вихід: 0 — усі шляхи в allowlist; 1 — є хоч один змістовний шлях.
|
|
12
|
+
is_tooling_only_change() {
|
|
13
|
+
local proj="$1"
|
|
14
|
+
local had_file=0
|
|
15
|
+
local f rel
|
|
16
|
+
while IFS= read -r f; do
|
|
17
|
+
[ -z "$f" ] && continue
|
|
18
|
+
had_file=1
|
|
19
|
+
case "$f" in
|
|
20
|
+
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
21
|
+
/*) return 1 ;;
|
|
22
|
+
*) rel="$f" ;;
|
|
23
|
+
esac
|
|
24
|
+
case "$rel" in
|
|
25
|
+
.cspell.json) ;;
|
|
26
|
+
docs/adr/*.md) ;;
|
|
27
|
+
AGENTS.md|CLAUDE.md) ;;
|
|
28
|
+
CHANGELOG.md) ;;
|
|
29
|
+
*/CHANGELOG.md) ;;
|
|
30
|
+
package.json|*/package.json)
|
|
31
|
+
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
32
|
+
return 1
|
|
33
|
+
fi
|
|
34
|
+
;;
|
|
35
|
+
*) return 1 ;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
[ "$had_file" = "1" ] && return 0
|
|
39
|
+
return 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
43
|
+
# Поза git-репо або при помилці — вертаємо 1 (не tooling).
|
|
44
|
+
git_diff_only_version_field() {
|
|
45
|
+
local proj="$1" path="$2"
|
|
46
|
+
[ -d "$proj/.git" ] || return 1
|
|
47
|
+
local diff
|
|
48
|
+
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
49
|
+
[ -z "$diff" ] && return 1
|
|
50
|
+
local line
|
|
51
|
+
while IFS= read -r line; do
|
|
52
|
+
case "$line" in
|
|
53
|
+
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
54
|
+
[+-]*'"version":'*) continue ;;
|
|
55
|
+
[+-]*) return 1 ;;
|
|
56
|
+
esac
|
|
57
|
+
done <<EOF
|
|
58
|
+
$diff
|
|
59
|
+
EOF
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
@@ -39,55 +39,10 @@ mkdir -p "$LOG_DIR"
|
|
|
39
39
|
|
|
40
40
|
log() { printf '%s %s\n' "$(date -Iseconds)" "$*" >> "$LOG"; }
|
|
41
41
|
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
proj="$1"
|
|
47
|
-
had_file=0
|
|
48
|
-
while IFS= read -r f; do
|
|
49
|
-
[ -z "$f" ] && continue
|
|
50
|
-
had_file=1
|
|
51
|
-
case "$f" in
|
|
52
|
-
"$proj"/*) rel="${f#"$proj"/}" ;;
|
|
53
|
-
/*) return 1 ;;
|
|
54
|
-
*) rel="$f" ;;
|
|
55
|
-
esac
|
|
56
|
-
case "$rel" in
|
|
57
|
-
.cspell.json) ;;
|
|
58
|
-
docs/adr/*.md) ;;
|
|
59
|
-
AGENTS.md|CLAUDE.md) ;;
|
|
60
|
-
CHANGELOG.md) ;;
|
|
61
|
-
*/CHANGELOG.md) ;;
|
|
62
|
-
package.json|*/package.json)
|
|
63
|
-
if ! git_diff_only_version_field "$proj" "$rel"; then
|
|
64
|
-
return 1
|
|
65
|
-
fi
|
|
66
|
-
;;
|
|
67
|
-
*) return 1 ;;
|
|
68
|
-
esac
|
|
69
|
-
done
|
|
70
|
-
[ "$had_file" = "1" ] && return 0
|
|
71
|
-
return 1
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
# Допоміжна: чи git-diff для файлу торкається ЛИШЕ рядків з `"version":`.
|
|
75
|
-
git_diff_only_version_field() {
|
|
76
|
-
proj="$1"; path="$2"
|
|
77
|
-
[ -d "$proj/.git" ] || return 1
|
|
78
|
-
diff=$(cd "$proj" && git diff HEAD --unified=0 -- "$path" 2>/dev/null) || return 1
|
|
79
|
-
[ -z "$diff" ] && return 1
|
|
80
|
-
while IFS= read -r line; do
|
|
81
|
-
case "$line" in
|
|
82
|
-
'+++ '*|'--- '*|'@@ '*|'') continue ;;
|
|
83
|
-
[+-]*'"version":'*) continue ;;
|
|
84
|
-
[+-]*) return 1 ;;
|
|
85
|
-
esac
|
|
86
|
-
done <<EOF
|
|
87
|
-
$diff
|
|
88
|
-
EOF
|
|
89
|
-
return 0
|
|
90
|
-
}
|
|
42
|
+
# Підвантажуємо спільний helper (sourcing — не sub-shell, функції видимі поточному скрипту).
|
|
43
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
44
|
+
# shellcheck source=lib/tooling-only.sh
|
|
45
|
+
. "$SCRIPT_DIR/lib/tooling-only.sh"
|
|
91
46
|
|
|
92
47
|
# Витягає поле `transcript:` з YAML frontmatter ADR-чернетки.
|
|
93
48
|
draft_transcript_path() {
|
package/CHANGELOG.md
CHANGED
|
@@ -4,70 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
6
|
|
|
7
|
-
## [1.
|
|
7
|
+
## [1.25.2] - 2026-05-26
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
- **`pi` manifest у `npm/package.json`** — `{"skills":"skills","extensions":".pi-template/extensions"}`. Pi.dev під час `pi install npm:@nitra/cursor` тепер бачить explicit-шляхи до bundled-ресурсів, замість convention-auto-discovery. `extensions: ".pi-template/extensions"` критичний — pi за замовч. шукає `extensions/` у корені пакета, а у нас шлях нестандартний.
|
|
13
|
-
- **`"pi-package"` keyword** у `npm/package.json` — пакет з’являється у pi-gallery для discoverability.
|
|
11
|
+
- **`stryker.config.mjs` baseline**: `incremental: true` + `incrementalFile: 'reports/stryker/incremental.json'` — Stryker зберігає результати між запусками і відновлює після краш/kill (SIGURG). Важливо для машин з обмеженою RAM де Stryker вбивається системою після ~100 мутантів.
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **`syncPiExtensions`** тепер копіює **всі файли** з теки `.pi-template/extensions/<name>/`, а не лише `index.ts`. Контракт повернення: `{ written, path, files }` — `path` тепер тека (а не файл), `files` — відсортований список базових імен. У `🤖 Claude-конфіг` логу та у `result.piExtension` caller-стороні виводиться тека.
|
|
18
|
-
- **`.pi-template/extensions/n-cursor-adr/index.ts`** — порядок імпортів виправлено (всі `node:*` імпорти підняті на самий верх, перед `interface PiContext`). Усувало `import/first` ESLint-помилку; функціонально без змін.
|
|
19
|
-
|
|
20
|
-
## [1.23.0] - 2026-05-25
|
|
13
|
+
## [1.25.1] - 2026-05-26
|
|
21
14
|
|
|
22
15
|
### Added
|
|
23
16
|
|
|
24
|
-
-
|
|
25
|
-
- `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).
|
|
26
|
-
- `npm/package.json` `files` array: додано `.pi-template` — bundled-директорія шипиться разом із пакетом.
|
|
27
|
-
- `npm/bin/n-cursor.js`: у `🤖 Claude-конфіг`-логу після sync додається `.pi/extensions/n-cursor-adr/index.ts` коли pi-extension згенерована.
|
|
28
|
-
|
|
29
|
-
## [1.22.0] - 2026-05-25
|
|
30
|
-
|
|
31
|
-
### Added
|
|
32
|
-
|
|
33
|
-
- **`npx @nitra/cursor lint`** — оркестратор лінт-ланцюжка з тайменгом на кожен крок. Послідовно запускає присутні у root `package.json` скрипти з фіксованого списку (`lint-ga`, `lint-js`, `lint-rego`, `lint-style`, `lint-text`, `lint-security`, `oxfmt`), **fail-fast** на першому ненульовому exit-коді. Наприкінці друкує таблицю `⏱ Lint timing` з часом кожного кроку — для атрибуції повільних кроків замість анонімного `&&`-агрегатора.
|
|
34
|
-
- **`runFixCommand` тепер друкує `⏱ Fix timing`** після прогону всіх `rules/<id>/fix.mjs` — per-rule час + сума. Маркер `❌` на впалих рядках.
|
|
35
|
-
- `npm/scripts/lib/timing-summary.mjs` — чистий форматер `formatTimingSummary(title, entries)` (спільний для fix і lint). 9 тестів у `tests/timing-summary.test.mjs`.
|
|
36
|
-
- `npm/scripts/lib/run-lint-cli.mjs` — `runLintCli({ cwd, spawnSyncFn, now, log, logError })` з DI для юніт-тестів. 7 тестів у `tests/run-lint-cli.test.mjs`.
|
|
37
|
-
|
|
38
|
-
### Changed
|
|
39
|
-
|
|
40
|
-
- Кореневий `package.json` цього монорепо: `lint` → `n-cursor lint`; додано окремий скрипт `oxfmt: "oxfmt ."`, який раніше йшов у хвості ланцюжка прямою командою.
|
|
41
|
-
- Скіли `/n-fix` і `/n-lint`: додано вимогу копіювати таблицю `⏱` з виводу інструмента у фінальне резюме відповіді користувачу.
|
|
42
|
-
|
|
43
|
-
## [1.21.0] - 2026-05-25
|
|
17
|
+
- **`skills/coverage-fix/SKILL.md`** — автономна команда `/n-coverage-fix`: запускає `n-cursor coverage`, читає JSON-масив вижилих мутантів із секції `## Вижилі мутанти` у COVERAGE.md і ітеративно пише тести до конвергенції (max 3 ітерації). Включає заборону паралельного запуску (Stryker пише в одну директорію).
|
|
44
18
|
|
|
45
19
|
### Changed
|
|
46
20
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
### Added
|
|
51
|
-
|
|
52
|
-
- `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`.
|
|
53
|
-
- `LEGACY_STOP_HOOK_COMMAND_MARKER` — публічний export для тестів і потенційних консьюмерів, які перевіряють відсутність застарілого хука.
|
|
54
|
-
|
|
55
|
-
### Removed
|
|
56
|
-
|
|
57
|
-
- `npm/scripts/claude-stop-hook.mjs` — більше не потрібен.
|
|
58
|
-
|
|
59
|
-
## [1.20.0] - 2026-05-25
|
|
60
|
-
|
|
61
|
-
### Added
|
|
62
|
-
|
|
63
|
-
- **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.
|
|
64
|
-
- **`rules/js-lint/coverage`**: `parseStrykerReport` тепер зчитує оригінальний код вижилих мутантів (`extractOriginal`), групує по файлах і повертає `survived: [{file, mutants: [{line,col,mutantType,original,replacement}], exampleTest, recommendationText}]`; `findExampleTest` + `extractFirstTestBlock` знаходять і витягують перший тест-блок із тест-файлу поруч — для стилю.
|
|
65
|
-
- **LLM-рекомендації у COVERAGE.md**: коли встановлено `ANTHROPIC_API_KEY`, `n-cursor coverage` робить один Anthropic API-виклик на кожен файл з вижилими мутантами та записує рекомендацію «Що треба протестувати» у секцію `## Recommendations`. Модель: `claude-haiku-4-5-20251001` з prompt caching (`ephemeral`). Без ключа — секція генерується без LLM-тексту.
|
|
66
|
-
- **`rules/js-lint/coverage/lib/generate-recommendation.mjs`**: `generateMutantRecommendation(client, sourceContent, mutants)` — ізольований модуль LLM-виклику.
|
|
67
|
-
- **`@anthropic-ai/sdk`** у dependencies — потрібен для LLM-рекомендацій (опціонально: якщо `ANTHROPIC_API_KEY` не задано, sdk не викликається).
|
|
68
|
-
- **`rules/test/coverage`**: `renderMarkdown` генерує секцію `## Recommendations` з per-file підрозділами (`### <file>`) — таблиця мутантів + приклад тесту + LLM-текст (якщо є).
|
|
69
|
-
- **Stryker incremental mode** у `stryker.config.baseline.mjs`: `incremental: true` + `incrementalFile: 'reports/stryker/stryker-incremental.json'` — Stryker зберігає прогрес між прогонами, відновлює стан після переривання (SIGURG, OOM тощо).
|
|
70
|
-
- **`skills/coverage-fix`**: новий скіл `/n-coverage-fix` — читає `## Recommendations` з COVERAGE.md і ітеративно дописує тести до конвергенції mutation score, включаючи LLM-рекомендації та приклади тестів у промпт агента.
|
|
21
|
+
- **`rules/test/coverage/coverage.mjs` → `renderMarkdown`**: секція вижилих мутантів перейменована `## Recommendations` → `## Вижилі мутанти`; доданий ` ```json ` блок з масивом survived перед таблицею — парситься скілами `/n-fix-tests` і `/n-coverage-fix`.
|
|
22
|
+
- **`skills/fix-tests/SKILL.md`**: конвенція test-файлів оновлена — цільовий файл завжди `<dir>/tests/<basename>.test.mjs`; якщо знайдено co-located тест (`.test.js`/`.test.mjs`) — переноситься в `tests/` з оновленням imports.
|
|
71
23
|
|
|
72
24
|
## [1.19.2] - 2026-05-25
|
|
73
25
|
|
package/bin/n-cursor.js
CHANGED
|
@@ -1401,6 +1401,11 @@ async function runSync() {
|
|
|
1401
1401
|
if (result.commands.length > 0) parts.push(`${result.commands.length} slash-commands`)
|
|
1402
1402
|
if (result.adrHook) parts.push('.claude/hooks/capture-decisions.sh')
|
|
1403
1403
|
if (result.adrNormalizeHook) parts.push('.claude/hooks/normalize-decisions.sh')
|
|
1404
|
+
if (result.adrHookLib?.length > 0) {
|
|
1405
|
+
for (const libPath of result.adrHookLib) {
|
|
1406
|
+
parts.push(libPath)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1404
1409
|
if (result.gitignoreAdr) parts.push('.gitignore (adr fragment)')
|
|
1405
1410
|
if (result.piExtension) parts.push('.pi/extensions/n-cursor-adr/')
|
|
1406
1411
|
if (parts.length > 0) {
|
package/package.json
CHANGED
|
@@ -92,7 +92,12 @@ 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('', '## Вижилі мутанти')
|
|
96
|
+
// JSON-блок для /n-fix-tests skill — парситься скілом для написання тестів
|
|
97
|
+
lines.push('', '```json')
|
|
98
|
+
lines.push(JSON.stringify(allSurvived, null, 2))
|
|
99
|
+
lines.push('```')
|
|
100
|
+
// Людиночитабельна таблиця
|
|
96
101
|
for (const group of allSurvived) {
|
|
97
102
|
lines.push('', `### ${group.file}`, '')
|
|
98
103
|
lines.push('| Рядок | Оригінал | Заміна | Тип |')
|
|
@@ -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
|
}
|
|
@@ -61,6 +61,7 @@ const CURSOR_DIR = '.cursor'
|
|
|
61
61
|
const CURSOR_HOOKS_FILE = `${CURSOR_DIR}/hooks.json`
|
|
62
62
|
const ADR_HOOK_SCRIPT_NAME = 'capture-decisions.sh'
|
|
63
63
|
const ADR_NORMALIZE_HOOK_SCRIPT_NAME = 'normalize-decisions.sh'
|
|
64
|
+
const ADR_HOOK_LIB_DIR = 'lib'
|
|
64
65
|
const TEMPLATE_DIR_NAME = '.claude-template'
|
|
65
66
|
|
|
66
67
|
/** Корінь pi.dev артефактів у проєкті-споживачі. */
|
|
@@ -414,6 +415,51 @@ export function syncAdrNormalizeHookScript(projectRoot, templateDir) {
|
|
|
414
415
|
return syncHookScript(projectRoot, templateDir, ADR_NORMALIZE_HOOK_SCRIPT_NAME)
|
|
415
416
|
}
|
|
416
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Копіює всі `.sh`-файли з `.claude-template/hooks/lib/` у `.claude/hooks/lib/` проєкту.
|
|
420
|
+
* Файли source-only (без exec bit) — їх `source`-ять capture/normalize-decisions.sh,
|
|
421
|
+
* щоб не дублювати спільну bash-логіку (`is_tooling_only_change`,
|
|
422
|
+
* `git_diff_only_version_field`).
|
|
423
|
+
* Тека fully-owned: при кожному sync-у перезаписується.
|
|
424
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
425
|
+
* @param {string} templateDir каталог `.claude-template/` усередині пакету
|
|
426
|
+
* @returns {Promise<Array<{ written: boolean, path: string }>>} перелік записаних файлів (порожній, якщо темплейту нема)
|
|
427
|
+
*/
|
|
428
|
+
export async function syncAdrHookLibScripts(projectRoot, templateDir) {
|
|
429
|
+
const libTemplateDir = join(templateDir, 'hooks', ADR_HOOK_LIB_DIR)
|
|
430
|
+
if (!existsSync(libTemplateDir)) {
|
|
431
|
+
return []
|
|
432
|
+
}
|
|
433
|
+
const entries = await readdir(libTemplateDir, { withFileTypes: true })
|
|
434
|
+
const libDestDir = join(projectRoot, CLAUDE_HOOKS_DIR, ADR_HOOK_LIB_DIR)
|
|
435
|
+
await mkdir(libDestDir, { recursive: true })
|
|
436
|
+
const written = []
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
if (!entry.isFile() || !entry.name.endsWith('.sh')) continue
|
|
439
|
+
const content = await readFile(join(libTemplateDir, entry.name), 'utf8')
|
|
440
|
+
// НЕ chmod 755 — source-файли не виконувані (їх лише `.`-ять caller-скрипти).
|
|
441
|
+
await writeFile(join(libDestDir, entry.name), content, 'utf8')
|
|
442
|
+
written.push({ written: true, path: `${CLAUDE_HOOKS_DIR}/${ADR_HOOK_LIB_DIR}/${entry.name}` })
|
|
443
|
+
}
|
|
444
|
+
return written
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Видаляє `.claude/hooks/lib/` директорію з проєкту-споживача.
|
|
449
|
+
* Викликається коли правило `adr` вимкнено — lib-файли не самостійні, без хуків,
|
|
450
|
+
* що їх source-ять, вони нікому не потрібні (симетрично до `removeOrphanPiExtension`).
|
|
451
|
+
* @param {string} projectRoot корінь проєкту-споживача
|
|
452
|
+
* @returns {Promise<{ removed: boolean, path: string }>} чи було щось видалено та відносний шлях
|
|
453
|
+
*/
|
|
454
|
+
export async function removeOrphanAdrHookLib(projectRoot) {
|
|
455
|
+
const libDir = join(projectRoot, CLAUDE_HOOKS_DIR, ADR_HOOK_LIB_DIR)
|
|
456
|
+
if (!existsSync(libDir)) {
|
|
457
|
+
return { removed: false, path: '' }
|
|
458
|
+
}
|
|
459
|
+
await rm(libDir, { recursive: true, force: true })
|
|
460
|
+
return { removed: true, path: `${CLAUDE_HOOKS_DIR}/${ADR_HOOK_LIB_DIR}` }
|
|
461
|
+
}
|
|
462
|
+
|
|
417
463
|
/**
|
|
418
464
|
* Копіює bundled pi.dev TS-extension `npm/.pi-template/extensions/n-cursor-adr/` (усі файли —
|
|
419
465
|
* `index.ts`, `tsconfig.json`, потенційні `package.json`/`.gitignore` тощо) у
|
|
@@ -558,7 +604,7 @@ export async function syncClaudeCommands(projectRoot, templateDir) {
|
|
|
558
604
|
* @param {string} options.bundledPackageRoot корінь установленого `@nitra/cursor`
|
|
559
605
|
* @param {boolean} options.enabled чи увімкнено sync (з `.n-cursor.json` `claude-config`)
|
|
560
606
|
* @param {string[]} [options.rules] список увімкнених правил із `.n-cursor.json` — впливає на ADR Stop-hook (`adr`)
|
|
561
|
-
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, gitignoreAdr: boolean, piExtension: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore`/pi-extension
|
|
607
|
+
* @returns {Promise<{ settings: boolean, cursorHooks: boolean, commands: string[], adrHook: boolean, adrNormalizeHook: boolean, adrHookLib: string[], gitignoreAdr: boolean, piExtension: boolean }>} прапорці записів settings/Cursor hooks/ADR-hook(s)/`.gitignore`/pi-extension, перелік lib-файлів і список slash-команд
|
|
562
608
|
*/
|
|
563
609
|
export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enabled, rules = [] }) {
|
|
564
610
|
if (!enabled) {
|
|
@@ -568,6 +614,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
568
614
|
commands: [],
|
|
569
615
|
adrHook: false,
|
|
570
616
|
adrNormalizeHook: false,
|
|
617
|
+
adrHookLib: [],
|
|
571
618
|
gitignoreAdr: false,
|
|
572
619
|
piExtension: false
|
|
573
620
|
}
|
|
@@ -580,6 +627,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
580
627
|
commands: [],
|
|
581
628
|
adrHook: false,
|
|
582
629
|
adrNormalizeHook: false,
|
|
630
|
+
adrHookLib: [],
|
|
583
631
|
gitignoreAdr: false,
|
|
584
632
|
piExtension: false
|
|
585
633
|
}
|
|
@@ -589,6 +637,11 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
589
637
|
const adrNormalizeHook = includeAdrHook
|
|
590
638
|
? await syncAdrNormalizeHookScript(projectRoot, templateDir)
|
|
591
639
|
: { written: false, path: '' }
|
|
640
|
+
// Lib-файли мають сенс лише з активним хоча б одним ADR-хуком — без caller'а
|
|
641
|
+
// нікому source-ити; при вимкненому правилі прибираємо орфан-теку.
|
|
642
|
+
const adrHookLibEntries = includeAdrHook
|
|
643
|
+
? await syncAdrHookLibScripts(projectRoot, templateDir)
|
|
644
|
+
: (await removeOrphanAdrHookLib(projectRoot), [])
|
|
592
645
|
const gitignoreAdr = includeAdrHook
|
|
593
646
|
? await syncGitignoreAdrFragment(projectRoot, bundledPackageRoot)
|
|
594
647
|
: { written: false, path: '' }
|
|
@@ -604,6 +657,7 @@ export async function syncClaudeConfig({ projectRoot, bundledPackageRoot, enable
|
|
|
604
657
|
commands,
|
|
605
658
|
adrHook: adrHook.written,
|
|
606
659
|
adrNormalizeHook: adrNormalizeHook.written,
|
|
660
|
+
adrHookLib: adrHookLibEntries.map(e => e.path),
|
|
607
661
|
gitignoreAdr: gitignoreAdr.written,
|
|
608
662
|
piExtension: piExtension.written
|
|
609
663
|
}
|
|
@@ -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` спочатку
|
|
@@ -43,13 +43,18 @@ description: >-
|
|
|
43
43
|
|
|
44
44
|
Згрупуй мутанти по полю `file`. Для кожної групи виконай:
|
|
45
45
|
|
|
46
|
-
**3a. Знайди
|
|
46
|
+
**3a. Знайди / визнач test файл (завжди у `tests/` директорії):**
|
|
47
|
+
|
|
48
|
+
Цільовий файл завжди: `<dir>/tests/<basename>.test.mjs`
|
|
49
|
+
(де `<dir>` — директорія source-файлу, `<basename>` — ім'я без розширення)
|
|
50
|
+
|
|
47
51
|
- Source: `<cwd>/<file>` (прочитай вміст)
|
|
48
|
-
- Test
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
- Test файл:
|
|
53
|
+
1. Якщо `<dir>/tests/<basename>.test.mjs` існує → використай його
|
|
54
|
+
2. Якщо `<dir>/<basename>.test.js` або `<dir>/<basename>.test.mjs` існує (co-located) →
|
|
55
|
+
- Перенеси файл до `<dir>/tests/<basename>.test.mjs`
|
|
56
|
+
- Оновити відносні `import` шляхи якщо є (тепер треба `../` рівень вгору)
|
|
57
|
+
3. Якщо жоден не знайдено → буде створено `<dir>/tests/<basename>.test.mjs`
|
|
53
58
|
|
|
54
59
|
**3b. Сформуй промпт для Agent:**
|
|
55
60
|
|
|
@@ -76,7 +81,9 @@ description: >-
|
|
|
76
81
|
Правила:
|
|
77
82
|
- НЕ видаляй і НЕ змінюй наявні тести
|
|
78
83
|
- Стиль тестів — відповідно до наявного файлу (той самий фреймворк, той самий стиль describe/test)
|
|
79
|
-
- Якщо файл ще не існує — створи
|
|
84
|
+
- Якщо файл ще не існує — створи `<dir>/tests/<basename>.test.mjs` з правильними імпортами.
|
|
85
|
+
Приклад: source `src/services/auth-store.js` → test `src/services/tests/auth-store.test.mjs`,
|
|
86
|
+
import: `import { ... } from '../auth-store.js'`
|
|
80
87
|
- Після написання запусти: `bun test <test-file>` і переконайся що всі тести проходять (виправ якщо падають)
|
|
81
88
|
```
|
|
82
89
|
|