@nitra/cursor 1.28.3 → 1.28.5
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 +15 -0
- package/package.json +1 -1
- package/rules/bun/bun.mdc +2 -2
- package/rules/bun/policy/package_json/package_json.rego +6 -3
- package/rules/js-lint/coverage/coverage.mjs +92 -38
- package/rules/test/auto.md +1 -1
- package/scripts/auto-rules.mjs +2 -0
- package/scripts/utils/resolve-js-root.mjs +34 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@
|
|
|
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.5] - 2026-05-28
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`scripts/utils/resolve-js-root.mjs`** — `resolveAllJsRoots()` тепер розгортає glob-патерни (`cf/*`, `packages/*` тощо) у списку `workspaces` через `node:fs/promises#glob`. Без цього `cf/*` як літерал не резолвився через `existsSync`, і `resolveJsRoot()` падав на fallback → `cwd`; `n-cursor coverage` із кореня запускав vitest у root-у проєкту без `vitest.config.js` (через `gt/tests/setup.mjs` не підвантажувався). `resolveJsRoot()` тепер тонкий wrapper над `resolveAllJsRoots()[0]`. Додано 2 тести: glob-розгортання `cf/*` і fallback на `[cwd]` при порожньому збігу.
|
|
12
|
+
- **`rules/js-lint/coverage/coverage.mjs#collect()`** — у monorepo тепер ітерує **всі** JS-roots з `resolveAllJsRoots()`, запускає vitest + Stryker у кожному окремо та сумує `lcov` (через локальний `addCoverage`) і `mutation` (через `addMutation`). Шляхи у `survived[].file` і `survived[].exampleTest.testFile` рібейзяться відносно `cwd` (`relative(cwd, jsRoot)`/`join(wsRel, file)`), щоб `coverage-fix.mjs#buildFixPrompt` коректно читав source-файли через `join(projectRoot, file)`. `detect()` повертає `true`, якщо vitest є хоча б в одному workspace АБО в кореневому `package.json`. Додано тест агрегування у monorepo з `cf/*`-glob.
|
|
13
|
+
|
|
14
|
+
## [1.28.4] - 2026-05-28
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **`rules/test/auto.md` → `завжди`**, **`scripts/auto-rules.mjs`** — правило `test` тепер додається у `.n-cursor.json#rules` беззастережно (як `adr`/`security`/`text`), без вимоги наявності `*.test.mjs`. `AUTO_RULE_ORDER` отримало `test`; `addRule('test')` викликається з безумовного блоку. Тест `auto-rules.test.mjs` оновлено: `'test'` додано в `ALL_RULES` і в очікуваний результат «додає правила за ознаками проєкту». Мотивація — узгодження з `bun.mdc`-винятком на root-only Vitest/Stryker peer/tools: щоб умова «корінь єдине місце для `vitest`/`@vitest/coverage-v8`/`@stryker-mutator/vitest-runner`» була правдою у кожному споживачі `@nitra/cursor`, а не «лише у dog food-репо».
|
|
19
|
+
- **`rules/bun/bun.mdc`** (`version` 1.9 → 2.0) і дзеркало `.cursor/rules/n-bun.mdc` — переформульовано пункт про root-only Vitest/Stryker peer/tools: прибрано згадку «лише для dog food-репо `@nitra/cursor`», тепер виняток описано як **загальний** для будь-якого монорепо, що вмикає правило `test` і виконує `n-cursor coverage`. Структурна причина: published workspace-и за `npm-module.mdc` не мають `devDependencies`, а оркестратор coverage запускається з кореня. Збігається з фактичною поведінкою `policy/package_json/package_json.rego` (`allowed_root_test_deps` — глобальний whitelist, без перевірки імені репо).
|
|
20
|
+
- **`rules/bun/policy/package_json/package_json.rego`** — оновлено docstring модуля і коментар на `allowed_root_dev_dependency`: тепер посилаються на always-on `test/auto.md` + `npm-module.mdc`, без «dog food-прогонів». Логіка `allowed_root_test_deps` не змінилася — лише пояснення синхронізовано з `bun.mdc`.
|
|
21
|
+
|
|
7
22
|
## [1.28.3] - 2026-05-28
|
|
8
23
|
|
|
9
24
|
### Changed
|
package/package.json
CHANGED
package/rules/bun/bun.mdc
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Bun як єдиний package manager у монорепо
|
|
3
3
|
globs: "**/package.json,**/bunfig.toml,**/bun.lock,**/bun.lockb"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '
|
|
5
|
+
version: '2.0'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Проект використовує тільки Bun для керування залежностями та запуску скриптів.
|
|
@@ -42,7 +42,7 @@ Lockfile у репозиторії: `bun.lock`.
|
|
|
42
42
|
- Якщо залежність потрібна лише одному пакету, додавати її в директорії цього пакета.
|
|
43
43
|
- У CI та локально запускати скрипти через `bun run`.
|
|
44
44
|
|
|
45
|
-
В кореневому
|
|
45
|
+
В кореневому `package.json` не повинно бути `dependencies`, а в `devDependencies` — тільки модулі `@nitra/*`. **Виняток (root-only)** — Vitest/Stryker peer/tools для `n-cursor coverage`: `vitest`, `@vitest/coverage-v8`, `@stryker-mutator/vitest-runner`. Тримати їх **у корені** доводиться у будь-якому монорепо-споживачі, бо правило `test` enabled завжди (`test/auto.md` = `завжди`), а класти ці пакети у workspace-и не можна: published пакети (`npm-module.mdc`) не мають мати `devDependencies`, а інші workspace-и однаково запускають coverage оркестратор з кореня. Якщо в package.json є поля `packageManager`, то прибрати їх, також прибрати всі директорії та файли для yarn.
|
|
46
46
|
|
|
47
47
|
- Заборонені top-level поля у root `package.json` (з причинами): [package.json.deny.json](./policy/package_json/template/package.json.deny.json)
|
|
48
48
|
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
# (top-level fields заборонені у root).
|
|
6
6
|
#
|
|
7
7
|
# Логіка, що ЛИШАЄТЬСЯ у rego (inverse-patterns, не виносяться у template):
|
|
8
|
-
# - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для
|
|
8
|
+
# - `devDependencies` лише `@nitra/*` + root-only тестові peer/tools для `n-cursor coverage`
|
|
9
|
+
# (правило `test` enabled завжди — див. `test/auto.md`; published workspace-и не мають
|
|
10
|
+
# `devDependencies` за `npm-module.mdc`)
|
|
9
11
|
# - Агрегований `lint` скрипт (cross-script aggregation logic)
|
|
10
12
|
#
|
|
11
13
|
# Перевірки, які ЗАЛИШИЛИСЬ у JS (потребують FS / cross-file):
|
|
@@ -71,8 +73,9 @@ allowed_root_test_deps := {"vitest", "@vitest/coverage-v8", "@stryker-mutator/vi
|
|
|
71
73
|
allowed_root_dev_dependency(name) if {
|
|
72
74
|
startswith(name, "@nitra/")
|
|
73
75
|
} else if {
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
+
# Vitest/Stryker peer/tools для `n-cursor coverage` живуть у корені будь-якого
|
|
77
|
+
# монорепо-споживача: правило `test` enabled завжди, а published workspace-и
|
|
78
|
+
# не мають `devDependencies` (`npm-module.mdc`).
|
|
76
79
|
name in allowed_root_test_deps
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -10,9 +10,9 @@ import { spawnSync } from 'node:child_process'
|
|
|
10
10
|
import { existsSync, readFileSync } from 'node:fs'
|
|
11
11
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
12
12
|
import { tmpdir } from 'node:os'
|
|
13
|
-
import { join } from 'node:path'
|
|
13
|
+
import { join, relative } from 'node:path'
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
16
16
|
|
|
17
17
|
const TEST_BLOCK_START = /^\s*(it|test)\(/
|
|
18
18
|
const FILE_EXTENSION = /\.[^.]+$/
|
|
@@ -30,21 +30,24 @@ function hasVitestDep(pkg) {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Чи провайдер застосовний у поточному cwd. Активується, коли `vitest`
|
|
33
|
-
* декларовано
|
|
34
|
-
* hoisted node_modules — типовий патерн bun monorepo,
|
|
35
|
-
* забороняє devDeps у published workspace-у, тож вони
|
|
36
|
-
* Інакше silent skip із hint у stderr (одноразово).
|
|
33
|
+
* декларовано хоча б в одному JS-root АБО у кореневому `package.json`
|
|
34
|
+
* (workspace-проєкт із hoisted node_modules — типовий патерн bun monorepo,
|
|
35
|
+
* де npm-module rule забороняє devDeps у published workspace-у, тож вони
|
|
36
|
+
* живуть у корені). Інакше silent skip із hint у stderr (одноразово).
|
|
37
37
|
* @param {string} cwd корінь проєкту
|
|
38
38
|
* @returns {Promise<boolean>} true, якщо проєкт сумісний з vitest-based coverage
|
|
39
39
|
*/
|
|
40
40
|
export async function detect(cwd) {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const jsRoots = await resolveAllJsRoots(cwd)
|
|
42
|
+
if (jsRoots.length === 0) return false
|
|
43
|
+
for (const jsRoot of jsRoots) {
|
|
44
|
+
const pkgPath = join(jsRoot, 'package.json')
|
|
45
|
+
if (!existsSync(pkgPath)) continue
|
|
46
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
|
|
47
|
+
if (hasVitestDep(pkg)) return true
|
|
48
|
+
}
|
|
49
|
+
const rootInJsRoots = jsRoots.includes(cwd)
|
|
50
|
+
if (!rootInJsRoots) {
|
|
48
51
|
const rootPkgPath = join(cwd, 'package.json')
|
|
49
52
|
if (existsSync(rootPkgPath)) {
|
|
50
53
|
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
@@ -224,40 +227,91 @@ const defaultRunner = {
|
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
/**
|
|
227
|
-
* Збирає JS-метрики покриття + мутаційного тестування.
|
|
230
|
+
* Збирає JS-метрики покриття + мутаційного тестування. У monorepo ітерує кожен
|
|
231
|
+
* JS-root з `resolveAllJsRoots()` (включно з glob-патернами на кшталт `cf/*`),
|
|
232
|
+
* запускає vitest+Stryker у кожному та сумує lcov/mutation. Шляхи файлів у
|
|
233
|
+
* `survived` рібейзяться відносно `cwd`, щоб `coverage-fix.mjs` знаходив джерела.
|
|
228
234
|
* @param {string} cwd корінь проєкту
|
|
229
235
|
* @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція для тестів
|
|
230
236
|
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
|
|
231
237
|
*/
|
|
232
238
|
export async function collect(cwd, opts = {}) {
|
|
233
239
|
const runner = opts.runner ?? defaultRunner
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
240
|
+
const jsRoots = await resolveAllJsRoots(cwd)
|
|
241
|
+
if (jsRoots.length === 0) throw new Error('js-lint coverage: package.json не знайдено')
|
|
242
|
+
|
|
243
|
+
let coverage = { lines: { covered: 0, total: 0 }, functions: { covered: 0, total: 0 } }
|
|
244
|
+
let mutation = { caught: 0, total: 0 }
|
|
245
|
+
const survived = []
|
|
246
|
+
|
|
247
|
+
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 })
|
|
236
274
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
+
}
|
|
246
287
|
}
|
|
247
288
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
+
}
|
|
259
306
|
}
|
|
260
|
-
|
|
307
|
+
}
|
|
261
308
|
|
|
262
|
-
|
|
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 }
|
|
263
317
|
}
|
package/rules/test/auto.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
завжди
|
package/scripts/auto-rules.mjs
CHANGED
|
@@ -51,6 +51,7 @@ export const AUTO_RULE_ORDER = Object.freeze([
|
|
|
51
51
|
'rust',
|
|
52
52
|
'security',
|
|
53
53
|
'style-lint',
|
|
54
|
+
'test',
|
|
54
55
|
'text',
|
|
55
56
|
'vue'
|
|
56
57
|
])
|
|
@@ -674,6 +675,7 @@ export async function detectAutoRules({
|
|
|
674
675
|
}
|
|
675
676
|
addRule('adr')
|
|
676
677
|
addRule('security')
|
|
678
|
+
addRule('test')
|
|
677
679
|
addRule('text')
|
|
678
680
|
if (facts.hasVueSource) {
|
|
679
681
|
addRule('vue')
|
|
@@ -1,33 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Резолвить корінь JS-коду в проєкті: для workspace-projects — перший workspace
|
|
3
|
-
* (
|
|
4
|
-
* для coverage-провайдера js-lint і test-концерну stryker_config (DRY).
|
|
3
|
+
* (з підтримкою glob-патернів типу `cf/*`), для single-package — корінь cwd.
|
|
4
|
+
* Спільна утиліта для coverage-провайдера js-lint і test-концерну stryker_config (DRY).
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
-
import { readFile } from 'node:fs/promises'
|
|
7
|
+
import { glob, readFile } from 'node:fs/promises'
|
|
8
8
|
import { join } from 'node:path'
|
|
9
9
|
|
|
10
|
+
const WORKSPACE_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Розгортає один workspace-патерн у список абсолютних шляхів каталогів з package.json.
|
|
14
|
+
* Літеральні патерни перевіряються через existsSync; glob-патерни — через node:fs/promises#glob.
|
|
15
|
+
* @param {string} cwd корінь проєкту
|
|
16
|
+
* @param {string} pattern workspace-патерн з package.json (наприклад, `app` або `cf/*`)
|
|
17
|
+
* @returns {Promise<string[]>} абсолютні шляхи до workspace-каталогів
|
|
18
|
+
*/
|
|
19
|
+
async function expandWorkspacePattern(cwd, pattern) {
|
|
20
|
+
if (!pattern.includes('*')) {
|
|
21
|
+
const wsPath = join(cwd, pattern)
|
|
22
|
+
return existsSync(join(wsPath, 'package.json')) ? [wsPath] : []
|
|
23
|
+
}
|
|
24
|
+
const results = []
|
|
25
|
+
for await (const rel of glob(`${pattern}/package.json`, { cwd, exclude: WORKSPACE_GLOB_IGNORE })) {
|
|
26
|
+
const wsRel = rel.replace(/[/\\]package\.json$/, '')
|
|
27
|
+
results.push(join(cwd, wsRel))
|
|
28
|
+
}
|
|
29
|
+
return results.sort()
|
|
30
|
+
}
|
|
31
|
+
|
|
10
32
|
/**
|
|
11
33
|
* @param {string} cwd корінь проєкту (де `.n-cursor.json` і кореневий package.json)
|
|
12
34
|
* @returns {Promise<string|null>} абсолютний шлях до JS-root або null без кореневого package.json
|
|
13
35
|
*/
|
|
14
36
|
export async function resolveJsRoot(cwd) {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
|
|
18
|
-
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
19
|
-
if (workspaces.length > 0) {
|
|
20
|
-
const wsPath = join(cwd, workspaces[0])
|
|
21
|
-
if (existsSync(join(wsPath, 'package.json'))) return wsPath
|
|
22
|
-
}
|
|
23
|
-
return cwd
|
|
37
|
+
const roots = await resolveAllJsRoots(cwd)
|
|
38
|
+
if (roots.length === 0) return null
|
|
39
|
+
return roots[0]
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
/**
|
|
27
43
|
* Plural-варіант: повертає всі JS-roots проєкту. Для workspace-projects — кожен
|
|
28
|
-
* workspace з власним `package.json
|
|
29
|
-
* масив без кореневого package.json.
|
|
30
|
-
* `stryker_config` для per-workspace baseline-копіювання.
|
|
44
|
+
* workspace з власним `package.json` (з розгортанням glob-патернів); для
|
|
45
|
+
* single-package — `[cwd]`. Порожній масив без кореневого package.json.
|
|
31
46
|
* @param {string} cwd корінь проєкту
|
|
32
47
|
* @returns {Promise<string[]>} абсолютні шляхи до всіх JS-roots
|
|
33
48
|
*/
|
|
@@ -35,12 +50,11 @@ export async function resolveAllJsRoots(cwd) {
|
|
|
35
50
|
const rootPkgPath = join(cwd, 'package.json')
|
|
36
51
|
if (!existsSync(rootPkgPath)) return []
|
|
37
52
|
const rootPkg = JSON.parse(await readFile(rootPkgPath, 'utf8'))
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
53
|
+
const patterns = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : []
|
|
54
|
+
if (patterns.length === 0) return [cwd]
|
|
40
55
|
const roots = []
|
|
41
|
-
for (const
|
|
42
|
-
|
|
43
|
-
if (existsSync(join(wsPath, 'package.json'))) roots.push(wsPath)
|
|
56
|
+
for (const pattern of patterns) {
|
|
57
|
+
roots.push(...(await expandWorkspacePattern(cwd, pattern)))
|
|
44
58
|
}
|
|
45
59
|
return roots.length > 0 ? roots : [cwd]
|
|
46
60
|
}
|