@nitra/cursor 1.25.2 → 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.
@@ -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,6 +4,27 @@
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.3] - 2026-05-26
8
+
9
+ ### Fixed
10
+
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`).
19
+
20
+ ### Changed
21
+
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).
27
+
7
28
  ## [1.25.2] - 2026-05-26
8
29
 
9
30
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.25.2",
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 (let i = 0; i < lines.length; i++) {
93
- if (startLine === -1 && /^\s*(it|test)\(/.test(lines[i])) startLine = i
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(lines[i])
96
- for (const ch of lines[i]) {
97
- if (ch === '{') { depth++; inBlock = true }
98
- else if (ch === '}') depth--
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 = JSON.parse(JSON.stringify(loadSnippetSpec(snippetName)))
4315
+ const spec = structuredClone(loadSnippetSpec(snippetName))
4316
4316
  spec.podSelector.matchLabels = { app: appLabel }
4317
- const specYaml = stringify(spec, { indent: 2 }).replaceAll(/^(?!$)/gm, ' ').trimEnd()
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 !== appLabel) {
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 = (meta !== null && typeof meta === 'object' && !Array.isArray(meta))
6383
- ? /** @type {Record<string, unknown>} */ (meta).annotations
6384
- : null
6385
- const rawKind = (annotations !== null && typeof annotations === 'object' && !Array.isArray(annotations))
6386
- ? /** @type {Record<string, unknown>} */ (annotations)['nitra.dev/workload-kind']
6387
- : null
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({ policyDirRel: t.dir, namespace: t.ns, files: t.files, templateData: t.templateData })
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 list_contains(object.get(input.spec, "egress", []), canon_rule)
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 list_contains(object.get(input.spec, "ingress", []), canon_rule)
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
- list_contains(items, item) if {
147
+ contains_item(items, item) if {
150
148
  is_array(items)
151
- some i
152
- items[i] == item
149
+ some candidate in items
150
+ candidate == item
153
151
  }
@@ -92,23 +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('', '## Вижилі мутанти')
96
- // JSON-блок для /n-fix-tests skill — парситься скілом для написання тестів
97
- lines.push('', '```json')
98
- lines.push(JSON.stringify(allSurvived, null, 2))
99
- lines.push('```')
95
+ lines.push('', '## Вижилі мутанти', '', '```json', JSON.stringify(allSurvived, null, 2), '```')
100
96
  // Людиночитабельна таблиця
101
97
  for (const group of allSurvived) {
102
- lines.push('', `### ${group.file}`, '')
103
- lines.push('| Рядок | Оригінал | Заміна | Тип |')
104
- lines.push('| --- | --- | --- | --- |')
98
+ lines.push('', `### ${group.file}`, '', '| Рядок | Оригінал | Заміна | Тип |', '| --- | --- | --- | --- |')
105
99
  for (const m of group.mutants) {
106
100
  lines.push(`| ${m.line} | \`${m.original}\` | \`${m.replacement}\` | ${m.mutantType} |`)
107
101
  }
108
102
  if (group.exampleTest) {
109
- lines.push('', `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`, '', '```js')
110
- lines.push(group.exampleTest.code ?? '')
111
- lines.push('```')
103
+ lines.push('', `**Приклад тесту** (\`${group.exampleTest.testFile}\`):`, '', '```js', group.exampleTest.code ?? '', '```')
112
104
  }
113
105
  if (group.recommendationText) {
114
106
  lines.push('', '**Що треба протестувати:**', '', group.recommendationText)
@@ -189,9 +181,7 @@ export async function runCoverageSteps(opts = {}) {
189
181
  if (opts.fix) {
190
182
  const allSurvived = rows.flatMap(r => r.survived ?? [])
191
183
  // eslint-disable-next-line no-unsanitized/method -- шлях відносний до пакету, не user-input
192
- const { fixSurvivedMutants } = await import(
193
- new URL('../../scripts/coverage-fix.mjs', import.meta.url).href
194
- )
184
+ const { fixSurvivedMutants } = await import(new URL('../../scripts/coverage-fix.mjs', import.meta.url).href)
195
185
  await fixSurvivedMutants(allSurvived, cwd)
196
186
  }
197
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
 
@@ -11,5 +11,5 @@ export default {
11
11
  coverageAnalysis: 'off',
12
12
  // incremental: зберігає результати між запусками, відновлює після краш/kill.
13
13
  incremental: true,
14
- incrementalFile: 'reports/stryker/incremental.json',
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
 
@@ -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
- srcLines = (await readFile(join(projectRoot, file), 'utf8')).split('\n')
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.map(m => {
69
- const ctxStart = Math.max(0, m.line - 4)
70
- const ctxEnd = Math.min(srcLines.length, m.line + 3)
71
- const context = srcLines
72
- .slice(ctxStart, ctxEnd)
73
- .map((l, i) => `${ctxStart + i + 1}: ${l}`)
74
- .join('\n')
75
- return [
76
- ` - Рядок ${m.line}, колонка ${m.col}, тип мутації \`${m.mutantType}\``,
77
- ` Оригінал: \`${m.original}\``,
78
- ` Вижив варіант: \`${m.replacement}\``,
79
- context ? ` Контекст:\n\`\`\`\n${context}\n\`\`\`` : ''
80
- ].filter(Boolean).join('\n')
81
- }).join('\n')
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
  */
@@ -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
 
@@ -107,6 +109,7 @@ bun run coverage # або coverage-команда з кроку 2
107
109
  `newCount = новий масив.length`
108
110
 
109
111
  **Рішення:**
112
+
110
113
  - Якщо `newCount < prevCount` → повтор з Кроку 1 з оновленим масивом
111
114
  - Якщо `newCount >= prevCount` → зупинись:
112
115
  `✓ Конвергенція: mutation score більше не покращується. Вижило: <newCount> мутантів.`