@nitra/cursor 1.28.6 → 1.28.7

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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
6
6
 
7
+ ## [1.28.7] - 2026-05-28
8
+
9
+ ### Fixed
10
+
11
+ - **`rules/js-lint/coverage/coverage.mjs`** — `n-cursor coverage` із кореня тепер працює у monorepo з частковим покриттям тестами (наприклад, `ai`: тести лише у `gt/`, інші `cf/*`/`run/*` без тестів). До цього перший workspace без тестів обривав ланцюг із `JS coverage exit 1`. Зміни: `defaultRunner.runJsCoverage` отримує `--passWithNoTests` (vitest 4.x — exit 0 у workspace без тестів); `collect()` розпиляно на `collectOneRoot(jsRoot, cwd, runner)` (per-workspace) і публічний агрегатор. `collectOneRoot` повертає `null` для workspace із порожнім lcov — Stryker у такому випадку не запускається. Реальні помилки (vitest exit ≠ 0, mutation.json відсутній при наявних тестах, compile errors) — throw, не маскуються. Якщо тестів немає у жодному workspace — `collect()` повертає `[]`, оркестратор `rules/test/coverage/coverage.mjs:runCoverageSteps` обробляє це як exit 1 із explainer-ом. Helpers `addCoverage`/`addMutation` беруться з `rules/test/coverage/coverage.mjs` (DRY, замість локальних копій). Додано 4 тести: monorepo з порожніми workspaces (skip), all-empty (`[]`), single-package без тестів, vitest exit ≠ 0 у monorepo (throw). ADR — `docs/adr/2026-05-28-coverage-multi-workspace-iteration.md`.
12
+ - **`rules/test/test.mdc`** (`version` 2.4 → 2.5) і дзеркало `.cursor/rules/n-test.mdc` — секція "Multi-workspace iteration" з описом ітерації `resolveAllJsRoots()` і поведінки skip workspaces без тестів.
13
+
14
+ ### Changed
15
+
16
+ - `abie` rule: `abie.package_json_docs` → `abie.package_json_shared`; пакет, що його перевіряє правило, перейменовано з `@nitra/abie-docs` на `@nitra/abie-shared` (паралель до `efes.package_json_shared` / `@nitra/efes-shared`). Реалізація: `npm/rules/abie/policy/package_json_shared/` (перейменовано з `package_json_docs/`). Bump `abie.mdc` `1.21` → `1.22`. Зачеплено: `.cursor/rules/conftest.mdc`.
17
+
7
18
  ## [1.28.5] - 2026-05-28
8
19
 
9
20
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/cursor",
3
- "version": "1.28.6",
3
+ "version": "1.28.7",
4
4
  "description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
