@nitra/cursor 1.32.0 → 1.35.1
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 +41 -0
- package/bin/n-cursor.js +13 -1
- package/github-actions/release/action.yml +9 -0
- package/package.json +4 -3
- package/rules/changelog/changelog.mdc +11 -12
- package/rules/changelog/js/consistency.mjs +70 -98
- package/rules/nginx-default-tpl/js/template.mjs +1 -0
- package/rules/release/change.mjs +57 -0
- package/rules/release/fix.mjs +17 -0
- package/rules/release/lib/aggregate.mjs +82 -0
- package/rules/release/lib/change-file.mjs +99 -0
- package/rules/release/lib/fallback.mjs +48 -0
- package/rules/release/release.mjs +135 -0
- package/rules/test/coverage/coverage.mjs +66 -5
- package/rules/test/js/no-relative-fs-path.mjs +3 -3
- package/scripts/coverage-classify/apply.mjs +67 -0
- package/scripts/coverage-classify/cache.mjs +77 -0
- package/scripts/coverage-classify/index.mjs +129 -0
- package/scripts/coverage-classify/prompt.mjs +126 -0
- package/scripts/coverage-classify/verdict-schema.mjs +35 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.35.1] - 2026-05-30
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Нові тести для підвищення coverage: run-dotenv-linter (spawnSync error paths), lint.mjs preflight-OK paths, ua_http_route.mjs (default cwd + readFile chmod-fail), auto-rules.mjs (readdir/JSON catch-блоки), run-shellcheck-errors, run-v8r, sync-claude-config, test-helpers absolute-path guard, rename-yaml-extensions sort, graphql langFromPath, capacitor segmentMinMajor, coverage-classify cache/retry/fallback, npm-module package_structure, changelog consistency, bun-sql-scan BinaryExpression. Lines coverage: ~83.59% → 88.32%.
|
|
8
|
+
|
|
9
|
+
## [1.35.0] - 2026-05-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- LLM-класифікатор survived мутантів у n-cursor coverage: для кожного survived Claude Sonnet 4.6 виносить verdict (worth-testing/equivalent/defensive/glue/wrapper) з reasoning + confidence. Allowed gaps виключаються з знаменника mutation score. Cache по git-blob-hash. Graceful skip без API key. Threshold у .n-cursor.json#coverage.classifyConfidenceThreshold (default 1.1 — rollout mode).
|
|
14
|
+
|
|
15
|
+
## [1.34.1] - 2026-05-30
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Нові тест-файли (10 файлів, 108+ тестів): rules/abie/js/tests/ (firebase_hosting, env_dns, hc_pairing, ua_node_selector, ua_http_route), rules/abie/lib/tests/hc-yaml.test.mjs, rules/abie/lib/tests/k8s-tree.test.mjs, rules/docker/lib/tests/docker-hadolint.test.mjs, rules/docker/lint/tests/lint.test.mjs, scripts/tests/coverage-fix.test.mjs; розширено scripts/tests/upgrade-nitra-cursor-and-install.test.mjs. Lines coverage: 77.62% → 78.80%, Functions: 84.47% → 86.13%.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- coverage: не додавати підсумковий рядок "Разом" коли провайдер один (дублював би єдиний рядок)
|
|
24
|
+
|
|
3
25
|
Усі помітні зміни цього модуля документуються тут.
|
|
4
26
|
|
|
5
27
|
Формат — [Keep a Changelog](https://keepachangelog.com/uk/1.1.0/), нумерація — [SemVer](https://semver.org/lang/uk/).
|
|
6
28
|
|
|
29
|
+
## [1.34.0] - 2026-05-30
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Тести для `bun-sql-scan.mjs`: `findPgFormatShimDefinitionInText`, `findPgFormatLikeQueryWrapperInText`, `findUnsafeBunSqlInListMissingEmptyGuardInText`, `findPgListenNotifyUsageInText`
|
|
34
|
+
- Тести для `capacitor/platforms.mjs`: semver edge-cases, `recordCapacitorFromOnePackageJson`, `collectCapacitorDataFromAllPackageJson`, nitra-exceptions via config files
|
|
35
|
+
- Тести для `ga/workflows.mjs`: no .github/workflows dir, .yaml extension, MegaLinter detection, apply-k8s paths trigger
|
|
36
|
+
- Тести для `nginx-default-tpl/template.mjs` і `vue/packages.mjs` через check-rule-fixtures.test.mjs
|
|
37
|
+
|
|
38
|
+
## [1.33.0] - 2026-05-30
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `n-cursor change` / `n-cursor release` — change-файли `<ws>/.changes/*.md` замість ручного bump/CHANGELOG; реліз агрегує їх у CI, ставить git-тег `<name>@<version>`. Підтримка npm і Python workspace.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- `n-changelog.mdc` v3.0: feature-флоу кладе change-файл; `fix changelog` приймає change-файл або ручний bump.
|
|
47
|
+
|
|
7
48
|
## [1.32.0] - 2026-05-30
|
|
8
49
|
|
|
9
50
|
### Added
|
package/bin/n-cursor.js
CHANGED
|
@@ -1501,6 +1501,18 @@ try {
|
|
|
1501
1501
|
|
|
1502
1502
|
break
|
|
1503
1503
|
}
|
|
1504
|
+
case 'change': {
|
|
1505
|
+
const { runChangeCli } = await import('../rules/release/change.mjs')
|
|
1506
|
+
process.exitCode = await runChangeCli(args)
|
|
1507
|
+
|
|
1508
|
+
break
|
|
1509
|
+
}
|
|
1510
|
+
case 'release': {
|
|
1511
|
+
const { runReleaseCli } = await import('../rules/release/release.mjs')
|
|
1512
|
+
process.exitCode = await runReleaseCli(args)
|
|
1513
|
+
|
|
1514
|
+
break
|
|
1515
|
+
}
|
|
1504
1516
|
case 'skill': {
|
|
1505
1517
|
process.exitCode = runSkillsCli(args)
|
|
1506
1518
|
|
|
@@ -1515,7 +1527,7 @@ try {
|
|
|
1515
1527
|
default: {
|
|
1516
1528
|
console.error(`❌ Невідома команда: ${command}`)
|
|
1517
1529
|
console.error(
|
|
1518
|
-
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, skill`
|
|
1530
|
+
` Очікується: (без аргументів) синхронізація правил, check, rename-yaml-extensions, post-tool-use-fix, lint, lint-ga, lint-rego, lint-k8s, lint-docker, lint-text, coverage, change, release, skill`
|
|
1519
1531
|
)
|
|
1520
1532
|
process.exitCode = 1
|
|
1521
1533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/cursor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.35.1",
|
|
4
4
|
"description": "CLI для завантаження cursor-правил (префікс n-) у локальний репозиторій",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -52,11 +52,12 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@anthropic-ai/claude-agent-sdk": "^0.3.0",
|
|
55
|
-
"@anthropic-ai/sdk": "^0.
|
|
55
|
+
"@anthropic-ai/sdk": "^0.100.1",
|
|
56
56
|
"oxc-parser": "^0.128.0",
|
|
57
57
|
"picomatch": "^4.0.4",
|
|
58
58
|
"smol-toml": "^1.6.1",
|
|
59
|
-
"yaml": "^2.8.3"
|
|
59
|
+
"yaml": "^2.8.3",
|
|
60
|
+
"zod": "^4.4.3"
|
|
60
61
|
},
|
|
61
62
|
"engines": {
|
|
62
63
|
"bun": ">=1.3",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: CHANGELOG.md в кожному workspace, з двома моделями бази порівняння (npm і Python)
|
|
3
|
-
version: '
|
|
3
|
+
version: '3.1'
|
|
4
4
|
alwaysApply: true
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -8,13 +8,13 @@ alwaysApply: true
|
|
|
8
8
|
|
|
9
9
|
> **Якщо в цій сесії ти змінив(ла) файли в пакетному workspace** (код, rego, правила, скіли, скрипти, конфіги, тести — **не** лише `docs/` / `doc/`) — **не завершуй задачу**, поки не виконаєш **усі три** кроки нижче в **тому ж** наборі змін. Це не «опційно після синку» — це частина PR.
|
|
10
10
|
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
3. **`npx @nitra/cursor fix changelog`**
|
|
11
|
+
1. **Поклади change-файл** `<ws>/.changes/<timestamp>-<rand>.md` з frontmatter `bump:` (`major|minor|patch`) + `section:` (`Added|Changed|Fixed|Removed`) і текстом опису. Команда: `npx @nitra/cursor change --bump <…> --section <…> --message "<…>" [--ws <шлях>]`.
|
|
12
|
+
2. **Ніколи** не редагуй `version` і `CHANGELOG.md` вручну — навіть для hotfix. Єдиний артефакт зміни — change-файл; `version`/CHANGELOG формує `n-cursor release` у CI на `main` (агрегує change-файли, ставить git-тег `<name>@<version>`). Будь-яка зміна `version` поза CI (drift від бази чи опублікованої) завалює `check changelog` — навіть якщо поряд є change-файл.
|
|
13
|
+
3. **`npx @nitra/cursor fix changelog`** → exit **`0`** (достатньо наявності change-файлу; `version` лишається незмінним).
|
|
14
14
|
|
|
15
15
|
**Тригер шляхів (приклади):** `npm/**`, `packages/foo/**`, будь-який каталог з власним `package.json` / `pyproject.toml`, куди потрапили правки.
|
|
16
16
|
|
|
17
|
-
**Інверсія (
|
|
17
|
+
**Інверсія (change-файл не потрібен):** лише `docs/` / `doc/`; синхронізований із `@nitra/cursor` інструментарій (`.cursor/`, `.claude/`); лише `.gitignore`. **Корінь монорепо** (воркспейс `.` за наявності підпакетів) не перевіряється взагалі — отже й кореневі `AGENTS.md` / `CLAUDE.md` та bump `@nitra/cursor` у `devDependencies`. Окремого «релізного кроку» у feature-флоу немає — `version`/`CHANGELOG.md` змінює лише CI.
|
|
18
18
|
|
|
19
19
|
**Pre-commit (людина):** `hk` у цьому репо також запускає `check changelog` при змінах під `npm/**` — агент не покладайся лише на commit hook; виконай кроки 1–3 **до** фінальної відповіді.
|
|
20
20
|
|
|
@@ -33,15 +33,14 @@ alwaysApply: true
|
|
|
33
33
|
|
|
34
34
|
Повний алгоритм — у блоці **STOP** вище; тут лише уточнення.
|
|
35
35
|
|
|
36
|
-
**Інверсія (за замовчуванням не вимагають
|
|
36
|
+
**Інверсія (за замовчуванням не вимагають change-файлу):**
|
|
37
37
|
|
|
38
38
|
- зміни **лише** під `docs/` або `doc/`;
|
|
39
39
|
- синхронізований із `@nitra/cursor` інструментарій під `.cursor/` (канонічні правила та скіли) і `.claude/` (ADR-хуки) — це дзеркало tooling-пакета, а не логіка воркспейсу;
|
|
40
40
|
- будь-які зміни в **корені монорепо** (воркспейс `.` за наявності підпакетів) — корінь веде glue/конфіг/tooling, власного CHANGELOG не має; помітні зміни документують підпакети. Сюди потрапляють і кореневі `AGENTS.md` / `CLAUDE.md`, і bump `@nitra/cursor` у `devDependencies`;
|
|
41
|
-
- файли під **`.gitignore
|
|
42
|
-
- правки **лише** `CHANGELOG.md` або поля `version` у маніфесті як сам релізний крок.
|
|
41
|
+
- файли під **`.gitignore`**.
|
|
43
42
|
|
|
44
|
-
**Вимагають
|
|
43
|
+
**Вимагають change-файл** — усі інші зміни в каталозі workspace (код, rego, правила, скіли, конфіги, тести тощо). Виняток `.cursor/` / `.claude/` **не** поширюється на джерело правил у репо `@nitra/cursor` — воно лежить під `npm/`, тож зміни в ньому далі вимагають change-файлу.
|
|
45
44
|
|
|
46
45
|
Перевірка програмна (`changelog/js/consistency.mjs`).
|
|
47
46
|
|
|
@@ -55,8 +54,8 @@ alwaysApply: true
|
|
|
55
54
|
|
|
56
55
|
**Python:** статичні `project.name` і `project.version` у `pyproject.toml` (або Poetry-секція).
|
|
57
56
|
|
|
58
|
-
1. **Локальна `version` ≠ опублікованій** (npm / PyPI):
|
|
59
|
-
2. **Версії збігаються**, але в git є **релевантні** зміни без
|
|
57
|
+
1. **Локальна `version` ≠ опублікованій** (npm / PyPI): drift поза CI → **fail** (ручний bump заборонено; навіть із change-файлом). Відкоти `version`.
|
|
58
|
+
2. **Версії збігаються**, але в git є **релевантні** зміни без change-файлу → fail. Для npm `"CHANGELOG.md"` має бути в `files` (публікується разом із пакетом).
|
|
60
59
|
3. **Реєстр недосяжний** — fail-safe pass.
|
|
61
60
|
4. **Немає релевантних змін** — pass.
|
|
62
61
|
|
|
@@ -67,7 +66,7 @@ alwaysApply: true
|
|
|
67
66
|
1. На **`dev`** local-only не активний (крім незакомічених registry-published).
|
|
68
67
|
2. На **`main`** — diff від **`origin/main`** (попередній опублікований `main`); без remote — від `HEAD~1`. **`dev` не використовується** як база на `main`.
|
|
69
68
|
3. На **feature-гілці** — `merge-base` з **`dev`**, якщо є; інакше з **`main`** (репо без `dev`).
|
|
70
|
-
4.
|
|
69
|
+
4. Drift `version` від бази → **fail** (ручний bump заборонено). Зміни фіксуй change-файлом; bump зробить CI.
|
|
71
70
|
|
|
72
71
|
Якщо немає git або немає `dev`/`main`/`origin/main` — local-only пропускається.
|
|
73
72
|
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Перевіряє, що
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Перевіряє, що кожен workspace із релізно-релевантними змінами зафіксував їх через
|
|
3
|
+
* change-файл `<ws>/.changes/*.md` — єдиний дозволений артефакт зміни. Bump `version`
|
|
4
|
+
* і генерацію `CHANGELOG.md` робить виключно `n-cursor release` у CI на `main`.
|
|
5
|
+
*
|
|
6
|
+
* Інваріант (на будь-якій гілці): `version` не має відхилятися від бази. Будь-який drift
|
|
7
|
+
* `version` (vs опублікована в реєстрі або vs git-база) — ручний bump поза CI — завалює
|
|
8
|
+
* перевірку, навіть якщо присутній change-файл. Pass лише коли є change-файл, а version
|
|
9
|
+
* не зрушено; зміни без change-файлу — fail.
|
|
5
10
|
*
|
|
6
11
|
* Дві моделі бази — на рівні воркспейсу (див. n-changelog.mdc):
|
|
7
12
|
*
|
|
8
13
|
* 1) **registry-published** (npm: `name` + `files`, не `private`; Python: `project.name` +
|
|
9
|
-
* статична `project.version`
|
|
10
|
-
*
|
|
11
|
-
* у `files`. Якщо версії збігаються, але в git є релевантні зміни без bump — fail.
|
|
12
|
-
*
|
|
13
|
-
* 2) **local-only** (приватні npm, без `files`, Python без імені/версії для реєстру):
|
|
14
|
+
* статична `project.version`): база = опублікована версія в npm / PyPI.
|
|
15
|
+
* 2) **local-only** (приватні npm без `files`, Python без імені/версії для реєстру):
|
|
14
16
|
* feature-гілка — `merge-base` з `dev`, інакше з `main`; на `main` — diff від
|
|
15
|
-
* `origin/main` (
|
|
17
|
+
* `origin/main` (або `HEAD~1` без remote).
|
|
16
18
|
*
|
|
17
19
|
* Усі `git` і зовнішні виклики — через `execFile` / `fetch`, без shell-інтерполяції.
|
|
18
20
|
*/
|
|
19
21
|
import { execFile } from 'node:child_process'
|
|
20
|
-
import { existsSync } from 'node:fs'
|
|
21
|
-
import { readFile } from 'node:fs/promises'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
22
|
import { promisify } from 'node:util'
|
|
24
23
|
|
|
25
24
|
import { createCheckReporter } from '../../../scripts/lib/check-reporter.mjs'
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
parsePyprojectFields,
|
|
30
29
|
readPackageManifest
|
|
31
30
|
} from '../lib/package-manifest.mjs'
|
|
31
|
+
import { readChangeFiles } from '../../release/lib/change-file.mjs'
|
|
32
32
|
|
|
33
33
|
const execFileAsync = promisify(execFile)
|
|
34
34
|
|
|
@@ -276,16 +276,6 @@ async function readBaseVersion(baseRef, manifest, cwd) {
|
|
|
276
276
|
return parsePyprojectFields(out).version
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
/**
|
|
280
|
-
* @param {string} text параметр
|
|
281
|
-
* @param {string} version параметр
|
|
282
|
-
* @returns {boolean} результат
|
|
283
|
-
*/
|
|
284
|
-
function changelogHasVersionEntry(text, version) {
|
|
285
|
-
const needle = `## [${version}]`
|
|
286
|
-
return text.startsWith(needle) || text.includes(`\n${needle}`)
|
|
287
|
-
}
|
|
288
|
-
|
|
289
279
|
/**
|
|
290
280
|
* @param {string} name параметр
|
|
291
281
|
* @returns {Promise<string | null>} результат
|
|
@@ -362,31 +352,6 @@ function checkNpmFilesArrayContainsChangelog(manifest, pass, fail) {
|
|
|
362
352
|
}
|
|
363
353
|
}
|
|
364
354
|
|
|
365
|
-
/**
|
|
366
|
-
* @param {string} ws параметр
|
|
367
|
-
* @param {string} version параметр
|
|
368
|
-
* @param {(msg: string) => void} pass параметр
|
|
369
|
-
* @param {(msg: string) => void} fail параметр
|
|
370
|
-
* @param {string} cwd робочий каталог
|
|
371
|
-
* @returns {Promise<boolean>} результат
|
|
372
|
-
*/
|
|
373
|
-
async function verifyChangelogEntry(ws, version, pass, fail, cwd) {
|
|
374
|
-
const label = ws === '.' ? '<root>' : ws
|
|
375
|
-
const changelogRel = join(ws, 'CHANGELOG.md')
|
|
376
|
-
const changelogAbs = join(cwd, changelogRel)
|
|
377
|
-
if (!existsSync(changelogAbs)) {
|
|
378
|
-
fail(`${label}: відсутній ${changelogRel} (Keep a Changelog, див. n-changelog.mdc)`)
|
|
379
|
-
return false
|
|
380
|
-
}
|
|
381
|
-
const text = await readFile(changelogAbs, 'utf8')
|
|
382
|
-
if (changelogHasVersionEntry(text, version)) {
|
|
383
|
-
pass(`${changelogRel}: знайдено запис для версії ${version}`)
|
|
384
|
-
return true
|
|
385
|
-
}
|
|
386
|
-
fail(`${changelogRel}: відсутній запис для ${version} (формат "## [${version}] - YYYY-MM-DD")`)
|
|
387
|
-
return false
|
|
388
|
-
}
|
|
389
|
-
|
|
390
355
|
/**
|
|
391
356
|
* @param {import('../lib/package-manifest.mjs').PackageManifest} manifest параметр
|
|
392
357
|
* @returns {string} результат
|
|
@@ -395,18 +360,49 @@ function workspaceLabel(manifest) {
|
|
|
395
360
|
return manifest.ws === '.' ? '<root>' : manifest.ws
|
|
396
361
|
}
|
|
397
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Повідомлення «поклади change-файл» для workspace з релевантними змінами без change-файлу.
|
|
365
|
+
* @param {string} label мітка воркспейсу
|
|
366
|
+
* @param {string} mf шлях до маніфесту
|
|
367
|
+
* @returns {string} текст fail
|
|
368
|
+
*/
|
|
369
|
+
function missingChangeFileMessage(label, mf) {
|
|
370
|
+
return (
|
|
371
|
+
`${label}: є релевантні зміни, але немає change-файлу (version у ${mf} не чіпай вручну). ` +
|
|
372
|
+
`Поклади change-файл: npx @nitra/cursor change --bump <major|minor|patch> --section <Added|Changed|Fixed|Removed> --message "<…>"; ` +
|
|
373
|
+
`bump зробить CI на main (n-changelog.mdc)`
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Чи має workspace незрелізні change-файли (намір зафіксовано — bump зробить CI).
|
|
379
|
+
* @param {string} ws workspace
|
|
380
|
+
* @param {string} cwd корінь
|
|
381
|
+
* @returns {Promise<boolean>} результат
|
|
382
|
+
*/
|
|
383
|
+
async function hasPendingChangeFiles(ws, cwd) {
|
|
384
|
+
const files = await readChangeFiles(ws, cwd)
|
|
385
|
+
return files.length > 0
|
|
386
|
+
}
|
|
387
|
+
|
|
398
388
|
/**
|
|
399
389
|
* @param {import('../lib/package-manifest.mjs').PackageManifest} manifest параметр
|
|
400
|
-
* @param {string}
|
|
390
|
+
* @param {string} _Vcurrent параметр (для сумісності сигнатури; bump робить CI)
|
|
401
391
|
* @param {string[]} subWorkspaces параметр
|
|
402
392
|
* @param {(msg: string) => void} pass параметр
|
|
403
393
|
* @param {(msg: string) => void} fail параметр
|
|
404
394
|
* @param {string} cwd робочий каталог
|
|
405
395
|
* @returns {Promise<void>} результат
|
|
406
396
|
*/
|
|
407
|
-
async function checkPublishedWorkspacePendingGitChanges(manifest,
|
|
397
|
+
async function checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, subWorkspaces, pass, fail, cwd) {
|
|
408
398
|
const label = workspaceLabel(manifest)
|
|
409
399
|
const mf = manifestFilePath(manifest.ws, manifest)
|
|
400
|
+
if (await hasPendingChangeFiles(manifest.ws, cwd)) {
|
|
401
|
+
pass(`${label}: є change-файл(и) у .changes/ — bump зробить CI (n-changelog.mdc)`)
|
|
402
|
+
// Реліз наближається → CHANGELOG має публікуватися разом із пакетом.
|
|
403
|
+
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
404
|
+
return
|
|
405
|
+
}
|
|
410
406
|
if (!(await isInsideGitRepo(cwd))) {
|
|
411
407
|
return
|
|
412
408
|
}
|
|
@@ -415,42 +411,19 @@ async function checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subW
|
|
|
415
411
|
|
|
416
412
|
if (branch === LOCAL_ONLY_SKIP_BRANCH) {
|
|
417
413
|
if (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd)) {
|
|
418
|
-
fail(
|
|
419
|
-
`${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
|
|
420
|
-
`Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
|
|
421
|
-
)
|
|
414
|
+
fail(missingChangeFileMessage(label, mf))
|
|
422
415
|
}
|
|
423
416
|
return
|
|
424
417
|
}
|
|
425
418
|
|
|
426
419
|
const comparison = await resolveChangelogComparisonPoint(branch, cwd)
|
|
427
|
-
if (
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
) {
|
|
431
|
-
const Vbase = await readBaseVersion(comparison.ref, manifest, cwd)
|
|
432
|
-
const baseLabel = comparison.label
|
|
433
|
-
if (Vbase === null) {
|
|
434
|
-
pass(
|
|
435
|
-
`${label}: новий registry-published воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`
|
|
436
|
-
)
|
|
437
|
-
await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd)
|
|
438
|
-
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
439
|
-
} else if (Vbase === Vcurrent) {
|
|
440
|
-
fail(
|
|
441
|
-
`${label}: у цій гілці є зміни в registry-published пакеті, але version у ${mf} ` +
|
|
442
|
-
`не підвищено (на ${baseLabel} — ${Vbase}). Bump + запис у CHANGELOG.md обов'язкові (n-changelog.mdc)`
|
|
443
|
-
)
|
|
444
|
-
} else {
|
|
445
|
-
pass(`${label}: version змінено (${Vbase} → ${Vcurrent}) — очікується запис CHANGELOG після bump`)
|
|
446
|
-
}
|
|
420
|
+
if (comparison && (await workspaceHasRelevantChangesAgainstBase(comparison.ref, manifest.ws, subWorkspaces, cwd))) {
|
|
421
|
+
fail(missingChangeFileMessage(label, mf))
|
|
422
|
+
return
|
|
447
423
|
}
|
|
448
424
|
|
|
449
425
|
if (branch === 'main' && (await workspaceHasRelevantChangesAgainstBase('HEAD', manifest.ws, subWorkspaces, cwd))) {
|
|
450
|
-
fail(
|
|
451
|
-
`${label}: у registry-published пакеті є незакомічені зміни при version ${Vcurrent}, що вже в реєстрі. ` +
|
|
452
|
-
`Підвищ version у ${mf} і додай запис у CHANGELOG.md (n-changelog.mdc)`
|
|
453
|
-
)
|
|
426
|
+
fail(missingChangeFileMessage(label, mf))
|
|
454
427
|
}
|
|
455
428
|
}
|
|
456
429
|
|
|
@@ -481,14 +454,18 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
|
|
|
481
454
|
pass(`${label}: ${name} — опублікована версія недоступна (мережа/реєстр), перевірку пропущено`)
|
|
482
455
|
return
|
|
483
456
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
457
|
+
// Drift від опублікованої версії має пріоритет над change-файлом: ручний bump
|
|
458
|
+
// заборонено навіть із change-файлом (симетрично з local-only-шляхом).
|
|
459
|
+
if (Vpublished !== Vcurrent) {
|
|
460
|
+
fail(
|
|
461
|
+
`${label}: version у ${mf} (${Vcurrent}) розходиться з опублікованою (${Vpublished}) — ` +
|
|
462
|
+
`ручний bump заборонено. Відкоти version і поклади change-файл ` +
|
|
463
|
+
`(npx @nitra/cursor change …); bump зробить CI на main (n-changelog.mdc)`
|
|
464
|
+
)
|
|
487
465
|
return
|
|
488
466
|
}
|
|
489
|
-
pass(`${label}: ${name} —
|
|
490
|
-
await
|
|
491
|
-
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
467
|
+
pass(`${label}: ${name}@${Vcurrent} збігається з реєстром — перевіряємо git на незрелізні зміни`)
|
|
468
|
+
await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail, cwd)
|
|
492
469
|
}
|
|
493
470
|
|
|
494
471
|
/**
|
|
@@ -503,26 +480,21 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
|
|
|
503
480
|
const label = workspaceLabel(manifest)
|
|
504
481
|
const mf = manifestFilePath(manifest.ws, manifest)
|
|
505
482
|
const Vcurrent = manifest.version
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return
|
|
509
|
-
}
|
|
483
|
+
// Drift version від бази має пріоритет над change-файлом: ручний bump заборонено
|
|
484
|
+
// навіть якщо change-файл присутній (симетрично з published-шляхом).
|
|
510
485
|
const Vbase = await readBaseVersion(comparisonRef, manifest, cwd)
|
|
511
|
-
if (Vbase
|
|
512
|
-
pass(`${label}: новий воркспейс (на ${baseLabel} відсутній ${mf}) — перевіряємо CHANGELOG для ${Vcurrent}`)
|
|
513
|
-
if (!(await verifyChangelogEntry(manifest.ws, Vcurrent, pass, fail, cwd))) return
|
|
514
|
-
checkNpmFilesArrayContainsChangelog(manifest, pass, fail)
|
|
515
|
-
return
|
|
516
|
-
}
|
|
517
|
-
if (Vbase === Vcurrent) {
|
|
486
|
+
if (Vbase !== null && Vcurrent !== null && Vbase !== Vcurrent) {
|
|
518
487
|
fail(
|
|
519
|
-
`${label}: у
|
|
488
|
+
`${label}: version у ${mf} змінено поза CI (${Vbase} → ${Vcurrent}) — ручний bump заборонено (на ${baseLabel} — ${Vbase}). ` +
|
|
489
|
+
`Відкоти version і поклади change-файл (npx @nitra/cursor change …); bump зробить CI (n-changelog.mdc)`
|
|
520
490
|
)
|
|
521
491
|
return
|
|
522
492
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
493
|
+
if (await hasPendingChangeFiles(manifest.ws, cwd)) {
|
|
494
|
+
pass(`${label}: є change-файл(и) у .changes/ — bump зробить CI (n-changelog.mdc)`)
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
fail(missingChangeFileMessage(label, mf))
|
|
526
498
|
}
|
|
527
499
|
|
|
528
500
|
/**
|
|
@@ -359,6 +359,7 @@ async function checkDockerfiles(root, ignorePaths, passFn, failFn) {
|
|
|
359
359
|
* `default.conf.template` (умовне правило — без шаблона цей крок не запускається).
|
|
360
360
|
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
361
361
|
* @param {(msg: string) => void} failFn callback при помилці
|
|
362
|
+
* @param {string} cwd корінь репозиторію
|
|
362
363
|
* @returns {void}
|
|
363
364
|
*/
|
|
364
365
|
function checkVscodeNginx(passFn, failFn, cwd) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `n-cursor change` — пише один change-файл `<ws>/.changes/<timestamp>-<rand>.md`.
|
|
3
|
+
* Замінює ручне редагування CHANGELOG у feature-флоу (n-changelog.mdc v3.0).
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { CHANGES_DIR, newChangeFileName, parseChangeFile, serializeChangeFile } from './lib/change-file.mjs'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} params параметри
|
|
12
|
+
* @param {string} params.bump `major|minor|patch`
|
|
13
|
+
* @param {string} params.section `Added|Changed|Fixed|Removed`
|
|
14
|
+
* @param {string} params.message опис
|
|
15
|
+
* @param {string} [params.ws] workspace (за замовчуванням `.`)
|
|
16
|
+
* @param {string} [params.cwd] корінь
|
|
17
|
+
* @returns {Promise<string>} відносний шлях створеного файлу (від ws)
|
|
18
|
+
*/
|
|
19
|
+
export async function writeChange({ bump, section, message, ws = '.', cwd = process.cwd() }) {
|
|
20
|
+
const description = (message ?? '').trim()
|
|
21
|
+
const content = serializeChangeFile({ bump, section, description })
|
|
22
|
+
// Валідація полів: parseChangeFile кидає зрозумілу помилку на невалідних bump/section/порожньому описі.
|
|
23
|
+
parseChangeFile(content)
|
|
24
|
+
|
|
25
|
+
const dir = join(cwd, ws, CHANGES_DIR)
|
|
26
|
+
await mkdir(dir, { recursive: true })
|
|
27
|
+
const name = newChangeFileName()
|
|
28
|
+
await writeFile(join(dir, name), content)
|
|
29
|
+
return join(CHANGES_DIR, name)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string[]} args аргументи CLI (`--bump`, `--section`, `--message`, `--ws`)
|
|
34
|
+
* @returns {Promise<number>} exit-код
|
|
35
|
+
*/
|
|
36
|
+
export async function runChangeCli(args) {
|
|
37
|
+
const get = flag => {
|
|
38
|
+
const i = args.indexOf(flag)
|
|
39
|
+
return i !== -1 && i + 1 < args.length ? args[i + 1] : undefined
|
|
40
|
+
}
|
|
41
|
+
const bump = get('--bump')
|
|
42
|
+
const section = get('--section')
|
|
43
|
+
const message = get('--message')
|
|
44
|
+
const ws = get('--ws') ?? '.'
|
|
45
|
+
if (!bump || !section || !message) {
|
|
46
|
+
console.error('❌ Використання: n-cursor change --bump <major|minor|patch> --section <Added|Changed|Fixed|Removed> --message "<опис>" [--ws <шлях>]')
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const rel = await writeChange({ bump, section, message, ws })
|
|
51
|
+
console.log(`✅ ${join(ws, rel)}`)
|
|
52
|
+
return 0
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`❌ ${error instanceof Error ? error.message : String(error)}`)
|
|
55
|
+
return 1
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isRunAsCli, runRuleCli } from '../../scripts/lib/run-rule-cli.mjs'
|
|
2
|
+
import { runStandardRule } from '../../scripts/lib/run-standard-rule.mjs'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Запускає правило: applies → JS-concerns → policy → mdc-refs (через runStandardRule).
|
|
6
|
+
* Library mode: викликається CLI orchestration через `import + run(ctx)`.
|
|
7
|
+
* @param {import('../../scripts/lib/run-standard-rule.mjs').RuleContext} [ctx] контекст прогону (walkCache тощо)
|
|
8
|
+
* @returns {Promise<number>} 0 — OK, 1 — порушення
|
|
9
|
+
*/
|
|
10
|
+
export function run(ctx) {
|
|
11
|
+
return runStandardRule(import.meta.dirname, ctx)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isRunAsCli(import.meta.url)) {
|
|
15
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit -- standalone entry-point має повертати exit-code для CI/IDE
|
|
16
|
+
process.exit(await runRuleCli(import.meta.dirname))
|
|
17
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Агрегація change-файлів одного workspace у version-bump + секцію CHANGELOG
|
|
3
|
+
* (Keep a Changelog 1.1.0, новіше зверху). Без побічних ефектів — лише обчислення/рендер;
|
|
4
|
+
* запис на диск і git — у release.mjs.
|
|
5
|
+
*/
|
|
6
|
+
import { VALID_BUMPS, VALID_SECTIONS } from './change-file.mjs'
|
|
7
|
+
|
|
8
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)$/
|
|
9
|
+
const CHANGELOG_HEADER = '# Changelog'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} version `x.y.z`
|
|
13
|
+
* @param {string} bump `major|minor|patch`
|
|
14
|
+
* @returns {string} нова версія
|
|
15
|
+
*/
|
|
16
|
+
export function bumpVersion(version, bump) {
|
|
17
|
+
const m = SEMVER_RE.exec(version)
|
|
18
|
+
if (!m) throw new Error(`aggregate: невалідний semver «${version}»`)
|
|
19
|
+
const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
20
|
+
if (bump === 'major') return `${major + 1}.0.0`
|
|
21
|
+
if (bump === 'minor') return `${major}.${minor + 1}.0`
|
|
22
|
+
return `${major}.${minor}.${patch + 1}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string[]} bumps непорожній список
|
|
27
|
+
* @returns {string} найвищий bump (major > minor > patch)
|
|
28
|
+
*/
|
|
29
|
+
export function maxBump(bumps) {
|
|
30
|
+
return VALID_BUMPS.find(level => bumps.includes(level)) ?? 'patch'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} version нова версія
|
|
35
|
+
* @param {string} date `YYYY-MM-DD`
|
|
36
|
+
* @param {Array<{ section: string, description: string }>} entries записи change-файлів
|
|
37
|
+
* @returns {string} markdown-блок секції
|
|
38
|
+
*/
|
|
39
|
+
export function renderChangelogSection(version, date, entries) {
|
|
40
|
+
let out = `## [${version}] - ${date}\n`
|
|
41
|
+
for (const section of VALID_SECTIONS) {
|
|
42
|
+
const bullets = entries.filter(e => e.section === section)
|
|
43
|
+
if (bullets.length === 0) continue
|
|
44
|
+
const bulletLines = bullets.map(b => '- ' + b.description).join('\n')
|
|
45
|
+
out += `\n### ${section}\n\n${bulletLines}\n`
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} existingText наявний CHANGELOG.md (може бути порожнім)
|
|
52
|
+
* @param {string} sectionBlock новий блок версії
|
|
53
|
+
* @returns {string} CHANGELOG із секцією зверху
|
|
54
|
+
*/
|
|
55
|
+
export function prependChangelogSection(existingText, sectionBlock) {
|
|
56
|
+
const text = existingText.trimStart()
|
|
57
|
+
if (!text.startsWith(CHANGELOG_HEADER)) {
|
|
58
|
+
return `${CHANGELOG_HEADER}\n\n${sectionBlock}`
|
|
59
|
+
}
|
|
60
|
+
const nl = text.indexOf('\n')
|
|
61
|
+
const head = text.slice(0, nl === -1 ? text.length : nl)
|
|
62
|
+
const rest = nl === -1 ? '' : text.slice(nl + 1).trimStart()
|
|
63
|
+
return `${head}\n\n${sectionBlock}\n${rest}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object} params параметри
|
|
68
|
+
* @param {string} params.currentVersion поточна version маніфесту
|
|
69
|
+
* @param {Array<{ file: string, entry: { bump: string, section: string, description: string } }>} params.changeFiles change-файли workspace
|
|
70
|
+
* @param {string} params.date `YYYY-MM-DD`
|
|
71
|
+
* @returns {{ newVersion: string, sectionBlock: string, consumedFiles: string[] } | null} результат або null, якщо змін нема
|
|
72
|
+
*/
|
|
73
|
+
export function aggregateWorkspace({ currentVersion, changeFiles, date }) {
|
|
74
|
+
if (changeFiles.length === 0) return null
|
|
75
|
+
const newVersion = bumpVersion(currentVersion, maxBump(changeFiles.map(c => c.entry.bump)))
|
|
76
|
+
const sectionBlock = renderChangelogSection(
|
|
77
|
+
newVersion,
|
|
78
|
+
date,
|
|
79
|
+
changeFiles.map(c => c.entry)
|
|
80
|
+
)
|
|
81
|
+
return { newVersion, sectionBlock, consumedFiles: changeFiles.map(c => c.file) }
|
|
82
|
+
}
|