@nitra/cursor 1.28.5 → 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 +11 -0
- package/package.json +1 -1
- package/rules/abie/abie.mdc +5 -5
- package/rules/abie/policy/{package_json_docs/package_json_docs.rego → package_json_shared/package_json_shared.rego} +5 -5
- package/rules/abie/policy/{package_json_docs → package_json_shared}/target.json +1 -1
- package/rules/js-bun-db/js-bun-db.mdc +74 -0
- package/rules/js-lint/coverage/coverage.mjs +104 -75
- package/rules/style-lint/style-lint.mdc +175 -0
- package/rules/test/test.mdc +5 -1
- package/rules/vue/vue.mdc +303 -5
- package/skills/llm-patch/SKILL.md +121 -92
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
package/rules/abie/abie.mdc
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Правила для проєктів AbInBev Efes
|
|
3
3
|
alwaysApply: true
|
|
4
|
-
version: '1.
|
|
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-
|
|
143
|
+
## `@nitra/abie-shared` у `devDependencies`
|
|
144
144
|
|
|
145
|
-
У кореневому **`package.json`** abie-проєкту в **`devDependencies`** має бути **`@nitra/abie-
|
|
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-
|
|
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
|
-
- **`
|
|
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-
|
|
3
|
-
# `node_modules/@nitra/abie-
|
|
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.
|
|
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-
|
|
15
|
-
msg := "package.json: devDependencies має містити @nitra/abie-
|
|
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-
|
|
4
|
+
"missingMessage": "package.json не існує — створи його, додай devDependencies['@nitra/abie-shared'] (abie.mdc)"
|
|
5
5
|
}
|
|
@@ -46,6 +46,60 @@ PostgreSQL 18+, MariaDB 10.6+ (сумісний з MySQL-протоколом,
|
|
|
46
46
|
|
|
47
47
|
Для multi-row `VALUES` у `MERGE` / `INSERT` з конкретними типами — паралельні масиви по колонках і `unnest($1::TYPE[], $2::TYPE[], ...) AS t(col1, col2, ...)`. Кожна колонка передається одним параметром-масивом; типи задаються кастом масиву (`::uuid[]`, `::bigint[]`, `::numeric[]`, `::text[]`, …).
|
|
48
48
|
|
|
49
|
+
#### Приклад: MERGE з UNNEST і динамічними колонками
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
// ❌ format + pgWrite.unsafe — N×7 окремих значень, план змінюється при кожному batch.length
|
|
53
|
+
const valuesSql = batch
|
|
54
|
+
.map(row => format('(%L::int, %L::date, %L::jsonb)', row.id, row.date, JSON.stringify(row.data)))
|
|
55
|
+
.join(',')
|
|
56
|
+
const sql = format(`MERGE INTO t USING (VALUES %s) AS s(id, date, data) ON ...`, valuesSql)
|
|
57
|
+
await pgWrite.unsafe(sql)
|
|
58
|
+
|
|
59
|
+
// ✅ UNNEST — 3 параметри незалежно від розміру batch; план стабільний і може кешуватись
|
|
60
|
+
const ids = batch.map(r => r.id)
|
|
61
|
+
const dates = batch.map(r => r.date)
|
|
62
|
+
const data = batch.map(r => JSON.stringify(r.data))
|
|
63
|
+
|
|
64
|
+
await pgWrite`
|
|
65
|
+
WITH s(id, date, data) AS (
|
|
66
|
+
SELECT * FROM unnest(
|
|
67
|
+
${ids}::int[],
|
|
68
|
+
${dates}::date[],
|
|
69
|
+
${data}::jsonb[]
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
MERGE INTO my_table AS t
|
|
73
|
+
USING s ON t.id = s.id
|
|
74
|
+
WHEN MATCHED THEN
|
|
75
|
+
UPDATE SET date = s.date, data = s.data
|
|
76
|
+
WHEN NOT MATCHED THEN
|
|
77
|
+
INSERT (id, date, data) VALUES (s.id, s.date, s.data)
|
|
78
|
+
`
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Якщо частина колонок у SET/INSERT залежить від параметра (plan/fact, тип тощо) — динамічні імена колонок не можна параметризувати через `${value}`; використовуй умовні Bun SQL фрагменти:
|
|
82
|
+
|
|
83
|
+
```javascript
|
|
84
|
+
// ✅ умовні фрагменти для динамічних ідентифікаторів колонок
|
|
85
|
+
const colFrag = isPlan ? pgWrite`plan_value` : pgWrite`fact_value`
|
|
86
|
+
const hashFrag = isPlan ? pgWrite`hash = s.hash,` : pgWrite``
|
|
87
|
+
|
|
88
|
+
await pgWrite`
|
|
89
|
+
...
|
|
90
|
+
WHEN MATCHED THEN
|
|
91
|
+
UPDATE SET
|
|
92
|
+
${colFrag} = s.value,
|
|
93
|
+
${hashFrag}
|
|
94
|
+
updated_by = s.updated_by
|
|
95
|
+
WHEN NOT MATCHED THEN
|
|
96
|
+
INSERT (id, ${colFrag}, updated_by)
|
|
97
|
+
VALUES (s.id, s.value, s.updated_by)
|
|
98
|
+
`
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`UNION ALL`-цикл замість `unnest` підходить для малих динамічних запитів (2–5 рядків), де кожна гілка семантично різна. Для bulk upsert — завжди `unnest`.
|
|
102
|
+
|
|
49
103
|
### Заборонений «drop-in» шим
|
|
50
104
|
|
|
51
105
|
```javascript
|
|
@@ -285,6 +339,26 @@ const ids = [1, 2, 3]
|
|
|
285
339
|
await sql`SELECT * FROM users WHERE id IN ${sql(ids)}`
|
|
286
340
|
```
|
|
287
341
|
|
|
342
|
+
Multi-row INSERT з масиву об'єктів — `sql(rows)` генерує column list і VALUES автоматично:
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
// ❌ format + pgWrite.unsafe — ручне склеювання рядків, injection-вектор
|
|
346
|
+
const insertWfQry = `insert into approval.workflow (request_id, job_title_id, name, status)
|
|
347
|
+
values ${approverJobs.map(job => `('${request.id}', ${job.id}, '${job.short_name}', 'pending')`).join(', ')}`
|
|
348
|
+
await pgWrite.unsafe(insertWfQry)
|
|
349
|
+
|
|
350
|
+
// ✅ sql(rows) — один параметр-масив, bind через wire-protocol
|
|
351
|
+
const wfRows = approverJobs.map(job => ({
|
|
352
|
+
request_id: request.id,
|
|
353
|
+
job_title_id: job.id,
|
|
354
|
+
name: job.short_name,
|
|
355
|
+
status: job.id === nextJobId ? 'current' : 'pending'
|
|
356
|
+
}))
|
|
357
|
+
await sql`INSERT INTO approval.workflow ${sql(wfRows)}`
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Коли потрібен стабільний план для великих batch'ів (N > 20) або строгі типи колонок — використовуй `unnest` (деталі у `#### Приклад: MERGE з UNNEST і динамічними колонками`). Для невеликих INSERT'ів де колонки відомі — `sql(rows)` коротший і зрозуміліший.
|
|
361
|
+
|
|
288
362
|
## `IN (...)`: значення з template literal — тільки через змінну + guard на пустоту
|
|
289
363
|
|
|
290
364
|
Якщо список для `IN (...)` підставляється через `${...}` у template literal, його **потрібно**:
|
|
@@ -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
|
-
[
|
|
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
|
-
* Збирає
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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}}>>}
|
|
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
|
-
|
|
244
|
-
let mutation = { caught: 0, total: 0 }
|
|
245
|
-
const survived = []
|
|
246
|
-
|
|
322
|
+
const results = []
|
|
247
323
|
for (const jsRoot of jsRoots) {
|
|
248
|
-
const
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
}
|
|
@@ -5,6 +5,92 @@ globs: "**/*.{css,scss,vue}"
|
|
|
5
5
|
alwaysApply: false
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
## Quasar як стильова основа
|
|
9
|
+
|
|
10
|
+
У Vue-проектах використовуй **Quasar** як базову стильову систему:
|
|
11
|
+
|
|
12
|
+
- **Класи компонентів:** `q-pa-*`, `q-ma-*`, `q-mt-*`, `q-mb-*`, `row`, `col`, `col-*`, `items-center`, `justify-between`, `text-*`, `bg-*`, `shadow-*` тощо — стандартні утиліти Quasar.
|
|
13
|
+
- **Кольори:** використовуй Quasar CSS-змінні через класи (`text-primary`, `bg-accent`, `text-negative` тощо) або через SCSS-змінні (`$primary`, `$accent`) у `.scss`-файлах.
|
|
14
|
+
- **Не пиши власні утиліти**, якщо є відповідний Quasar-клас.
|
|
15
|
+
|
|
16
|
+
## Кольори: quasar-variables.scss і app.scss
|
|
17
|
+
|
|
18
|
+
Основні кольори визначай виключно через стандартні Quasar-змінні: `$primary`, `$secondary`, `$accent`, `$positive`, `$negative`, `$warning`.
|
|
19
|
+
|
|
20
|
+
Якщо потрібен додатковий колір — визнач його в `quasar-variables.scss`, і **обов'язково** додай до `app.scss` відповідні `.text-*` / `.bg-*` класи:
|
|
21
|
+
|
|
22
|
+
```scss
|
|
23
|
+
/* quasar-variables.scss */
|
|
24
|
+
$white-a1: color.adjust(white, $alpha: -0.85);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```scss
|
|
28
|
+
/* app.scss */
|
|
29
|
+
.text-white-a1 {
|
|
30
|
+
color: $white-a1 !important;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.bg-white-a1 {
|
|
34
|
+
background-color: $white-a1 !important;
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Gap між flex/grid-елементами — .n-gap-*
|
|
39
|
+
|
|
40
|
+
Для відступів між плаваючими елементами (flex, grid) використовуй класи `.n-gap-*` замість `q-gutter-*`. Якщо класів немає в `app.scss` — додай:
|
|
41
|
+
|
|
42
|
+
```scss
|
|
43
|
+
// GAP
|
|
44
|
+
.n-gap-xs {
|
|
45
|
+
gap: 4px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.n-gap-sm {
|
|
49
|
+
gap: 8px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.n-gap-md {
|
|
53
|
+
gap: 16px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.n-gap-lg {
|
|
57
|
+
gap: 24px;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Фікси компонентів Quasar
|
|
62
|
+
|
|
63
|
+
При використанні компонента `q-scroll-area` або `q-tooltip` додай до `app.scss` відповідні фікси. Аналогічно — фікс масштабування на iOS.
|
|
64
|
+
|
|
65
|
+
```scss
|
|
66
|
+
// FIX стилі для прокручуваної області по висоті екрана
|
|
67
|
+
.q-scrollarea {
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
height: auto;
|
|
71
|
+
min-height: 0;
|
|
72
|
+
max-height: 100%;
|
|
73
|
+
|
|
74
|
+
.scroll {
|
|
75
|
+
flex: 10000 1 0%;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// FIX tooltip
|
|
80
|
+
.q-tooltip {
|
|
81
|
+
font-size: 1em;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// FIX не зумувати на iOS
|
|
85
|
+
@media (width <= 600px) {
|
|
86
|
+
input,
|
|
87
|
+
textarea,
|
|
88
|
+
select {
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
8
94
|
## Генерація та редагування стилів (Cursor і інші агенти)
|
|
9
95
|
|
|
10
96
|
- **Джерело правил:** перед тим як писати або суттєво змінювати **`.css`**, **`.scss`** або стилі в **`.vue`**, переглянь у корені проєкту (і в релевантних пакетах монорепо, якщо є) поле **`stylelint`** у **`package.json`** (зокрема `extends`), наявні **`.stylelintrc.*`**, **`stylelint.config.*`** та **`.stylelintignore`**. Не покладайся на «типові» правила stylelint з пам’яті — дотримуйся **проєктного** **`@nitra/stylelint-config`** і будь-яких локальних доповнень у репозиторії.
|
|
@@ -94,3 +180,92 @@ jobs:
|
|
|
94
180
|
```text title=".stylelintignore"
|
|
95
181
|
dist/
|
|
96
182
|
```
|
|
183
|
+
|
|
184
|
+
## Адмінські таблиці — n-admin-table
|
|
185
|
+
|
|
186
|
+
Для таблиць, що мають займати всю висоту сторінки (або блоку), додавай клас `n-admin-table` і атрибути `hide-no-data`, `dense`, `flat`. Таблиця автоматично розтягнеться на весь доступний простір. У блоках меншої висоти — задай їм явний `height`.
|
|
187
|
+
|
|
188
|
+
```vue
|
|
189
|
+
<!-- ТАБЛИЦЯ -->
|
|
190
|
+
<q-table class="n-admin-table" hide-no-data flat dense />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Якщо клас `.n-admin-table` не визначено в `app.scss` — додай:
|
|
194
|
+
|
|
195
|
+
```scss
|
|
196
|
+
// ADMIN TABLE
|
|
197
|
+
.n-admin-table {
|
|
198
|
+
height: calc(100vh - 106px);
|
|
199
|
+
|
|
200
|
+
thead {
|
|
201
|
+
position: sticky;
|
|
202
|
+
z-index: 3;
|
|
203
|
+
top: 0;
|
|
204
|
+
|
|
205
|
+
tr th {
|
|
206
|
+
background-color: $grey-3;
|
|
207
|
+
height: 36px !important;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// tbody td {
|
|
212
|
+
// font-size: 14px;
|
|
213
|
+
// }
|
|
214
|
+
|
|
215
|
+
tbody tr:last-child td {
|
|
216
|
+
border-bottom: 1px solid rgb(0 0 0 / 12%) !important;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
th:first-child,
|
|
220
|
+
td:first-child,
|
|
221
|
+
th:last-child,
|
|
222
|
+
td:last-child {
|
|
223
|
+
width: 1px;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.q-table__bottom {
|
|
228
|
+
background-color: $grey-3;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.q-table__progress th {
|
|
232
|
+
height: 1px !important;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.text-wrap {
|
|
236
|
+
max-width: 250px;
|
|
237
|
+
white-space: normal;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// sticky columns
|
|
241
|
+
td.sticky-left {
|
|
242
|
+
position: sticky;
|
|
243
|
+
left: 0;
|
|
244
|
+
z-index: 2;
|
|
245
|
+
background-color: white;
|
|
246
|
+
box-shadow: 2px 0 5px #0002;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
td.sticky-right {
|
|
250
|
+
position: sticky;
|
|
251
|
+
right: 0;
|
|
252
|
+
z-index: 2;
|
|
253
|
+
background-color: white;
|
|
254
|
+
box-shadow: -2px 0 5px #0002;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
th.sticky-left {
|
|
258
|
+
position: sticky;
|
|
259
|
+
left: 0;
|
|
260
|
+
z-index: 3;
|
|
261
|
+
box-shadow: 2px 0 5px #0002;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
th.sticky-right {
|
|
265
|
+
position: sticky;
|
|
266
|
+
right: 0;
|
|
267
|
+
z-index: 3;
|
|
268
|
+
box-shadow: -2px 0 5px #0002;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
```
|
package/rules/test/test.mdc
CHANGED
|
@@ -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.
|
|
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`.
|
package/rules/vue/vue.mdc
CHANGED
|
@@ -12,18 +12,30 @@ alwaysApply: false
|
|
|
12
12
|
```javascript
|
|
13
13
|
const vue3CompositionApiBestPractices = [
|
|
14
14
|
'Використовуй функцію setup() для логіки компонента',
|
|
15
|
-
'Реалізуй computed
|
|
16
|
-
'
|
|
15
|
+
'Реалізуй computed змінні через $computed()',
|
|
16
|
+
'Реалізуй ref змінні через $ref',
|
|
17
|
+
'Використовуй watch і watchEffect для побічних ефектів',
|
|
17
18
|
'Підключай lifecycle hooks: onMounted, onUpdated тощо',
|
|
18
|
-
'
|
|
19
|
+
'Для глибоко вкладених залежностей використовуй composables, props/emits або store'
|
|
20
|
+
'не використовуй provide/inject для залежностей'
|
|
19
21
|
]
|
|
20
22
|
```
|
|
21
23
|
|
|
24
|
+
## Quasar як UI-основа
|
|
25
|
+
|
|
26
|
+
У Vue-проектах використовуй **Quasar** як базовий UI-фреймворк:
|
|
27
|
+
|
|
28
|
+
- **Компоненти:** `q-btn`, `q-input`, `q-select`, `q-table`, `q-dialog`, `q-card`, `q-layout`, `q-page`, `q-drawer` тощо — основа UI.
|
|
29
|
+
- **Плагіни:** `Notify`, `Dialog`, `Loading` та інші Quasar-плагіни.
|
|
30
|
+
- **Кольори:** використовуй Quasar CSS-змінні (`primary`, `secondary`, `accent`, `positive`, `negative`, `warning`, `info`, `dark`) і утиліти (`text-primary`, `bg-accent` тощо).
|
|
31
|
+
- **Утиліти:** flex-layout (`row`, `col`, `items-center`), spacing (`q-pa-md`, `q-mt-sm`), shadow (`shadow-2`) — зі стандартної бібліотеки Quasar.
|
|
32
|
+
- **Кастомні компоненти** `@nitra/components` — **надбудова** над Quasar, а не заміна; їх слід надавати перевагу лише там, де вони є (див. розділ `@nitra/components` нижче).
|
|
33
|
+
|
|
22
34
|
## Структура папок
|
|
23
35
|
|
|
24
36
|
```javascript
|
|
25
37
|
const folderStructure = `
|
|
26
|
-
src/
|
|
38
|
+
src/
|
|
27
39
|
components/
|
|
28
40
|
composables/
|
|
29
41
|
views/
|
|
@@ -76,7 +88,7 @@ const additionalInstructions = `
|
|
|
76
88
|
|
|
77
89
|
### Патерни та антипатерни
|
|
78
90
|
|
|
79
|
-
-
|
|
91
|
+
- Для глибоко вкладених залежностей використовуй **composables**, **props/emits** або **store**; **renderless**-компоненти / **slots** — коли логіку відділяєш від розмітки.
|
|
80
92
|
- **HTTP:** окремі модулі **services** або **composables** для API; **async/await**.
|
|
81
93
|
- **Події:** батько–дитина через **emits**; для не пов’язаних гілок — **store**.
|
|
82
94
|
- Не мутуй **props** напряму — оновлення через подію вгору або v-model.
|
|
@@ -343,6 +355,292 @@ import path from 'node:path'
|
|
|
343
355
|
Правило стосується саме `.vue` файлів. Допоміжні `.ts`/`.js` модулі, які споживаються лише server-side
|
|
344
356
|
(наприклад, окремий пакет утіліт), можуть імпортувати Node-built-ins без обмежень.
|
|
345
357
|
|
|
358
|
+
## @nitra/components — надавай перевагу перед Quasar-компонентами
|
|
359
|
+
|
|
360
|
+
При створенні нової функціональності використовуй компоненти `@nitra/components`, якщо логіка компонента дозволяє отримати потрібний функціонал. Заміни:
|
|
361
|
+
|
|
362
|
+
| Завдання | `@nitra/components` | Замість Quasar |
|
|
363
|
+
| --- | --- | --- |
|
|
364
|
+
| Діалоги | `NDialog` | `q-dialog` |
|
|
365
|
+
| Multi-вибір | `NSelectMulti` | `q-select` (multiple) |
|
|
366
|
+
| Текстовий редактор | `NEditor` | `q-editor` |
|
|
367
|
+
| Вибір дати | `NDate` | — |
|
|
368
|
+
| Вибір місяця/року | `NDateMonthYear` | — |
|
|
369
|
+
| Діапазон дат | `NDateRange` | — |
|
|
370
|
+
| Дата і час | `NDateTime` | — |
|
|
371
|
+
| Drag&drop список | `NDraggableList` | — |
|
|
372
|
+
| Редаговане значення | `NEditableString` | — |
|
|
373
|
+
| Повідомлення | `NCallout` | — |
|
|
374
|
+
| Хедер проекту | `NHeader` | — |
|
|
375
|
+
| Перемикання мов | `NLang` | — (якщо `NHeader` не використовується) |
|
|
376
|
+
| Меню проекту | `NMenu` | — |
|
|
377
|
+
| Зображення (масив / одиночне) | `NImages` | — |
|
|
378
|
+
| Завантаження файлів | `NUploader` | — |
|
|
379
|
+
| Вибір колонок `q-table` | `NTableColumns` | — |
|
|
380
|
+
|
|
381
|
+
## NHeader — телепорт-слоти на сторінках
|
|
382
|
+
|
|
383
|
+
У проектах з `NHeader` використовуй `<teleport>` для вбудовування контенту сторінки в шапку:
|
|
384
|
+
|
|
385
|
+
| Слот | Призначення |
|
|
386
|
+
| --- | --- |
|
|
387
|
+
| `#header-subtitle` | Заголовок сторінки (якщо потрібно перевизначити subtitle) |
|
|
388
|
+
| `#header-center` | Важливі повідомлення та елементи управління |
|
|
389
|
+
| `#header-filters` | Фільтри, кнопки та інші елементи управління сторінки |
|
|
390
|
+
|
|
391
|
+
Оскільки `NHeader` за замовчуванням темний, додавай до полів і селектів у телепортах: `dark`, `standout="bg-white text-primary"`, `:options-dark="false"`.
|
|
392
|
+
|
|
393
|
+
Завжди обгортай `<teleport>` в `v-if="mounted"` (де `mounted` — `$ref(false)`, що встановлюється в `onMounted`), щоб уникнути помилки відсутності target-елемента при SSR / першому рендері.
|
|
394
|
+
|
|
395
|
+
```vue
|
|
396
|
+
<template>
|
|
397
|
+
<q-page>
|
|
398
|
+
<!-- ФІЛЬТРИ В ШАПЦІ -->
|
|
399
|
+
<teleport v-if="mounted" to="#header-filters">
|
|
400
|
+
<div class="col row items-center n-gap-sm q-pa-sm">
|
|
401
|
+
<q-input
|
|
402
|
+
v-model="pageStore.filterName"
|
|
403
|
+
:label="t`Поиск по названию`"
|
|
404
|
+
debounce="200"
|
|
405
|
+
standout="bg-white text-primary"
|
|
406
|
+
clearable
|
|
407
|
+
dense
|
|
408
|
+
dark
|
|
409
|
+
class="col"
|
|
410
|
+
style="min-width: 120px">
|
|
411
|
+
<template #prepend>
|
|
412
|
+
<q-icon name="search" />
|
|
413
|
+
</template>
|
|
414
|
+
</q-input>
|
|
415
|
+
|
|
416
|
+
<n-select-multi
|
|
417
|
+
v-model="pageStore.filterRequestTypes"
|
|
418
|
+
:options="requestTypeOptions"
|
|
419
|
+
:label="t`Тип запроса`"
|
|
420
|
+
dark
|
|
421
|
+
standout="bg-white text-primary"
|
|
422
|
+
:options-dark="false"
|
|
423
|
+
style="min-width: 180px; max-width: 400px"
|
|
424
|
+
dense
|
|
425
|
+
emit-value
|
|
426
|
+
map-options
|
|
427
|
+
clearable
|
|
428
|
+
searchable
|
|
429
|
+
:stack-label="false" />
|
|
430
|
+
|
|
431
|
+
<q-btn
|
|
432
|
+
@click="addItem"
|
|
433
|
+
icon="add"
|
|
434
|
+
:label="t`Добавить`"
|
|
435
|
+
color="primary"
|
|
436
|
+
no-caps
|
|
437
|
+
padding="8px 12px"
|
|
438
|
+
unelevated />
|
|
439
|
+
</div>
|
|
440
|
+
</teleport>
|
|
441
|
+
|
|
442
|
+
<!-- ОСНОВНИЙ КОНТЕНТ -->
|
|
443
|
+
...
|
|
444
|
+
</q-page>
|
|
445
|
+
</template>
|
|
446
|
+
<script setup>
|
|
447
|
+
const mounted = $ref(false)
|
|
448
|
+
onMounted(() => { mounted = true })
|
|
449
|
+
</script>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## @nitra/tfm — переклади
|
|
453
|
+
|
|
454
|
+
Використовуй `@nitra/tfm` для всіх текстів. Переклади для всіх мов проекту оголошуй наприкінці `<script setup>` у функції `getTr()`. Змінна `lang` із `@nitra/tfm` — для визначення або зміни поточної мови застосунку.
|
|
455
|
+
|
|
456
|
+
```vue
|
|
457
|
+
<template>
|
|
458
|
+
{{ lang }}
|
|
459
|
+
{{ t`Анкеты` }}
|
|
460
|
+
{{ subtitle }}
|
|
461
|
+
</template>
|
|
462
|
+
<script setup>
|
|
463
|
+
import { lang, tf as tfm } from '@nitra/tfm'
|
|
464
|
+
const t = tfm.bind({ tr: getTr() })
|
|
465
|
+
const subtitle = $computed(() => t`Анкеты`)
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* LOCALIZATION
|
|
469
|
+
* @returns {object} translations
|
|
470
|
+
*/
|
|
471
|
+
function getTr() {
|
|
472
|
+
return {
|
|
473
|
+
Анкеты: { en: 'Surveys', ro: 'Sondaje', tr: 'Anketler' }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
</script>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## Layout з NHeader
|
|
480
|
+
|
|
481
|
+
Для нових layout-ів використовуй `NHeader` з вбудованими `NLang` і `NMenu`.
|
|
482
|
+
|
|
483
|
+
```vue
|
|
484
|
+
<template>
|
|
485
|
+
<q-layout view="hHh Lpr lFf">
|
|
486
|
+
<n-header
|
|
487
|
+
v-model="leftSideOpened"
|
|
488
|
+
:logo="baseUrl + 'logo.png'"
|
|
489
|
+
:logo-url="homeUrl"
|
|
490
|
+
title="My App"
|
|
491
|
+
:subtitle="subtitle"
|
|
492
|
+
:username="userName"
|
|
493
|
+
toolbar-dark>
|
|
494
|
+
<template #top-toolbar>
|
|
495
|
+
<div class="platform-ios-only q-py-lg" />
|
|
496
|
+
</template>
|
|
497
|
+
<div>default slot content</div>
|
|
498
|
+
</n-header>
|
|
499
|
+
|
|
500
|
+
<!-- ЛЕВАЯ КОЛОНКА -->
|
|
501
|
+
<q-drawer v-model="leftSideOpened" :breakpoint="700" :width="300" overlay class="col column shadow-5 bg-grey-1">
|
|
502
|
+
<div class="platform-ios-only q-py-lg" />
|
|
503
|
+
<div v-if="!$q.screen.gt.xs" class="q-pa-sm row items-center">
|
|
504
|
+
<q-icon name="account_circle" color="primary" size="32px" class="q-mr-sm" />
|
|
505
|
+
<div class="text-subtitle1">{{ user.name }}</div>
|
|
506
|
+
</div>
|
|
507
|
+
<n-menu v-model="activeMenu" :menu="menu" />
|
|
508
|
+
<q-space />
|
|
509
|
+
<n-menu :menu="homeMenu" />
|
|
510
|
+
<div class="platform-ios-only q-py-md" />
|
|
511
|
+
</q-drawer>
|
|
512
|
+
|
|
513
|
+
<q-page-container>
|
|
514
|
+
<router-view />
|
|
515
|
+
</q-page-container>
|
|
516
|
+
</q-layout>
|
|
517
|
+
</template>
|
|
518
|
+
<script setup>
|
|
519
|
+
import { lang, tf as tfm } from '@nitra/tfm'
|
|
520
|
+
const t = tfm.bind({ tr: getTr() })
|
|
521
|
+
|
|
522
|
+
const baseUrl = import.meta.env.BASE_URL
|
|
523
|
+
const homeUrl = String.raw`https:\\` + import.meta.env.VITE_DOMAIN
|
|
524
|
+
|
|
525
|
+
// Ліва колонка
|
|
526
|
+
const leftSideOpened = $ref(false)
|
|
527
|
+
const activeMenu = $ref(null)
|
|
528
|
+
|
|
529
|
+
// Заголовок у шапці з поточного пункту меню
|
|
530
|
+
const subtitle = computed(() => (activeMenu ? getTr()[activeMenu.labelKey]?.[lang.value] || activeMenu?.labelKey : ''))
|
|
531
|
+
|
|
532
|
+
// Меню
|
|
533
|
+
const menu = computed(() =>
|
|
534
|
+
[
|
|
535
|
+
{
|
|
536
|
+
icon: 'sym_o_store',
|
|
537
|
+
label: t`Клиенты`,
|
|
538
|
+
labelKey: t`Клиенты`,
|
|
539
|
+
routeName: 'customer'
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
icon: 'sym_o_route',
|
|
543
|
+
label: t`Маршруты и визиты`,
|
|
544
|
+
items: [
|
|
545
|
+
{
|
|
546
|
+
icon: 'sym_o_route',
|
|
547
|
+
label: t`Маршруты`,
|
|
548
|
+
labelKey: t`Маршруты`,
|
|
549
|
+
routeName: 'route'
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
icon: 'sym_o_event_upcoming',
|
|
553
|
+
label: t`Переносы маршрутов`,
|
|
554
|
+
labelKey: t`Переносы маршрутов`,
|
|
555
|
+
routeName: 'route_postpone'
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
}
|
|
559
|
+
].filter(item => {
|
|
560
|
+
if (item.items?.length) {
|
|
561
|
+
item.items = item.items.filter(i => can[i.permissionRoute || i.routeName])
|
|
562
|
+
return item.items.length > 0
|
|
563
|
+
}
|
|
564
|
+
return can[item.routeName]
|
|
565
|
+
})
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* LOCALIZATION
|
|
570
|
+
* @returns {object} translations
|
|
571
|
+
*/
|
|
572
|
+
function getTr() {
|
|
573
|
+
return {
|
|
574
|
+
Клиенты: { en: 'Customers', ro: 'Clienți', tr: 'Müşteriler' }
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
</script>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Pinia store для стану сторінки
|
|
581
|
+
|
|
582
|
+
Зберігай у Pinia store:
|
|
583
|
+
|
|
584
|
+
- вибрані значення фільтрів
|
|
585
|
+
- вибрані для відображення колонки (`NTableColumns`)
|
|
586
|
+
- кількість записів на сторінці (pagination)
|
|
587
|
+
|
|
588
|
+
Називай store за назвою сторінки або компонента — `customerPageStore`, `routePageStore` тощо. На сторінці звертайся до нього через змінну `pageStore`.
|
|
589
|
+
|
|
590
|
+
```javascript
|
|
591
|
+
// store/customerPage.js
|
|
592
|
+
export const useCustomerPageStore = defineStore('customerPage', {
|
|
593
|
+
state: () => ({
|
|
594
|
+
filterName: '',
|
|
595
|
+
filterStatus: [],
|
|
596
|
+
columns: [],
|
|
597
|
+
rowsPerPage: 20
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
```vue
|
|
603
|
+
<script setup>
|
|
604
|
+
const pageStore = useCustomerPageStore()
|
|
605
|
+
</script>
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Коментарі в `<template>`
|
|
609
|
+
|
|
610
|
+
Додавай коментарі в `<template>` відповідно до логічного призначення блоку. Коментарі допомагають швидко орієнтуватися в розмітці.
|
|
611
|
+
|
|
612
|
+
```vue
|
|
613
|
+
<template>
|
|
614
|
+
<q-page>
|
|
615
|
+
<!-- ФІЛЬТРИ В ШАПЦІ -->
|
|
616
|
+
<teleport v-if="mounted" to="#header-filters">...</teleport>
|
|
617
|
+
|
|
618
|
+
<!-- ТАБЛИЦЯ -->
|
|
619
|
+
<q-table ... />
|
|
620
|
+
|
|
621
|
+
<!-- ДІАЛОГ РЕДАГУВАННЯ -->
|
|
622
|
+
<n-dialog v-model="editDialog">...</n-dialog>
|
|
623
|
+
</q-page>
|
|
624
|
+
</template>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Статичні файли — BASE_URL
|
|
628
|
+
|
|
629
|
+
Для підключення статичних файлів не використовуй відносні шляхи. Завжди будуй URL через `import.meta.env.BASE_URL`:
|
|
630
|
+
|
|
631
|
+
```vue
|
|
632
|
+
<!-- Погано -->
|
|
633
|
+
<img src="/logo.png" />
|
|
634
|
+
<img src="./assets/logo.png" />
|
|
635
|
+
|
|
636
|
+
<!-- Добре -->
|
|
637
|
+
<img :src="baseUrl + 'logo.png'" />
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
const baseUrl = import.meta.env.BASE_URL
|
|
642
|
+
```
|
|
643
|
+
|
|
346
644
|
## Перевірка
|
|
347
645
|
|
|
348
646
|
`npx @nitra/cursor fix vue` — перевіряє залежності, `vite.config`, наявність **`src/vite-env.d.ts`** з `/// <reference types="vite/client" />` та **`jsconfig.json`** у корені Vue-пакета; обходить джерела Vue-пакета (`.vue`, `.ts`, `.js` тощо) на заборонені value-імпорти з модуля `vue` (дозволені лише type-only та side-effect `import 'vue'`) і додатково сканує `.vue` SFC на імпорти Node-нативних модулів (`node:*` префікс або bare-ім’я вбудованого модуля Node — `fs`, `path`, `timers/promises` тощо). Імпорти аналізуються через **oxc-parser** (`module.staticImports`); для `.vue` вміст `<script>` витягується з SFC, далі той самий парсер (логіка в `npm/rules/vue/js/packages/vue-forbidden-imports.mjs`).
|
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
# Контекст проєкту
|
|
133
|
+
<1-3 речення: що треба і яка причина (баг / новий стек / requirement).
|
|
134
|
+
Якщо bugfix — вкажи симптом одним рядком.>
|
|
83
135
|
|
|
84
|
-
|
|
85
|
-
- Тип: <library | app | eslint-config | monorepo | …>
|
|
86
|
-
- Стек: Node `<engines.node>`, <TS/JS>, <ключові залежності>
|
|
87
|
-
- Документи правил: <CLAUDE.md / .cursor/rules — або "немає">
|
|
136
|
+
# Симптом / репро
|
|
88
137
|
|
|
89
|
-
|
|
90
|
-
|
|
138
|
+
<тільки для bugfix: точна команда + ключовий рядок stderr/exit code.
|
|
139
|
+
Пропусти секцію, якщо не bugfix.>
|
|
91
140
|
|
|
92
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
- крок 3 — оновити `CHANGELOG.md` / bump `version`, якщо це npm-пакет
|
|
148
|
+
<бажана поведінка input → output, edge cases, без псевдо-коду й
|
|
149
|
+
покрокових імплементаційних підказок. 3-8 буллетів.>
|
|
114
150
|
|
|
115
151
|
# Обмеження
|
|
116
152
|
|
|
117
|
-
-
|
|
118
|
-
- <constraints, виявлені з package.json / конфігів — наприклад "engines.node вже >=22, треба >=25">
|
|
119
|
-
- Дотриматись правил репо (CLAUDE.md / .cursor/rules — посилання вище)
|
|
153
|
+
<тільки реальні, не виводяться з коду: версії у репо-споживачі, breaking-change політика, inflight-міграції. Пропусти секцію, якщо їх нема.>
|
|
120
154
|
|
|
121
155
|
# Як перевірити
|
|
122
156
|
|
|
123
|
-
-
|
|
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
|
-
- **Обсяг:** прагни
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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`
|
|
174
|
-
|
|
216
|
+
Підняти `engines.node` у `@nitra/eslint-config` з `>=22` до `>=25`,
|
|
217
|
+
переконатися що peer `eslint ^9` сумісний з Node 25.
|
|
175
218
|
|
|
176
|
-
#
|
|
219
|
+
# Точки правки
|
|
177
220
|
|
|
178
|
-
-
|
|
179
|
-
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
…
|
|
224
|
+
# Обмеження
|
|
186
225
|
|
|
187
|
-
|
|
226
|
+
- Дотриматись `.cursor/rules/n-js-lint.mdc`.
|
|
227
|
+
- Якщо `eslint ^9` офіційно не підтримує Node 25 — підняти peer range.
|
|
188
228
|
|
|
189
|
-
|
|
229
|
+
# Як перевірити
|
|
190
230
|
|
|
191
|
-
|
|
192
|
-
|
|
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
|
```
|