5
5
  "keywords": [
6
6
  "cli",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  description: Правила для проєктів AbInBev Efes
3
3
  alwaysApply: true
4
- version: '1.21'
4
+ version: '1.22'
5
5
  ---
6
6
 
7
7
  Правило **abie** для споживачів **@nitra/cursor**: **k8s** (Deployment + **HealthCheckPolicy** у **`hc.yaml`**, overlay **ua** — **nodeSelector**, **HTTPRoute** (будь-який непорожній **`target.name`**, для спільних сервісів **`auth-run-hl`** / **`file-link-hl`** — **`namespace: dev`** у base та patch **`…/backendRefs/…/namespace`** у **ua**)), гілки **dev**, **ua** у **clean-merged-branch**, а також заборона тримати артефакти **Firebase Hosting** у **підкаталогах першого рівня** (безпосередні діти кореня репозиторію; у самому корені ці імена не вимагаються до видалення).
@@ -140,12 +140,12 @@ KVCMS_URL=http://kvcms-hl.ua-apruv.svc.abie-ua.internal:8080
140
140
 
141
141
  Загальне правило про **внутрішній** URL (не публічний домен) для `HASURA_GRAPHQL_ENDPOINT` лишається у **`hasura.mdc`** (для nitra і abie) — `rules/hasura/fix.mjs` приймає кластерний DNS-формат `<cluster>.internal`.
142
142
 
143
- ## `@nitra/abie-docs` у `devDependencies`
143
+ ## `@nitra/abie-shared` у `devDependencies`
144
144
 
145
- У кореневому **`package.json`** abie-проєкту в **`devDependencies`** має бути **`@nitra/abie-docs`** — пакет з канонічними контрактами/схемами abie-сервісів (наприклад, шляхи `node_modules/@nitra/abie-docs/...` для імпорту схем). Версію правило не фіксує — лише presence. Додати:
145
+ У кореневому **`package.json`** abie-проєкту в **`devDependencies`** має бути **`@nitra/abie-shared`** — пакет зі спільними abie-ресурсами: канонічні GraphQL-схеми, скіли, типи (наприклад, шляхи `node_modules/@nitra/abie-shared/schema/...` для імпорту схем). Версію правило не фіксує — лише presence. Додати:
146
146
 
147
147
  ```bash
148
- bun add -d @nitra/abie-docs
148
+ bun add -d @nitra/abie-shared
149
149
  ```
150
150
 
151
151
  ## Firebase Hosting
@@ -168,7 +168,7 @@ bun add -d @nitra/abie-docs
168
168
  - **`health_check_policy/`** → `abie.health_check_policy` — структура HealthCheckPolicy: `apiVersion: networking.gke.io/v1`, `metadata.name`, `spec.default.config.type: HTTP`, `httpHealthCheck.requestPath` починається з `/`, `port: 8080`, `targetRef.kind: Service`, `targetRef.name` має суфікс `-hl`. **Цільові файли:** `…/k8s/.../hc.yaml`.
169
169
  - **`base_deployment_preem/`** → `abie.base_deployment_preem` — Deployment у base/ має `spec.template.spec.nodeSelector.preem` зі значенням `true` (boolean або рядок). **Цільові файли:** ресурсні YAML під `…/k8s/.../base/...`.
170
170
  - **`clean_merged_ignore_branches/`** → `abie.clean_merged_ignore_branches` — у workflow `.github/workflows/clean-merged-branch.yml` крок з `uses: phpdocker-io/github-actions-delete-abandoned-branches` має `with.ignore_branches`, що містить токени `dev,ua` (case-insensitive). **Цільові файли:** `.github/workflows/clean-merged-branch.yml`.
171
- - **`package_json_docs/`** → `abie.package_json_docs` — у кореневому `package.json` `devDependencies` має містити `@nitra/abie-docs` (presence-only, версію не фіксуємо). **Цільові файли:** `package.json`.
171
+ - **`package_json_shared/`** → `abie.package_json_shared` — у кореневому `package.json` `devDependencies` має містити `@nitra/abie-shared` (presence-only, версію не фіксуємо). **Цільові файли:** `package.json`.
172
172
 
173
173
  Cross-file / FS-логіка лишається у JS-частинах (`js/<concern>.mjs`) — Rego не читає файлову систему й не робить cross-document резолюцію:
174
174
 
@@ -1,16 +1,16 @@
1
1
  # Перевірка кореневого `package.json` abie-проєкту: у `devDependencies` має бути
2
- # `@nitra/abie-docs` (контракти/схеми abie-сервісів — наприклад
3
- # `node_modules/@nitra/abie-docs/...`). Версію не фіксуємо — лише presence.
2
+ # `@nitra/abie-shared` (контракти/схеми/скіли abie-сервісів — наприклад
3
+ # `node_modules/@nitra/abie-shared/...`). Версію не фіксуємо — лише presence.
4
4
  #
5
5
  # Inverse-presence перевірка — лишається inline у rego (як `@nitra/cspell-dict`
6
6
  # у `text.package_json`), бо у template/ зберігаємо позитивні snippet/deny канони,
7
7
  # а не одиничний required-ключ.
8
- package abie.package_json_docs
8
+ package abie.package_json_shared
9
9
 
10
10
  import rego.v1
11
11
 
12
12
  deny contains msg if {
13
13
  dev := object.get(input, "devDependencies", {})
14
- not "@nitra/abie-docs" in object.keys(dev)
15
- msg := "package.json: devDependencies має містити @nitra/abie-docs — bun add -d @nitra/abie-docs (abie.mdc)"
14
+ not "@nitra/abie-shared" in object.keys(dev)
15
+ msg := "package.json: devDependencies має містити @nitra/abie-shared — bun add -d @nitra/abie-shared (abie.mdc)"
16
16
  }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "$schema": "https://unpkg.com/@nitra/cursor/schemas/target.json",
3
3
  "files": { "single": "package.json", "required": true },
4
- "missingMessage": "package.json не існує — створи його, додай devDependencies['@nitra/abie-docs'] (abie.mdc)"
4
+ "missingMessage": "package.json не існує — створи його, додай devDependencies['@nitra/abie-shared'] (abie.mdc)"
5
5
  }
@@ -13,6 +13,7 @@ import { tmpdir } from 'node:os'
13
13
  import { join, relative } from 'node:path'
14
14
 
15
15
  import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
16
+ import { addCoverage, addMutation } from '../../test/coverage/coverage.mjs'
16
17
 
