@nitra/cursor 3.10.0 → 3.12.0
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 +18 -0
- package/bin/n-cursor.js +3 -2
- package/package.json +1 -1
- package/rules/changelog/changelog.mdc +7 -1
- package/rules/flow/flow.mdc +6 -3
- package/rules/js-lint/coverage/coverage.mjs +89 -22
- package/rules/npm-module/js/package_structure.mjs +5 -147
- package/rules/npm-module/npm-module.mdc +4 -14
- package/rules/rust/coverage/coverage.mjs +13 -2
- package/rules/test/coverage/coverage.mjs +50 -6
- package/rules/test/test.mdc +2 -0
- package/scripts/dispatcher/index.mjs +36 -3
- package/scripts/dispatcher/lib/commands.mjs +100 -8
- package/scripts/dispatcher/lib/flow-resolve.mjs +154 -0
- package/scripts/dispatcher/lib/gate.mjs +11 -3
- package/scripts/dispatcher/lib/plan.mjs +11 -3
- package/scripts/dispatcher/lib/review.mjs +11 -3
- package/scripts/dispatcher/lib/reviewer.mjs +9 -5
- package/scripts/dispatcher/lib/spec.mjs +11 -3
- package/scripts/dispatcher/trace.mjs +50 -11
- package/scripts/lib/changed-files.mjs +28 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.12.0] - 2026-06-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- flow: cwd-незалежний резолвинг активного стану — spec/plan/verify/review/gate/release знаходять .flow.json поточної гілки навіть із головного дерева (швидкий шлях без git, toplevel-резолв, авторезолв єдиного активного flow), + опційний --branch <гілка>. Гейти виконуються у теці worktree.
|
|
8
|
+
- flow release: інференс зміненого воркспейсу з diff від base_commit — авто-додає --ws у change, якщо не задано явно (один змінений subworkspace → його .changes/; кілька → fail з підказкою явного --ws; лише корінь → дефолт). Усуває потрапляння change-файлу в корінь монорепо при змінах під підпакетом.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Усунуто суперечність n-changelog.mdc ↔ n-npm-module.mdc: прибрано перевірки version/CHANGELOG у package_structure.mjs, що штовхали до ручного bump (єдиний артефакт змін — change-файл; узгодженість валідує changelog/consistency.mjs); npm-module.mdc делегує bump/CHANGELOG у changelog.mdc, який отримав post-release-інваріант.
|
|
13
|
+
- trace: резолв лінків front-matter відносно теки артефакту (+ root-relative fallback) — file-relative spec/plan лінки (`../specs/…`) більше не дають хибний «розрив ланцюга»; поле `flow` (runtime `.flow.json`) показується, але не рахується розривом. Розрив визначають лише chain-поля (adr/spec/plan/change/task).
|
|
14
|
+
|
|
15
|
+
## [3.11.0] - 2026-06-02
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- coverage: scoped режим --changed — flow-турнікет (DEFAULT_GATES) перевіряє лише змінені від base_commit файли (vitest --changed + Stryker --mutate), однаково для закомічених і незакомічених змін; повний coverage лишається для bun run coverage / n-coverage-fix
|
|
20
|
+
|
|
3
21
|
## [3.10.0] - 2026-06-01
|
|
4
22
|
|
|
5
23
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1527,9 +1527,10 @@ try {
|
|
|
1527
1527
|
}
|
|
1528
1528
|
case 'coverage': {
|
|
1529
1529
|
// n-cursor coverage — оркестратор покриття + мутаційного тестування з discovery
|
|
1530
|
-
// провайдерів через .n-cursor.json#rules (test.mdc).
|
|
1530
|
+
// провайдерів через .n-cursor.json#rules (test.mdc). --changed звужує scope до
|
|
1531
|
+
// змінених від base файлів (flow-турнікет: лише vitest/Stryker по diff).
|
|
1531
1532
|
const { runCoverageCli } = await import('../rules/test/coverage/coverage.mjs')
|
|
1532
|
-
process.exitCode = await runCoverageCli({ fix: args.includes('--fix') })
|
|
1533
|
+
process.exitCode = await runCoverageCli({ fix: args.includes('--fix'), changed: args.includes('--changed') })
|
|
1533
1534
|
|
|
1534
1535
|
break
|
|
1535
1536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
|
|
3
|
-
version: '3.
|
|
3
|
+
version: '3.2'
|
|
4
4
|
alwaysApply: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -97,3 +97,9 @@ alwaysApply: true
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Секції — підмножина `### Added`, `### Changed`, `### Fixed`, `### Removed` (одна або кілька).
|
|
100
|
+
|
|
101
|
+
## Post-release інваріант (гарантує CI)
|
|
102
|
+
|
|
103
|
+
Перша (верхня) секція `## [version]` у `CHANGELOG.md` дорівнює полю `version` у маніфесті — але це **post-release** твердження, яке забезпечує `n-cursor release` у CI, агрегуючи change-файли (bump `version` + генерація секції + git-тег `<name>@<version>`). **Локально цю рівність руками не підтримують**: у feature-флоу `version`/`CHANGELOG.md` не чіпають, тож верхня секція може відставати від майбутньої версії — це нормально. Drift `version` поза CI (vs реєстр / vs git-база) ловить `check changelog` як заборонений ручний bump.
|
|
104
|
+
|
|
105
|
+
Інструкції щодо bump `version` і редагування `CHANGELOG.md` живуть **лише** в цьому правилі — джерелі істини. Інші правила (зокрема `n-npm-module.mdc`) їй підпорядковані й власних інструкцій bump/CHANGELOG не дублюють.
|
package/rules/flow/flow.mdc
CHANGED
|
@@ -78,9 +78,12 @@ Composer, Claude Code) працює **Пасивний Турнікет**: ти,
|
|
|
78
78
|
npx @nitra/cursor flow verify
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Проганяє Quality
|
|
82
|
-
проваленого gate.
|
|
83
|
-
|
|
81
|
+
Проганяє Quality Gates (lint + coverage). Повертає `0` (pass) або `1` із
|
|
82
|
+
виводом проваленого gate. **Обидва гейти перевіряють лише змінені файли**
|
|
83
|
+
(diff від `base_commit`): `lint` — quick-режим, `coverage --changed` — vitest
|
|
84
|
+
`--changed` + Stryker `--mutate` по diff. Працює однаково, незалежно від того,
|
|
85
|
+
чи зміни вже закомічені у worktree. Повний coverage (увесь проєкт) — окремо:
|
|
86
|
+
`bun run coverage` або `/n-coverage-fix`.
|
|
84
87
|
|
|
85
88
|
6. **Review (adversarial)** — рекомендовано перед release:
|
|
86
89
|
|
|
@@ -10,13 +10,36 @@ 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, relative } from 'node:path'
|
|
13
|
+
import { isAbsolute, join, relative } from 'node:path'
|
|
14
14
|
|
|
15
15
|
import { resolveAllJsRoots } from '../../../scripts/utils/resolve-js-root.mjs'
|
|
16
16
|
import { addCoverage, addMutation } from '../../test/coverage/coverage.mjs'
|
|
17
17
|
|
|
18
18
|
const TEST_BLOCK_START = /^\s*(it|test)\(/
|
|
19
19
|
const FILE_EXTENSION = /\.[^.]+$/
|
|
20
|
+
/** JS/TS-розширення — файли, які мутує Stryker і покриває vitest. */
|
|
21
|
+
const JS_FILE = /\.(c|m)?[jt]sx?$/
|
|
22
|
+
/** Тест-файли (`*.test.*` / `*.spec.*`) — НЕ production-код, не йдуть у Stryker `--mutate`. */
|
|
23
|
+
const TEST_FILE = /\.(test|spec)\.[^.]+$/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Звужує список змінених файлів (relative до cwd) до тих, що лежать під `jsRoot`,
|
|
27
|
+
* мають JS/TS-розширення, і рібейзить їх відносно `jsRoot`.
|
|
28
|
+
* @param {string[]} changedFiles relative-до-cwd шляхи змінених файлів
|
|
29
|
+
* @param {string} cwd корінь проєкту
|
|
30
|
+
* @param {string} jsRoot абсолютний шлях workspace-кореня
|
|
31
|
+
* @returns {string[]} JS-файли під jsRoot, шляхи relative до jsRoot
|
|
32
|
+
*/
|
|
33
|
+
export function scopeToRoot(changedFiles, cwd, jsRoot) {
|
|
34
|
+
const out = []
|
|
35
|
+
for (const f of changedFiles) {
|
|
36
|
+
if (!JS_FILE.test(f)) continue
|
|
37
|
+
const rel = relative(jsRoot, join(cwd, f))
|
|
38
|
+
if (rel.startsWith('..') || isAbsolute(rel)) continue
|
|
39
|
+
out.push(rel)
|
|
40
|
+
}
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
20
43
|
const VITEST_HINT =
|
|
21
44
|
'js-lint coverage: vitest відсутній у package.json — додай `vitest`, `@vitest/coverage-v8` та `@stryker-mutator/vitest-runner` у devDependencies (див. test.mdc)'
|
|
22
45
|
|
|
@@ -217,7 +240,11 @@ export function parseStrykerReport(report, jsRoot) {
|
|
|
217
240
|
* у такому випадку сигналізує "no tests" → collectOneRoot пропускає workspace.
|
|
218
241
|
*/
|
|
219
242
|
const defaultRunner = {
|
|
220
|
-
runJsCoverage({ cwd, lcovDir }) {
|
|
243
|
+
runJsCoverage({ cwd, lcovDir, base }) {
|
|
244
|
+
// base !== undefined ⇔ --changed-режим: vitest сам рахує зачеплені змінами тести
|
|
245
|
+
// через граф імпортів. `--changed <base>` порівнює base↔робоче дерево (committed і
|
|
246
|
+
// uncommitted разом); `--changed` без аргументу — uncommitted vs HEAD.
|
|
247
|
+
const changedArgs = base === undefined ? [] : base === null ? ['--changed'] : ['--changed', base]
|
|
221
248
|
const r = spawnSync(
|
|
222
249
|
'bunx',
|
|
223
250
|
[
|
|
@@ -226,13 +253,14 @@ const defaultRunner = {
|
|
|
226
253
|
'--passWithNoTests',
|
|
227
254
|
'--coverage',
|
|
228
255
|
'--coverage.reporter=lcov',
|
|
229
|
-
`--coverage.reportsDirectory=${lcovDir}
|
|
256
|
+
`--coverage.reportsDirectory=${lcovDir}`,
|
|
257
|
+
...changedArgs
|
|
230
258
|
],
|
|
231
259
|
{ cwd, stdio: 'inherit', env: process.env }
|
|
232
260
|
)
|
|
233
261
|
return r.status ?? 1
|
|
234
262
|
},
|
|
235
|
-
runStryker({ cwd }) {
|
|
263
|
+
runStryker({ cwd, mutate }) {
|
|
236
264
|
// `npx`, не `bunx`: bunx завжди ставить пакет у `T/bunx-<uid>-<pkg>@latest` і запускає
|
|
237
265
|
// Stryker звідти. Плагін-discovery у Stryker (`@stryker-mutator/*`) globится відносно
|
|
238
266
|
// CORE-install-каталогу (`core/dist/src/di/plugin-loader.js` → `../../../../../@stryker-mutator/*`),
|
|
@@ -240,30 +268,48 @@ const defaultRunner = {
|
|
|
240
268
|
// встановлений `@stryker-mutator/vitest-runner` залишається невидимим, і workers падають з
|
|
241
269
|
// `Cannot find TestRunner plugin "vitest"`. `npx` ходить угору по `node_modules/.bin/` і
|
|
242
270
|
// запускає Stryker з локального hoisted-install, де поряд лежить vitest-runner.
|
|
243
|
-
|
|
271
|
+
// mutate (непорожній) ⇔ --changed-режим: мутуємо лише змінені production-файли цього root.
|
|
272
|
+
const mutateArgs = mutate && mutate.length > 0 ? ['--mutate', mutate.join(',')] : []
|
|
273
|
+
const r = spawnSync('npx', ['@stryker-mutator/core', 'run', ...mutateArgs], {
|
|
274
|
+
cwd,
|
|
275
|
+
stdio: 'inherit',
|
|
276
|
+
env: process.env
|
|
277
|
+
})
|
|
244
278
|
return r.status ?? 1
|
|
245
279
|
}
|
|
246
280
|
}
|
|
247
281
|
|
|
248
282
|
/**
|
|
249
283
|
* Збирає метрики покриття + мутаційного тестування для **одного** JS-root.
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
284
|
+
*
|
|
285
|
+
* Full-режим (`scope === null`): vitest на всьому suite + Stryker на всіх файлах
|
|
286
|
+
* config-глоба. Пропускає workspace без тестів (повертає `null`): vitest пройшов з
|
|
287
|
+
* `--passWithNoTests`, але lcov порожній — нема сенсу запускати Stryker.
|
|
288
|
+
*
|
|
289
|
+
* Changed-режим (`scope = { files, base }`): vitest `--changed <base>` (лише
|
|
290
|
+
* зачеплені тести) + Stryker `--mutate` лише по змінених production-файлах. Тут
|
|
291
|
+
* **не** пропускаємо на порожньому lcov — змінений src без тестів має дати
|
|
292
|
+
* NoCoverage-мутанти (gate впаде, як і має). Якщо змінено лише тест-файли (нема
|
|
293
|
+
* production-src) — Stryker не запускаємо (мутувати нічого), повертаємо лише coverage.
|
|
294
|
+
*
|
|
295
|
+
* Реальні помилки (vitest exit ≠ 0, відсутній mutation.json попри запуск Stryker)
|
|
296
|
+
* кидаються — у multi-root режимі це не маскує справжній збій.
|
|
254
297
|
* @param {string} jsRoot абсолютний шлях до workspace-кореня
|
|
255
298
|
* @param {string} cwd корінь проєкту (для рібейзингу `survived[].file`)
|
|
256
299
|
* @param {{runJsCoverage:Function, runStryker:Function}} runner spawn-ін'єкція
|
|
257
|
-
* @
|
|
300
|
+
* @param {{files:string[], base:string|null}|null} [scope] changed-scope (null = full-режим)
|
|
301
|
+
* @returns {Promise<{coverage:object, mutation:{caught:number,total:number}, survived:Array<object>} | null>} результати або null коли full-режим і workspace без тестів
|
|
258
302
|
*/
|
|
259
|
-
async function collectOneRoot(jsRoot, cwd, runner) {
|
|
303
|
+
async function collectOneRoot(jsRoot, cwd, runner, scope = null) {
|
|
260
304
|
const wsRel = relative(cwd, jsRoot)
|
|
305
|
+
// У changed-режимі production-файли для мутації = змінені JS цього root без тест-файлів.
|
|
306
|
+
const mutateSrc = scope ? scope.files.filter(f => !TEST_FILE.test(f)) : null
|
|
261
307
|
|
|
262
|
-
// 1. Coverage через vitest run --passWithNoTests --coverage
|
|
308
|
+
// 1. Coverage через vitest run --passWithNoTests --coverage (+ --changed у changed-режимі)
|
|
263
309
|
const lcovDir = await mkdtemp(join(tmpdir(), 'js-lint-cov-'))
|
|
264
310
|
let coverage
|
|
265
311
|
try {
|
|
266
|
-
const code = await runner.runJsCoverage({ cwd: jsRoot, lcovDir })
|
|
312
|
+
const code = await runner.runJsCoverage(scope ? { cwd: jsRoot, lcovDir, base: scope.base } : { cwd: jsRoot, lcovDir })
|
|
267
313
|
if (code !== 0) throw new Error(`JS coverage exit ${code}`)
|
|
268
314
|
const lcovPath = join(lcovDir, 'lcov.info')
|
|
269
315
|
coverage = existsSync(lcovPath)
|
|
@@ -273,13 +319,20 @@ async function collectOneRoot(jsRoot, cwd, runner) {
|
|
|
273
319
|
await rm(lcovDir, { recursive: true, force: true })
|
|
274
320
|
}
|
|
275
321
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
322
|
+
// Full-режим: порожній lcov ⇔ vitest не знайшов тестів → пропускаємо workspace,
|
|
323
|
+
// щоб не ганяти Stryker марно. У changed-режимі НЕ пропускаємо (див. JSDoc).
|
|
324
|
+
if (!scope) {
|
|
325
|
+
const hasTests = coverage.lines.total > 0 || coverage.functions.total > 0
|
|
326
|
+
if (!hasTests) return null
|
|
327
|
+
}
|
|
280
328
|
|
|
281
|
-
//
|
|
282
|
-
|
|
329
|
+
// Changed-режим без production-src (змінено лише тест-файли) → мутувати нічого.
|
|
330
|
+
if (scope && mutateSrc.length === 0) {
|
|
331
|
+
return { coverage, mutation: { caught: 0, total: 0 }, survived: [] }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 2. Mutation через Stryker (у changed-режимі — лише по mutateSrc)
|
|
335
|
+
await runner.runStryker(scope ? { cwd: jsRoot, mutate: mutateSrc } : { cwd: jsRoot })
|
|
283
336
|
const mutationPath = join(jsRoot, 'reports', 'stryker', 'mutation.json')
|
|
284
337
|
if (!existsSync(mutationPath)) {
|
|
285
338
|
throw new Error(
|
|
@@ -317,22 +370,36 @@ async function collectOneRoot(jsRoot, cwd, runner) {
|
|
|
317
370
|
* з зрозумілим повідомленням ("Жодного провайдера покриття не знайдено").
|
|
318
371
|
* Шляхи у `survived` рібейзяться відносно `cwd`, щоб `coverage-fix.mjs`
|
|
319
372
|
* знаходив джерела через `join(projectRoot, file)`.
|
|
373
|
+
*
|
|
374
|
+
* Changed-режим (`opts.changedFiles` задано): кожен root отримує лише свої змінені
|
|
375
|
+
* JS-файли (`scopeToRoot`); roots без змінених JS пропускаються повністю (ні vitest,
|
|
376
|
+
* ні Stryker). Якщо змін нема ніде — повертає `[]` без error-логу (оркестратор
|
|
377
|
+
* трактує порожній changed-scope як pass).
|
|
320
378
|
* @param {string} cwd корінь проєкту
|
|
321
|
-
* @param {{runner?: typeof defaultRunner}} [opts] runner-ін'єкція
|
|
322
|
-
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}, survived:Array<object>}>>} рядок `JS` або `[]` коли
|
|
379
|
+
* @param {{runner?: typeof defaultRunner, changedFiles?: string[], base?: string|null}} [opts] runner-ін'єкція + changed-scope
|
|
380
|
+
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}, survived:Array<object>}>>} рядок `JS` або `[]` коли тестів/змін нема ніде
|
|
323
381
|
*/
|
|
324
382
|
export async function collect(cwd, opts = {}) {
|
|
325
383
|
const runner = opts.runner ?? defaultRunner
|
|
384
|
+
const changed = Array.isArray(opts.changedFiles)
|
|
326
385
|
const jsRoots = await resolveAllJsRoots(cwd)
|
|
327
386
|
if (jsRoots.length === 0) throw new Error('js-lint coverage: package.json не знайдено')
|
|
328
387
|
|
|
329
388
|
const results = []
|
|
330
389
|
for (const jsRoot of jsRoots) {
|
|
331
|
-
|
|
390
|
+
let scope = null
|
|
391
|
+
if (changed) {
|
|
392
|
+
const files = scopeToRoot(opts.changedFiles, cwd, jsRoot)
|
|
393
|
+
if (files.length === 0) continue // root без змінених JS — пропускаємо
|
|
394
|
+
scope = { files, base: opts.base ?? null }
|
|
395
|
+
}
|
|
396
|
+
const r = await collectOneRoot(jsRoot, cwd, runner, scope)
|
|
332
397
|
if (r !== null) results.push(r)
|
|
333
398
|
}
|
|
334
399
|
|
|
335
400
|
if (results.length === 0) {
|
|
401
|
+
// Changed-режим: нема змінених JS у жодному root → тихо порожньо (це pass, не помилка).
|
|
402
|
+
if (changed) return []
|
|
336
403
|
console.error(
|
|
337
404
|
'js-lint coverage: жоден workspace не має тестів ' +
|
|
338
405
|
'(`*.test.{js,mjs}` у `tests/` або поряд із джерелом) — ' +
|
|
@@ -20,16 +20,15 @@
|
|
|
20
20
|
* test-фреймворків (`bun:test`, `node:test`, `vitest`, `@jest/globals`, `mocha`, `jest`, `ava`, …).
|
|
21
21
|
* Виняток: `*_test.rego` дозволені поруч з полісі — це конвенція conftest.
|
|
22
22
|
*
|
|
23
|
-
* Версія та CHANGELOG
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
23
|
+
* Версія та CHANGELOG тут НЕ перевіряються: єдиний артефакт зміни — change-файл, а узгодженість
|
|
24
|
+
* `version`/`CHANGELOG.md` (включно з drift від ручного bump) валідує `changelog/js/consistency.mjs`
|
|
25
|
+
* за моделлю `n-changelog.mdc`. Інваріант «верхня секція CHANGELOG == package.json.version» істинний
|
|
26
|
+
* лише post-release і його гарантує `n-cursor release` у CI — локально його не підтримують руками.
|
|
26
27
|
* @param {string} cwd корінь репозиторію
|
|
27
28
|
*/
|
|
28
|
-
import { execFile } from 'node:child_process'
|
|
29
29
|
import { existsSync } from 'node:fs'
|
|
30
30
|
import { readFile, stat } from 'node:fs/promises'
|
|
31
31
|
import { join, sep } from 'node:path'
|
|
32
|
-
import { promisify } from 'node:util'
|
|
33
32
|
|
|
34
33
|
import { parseSync } from 'oxc-parser'
|
|
35
34
|
|
|
@@ -43,14 +42,6 @@ import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
|
43
42
|
import { loadCursorIgnorePaths } from '../../../scripts/lib/load-cursor-config.mjs'
|
|
44
43
|
import { walkDir } from '../../../scripts/utils/walkDir.mjs'
|
|
45
44
|
|
|
46
|
-
const execFileAsync = promisify(execFile)
|
|
47
|
-
|
|
48
|
-
/** Перший заголовок релізу у Keep a Changelog (`## [1.2.3]`). */
|
|
49
|
-
const CHANGELOG_FIRST_VERSION_RE = /^## \[([^\]]+)\]/m
|
|
50
|
-
|
|
51
|
-
/** Поле `version` у текстовому зрізі `package.json` (для `git show HEAD:npm/package.json`). */
|
|
52
|
-
const PACKAGE_JSON_VERSION_RE = /"version":\s*"([^"]+)"/u
|
|
53
|
-
|
|
54
45
|
/** Файл проєкту TypeScript для emit без каталогу `src` (див. npm-module.mdc) */
|
|
55
46
|
const EMIT_TYPES_CONFIG = 'npm/tsconfig.emit-types.json'
|
|
56
47
|
|
|
@@ -91,9 +82,9 @@ const GLOBSTAR_TRAILING_RE = /\/__GLOBSTAR__$/u
|
|
|
91
82
|
|
|
92
83
|
/**
|
|
93
84
|
* Чи є під `npm/src` хоча б один `.js` (рекурсивно).
|
|
85
|
+
* @param {string} cwd корінь репозиторію
|
|
94
86
|
* @param {string[]} [ignorePaths] абсолютні шляхи каталогів, повністю виключених з обходу
|
|
95
87
|
* @returns {Promise<boolean>} `true`, якщо знайдено хоча б один `.js`
|
|
96
|
-
* @param {string} cwd корінь репозиторію
|
|
97
88
|
*/
|
|
98
89
|
async function npmSrcTreeHasJsFile(cwd, ignorePaths = []) {
|
|
99
90
|
const root = join(cwd, 'npm/src')
|
|
@@ -215,136 +206,6 @@ function checkEmitTypesConfig(passFn, failFn, cwd) {
|
|
|
215
206
|
passFn(`${EMIT_TYPES_CONFIG} є (структуру перевіряє npx @nitra/cursor fix → npm_module.emit_types_config)`)
|
|
216
207
|
}
|
|
217
208
|
|
|
218
|
-
/**
|
|
219
|
-
* Перевіряє npm-publish.yml workflow.
|
|
220
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
221
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
222
|
-
* @param {string} cwd корінь репозиторію
|
|
223
|
-
*/
|
|
224
|
-
/**
|
|
225
|
-
* Чи виконано `git` у корені робочого дерева.
|
|
226
|
-
* @returns {Promise<boolean>} true, якщо процес запущено в межах git work tree
|
|
227
|
-
* @param {string} cwd корінь репозиторію
|
|
228
|
-
*/
|
|
229
|
-
async function gitInsideWorkTree(cwd) {
|
|
230
|
-
try {
|
|
231
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { encoding: 'utf8', cwd })
|
|
232
|
-
return stdout.trim() === 'true'
|
|
233
|
-
} catch {
|
|
234
|
-
return false
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Список незакомічених шляхів під `npm/` відносно `HEAD`.
|
|
240
|
-
* @param {string} cwd корінь репозиторію
|
|
241
|
-
* @returns {Promise<string[] | null>} шляхи або `null`, якщо `git` недоступний
|
|
242
|
-
*/
|
|
243
|
-
async function gitDiffNameOnlyNpm(cwd) {
|
|
244
|
-
try {
|
|
245
|
-
const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD', '--', 'npm'], {
|
|
246
|
-
encoding: 'utf8',
|
|
247
|
-
cwd
|
|
248
|
-
})
|
|
249
|
-
return stdout.trim().split('\n').filter(Boolean)
|
|
250
|
-
} catch {
|
|
251
|
-
return null
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Поле `version` з `npm/package.json` на заданому git-ref (`HEAD:npm/package.json`).
|
|
257
|
-
* @param {string} refPath на кшталт `HEAD:npm/package.json`
|
|
258
|
-
* @param {string} cwd корінь репозиторію
|
|
259
|
-
* @returns {Promise<string | null>} значення поля `version` або `null`, якщо ref недоступний
|
|
260
|
-
*/
|
|
261
|
-
async function gitShowNpmPackageVersionAt(refPath, cwd) {
|
|
262
|
-
try {
|
|
263
|
-
const { stdout } = await execFileAsync('git', ['show', refPath], { encoding: 'utf8', cwd })
|
|
264
|
-
const m = stdout.match(PACKAGE_JSON_VERSION_RE)
|
|
265
|
-
return m ? m[1] : null
|
|
266
|
-
} catch {
|
|
267
|
-
return null
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Версія з першого заголовка `## […]` у тексті CHANGELOG.
|
|
273
|
-
* @param {string} changelogText вміст файлу CHANGELOG.md
|
|
274
|
-
* @returns {string | null} версія з першої секції або `null`, якщо заголовка немає
|
|
275
|
-
*/
|
|
276
|
-
function firstChangelogSectionVersion(changelogText) {
|
|
277
|
-
const m = changelogText.match(CHANGELOG_FIRST_VERSION_RE)
|
|
278
|
-
return m ? m[1] : null
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Перший реліз у CHANGELOG має збігатися з `version` у `npm/package.json`.
|
|
283
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
284
|
-
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
285
|
-
* @returns {Promise<void>}
|
|
286
|
-
* @param {string} cwd корінь репозиторію
|
|
287
|
-
*/
|
|
288
|
-
async function checkChangelogTopMatchesPackageVersion(passFn, failFn, cwd) {
|
|
289
|
-
if (!existsSync(join(cwd, 'npm/CHANGELOG.md')) || !existsSync(join(cwd, 'npm/package.json'))) return
|
|
290
|
-
const pkg = JSON.parse(await readFile(join(cwd, 'npm/package.json'), 'utf8'))
|
|
291
|
-
const ver = typeof pkg.version === 'string' ? pkg.version : null
|
|
292
|
-
if (!ver) {
|
|
293
|
-
failFn('npm/package.json: відсутнє поле version')
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
const cl = await readFile(join(cwd, 'npm/CHANGELOG.md'), 'utf8')
|
|
297
|
-
const first = firstChangelogSectionVersion(cl)
|
|
298
|
-
if (!first) {
|
|
299
|
-
failFn('npm/CHANGELOG.md: не знайдено жодного заголовка ## [version]')
|
|
300
|
-
return
|
|
301
|
-
}
|
|
302
|
-
if (first !== ver) {
|
|
303
|
-
failFn(
|
|
304
|
-
`npm/CHANGELOG.md: перша секція [${first}] не збігається з npm/package.json version "${ver}" ` +
|
|
305
|
-
'(зверху має бути найсвіжіший реліз і той самий номер — npm-module.mdc).'
|
|
306
|
-
)
|
|
307
|
-
return
|
|
308
|
-
}
|
|
309
|
-
passFn(`npm/CHANGELOG.md: перша секція [${first}] збігається з npm/package.json`)
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Незакомічені зміни під `npm/` вимагають підвищення `version` відносно `HEAD`.
|
|
314
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
315
|
-
* @param {(msg: string) => void} failFn callback при виявленому порушенні
|
|
316
|
-
* @returns {Promise<void>}
|
|
317
|
-
* @param {string} cwd корінь репозиторію
|
|
318
|
-
*/
|
|
319
|
-
async function checkDirtyNpmRequiresVersionBump(passFn, failFn, cwd) {
|
|
320
|
-
if (!(await gitInsideWorkTree(cwd))) {
|
|
321
|
-
passFn('npm-module: git недоступний або поза work tree — перевірку незакоміченого bump пропущено')
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
const changed = await gitDiffNameOnlyNpm(cwd)
|
|
325
|
-
if (changed === null) {
|
|
326
|
-
passFn('npm-module: git diff під npm/ недоступний — пропущено')
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
if (changed.length === 0) return
|
|
330
|
-
|
|
331
|
-
const headVer = await gitShowNpmPackageVersionAt('HEAD:npm/package.json', cwd)
|
|
332
|
-
if (headVer === null) return
|
|
333
|
-
|
|
334
|
-
const pkg = JSON.parse(await readFile(join(cwd, 'npm/package.json'), 'utf8'))
|
|
335
|
-
const cur = typeof pkg.version === 'string' ? pkg.version : null
|
|
336
|
-
if (!cur) return
|
|
337
|
-
|
|
338
|
-
if (cur === headVer) {
|
|
339
|
-
failFn(
|
|
340
|
-
`Незакомічені зміни під npm/ (${changed.join(', ')}), але "version" у npm/package.json лишився ${cur} ` +
|
|
341
|
-
'(як у HEAD). Підвищ version (+1) і додай секцію ## [нова версія] зверху CHANGELOG (npm-module.mdc).'
|
|
342
|
-
)
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
passFn(`npm/: незакомічені зміни під npm/ узгоджені з підвищенням version (${headVer} → ${cur})`)
|
|
346
|
-
}
|
|
347
|
-
|
|
348
209
|
/**
|
|
349
210
|
* FS-existence для `npm-publish.yml` workflow. Поля workflow (`on.push.paths`,
|
|
350
211
|
* `branches`, `id-token: write`, JS-DevTools/npm-publish step) валідує
|
|
@@ -614,8 +475,5 @@ export async function check(cwd = process.cwd()) {
|
|
|
614
475
|
|
|
615
476
|
await checkPublishWorkflow(pass, fail, cwd)
|
|
616
477
|
|
|
617
|
-
await checkChangelogTopMatchesPackageVersion(pass, fail, cwd)
|
|
618
|
-
await checkDirtyNpmRequiresVersionBump(pass, fail, cwd)
|
|
619
|
-
|
|
620
478
|
return reporter.getExitCode()
|
|
621
479
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
description: Оформлення репозиторію для npm модуля
|
|
3
3
|
globs: "npm/**,**/package.json,**/hk.pkl,.github/workflows/npm-publish.yml,**/tsconfig*.json"
|
|
4
4
|
alwaysApply: false
|
|
5
|
-
version: '1.
|
|
5
|
+
version: '1.14'
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
Bun monorepo: workspace **`npm/`**, кореневий **`package.json`**, **`.github/workflows/`**; опційно **`demo/`**.
|
|
@@ -55,21 +55,11 @@ bunx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly
|
|
|
55
55
|
|
|
56
56
|
Після додавання **`hk.pkl`**: **`hk install`**.
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## Версія та CHANGELOG
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Версію (`version` у **`npm/package.json`**) і **`npm/CHANGELOG.md`** **не редагуй вручну** — навіть для hotfix. Єдиний артефакт зміни — **change-файл** (`npx @nitra/cursor change --bump <major|minor|patch> --section <Added|Changed|Fixed|Removed> --message "<…>"`); bump `version` і генерацію секції CHANGELOG робить `n-cursor release` у CI на `main`. Будь-який ручний bump `version` поза CI завалює `check changelog` — навіть із change-файлом.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
**Чеклист у тому ж наборі змін, що й правки під `npm/`:** `version` у **`npm/package.json`** → **+1**; зверху **`npm/CHANGELOG.md`** нова секція **`## [нова версія] - …`**; у секції лише те, що входить у цей реліз.
|
|
65
|
-
|
|
66
|
-
**Антипатерн:** не дописувати нові bullet-и в уже існуючу секцію **`## [X.Y.Z]`**, якщо паралельно не піднімаєш **`version`** до нового номера й не створюєш **нову** секцію зверху. Інакше змішуються різні релізи в одному номері, а `check npm-module` / `check changelog` гірше ловлять порушення.
|
|
67
|
-
|
|
68
|
-
**Підказка:** щоб не дублювати bump і бачити різницю зі збереженим деревом, перевір `git status npm/package.json` або `git diff HEAD -- npm/package.json` перед другим підвищенням у тій самій гілці / наборі змін.
|
|
69
|
-
|
|
70
|
-
## CHANGELOG
|
|
71
|
-
|
|
72
|
-
Найновіша версія — **перша** секція **`## [version]`** у файлі (зверху після заголовка). Вона **має збігатися** з полем **`version`** у **`npm/package.json`**.
|
|
62
|
+
Повна модель (база порівняння, інверсія шляхів, формат CHANGELOG, post-release-інваріант «верхня секція CHANGELOG == `version`») — у **`n-changelog.mdc`** (джерело істини). Це правило їй підпорядковане й власних інструкцій bump/CHANGELOG не дублює.
|
|
73
63
|
|
|
74
64
|
## npm publish
|
|
75
65
|
|
|
@@ -16,6 +16,8 @@ import { hasCargoTomlInTree } from '../lib/has-cargo-toml.mjs'
|
|
|
16
16
|
import { resolveCargoManifest } from '../../../scripts/utils/resolve-cargo-manifest.mjs'
|
|
17
17
|
|
|
18
18
|
const IGNORED_DIR_NAMES = new Set(['node_modules', '.git', '.next', '.turbo', 'target'])
|
|
19
|
+
/** Rust-релевантні зміни: `.rs`-джерела або маніфести Cargo. */
|
|
20
|
+
const RUST_CHANGE = /(\.rs$)|((^|\/)Cargo\.(toml|lock)$)/
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Чи провайдер застосовний у поточному cwd.
|
|
@@ -33,7 +35,7 @@ export function detect(cwd) {
|
|
|
33
35
|
* на ≤2 ядрах = 1, на 4 = 2, на 8+ = 4. Стеля 4 — Rust linker bottleneck:
|
|
34
36
|
* вище практичного приросту не дає навіть на 16+ ядрах.
|
|
35
37
|
* @param {string | undefined} envValue значення `process.env.CARGO_MUTANTS_JOBS`
|
|
36
|
-
* @returns {number}
|
|
38
|
+
* @returns {number} кількість паралельних воркерів (>= 1)
|
|
37
39
|
*/
|
|
38
40
|
export function resolveJobs(envValue) {
|
|
39
41
|
if (envValue !== undefined && envValue !== '') {
|
|
@@ -119,12 +121,21 @@ const defaultRunner = {
|
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
123
|
* Збирає Rust-метрики покриття + мутаційного тестування.
|
|
124
|
+
*
|
|
125
|
+
* Changed-режим (`opts.changedFiles` задано): якщо серед змінених немає Rust-релевантних
|
|
126
|
+
* файлів (`.rs` / `Cargo.toml` / `Cargo.lock`) — повертає `[]` (skip), щоб JS-only крок
|
|
127
|
+
* турнікета не ганяв повний `cargo mutants`. Якщо Rust змінено — наразі прогін повний по
|
|
128
|
+
* crate (per-file scoping cargo-mutants — окремий крок).
|
|
122
129
|
* @param {string} cwd корінь проєкту
|
|
123
|
-
* @param {{runner?: typeof defaultRunner}} [opts] ін'єкція runner-а
|
|
130
|
+
* @param {{runner?: typeof defaultRunner, changedFiles?: string[]}} [opts] ін'єкція runner-а + changed-scope
|
|
124
131
|
* @returns {Promise<Array<{area:string, coverage:object, mutation:{caught:number,total:number}}>>} рядки для COVERAGE.md
|
|
125
132
|
*/
|
|
126
133
|
export async function collect(cwd, opts = {}) {
|
|
127
134
|
const runner = opts.runner ?? defaultRunner
|
|
135
|
+
// Changed-режим без Rust-релевантних змін → не запускаємо повний crate-прогін.
|
|
136
|
+
if (Array.isArray(opts.changedFiles) && !opts.changedFiles.some(f => RUST_CHANGE.test(f))) {
|
|
137
|
+
return []
|
|
138
|
+
}
|
|
128
139
|
const manifestPath = await resolveCargoManifest(cwd)
|
|
129
140
|
if (manifestPath === null) {
|
|
130
141
|
throw new Error('rust coverage: Cargo.toml не знайдено (cwd + workspaces)')
|