17
18
  const TEST_BLOCK_START = /^\s*(it|test)\(/
18
19
  const FILE_EXTENSION = /\.[^.]+$/
@@ -210,12 +211,23 @@ export function parseStrykerReport(report, jsRoot) {
210
211
  /**
211
212
  * Default runner — спавнить реальні bun-команди через `node:child_process.spawnSync`
212
213
  * (працює і в Node-runtime через shebang `n-cursor`, і в Bun). Замінюється у тестах.
214
+ *
215
+ * Прапор `--passWithNoTests` робить vitest non-failing у workspaces без тестів
216
+ * (типовий патерн monorepo, де тести зосереджені в одному пакеті); пустий lcov
217
+ * у такому випадку сигналізує "no tests" → collectOneRoot пропускає workspace.
213
218
  */
214
219
  const defaultRunner = {
215
220
  runJsCoverage({ cwd, lcovDir }) {
216
221
  const r = spawnSync(
217
222
  'bunx',
218
- ['vitest', 'run', '--coverage', '--coverage.reporter=lcov', `--coverage.reportsDirectory=${lcovDir}`],
223
+ [
224
+ 'vitest',
225
+ 'run',
226
+ '--passWithNoTests',
227
+ '--coverage',
228
+ '--coverage.reporter=lcov',
229
+ `--coverage.reportsDirectory=${lcovDir}`
230
+ ],
219
231
  { cwd, stdio: 'inherit', env: process.env }
220
232
  )
221
233
  return r.status ?? 1
@@ -227,91 +239,108 @@ const defaultRunner = {
227
239
  }
228
240
 
229
241
  /**
230
- * Збирає JS-метрики покриття + мутаційного тестування. У monorepo ітерує кожен
231
- * JS-root з `resolveAllJsRoots()` (включно з glob-патернами на кшталт `cf/*`),
232
- * запускає vitest+Stryker у кожному та сумує lcov/mutation. Шляхи файлів у
233
- * `survived` рібейзяться відносно `cwd`, щоб `coverage-fix.mjs` знаходив джерела.
242
+ * Збирає метрики покриття + мутаційного тестування для **одного** JS-root.
243
+ * Пропускає workspace без тестів (повертає `null`): vitest у такому випадку
244
+ * пройшов з `--passWithNoTests`, але lcov порожній нема сенсу запускати
245
+ * Stryker. Реальні помилки (vitest exit ≠ 0, mutation.json відсутній попри
246
+ * наявні тести) кидаються — у multi-root режимі це не маскує справжній збій.
247
+ * @param {string} jsRoot абсолютний шлях до workspace-кореня
248
+ * @param {string} cwd корінь проєкту (для рібейзингу `survived[].file`)
249
+ * @param {{runJsCoverage:Function, runStryker:Function}} runner spawn-ін'єкція
250
+ * @returns {Promise<{coverage:object, mutation:{caught:number,total:number}, survived:Array<object>} | null>} результати або null коли workspace без тестів
251
+ */
252
+ async function collectOneRoot(jsRoot, cwd, runner) {
253
+ const wsRel = relative(cwd, jsRoot)
254
+
255
+ // 1. Coverage через vitest run --passWithNoTests --coverage
256
+ const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
257
+ let coverage
258
+ try {
259
+ const code = await runner.runJsCoverage({ cwd: jsRoot, lcovDir })
260
+ if (code !== 0) throw new Error(`JS coverage exit ${code}`)
261
+ const lcovPath = join(lcovDir, 'lcov.info')
262
+ coverage = existsSync(lcovPath)
263
+ ? parseLcov(await readFile(lcovPath, 'utf8'))
264
+ : { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
265
+ } finally {
266
+ await rm(lcovDir, { recursive: true, force: true })
267
+ }
268
+
269
+ // Порожній lcov ⇔ vitest з --passWithNoTests не знайшов тестів. Пропускаємо
270
+ // workspace, щоб не запускати Stryker марно і не псувати агрегат.
271
+ const hasTests = coverage.lines.total > 0 || coverage.functions.total > 0
272
+ if (!hasTests) return null
273
+
274
+ // 2. Mutation через Stryker
275
+ await runner.runStryker({ cwd: jsRoot })
276
+ const mutationPath = join(jsRoot, 'reports', 'stryker', 'mutation.json')
277
+ if (!existsSync(mutationPath)) {
278
+ throw new Error(
279
+ 'js-lint coverage: stryker не залишив mutation.json — ' +
280
+ 'запусти `npx @nitra/cursor fix test` для встановлення canonical stryker.config.mjs, ' +
281
+ 'або налаштуй його вручну'
282
+ )
283
+ }
284
+ const mutationReport = JSON.parse(await readFile(mutationPath, 'utf8'))
285
+ const parsed = parseStrykerReport(mutationReport, jsRoot)
286
+
287
+ return {
288
+ coverage,
289
+ mutation: { caught: parsed.caught, total: parsed.total },
290
+ survived: parsed.survived.map(group => ({
291
+ ...group,
292
+ file: wsRel === '' ? group.file : join(wsRel, group.file),
293
+ exampleTest: group.exampleTest
294
+ ? {
295
+ ...group.exampleTest,
296
+ testFile: wsRel === '' ? group.exampleTest.testFile : join(wsRel, group.exampleTest.testFile)
297
+ }
298
+ : null
299
+ }))
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Збирає JS-метрики покриття + мутаційного тестування. У monorepo ітерує усі
305
+ * JS-roots з `resolveAllJsRoots()` (включно з glob-патернами `cf/*`), запускає
306
+ * vitest+Stryker у кожному та сумує lcov/mutation через `addCoverage`/`addMutation`
307
+ * з оркестратора. Workspaces без тестів пропускаються (див. `collectOneRoot`).
308
+ * Якщо тестів немає у жодному workspace — повертає `[]`; оркестратор
309
+ * `rules/test/coverage/coverage.mjs:runCoverageSteps` обробить це як exit 1
310
+ * з зрозумілим повідомленням ("Жодного провайдера покриття не знайдено").
311
+ * Шляхи у `survived` рібейзяться відносно `cwd`, щоб `coverage-fix.mjs`
312
+ * знаходив джерела через `join(projectRoot, file)`.
234
313
  * @param {string} cwd корінь проєкту
235
314
  * @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція для тестів
236
- * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
315
+ * @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}, survived:Array<object>}>>} рядок `JS` або `[]` коли тестів нема ніде
237
316
  */
238
317
  export async function collect(cwd, opts = {}) {
239
318
  const runner = opts.runner ?? defaultRunner
240
319
  const jsRoots = await resolveAllJsRoots(cwd)
241
320
  if (jsRoots.length === 0) throw new Error('js-lint coverage: package.json не знайдено')
242
321
 
243
- let coverage = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
244
- let mutation = { caught: 0, total: 0 }
245
- const survived = []
246
-
322
+ const results = []
247
323
  for (const jsRoot of jsRoots) {
248
- const wsRel = relative(cwd, jsRoot)
249
-
250
- // 1. Coverage через vitest run --coverage (v8 provider пише lcov.info у lcovDir)
251
- const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
252
- try {
253
- const code = await runner.runJsCoverage({ cwd: jsRoot, lcovDir })
254
- if (code !== 0) throw new Error(`JS coverage exit ${code}`)
255
- coverage = addCoverage(coverage, parseLcov(await readFile(join(lcovDir, 'lcov.info'), 'utf8')))
256
- } finally {
257
- await rm(lcovDir, { recursive: true, force: true })
258
- }
259
-
260
- // 2. Mutation через Stryker
261
- await runner.runStryker({ cwd: jsRoot })
262
- let mutationReport
263
- try {
264
- mutationReport = JSON.parse(await readFile(join(jsRoot, 'reports', 'stryker', 'mutation.json'), 'utf8'))
265
- } catch {
266
- throw new Error(
267
- 'js-lint coverage: stryker не залишив mutation.json — ' +
268
- 'запусти `npx @nitra/cursor fix test` для встановлення canonical stryker.config.mjs, ' +
269
- 'або налаштуй його вручну'
270
- )
271
- }
272
- const parsed = parseStrykerReport(mutationReport, jsRoot)
273
- mutation = addMutation(mutation, { caught: parsed.caught, total: parsed.total })
274
-
275
- for (const group of parsed.survived) {
276
- survived.push({
277
- ...group,
278
- file: wsRel === '' ? group.file : join(wsRel, group.file),
279
- exampleTest: group.exampleTest
280
- ? {
281
- ...group.exampleTest,
282
- testFile: wsRel === '' ? group.exampleTest.testFile : join(wsRel, group.exampleTest.testFile)
283
- }
284
- : null
285
- })
286
- }
324
+ const r = await collectOneRoot(jsRoot, cwd, runner)
325
+ if (r !== null) results.push(r)
287
326
  }
288
327
 
289
- return [{ area: 'JS', coverage, mutation, survived }]
290
- }
291
-
292
- /**
293
- * Сумування coverage-totals — імпортується з оркестратора при потребі; тут
294
- * локальна копія, щоб уникнути циклу test/coverage → js-lint/coverage.
295
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} a перший
296
- * @param {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} b другий
297
- * @returns {{lines:{covered:number,total:number}, functions:{covered:number,total:number}}} сума
298
- */
299
- function addCoverage(a, b) {
300
- return {
301
- lines: { covered: a.lines.covered + b.lines.covered, total: a.lines.total + b.lines.total },
302
- functions: {
303
- covered: a.functions.covered + b.functions.covered,
304
- total: a.functions.total + b.functions.total
305
- }
328
+ if (results.length === 0) {
329
+ console.error(
330
+ 'js-lint coverage: жоден workspace не має тестів ' +
331
+ '(`*.test.{js,mjs}` у `tests/` або поряд із джерелом) — ' +
332
+ 'додай тести або вилучи `js-lint` з .n-cursor.json#rules'
333
+ )
334
+ return []
306
335
  }
307
- }
308
336
 
309
- /**
310
- * Сумування mutation-counts.
311
- * @param {{caught:number,total:number}} a перший
312
- * @param {{caught:number,total:number}} b другий
313
- * @returns {{caught:number,total:number}} сума
314
- */
315
- function addMutation(a, b) {
316
- return { caught: a.caught + b.caught, total: a.total + b.total }
337
+ let coverage = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
338
+ let mutation = { caught: 0, total: 0 }
339
+ const survived = []
340
+ for (const r of results) {
341
+ coverage = addCoverage(coverage, r.coverage)
342
+ mutation = addMutation(mutation, r.mutation)
343
+ survived.push(...r.survived)
344
+ }
345
+ return [{ area: 'JS', coverage, mutation, survived }]
317
346
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: JS-тести (*.test.mjs) живуть у tests/. Правило `test` керує stryker.config.mjs + vitest.config.js (якщо js-lint enabled) і .cargo/mutants.toml (якщо rust enabled).
3
- version: '2.4'
3
+ version: '2.5'
4
4
  globs: "**/{.n-cursor.json,package.json,Cargo.toml,stryker.config.mjs,vitest.config.js,.cargo/mutants.toml},**/*.test.mjs"
5
5
  alwaysApply: false
6
6
  ---
@@ -91,6 +91,10 @@ Canonical `vitest.config.js` (для довідки — `pool: 'forks'` + `inclu
91
91
 
92
92
  Канон `scripts.coverage` (substring requirement): [package.json.contains.json](./policy/package_json/template/package.json.contains.json)
93
93
 
94
+ ### Multi-workspace iteration
95
+
96
+ У monorepo `n-cursor coverage` ітерує усі workspaces з власним `package.json` (через `resolveAllJsRoots()`, з розгортанням glob-патернів `cf/*`/`packages/*`) і агрегує метрики lcov + Stryker у єдиний `JS`-рядок `COVERAGE.md`. Workspace без тестів пропускається без помилки (vitest викликається з `--passWithNoTests`, порожній lcov → skip-сигнал, Stryker не запускається). `n-cursor coverage` падає лише коли тестів немає в **жодному** workspace проєкту або коли vitest повертає non-zero exit (реальний test failure / compile error — це не маскується).
97
+
94
98
  ## Налаштування mutation-testing
95
99
 
96
100
  Якщо у `.n-cursor.json#rules` присутнє правило `js-lint` — правило `test` створює canonical baseline `stryker.config.mjs` + `vitest.config.js` у **кожному** JS-root проєкту: у кожному workspace з власним `package.json` (або в корені для single-package). У monorepo з `workspaces: ['app', 'scripts']` отримаєте `app/stryker.config.mjs` + `app/vitest.config.js` і `scripts/stryker.config.mjs` + `scripts/vitest.config.js`.
@@ -25,6 +25,54 @@ description: >-
25
25
  на XML-тегах, file refs (`path/to/file.ts:42`), markdown.
26
26
  - **Один блок виводу:** результат — це **один** markdown-блок у відповіді
27
27
  чату, готовий до копіювання.
28
+ - **Лаконічність — головне:** промпт містить **інтент + покажчики**, а не
29
+ цитати + готовий код. Цільова LLM має повний доступ до свого CWD через
30
+ `Read`/`Grep`/`Glob` і прочитає актуальний стан сама — наш патч не повинен
31
+ дублювати те, до чого вона додумається з 2-3 файлів.
32
+
33
+ ## Що **не** включати у промпт (cut list)
34
+
35
+ Цільова LLM сама прочитає файли — не дублюй їх. Викидай:
36
+
37
+ - **Великі цитати коду** з файлів, які LLM знайде за file ref. Замість блоку
38
+ з 30 рядками функції — один рядок: `див. rules/foo/bar.mjs:120-150 (collect)`.
39
+ Цитуй **тільки**: вже видалений код (тобто LLM його не знайде), або фрагмент
40
+ з зовнішнього джерела (логи, stderr, помилка з CI).
41
+ - **Готові імплементації** ("ось як має виглядати новий `defaultRunner`")
42
+ з повним кодом. Опиши інтент і обмеження — LLM напише код сама й краще
43
+ під поточний стиль файлу. Виняток: один-два рядки, які важко описати словами
44
+ (наприклад, точна назва прапора CLI).
45
+ - **Покрокові підказки рівня "крок 1: import X, крок 2: split into a) і b)…"**
46
+ з псевдо-кодом. Це передчасна архітектура — описуй **бажану поведінку**
47
+ (input → output, edge cases), а декомпозицію довір LLM.
48
+ - **Повний вміст `package.json`, конфігів, helpers** — досить імені поля та
49
+ значення, що змінюється: `engines.node: ">=22" → ">=25"`. Якщо значення
50
+ специфічне (peer ranges, exports map) — цитуй **тільки цей фрагмент**.
51
+ - **Огляд альтернатив, які ти відкинув,** із розгорнутими аргументами. Один
52
+ рядок: "розглянутий варіант X відкинуто — Y" або взагалі прибрати, якщо
53
+ LLM сама дійде того самого висновку.
54
+ - **Списки існуючих helpers ("вже є addCoverage у rules/test/coverage")** —
55
+ досить рядка-покажчика; LLM зайде `Grep`'ом і прочитає сигнатуру сама.
56
+ - **Дублювання правил репо** (`CLAUDE.md`, `.cursor/rules/*.mdc`) — досить
57
+ згадки "дотриматись n-test.mdc"; LLM прочитає файл сама.
58
+ - **Готовий `tree -L 2`-дамп** — досить рядка "монорепо з workspaces
59
+ cf/_, run/_, gt (bun)"; повна структура майже завжди шум.
60
+ - **Самоочевидні acceptance-checks** ("тести зелені", "лінтер чистий") —
61
+ пиши тільки специфічні до завдання ("на репо `ai`: `bun run coverage`
62
+ не падає з `JS coverage exit 1`").
63
+
64
+ ## Що **обов'язково** включити
65
+
66
+ - **Інтент** — 1-3 речення, що саме треба і чому (1 речення на причину).
67
+ - **Симптом / репро,** якщо це bugfix: справжній stderr, точна команда,
68
+ exit code. Це **не** виводиться з коду — без нього LLM не знає, що
69
+ саме ламається.
70
+ - **File refs** на ключові точки правки: `path:line` або `path:line-range`,
71
+ з одним рядком пояснення кожна.
72
+ - **Реальні обмеження,** які не виводяться з коду: версії в репо-споживачі,
73
+ inflight-міграції, breaking-change політика, зовнішні залежності.
74
+ - **Як перевірити** — конкретні команди й специфічні до завдання сигнали
75
+ успіху.
28
76
 
29
77
  ## Виклик
30
78
 
@@ -48,102 +96,98 @@ description: >-
48
96
  пакета / шлях. Аргумент = намір користувача, передається у промпт
49
97
  як розділ "Завдання".
50
98
 
51
- 2. **Зібрати read-only контекст з CWD:**
52
- - `package.json` — поля `name`, `version`, `engines`, `peerDependencies`,
53
- `dependencies`, `devDependencies`, `scripts`, `type`, `exports`.
54
- - Структура репо: `tree -L 2 -I 'node_modules|.git|dist|build|.next|.nuxt'`
55
- (або `ls -la` коли `tree` недоступний).
56
- - `README.md`перші 40-60 рядків (head).
57
- - `CLAUDE.md` / `AGENTS.md` / `.cursor/rules/*.mdc`якщо є, відмітити їх
58
- існування й перелік.
59
- - **Релевантні до завдання config-файли** підбирати за ключовими
60
- словами з аргументу (наприклад, "node" `engines`, `.nvmrc`,
61
- `.node-version`; "eslint" `eslint.config.*`, `.eslintrc*`;
62
- "vite" → `vite.config.*`; "ts" → `tsconfig.json`).
63
-
64
- 3. **Визначити "точку патчу"** короткий список файлів, які найімовірніше
65
- доведеться правити цільовому агенту, з обґрунтуванням.
66
-
67
- 4. **Сформувати промпт за шаблоном нижче.** Дрібні релевантні файли
68
- (≤ ~80 рядків) вбудовуй **повністю**; великі цитуй фрагмент з
69
- посиланням на шлях:рядки.
70
-
71
- 5. **Вивести один markdown-блок у чат** без додаткових коментарів
99
+ 2. **Зібрати read-only контекст з CWD — мінімум, потрібний для опису
100
+ інтенту й покажчиків:**
101
+ - `package.json` лише поля, релевантні до завдання (напр., для "node"
102
+ `engines`, `name`, `version`; для "eslint" `peerDependencies`,
103
+ `exports`). Не читай поля, які потім не з'являться у промпті.
104
+ - Структура репо швидкий огляд (`ls` верхнього рівня), щоб згадати
105
+ **тип** ("монорепо bun з 7 workspaces"рядком, не дампом).
106
+ - **Релевантні файли** — відкривай саме ті, що потрібні щоб знайти
107
+ `file:line` покажчики на точки правки. Не читай "про запас".
108
+ - `CLAUDE.md` / `.cursor/rules/*.mdc` лише **перевір наявність**
109
+ і назви файлів-правил, які стосуються завдання (LLM прочитає сама).
110
+
111
+ 3. **Визначити "точку патчу"** — 1-5 `path:line` покажчиків на місця, які
112
+ найімовірніше треба правити, з одним рядком пояснення на кожен.
113
+
114
+ 4. **Сформувати промпт за шаблоном нижче.** Цитуй код **тільки** коли він
115
+ зовнішній (stderr, лог CI) або уже видалений. Існуючий код у CWD не
116
+ цитуйдавай `path:line` ref.
117
+
118
+ 5. **Прорахуй cut list:** перед видачею пройдись по секції "Що **не**
119
+ включати" вище і видали все, що цільова LLM може отримати з 2-3
120
+ `Read`/`Grep`-викликів у своєму CWD.
121
+
122
+ 6. **Вивести один markdown-блок у чат** — без додаткових коментарів
72
123
  поза блоком, окрім однорядкового підпису "готово до копіювання".
73
124
 
74
125
  ## Шаблон вихідного промпта
75
126
 
127
+ Тільки секції, які несуть навантаження. Порожніх не залишай. Зразок:
128
+
76
129
  ````markdown
77
130
  ```markdown
78
131
  # Завдання
79
132
 
80
- <нормалізований опис з аргументу 1-3 речення без води>
81
-
82
- # Контекст проєкту
133
+ <1-3 речення: що треба і яка причина (баг / новий стек / requirement).
134
+ Якщо bugfix — вкажи симптом одним рядком.>
83
135
 
84
- - Назва / версія: `<name>@<version>`
85
- - Тип: <library | app | eslint-config | monorepo | …>
86
- - Стек: Node `<engines.node>`, <TS/JS>, <ключові залежності>
87
- - Документи правил: <CLAUDE.md / .cursor/rules — або "немає">
136
+ # Симптом / репро
88
137
 
89
- ## Структура (skim)
90
- ```
138
+ <тільки для bugfix: точна команда + ключовий рядок stderr/exit code.
139
+ Пропусти секцію, якщо не bugfix.>
91
140
 
92
- <вивід tree -L 2>
93
- ````
141
+ # Точки правки
94
142
 
95
- # Релевантні файли
96
-
97
- ## `package.json`
98
-
99
- ```text
100
- <повний вміст або ключові поля>
101
- ```
102
-
103
- ## `<інший релевантний файл>`
104
-
105
- ```<lang>
106
- <вміст або фрагмент з посиланням на шлях:рядки>
107
- ```
143
+ - `path/to/file.mjs:120` — <одне речення: що тут не так / що зробити>
144
+ - `path/to/other.mjs:34-50` — <…>
108
145
 
109
146
  # Що треба зробити
110
147
 
111
- - крок 1 у файлі `X` (`<коротке пояснення>`)
112
- - крок 2 у файлі `Y`
113
- - крок 3 — оновити `CHANGELOG.md` / bump `version`, якщо це npm-пакет
148
+ <бажана поведінка input output, edge cases, без псевдо-коду й
149
+ покрокових імплементаційних підказок. 3-8 буллетів.>
114
150
 
115
151
  # Обмеження
116
152
 
117
- - Не ламати публічний API
118
- - <constraints, виявлені з package.json / конфігів — наприклад "engines.node вже >=22, треба >=25">
119
- - Дотриматись правил репо (CLAUDE.md / .cursor/rules — посилання вище)
153
+ <тільки реальні, не виводяться з коду: версії у репо-споживачі, breaking-change політика, inflight-міграції. Пропусти секцію, якщо їх нема.>
120
154
 
121
155
  # Як перевірити
122
156
 
123
- - `<команда з scripts npm test / bun test / lint>`
124
- - <конкретні acceptance-checks: "у `engines.node` має бути `>=25`",
125
- "CI зелений" тощо>
126
-
157
+ - `<команда>`<специфічний до завдання сигнал успіху>
127
158
  ```
159
+ ````
128
160
 
129
- ```
161
+ ### Контр-приклад (так робити не треба)
130
162
 
131
- ````
163
+ - Секція `# Контекст проєкту` з name/version/stack/docs — LLM прочитає
164
+ `package.json` сама за 1 виклик.
165
+ - `## Структура (skim)` з `tree -L 2` дампом — шум.
166
+ - `## package.json` з повним JSON — досить рядка про конкретне поле
167
+ у `Що треба зробити` або `Обмеження`.
168
+ - `## <функція>` з 30 рядками існуючого коду — давай `path:line` ref
169
+ у `Точки правки`.
170
+ - Покрокові підказки з кодом ("крок 1: import X, крок 2: split…") —
171
+ опиши поведінку, код напише LLM.
132
172
 
133
173
  ## Правила
134
174
 
135
175
  - **Мова промпту:** українська за замовчуванням; якщо аргумент англійською —
136
176
  можна англійською. Технічні терміни — англійською.
137
- - **Обсяг:** прагни вмістити промпт у ~200-500 рядків. Якщо релевантних
138
- файлів багато обери 3-5 найважливіших, решту згадай посиланнями.
139
- - **Без галюцинацій:** не вигадуй полів `package.json`, версій, шляхів
140
- лише те, що реально прочитав з CWD. Якщо чогось бракує — явно так і
141
- напиши ("`engines.node` відсутнє").
177
+ - **Обсяг:** прагни до **30-100 рядків**. Якщо вийшло більше — пройдись
178
+ по cut list і викинь усе, до чого LLM додумається з кількох
179
+ `Read`/`Grep`. Більше 150 рядків майже завжди ознака, що ти цитуєш
180
+ код, який LLM прочитає сама.
181
+ - **Без галюцинацій:** не вигадуй полів `package.json`, версій, шляхів,
182
+ номерів рядків — лише те, що реально прочитав з CWD. Якщо чогось
183
+ бракує — явно так і напиши ("`engines.node` відсутнє").
142
184
  - **Без імперативного тону до користувача:** промпт адресований **іншому
143
185
  агенту**, не людині. Використовуй "зроби", "онови", "додай".
144
- - **Без секцій-пустушок:** якщо немає `Обмежень` — пропусти секцію.
186
+ - **Без секцій-пустушок:** якщо немає `Обмежень` чи `Симптому` — пропусти
187
+ секцію цілком.
145
188
  - **Не вмикай у промпт:** секрети, `.env`, `node_modules`, бінарні файли,
146
- довгі логи.
189
+ довгі логи, дампи `tree`, повні JSON конфігів, цитати існуючих
190
+ helpers/функцій з CWD.
147
191
  - **Усе в одному блоці:** результат — це **один** ` ```markdown ` блок;
148
192
  ніяких додаткових міркувань поза ним (крім фінального підпису
149
193
  "готово до копіювання").
@@ -163,47 +207,32 @@ description: >-
163
207
  /n-llm-patch у @nitra/eslint-config підняти engines.node до >=25
164
208
  ```
165
209
 
166
- Очікуваний вивід (схематично):
167
-
168
- ````
210
+ Очікуваний вивід:
169
211
 
170
212
  ````markdown
213
+ ```markdown
171
214
  # Завдання
172
215
 
173
- Підняти `engines.node` у `@nitra/eslint-config` до `>=25` і переглянути
174
- сумісність peer-залежностей з новою версією.
216
+ Підняти `engines.node` у `@nitra/eslint-config` з `>=22` до `>=25`,
217
+ переконатися що peer `eslint ^9` сумісний з Node 25.
175
218
 
176
- # Контекст проєкту
219
+ # Точки правки
177
220
 
178
- - Назва / версія: `@nitra/eslint-config@2.4.0`
179
- - Тип: shared eslint preset (library)
180
- - Стек: Node `>=22` (поточне), ESM, eslint `^9`
181
- - Документи правил: `.cursor/rules/n-js-lint.mdc`
221
+ - `package.json:18` `engines.node`
222
+ - `CHANGELOG.md` додати запис; bump `version` (minor)
182
223
 
183
- ## Структура (skim)
184
-
185
-
224
+ # Обмеження
186
225
 
187
- # Релевантні файли
226
+ - Дотриматись `.cursor/rules/n-js-lint.mdc`.
227
+ - Якщо `eslint ^9` офіційно не підтримує Node 25 — підняти peer range.
188
228
 
189
- ## `package.json`
229
+ # Як перевірити
190
230
 
191
- ```json
192
- { "engines": { "node": ">=22" }, "peerDependencies": { "eslint": "^9" } }
231
+ - `bun test` — зелений
232
+ - `node -p "require('./package.json').engines.node"` `>=25`
193
233
  ```
194
234
  ````
195
235
 
196
- # Що треба зробити
197
-
198
- - `package.json` → `engines.node`: `>=22` → `>=25`
199
- - Перевірити, чи `peerDependencies.eslint ^9` сумісне з Node 25
200
- - Bump `version` (minor) + запис у `CHANGELOG.md`
201
-
202
- # Як перевірити
203
-
204
- - `bun test`
205
- - `node -v` у CI ≥ 25
206
-
207
236
  ```
208
237
  готово до копіювання — встав у чат з агентом у цільовому проєкті
209
238
  ```