@nitra/cursor 3.19.0 → 3.21.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/.claude-template/hooks/capture-decisions.sh +1 -1
- package/.claude-template/hooks/normalize-decisions.sh +8 -4
- package/CHANGELOG.md +33 -0
- package/bin/n-cursor.js +53 -0
- package/package.json +1 -1
- package/rules/adr/adr.mdc +5 -5
- package/rules/adr/js/templates/hooks/.gitignore.snippet +1 -0
- package/rules/changelog/changelog.mdc +1 -1
- package/rules/changelog/js/consistency.mjs +69 -12
- package/rules/ci4/ci4.mdc +2 -2
- package/rules/docker/docker.mdc +3 -3
- package/rules/docker/js/lint.mjs +1 -1
- package/rules/docker/lib/docker-hadolint.mjs +27 -55
- package/rules/ga/lint/lint.mjs +18 -54
- package/rules/js-run/js/runtime.mjs +32 -0
- package/rules/js-run/js-run.mdc +6 -0
- package/rules/js-run/lib/temporal-scan.mjs +52 -0
- package/rules/k8s/lint/lint.mjs +3 -10
- package/rules/nginx-default-tpl/js/template.mjs +39 -1
- package/rules/nginx-default-tpl/nginx-default-tpl.mdc +3 -1
- package/rules/npm-module/js/skill_meta.mjs +12 -0
- package/rules/npm-module/npm-module.mdc +1 -1
- package/rules/npm-module/policy/npm_publish_yml/target.json +1 -0
- package/rules/rego/lint/lint.mjs +10 -55
- package/rules/release/change.mjs +34 -5
- package/rules/release/lib/change-file.mjs +26 -11
- package/rules/text/lint/lint.mjs +11 -40
- package/rules/worktree/policy/vscode_settings/target.json +5 -0
- package/rules/worktree/policy/vscode_settings/template/settings.json.snippet.json +8 -0
- package/rules/worktree/policy/zed_settings/target.json +5 -0
- package/rules/worktree/policy/zed_settings/template/settings.json.snippet.json +12 -0
- package/rules/worktree/worktree.mdc +52 -0
- package/schemas/target.json +5 -0
- package/scripts/lib/assert-project-root.mjs +80 -0
- package/scripts/lib/ensure-tool.mjs +352 -0
- package/scripts/lib/root-notice.mjs +64 -0
- package/scripts/lib/run-conftest-batch.mjs +6 -28
- package/scripts/lib/run-rule.mjs +61 -5
- package/scripts/lib/skill-meta.mjs +16 -2
- package/scripts/lib/template.mjs +29 -3
- package/scripts/lib/worktree-notice.mjs +121 -73
- package/scripts/sync-claude-config.mjs +2 -2
- package/skills/fix/SKILL.md +4 -4
- package/skills/llm-patch/meta.json +1 -1
- package/skills/publish-telegram/meta.json +1 -1
- package/skills/start-check/meta.json +1 -1
- package/skills/worktree/meta.json +1 -1
- package/types/bin/n-cursor.d.ts +1 -1
- package/rules/npm-module/policy/npm_publish_yml/npm_publish_yml.rego +0 -87
|
@@ -199,7 +199,7 @@ if ! printf '%s' "$RESPONSE_TRIMMED" | grep -q '^## '; then
|
|
|
199
199
|
exit 0
|
|
200
200
|
fi
|
|
201
201
|
|
|
202
|
-
TS=$(date +%
|
|
202
|
+
TS=$(date +%y%m%d-%H%M)
|
|
203
203
|
|
|
204
204
|
# Slug із першого `## [ADR|Runbook|Knowledge] <heading>`-рядка відповіді.
|
|
205
205
|
# Логіка локальна (без додаткового LLM-виклику): використовуємо вже згенерований heading.
|
|
@@ -407,11 +407,15 @@ while IFS= read -r op_json; do
|
|
|
407
407
|
continue
|
|
408
408
|
;;
|
|
409
409
|
esac
|
|
410
|
-
# Keep the draft's
|
|
410
|
+
# Keep the draft's timestamp prefix on the clean file: the name
|
|
411
411
|
# stays anchored to capture time, only the slug part changes between draft
|
|
412
|
-
# and clean, and docs/adr/ keeps sorting chronologically.
|
|
413
|
-
#
|
|
412
|
+
# and clean, and docs/adr/ keeps sorting chronologically. New drafts use
|
|
413
|
+
# `YYMMDD-HHMM-`; older `YYYYMMDD-HHMMSS-` drafts are still supported.
|
|
414
|
+
# Drafts without a timestamp prefix fall back to a bare `<slug>.md`.
|
|
414
415
|
case "$FILE" in
|
|
416
|
+
[0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9]-*)
|
|
417
|
+
DEST_SLUG="$(printf '%s' "$FILE" | cut -c1-11)-$SLUG"
|
|
418
|
+
;;
|
|
415
419
|
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]-*)
|
|
416
420
|
DEST_SLUG="$(printf '%s' "$FILE" | cut -c1-15)-$SLUG"
|
|
417
421
|
;;
|
|
@@ -444,7 +448,7 @@ while IFS= read -r op_json; do
|
|
|
444
448
|
;;
|
|
445
449
|
esac
|
|
446
450
|
# Resolve the target clean file. The LLM gives a bare `<slug>.md`, but the
|
|
447
|
-
# real file usually carries a
|
|
451
|
+
# real file usually carries a timestamp prefix. Try, in order:
|
|
448
452
|
# 1. exact name in docs/adr/,
|
|
449
453
|
# 2. a rewrite of this batch that produced that slug (SLUG_MAP),
|
|
450
454
|
# 3. a unique existing clean file whose name ends with `-<slug>.md`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.21.0] - 2026-06-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Root-guard для всіх деструктивних точок: CLI-команди fix/lint/coverage/change/release + дефолтний sync хардом перевіряють cwd===git-toplevel (assertCwdIsProjectRoot); worktree-preflight підсилено root-assert (pwd vs toplevel); новий meta.json-флаг requireRoot + injectRootNotice для in-place root-only скілів (n-start-check); skillRequiresRoot як явна похідна ознака захисту; валідація requireRoot у npm-module
|
|
8
|
+
- sync .gitignore: ігнорувати .claude/scheduled_tasks.lock (runtime-lock планувальника)
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Скорочено timestamp-префікси ADR і change-файлів до YYMMDD-HHMM; додано атомарний suffix для локальних колізій та заборону Temporal у Bun runtime.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- n-changelog: directional version-drift — лише version ВПЕРЕД (> опублікованої/git-бази) валить як ручний bump; version ПОЗАДУ (локаль відстала від CI-релізу, ще не git pull) більше не блокує коміт (compareSemverCore/versionIsAhead)
|
|
17
|
+
|
|
18
|
+
## [3.20.0] - 2026-06-03
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- ensure-tool: авто-встановлення зовнішніх CLI-залежностей (hk, conftest, shellcheck, actionlint, dotenv-linter) — brew/scoop/GitHub Release per-platform; hk install після fix; conftest авто-встановлюється перед fix та lint-ga
|
|
23
|
+
- ensureTool: розширено на opa/regal/hadolint/kubeconform/kubescape (brew/scoop/GitHub Release per-platform) + підтримка сирих бінарників (archive:false → download+chmod, без tar). Мігровано call-sites rego-lint (opa/regal), docker (hadolint з docker-fallback), k8s-lint (kubeconform/kubescape). withBinRemovedFromPath виставляє N_CURSOR_NO_AUTO_INSTALL=1.
|
|
24
|
+
- Guard: дефолтний sync (`npx @nitra/cursor` без підкоманди) забороняє запуск із піддиректорії git-репо — STOP до мутацій, замість скаффолда .cursor/.claude/CLAUDE.md/.n-cursor.json не в той каталог
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- worktree-only скіли: bootstrap-виклик npx @nitra/cursor у новоствореному worktree тепер ретраїться при транзитних помилках реєстру/CDN (ETARGET/notarget, ENOTFOUND, ETIMEDOUT, EAI_AGAIN, ECONNRESET, 5xx) кожні 30с до 5 хв (env N_CURSOR_NPX_RETRY_MAX_MIN, ceiling 10 хв); реальний nonzero CLI віддається одразу. worktree-notice додає bun install у дереві (локальна копія усуває гонку з CDN) і shell-обгортку n_cursor_npx; fix-скіл кроки 1/6 використовують її
|
|
29
|
+
- npm-module: npm_publish_yml тепер звіряє ВЕСЬ канонічний сніпет напряму (target.json "check":"template", generic deep-subset) замість bespoke subset-of rego — редагування сніпета одразу змінює enforce, без правок rego й міграторів; масиви (steps) матчаться структурним subset-ом за наявністю (order/key-insensitive, зайві кроки дозволені); legacy publish-only workflow тепер падає check (вимагає release-publish job). Новий режим check:template перевикористовний для будь-якого whole-file концерну зі сніпетом.
|
|
30
|
+
- docker hadolint: прибрано docker-run fallback — hadolint тепер лише нативний бінарник через ensureTool (brew/scoop/GitHub Release). Видалено HADOLINT_IMAGE; оновлено docker.mdc і тести.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- nginx-default-tpl: error_log off → error_log /dev/null crit; (error_log off — НЕ валідний nginx, падає під readOnlyRootFilesystem); авто-заміна в шаблонах + оновлено канон .mdc/фікстуру
|
|
35
|
+
|
|
3
36
|
## [3.19.0] - 2026-06-03
|
|
4
37
|
|
|
5
38
|
### Changed
|
package/bin/n-cursor.js
CHANGED
|
@@ -92,10 +92,12 @@ import {
|
|
|
92
92
|
import { detectAutoSkills } from '../scripts/auto-skills.mjs'
|
|
93
93
|
import { readSkillMetaRaw } from '../scripts/lib/skill-meta.mjs'
|
|
94
94
|
import { injectWorktreeNotice } from '../scripts/lib/worktree-notice.mjs'
|
|
95
|
+
import { injectRootNotice } from '../scripts/lib/root-notice.mjs'
|
|
95
96
|
import { runPostToolUseFixCli } from '../scripts/post-tool-use-fix.mjs'
|
|
96
97
|
import { discoverCheckRulesFromCursorRules } from '../scripts/lib/discover-check-rules-from-cursor.mjs'
|
|
97
98
|
import { listRuleIds } from '../scripts/lib/list-rule-ids.mjs'
|
|
98
99
|
import { ensureNitraCursorInRootDevDependencies } from '../scripts/ensure-nitra-cursor-dev-dependencies.mjs'
|
|
100
|
+
import { assertCwdIsProjectRoot } from '../scripts/lib/assert-project-root.mjs'
|
|
99
101
|
import { runLintDocker } from '../rules/docker/lint/lint.mjs'
|
|
100
102
|
import { runLintGaCli } from '../rules/ga/lint/lint.mjs'
|
|
101
103
|
import { runLintK8s } from '../rules/k8s/lint/lint.mjs'
|
|
@@ -110,6 +112,7 @@ import { runWorktreeCli } from '../scripts/worktree-cli.mjs'
|
|
|
110
112
|
import { syncSetupBunDepsAction } from '../scripts/sync-setup-bun-deps-action.mjs'
|
|
111
113
|
import { runLint } from '../scripts/lint-cli.mjs'
|
|
112
114
|
import { formatTimingSummary } from '../scripts/lib/timing-summary.mjs'
|
|
115
|
+
import { ensureHkInstall, ensureTool } from '../scripts/lib/ensure-tool.mjs'
|
|
113
116
|
|
|
114
117
|
const PACKAGE_NAME = '@nitra/cursor'
|
|
115
118
|
const CONFIG_FILE = '.n-cursor.json'
|
|
@@ -785,6 +788,9 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
|
785
788
|
await mkdir(destDir, { recursive: true })
|
|
786
789
|
const meta = readSkillMetaRaw(srcDir)
|
|
787
790
|
const worktree = meta?.worktree === true
|
|
791
|
+
// root-guard для in-place скілів (мутують CWD без worktree-ізоляції).
|
|
792
|
+
// Worktree-скіли root-assert уже мають у worktree-блоці, тож тут лише !worktree.
|
|
793
|
+
const rootOnly = !worktree && meta?.requireRoot === true
|
|
788
794
|
const entries = await readdir(srcDir, { withFileTypes: true })
|
|
789
795
|
for (const entry of entries) {
|
|
790
796
|
// Лише top-level файли скіла. `meta.json` — метадані (не для споживача);
|
|
@@ -794,6 +800,7 @@ async function syncSkills(configSkills, bundledSkillsDir = BUNDLED_SKILLS_DIR) {
|
|
|
794
800
|
let content = await readFile(join(srcDir, entry.name), 'utf8')
|
|
795
801
|
if (entry.name === 'SKILL.md') {
|
|
796
802
|
content = injectWorktreeNotice(content, worktree)
|
|
803
|
+
content = injectRootNotice(content, rootOnly)
|
|
797
804
|
}
|
|
798
805
|
await writeFile(join(destDir, entry.name), content, 'utf8')
|
|
799
806
|
}
|
|
@@ -1184,6 +1191,10 @@ function logRemovedManagedItems(title, basePath, names) {
|
|
|
1184
1191
|
* @returns {Promise<void>}
|
|
1185
1192
|
*/
|
|
1186
1193
|
async function runFixCommand(requestedRules) {
|
|
1194
|
+
const hkBin = ensureTool('hk')
|
|
1195
|
+
ensureHkInstall(hkBin)
|
|
1196
|
+
ensureTool('conftest')
|
|
1197
|
+
|
|
1187
1198
|
const available = await listRuleIds(BUNDLED_RULES_DIR)
|
|
1188
1199
|
if (available.length === 0) {
|
|
1189
1200
|
console.error('❌ Не знайдено жодного правила у пакеті')
|
|
@@ -1453,10 +1464,52 @@ async function runSync() {
|
|
|
1453
1464
|
}
|
|
1454
1465
|
}
|
|
1455
1466
|
|
|
1467
|
+
/**
|
|
1468
|
+
* Команди, що мутують проєкт у CWD і вимагають кореня репо. `undefined`/`''` —
|
|
1469
|
+
* дефолтний sync; `check` — deprecated-alias `fix`. Решта (read-only `trace`/
|
|
1470
|
+
* `graph`, `--root`-команди `docgen`/`rename-yaml-extensions`, `worktree`,
|
|
1471
|
+
* sub-лінтери) гард не зачіпає.
|
|
1472
|
+
*/
|
|
1473
|
+
const ROOT_GUARDED_COMMANDS = new Set([undefined, '', 'fix', 'check', 'lint', 'coverage', 'change', 'release'])
|
|
1474
|
+
|
|
1475
|
+
/**
|
|
1476
|
+
* Короткий опис дії для тексту root-guard помилки за іменем команди.
|
|
1477
|
+
* @param {string | undefined} cmd підкоманда CLI (або undefined для дефолтного sync)
|
|
1478
|
+
* @returns {string} фраза «що саме мутує CWD»
|
|
1479
|
+
*/
|
|
1480
|
+
function describeRootGuardedAction(cmd) {
|
|
1481
|
+
switch (cmd) {
|
|
1482
|
+
case undefined:
|
|
1483
|
+
case '':
|
|
1484
|
+
return 'Дефолтна синхронізація скаффолдить .cursor/, .claude/, CLAUDE.md, .n-cursor.json і робить bun install у поточному каталозі'
|
|
1485
|
+
case 'fix':
|
|
1486
|
+
case 'check':
|
|
1487
|
+
return '`fix` запускає programmatic-перевірки правил, що переписують конфіги в поточному каталозі'
|
|
1488
|
+
case 'lint':
|
|
1489
|
+
return '`lint` запускає авто-fix лінтерів (oxfmt/eslint --fix/stylelint --fix) у поточному каталозі'
|
|
1490
|
+
case 'coverage':
|
|
1491
|
+
return '`coverage` генерує COVERAGE.md і Stryker-артефакти в поточному каталозі'
|
|
1492
|
+
case 'change':
|
|
1493
|
+
return '`change` пише change-файл у .changes/ поточного каталогу'
|
|
1494
|
+
case 'release':
|
|
1495
|
+
return '`release` бампає version і переписує CHANGELOG у поточному каталозі'
|
|
1496
|
+
default:
|
|
1497
|
+
return 'Команда @nitra/cursor мутує проєкт у поточному каталозі'
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1456
1501
|
// CLI: маршрутизація команд
|
|
1457
1502
|
const [command, ...args] = process.argv.slice(2)
|
|
1458
1503
|
|
|
1459
1504
|
try {
|
|
1505
|
+
// Root-guard до перших мутацій: дефолтний sync скаффолдить .cursor/.claude/CLAUDE.md/
|
|
1506
|
+
// .n-cursor.json + bun install, а fix/lint/coverage/change/release переписують файли в CWD —
|
|
1507
|
+
// усе це ключиться на cwd(). Запуск із піддиректорії git-репо (типово прямий
|
|
1508
|
+
// `bun npm/bin/n-cursor.js` не з кореня) зачепив би не той каталог → STOP. Read-only та
|
|
1509
|
+
// `--root`-команди (trace, graph, docgen, rename-yaml-extensions) не зачіпаємо.
|
|
1510
|
+
if (ROOT_GUARDED_COMMANDS.has(command)) {
|
|
1511
|
+
assertCwdIsProjectRoot(cwd(), describeRootGuardedAction(command))
|
|
1512
|
+
}
|
|
1460
1513
|
await ensureNitraCursorInRootDevDependencies(cwd())
|
|
1461
1514
|
switch (command) {
|
|
1462
1515
|
case 'fix': {
|
package/package.json
CHANGED
package/rules/adr/adr.mdc
CHANGED
|
@@ -18,8 +18,8 @@ ADR живуть у єдиному каталозі **`docs/adr/`**. Clean ADR-
|
|
|
18
18
|
|
|
19
19
|
Є два стани файлу, які відрізняються YAML frontmatter:
|
|
20
20
|
|
|
21
|
-
- **Draft** — файл з frontmatter `session: …`, `captured: …`, `transcript: …` та timestamp-іменем `
|
|
22
|
-
- **Clean** — файл без frontmatter, з kebab-case-іменем. `normalize-decisions.sh` зберігає timestamp-префікс чернетки → `
|
|
21
|
+
- **Draft** — файл з frontmatter `session: …`, `captured: …`, `transcript: …` та timestamp-іменем `YYMMDD-HHMM-<sid>.md`. Пише `capture-decisions.sh` після кожної сесії.
|
|
22
|
+
- **Clean** — файл без frontmatter, з kebab-case-іменем. `normalize-decisions.sh` зберігає timestamp-префікс чернетки → `YYMMDD-HHMM-<slug>.md` (наприклад `260518-0928-ланцюжок-запуску-abie.md`); створений руками clean-файл може мати просто `<slug>.md`.
|
|
23
23
|
|
|
24
24
|
`normalize-decisions.sh` ніколи не чіпає clean-файли — крім випадку `merge-into`, коли дописує `## Update YYYY-MM-DD` в кінець наявного clean-файлу.
|
|
25
25
|
|
|
@@ -50,7 +50,7 @@ LLM повертає масив операцій:
|
|
|
50
50
|
| `rewrite` | Чернетка стає окремим clean-файлом MADR v4 minimal: frontmatter знімається, ім'я → `<timestamp>-<slug>.md` (timestamp-префікс чернетки збережено), додаються `**Status:** Accepted`, `**Date:**` з `captured` і canonical MADR headings. | `file`, `slug`, `content` |
|
|
51
51
|
| `merge-into` | Чернетка повторює тему вже існуючого clean-файлу; дописуємо `## Update YYYY-MM-DD` у кінець `target`. | `file`, `target`, `additions` |
|
|
52
52
|
|
|
53
|
-
`slug` — kebab-case українською (`ланцюжок-запуску-abie`, `npm-publish-flow`); англійські технічні терміни лишаються англійською без транслітерації. До імені clean-файлу скрипт додає `
|
|
53
|
+
`slug` — kebab-case українською (`ланцюжок-запуску-abie`, `npm-publish-flow`); англійські технічні терміни лишаються англійською без транслітерації. До імені clean-файлу скрипт додає `YYMMDD-HHMM-` чернетки, тож запис лишається прив'язаним до часу capture, а `docs/adr/` сортується хронологічно. Колізія імен обробляється детермінованим суфіксом `-2`, `-3`. Старі чернетки з `YYYYMMDD-HHMMSS-` prefix нормалізатор також розпізнає, щоб не ламати наявний inbox.
|
|
54
54
|
|
|
55
55
|
### Жодних git-операцій
|
|
56
56
|
|
|
@@ -86,8 +86,8 @@ LLM повертає масив операцій:
|
|
|
86
86
|
|
|
87
87
|
```text
|
|
88
88
|
docs/adr/
|
|
89
|
-
├──
|
|
90
|
-
└──
|
|
89
|
+
├── YYMMDD-HHMM-<sid>.md # drafts (frontmatter session:/captured:/transcript:)
|
|
90
|
+
└── YYMMDD-HHMM-<slug>.md # clean ADR-и (без frontmatter, timestamp-префікс чернетки збережено)
|
|
91
91
|
.claude/hooks/
|
|
92
92
|
├── capture-decisions.sh # auto-synced з пакета
|
|
93
93
|
├── normalize-decisions.sh # auto-synced з пакета
|
|
@@ -8,7 +8,7 @@ alwaysApply: true
|
|
|
8
8
|
|
|
9
9
|
> **Якщо в цій сесії ти змінив(ла) файли в пакетному workspace** (код, rego, правила, скіли, скрипти, конфіги, тести — **не** лише `docs/` / `doc/`) — **не завершуй задачу**, поки не виконаєш **усі три** кроки нижче в **тому ж** наборі змін. Це не «опційно після синку» — це частина PR.
|
|
10
10
|
|
|
11
|
-
1. **Поклади change-файл** `<ws>/.changes
|
|
11
|
+
1. **Поклади change-файл** `<ws>/.changes/YYMMDD-HHMM.md` з frontmatter `bump:` (`major|minor|patch`) + `section:` (`Added|Changed|Fixed|Removed`) і текстом опису. Команда: `npx @nitra/cursor change --bump <…> --section <…> --message "<…>" [--ws <шлях>]`. Якщо файл за ту саму хвилину вже існує, CLI атомарно створює `YYMMDD-HHMM-2.md`, потім `-3` тощо.
|
|
12
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
13
|
3. **`npx @nitra/cursor fix changelog`** → exit **`0`** (достатньо наявності change-файлу; `version` лишається незмінним).
|
|
14
14
|
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* change-файл `<ws>/.changes/*.md` — єдиний дозволений артефакт зміни. Bump `version`
|
|
4
4
|
* і генерацію `CHANGELOG.md` робить виключно `n-cursor release` у CI на `main`.
|
|
5
5
|
*
|
|
6
|
-
* Інваріант (на будь-якій гілці): `version` не має
|
|
7
|
-
* `version`
|
|
8
|
-
* перевірку, навіть
|
|
9
|
-
* не
|
|
6
|
+
* Інваріант (на будь-якій гілці): `version` у дереві не має **випереджати** базу. Лише
|
|
7
|
+
* drift **уперед** (`version` > опублікованої в реєстрі / > git-бази) — ручний bump поза
|
|
8
|
+
* CI — завалює перевірку, навіть із change-файлом. Version **позаду** бази (локаль відстала
|
|
9
|
+
* від уже опублікованого CI-релізу, ще не зроблено `git pull`) — НЕ порушення: це не ручний
|
|
10
|
+
* bump, а git і так не дасть запушити non-fast-forward. Pass лише коли є change-файл, а
|
|
11
|
+
* version не випереджає базу; зміни без change-файлу — fail.
|
|
10
12
|
*
|
|
11
13
|
* Дві моделі бази — на рівні воркспейсу (див. n-changelog.mdc):
|
|
12
14
|
*
|
|
@@ -318,6 +320,48 @@ function resolvePublishedVersion(manifest, getPublishedVersion) {
|
|
|
318
320
|
return getPublishedVersion(manifest.name, manifest.kind)
|
|
319
321
|
}
|
|
320
322
|
|
|
323
|
+
/** Числове ядро semver (`x.y.z`); хвіст (prerelease/build) ігнорується. */
|
|
324
|
+
const SEMVER_CORE_RE = /^(\d+)\.(\d+)\.(\d+)/
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Парсить числове ядро semver-рядка.
|
|
328
|
+
* @param {unknown} v версія
|
|
329
|
+
* @returns {{ major: number, minor: number, patch: number } | null} ядро або null
|
|
330
|
+
*/
|
|
331
|
+
function parseSemverCore(v) {
|
|
332
|
+
const m = typeof v === 'string' ? v.match(SEMVER_CORE_RE) : null
|
|
333
|
+
if (!m) return null
|
|
334
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Порівнює semver-ядра двох версій.
|
|
339
|
+
* @param {unknown} a перша версія
|
|
340
|
+
* @param {unknown} b друга версія
|
|
341
|
+
* @returns {-1 | 0 | 1 | null} a<b → -1, a==b → 0, a>b → 1; null — нерозпізнано
|
|
342
|
+
*/
|
|
343
|
+
function compareSemverCore(a, b) {
|
|
344
|
+
const pa = parseSemverCore(a)
|
|
345
|
+
const pb = parseSemverCore(b)
|
|
346
|
+
if (!pa || !pb) return null
|
|
347
|
+
if (pa.major !== pb.major) return pa.major > pb.major ? 1 : -1
|
|
348
|
+
if (pa.minor !== pb.minor) return pa.minor > pb.minor ? 1 : -1
|
|
349
|
+
if (pa.patch !== pb.patch) return pa.patch > pb.patch ? 1 : -1
|
|
350
|
+
return 0
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Чи `current` випереджає `base` (ручний bump поза CI). Якщо semver нерозпізнаний
|
|
355
|
+
* (null) — fail-closed на будь-яку нерівність (консервативно, як до directional-фікса).
|
|
356
|
+
* @param {unknown} current версія в дереві
|
|
357
|
+
* @param {unknown} base база (опублікована / git-база)
|
|
358
|
+
* @returns {boolean} true — current попереду base (або нерозпізнано й не рівні)
|
|
359
|
+
*/
|
|
360
|
+
function versionIsAhead(current, base) {
|
|
361
|
+
const cmp = compareSemverCore(current, base)
|
|
362
|
+
return cmp === null ? current !== base : cmp > 0
|
|
363
|
+
}
|
|
364
|
+
|
|
321
365
|
/**
|
|
322
366
|
* @param {string} name пакет
|
|
323
367
|
* @param {import('../lib/package-manifest.mjs').PackageKind} [kind] тип пакета
|
|
@@ -454,16 +498,28 @@ async function checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVers
|
|
|
454
498
|
pass(`${label}: ${name} — опублікована версія недоступна (мережа/реєстр), перевірку пропущено`)
|
|
455
499
|
return
|
|
456
500
|
}
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
|
|
501
|
+
// Лише drift УПЕРЕД (version > опублікованої) — ручний bump поза CI; має пріоритет
|
|
502
|
+
// над change-файлом (симетрично з local-only-шляхом). Версія ПОЗАДУ реєстру — локаль
|
|
503
|
+
// відстала від уже опублікованого релізу, не порушення (нижче).
|
|
504
|
+
if (versionIsAhead(Vcurrent, Vpublished)) {
|
|
460
505
|
fail(
|
|
461
|
-
`${label}: version у ${mf} (${Vcurrent})
|
|
462
|
-
`ручний bump заборонено. Відкоти version і поклади change-файл ` +
|
|
506
|
+
`${label}: version у ${mf} (${Vcurrent}) випереджає опубліковану (${Vpublished}) — ` +
|
|
507
|
+
`ручний bump поза CI заборонено. Відкоти version і поклади change-файл ` +
|
|
463
508
|
`(npx @nitra/cursor change …); bump зробить CI на main (n-changelog.mdc)`
|
|
464
509
|
)
|
|
465
510
|
return
|
|
466
511
|
}
|
|
512
|
+
if (compareSemverCore(Vcurrent, Vpublished) < 0) {
|
|
513
|
+
// Локаль ПОЗАДУ реєстру: CI вже опублікував новішу версію й закомітив bump назад,
|
|
514
|
+
// а ти ще не зробив `git pull`. Це не ручний bump (git не дасть запушити
|
|
515
|
+
// non-fast-forward), тож коміт не блокуємо — лише вимагаємо change-файл на наявні зміни.
|
|
516
|
+
pass(
|
|
517
|
+
`${label}: version у ${mf} (${Vcurrent}) позаду опублікованої (${Vpublished}) — ` +
|
|
518
|
+
`локаль відстала від реєстру (зроби git pull); це не ручний bump`
|
|
519
|
+
)
|
|
520
|
+
await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail, cwd)
|
|
521
|
+
return
|
|
522
|
+
}
|
|
467
523
|
pass(`${label}: ${name}@${Vcurrent} збігається з реєстром — перевіряємо git на незрелізні зміни`)
|
|
468
524
|
await checkPublishedWorkspacePendingGitChanges(manifest, Vcurrent, subWorkspaces, pass, fail, cwd)
|
|
469
525
|
}
|
|
@@ -480,10 +536,11 @@ async function checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel
|
|
|
480
536
|
const label = workspaceLabel(manifest)
|
|
481
537
|
const mf = manifestFilePath(manifest.ws, manifest)
|
|
482
538
|
const Vcurrent = manifest.version
|
|
483
|
-
//
|
|
484
|
-
// навіть
|
|
539
|
+
// Лише drift УПЕРЕД (version > бази) має пріоритет над change-файлом: ручний bump
|
|
540
|
+
// заборонено навіть із change-файлом (симетрично з published-шляхом). Version позаду
|
|
541
|
+
// бази (гілка відстала від base-ref) — не порушення, не блокуємо.
|
|
485
542
|
const Vbase = await readBaseVersion(comparisonRef, manifest, cwd)
|
|
486
|
-
if (Vbase !== null && Vcurrent !== null && Vbase
|
|
543
|
+
if (Vbase !== null && Vcurrent !== null && versionIsAhead(Vcurrent, Vbase)) {
|
|
487
544
|
fail(
|
|
488
545
|
`${label}: version у ${mf} змінено поза CI (${Vbase} → ${Vcurrent}) — ручний bump заборонено (на ${baseLabel} — ${Vbase}). ` +
|
|
489
546
|
`Відкоти version і поклади change-файл (npx @nitra/cursor change …); bump зробить CI (n-changelog.mdc)`
|
package/rules/ci4/ci4.mdc
CHANGED
|
@@ -66,7 +66,7 @@ RAG витягує **фрагменти**, не цілі документи. Т
|
|
|
66
66
|
docs/
|
|
67
67
|
├── adr/
|
|
68
68
|
│ ├── <slug>.md # clean ADR-и (MADR v4 minimal, без frontmatter)
|
|
69
|
-
│ ├──
|
|
69
|
+
│ ├── YYMMDD-HHMM-<sid>.md # drafts (frontmatter session:/captured:/transcript:)
|
|
70
70
|
│ └── index.md # autogen — список accepted ADR за датою
|
|
71
71
|
├── explanation/ # Diátaxis: чому і що
|
|
72
72
|
│ ├── overview.md # Capability Map (autogen)
|
|
@@ -86,7 +86,7 @@ docs/
|
|
|
86
86
|
|
|
87
87
|
## ADR як вхід проекцій
|
|
88
88
|
|
|
89
|
-
Регенератор працює **тільки з clean ADR** (`docs/adr/<slug>.md`, без frontmatter, з рядком `**Status:** Accepted`). Drafts (`
|
|
89
|
+
Регенератор працює **тільки з clean ADR** (`docs/adr/<slug>.md`, без frontmatter, з рядком `**Status:** Accepted`). Drafts (`YYMMDD-HHMM-*.md`) ігноруються — їх перетворить у clean ADR `normalize-decisions.sh` із правила `adr`.
|
|
90
90
|
|
|
91
91
|
Поля, які регенератор читає з clean ADR:
|
|
92
92
|
|
package/rules/docker/docker.mdc
CHANGED
|
@@ -181,7 +181,7 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
|
|
|
181
181
|
|
|
182
182
|
**Область lint-docker (вужча, ніж `check docker`):** лише файли з іменем **`Dockerfile`** та **`*.Dockerfile`** (суфікс **`.dockerfile`** без урахування регістру, наприклад **`api.Dockerfile`**). Файли **`Dockerfile.prod`**, **`Containerfile`** тощо **не** входять у **`lint-docker`**; їх ловить **`check docker`** (`rules/docker/fix.mjs`).
|
|
183
183
|
|
|
184
|
-
Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`rules/docker/fix.mjs`**. Виклик **`hadolint
|
|
184
|
+
Обхід: **`walkDir`** з тими самими пропусками каталогів, що й **`rules/docker/fix.mjs`**. Виклик **`hadolint`** як **нативного бінарника** через **`ensureTool`** (PATH → кеш → авто-install brew/scoop/GitHub Release; **без** `docker run`) — спільна логіка **`npm/rules/docker/lib/docker-hadolint.mjs`**.
|
|
185
185
|
|
|
186
186
|
- Канон `package.json#scripts.lint-docker`: [package.json.snippet.json](./policy/package_json/template/package.json.snippet.json)
|
|
187
187
|
|
|
@@ -191,14 +191,14 @@ CLI **`hadolint`** приймає лише **явні шляхи** (`[DOCKERFILE
|
|
|
191
191
|
|
|
192
192
|
- Канон: [lint-docker.yml.snippet.yml](./policy/lint_docker_yml/template/lint-docker.yml.snippet.yml)
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Локально hadolint авто-встановлюється через **`ensureTool`** (latest, без піну версії). У CI встанови його кроком із workflow-сніпета (curl-download бінарника — без `docker run`).
|
|
195
195
|
|
|
196
196
|
Кореневий скрипт **`lint`** (див. **`n-bun.mdc`**) **обов'язково** містить **`bun run lint-docker`**, коли в проєкті підключено правило **`docker`**.
|
|
197
197
|
|
|
198
198
|
## Запуск
|
|
199
199
|
|
|
200
200
|
1. **`bun run lint-docker`** — **`run-docker.mjs`**: **`Dockerfile`** та **`*.Dockerfile`** (див. **`lint-docker`**); у CI встанови hadolint (приклад у workflow).
|
|
201
|
-
2. **`npx @nitra/cursor fix docker`** — **`rules/docker/fix.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (**`
|
|
201
|
+
2. **`npx @nitra/cursor fix docker`** — **`rules/docker/fix.mjs`**, виклик hadolint як у **`docker-hadolint.mjs`** (нативний бінарник через **`ensureTool`**; **без** `docker run`).
|
|
202
202
|
3. Кореневий **`.hadolint.yaml`**: вимкнення правил, trusted registries — [документація](https://github.com/hadolint/hadolint#configure). Щоб не додавати **`# hadolint ignore=DL3007`** у кожному **`FROM`** з **`:latest`**, у корені репозиторію задати глобально:
|
|
203
203
|
|
|
204
204
|
```yaml title=".hadolint.yaml"
|
package/rules/docker/js/lint.mjs
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `USER` у Dockerfile — перевірка non-root для нього пропускається.
|
|
30
30
|
*
|
|
31
31
|
* Знаходить Dockerfile, Dockerfile.*, Containerfile, Containerfile.*; пропускає node_modules, .git
|
|
32
|
-
* тощо.
|
|
32
|
+
* тощо. hadolint — нативний бінарник через `ensureTool` (PATH/кеш/авто-install; без docker run).
|
|
33
33
|
* Кореневий .hadolint.yaml підхоплюється hadolint автоматично.
|
|
34
34
|
*/
|
|
35
35
|
import { readFile } from 'node:fs/promises'
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Спільна логіка виклику hadolint для шляхів до Dockerfile (див. docker.mdc).
|
|
3
3
|
*
|
|
4
|
-
* Відносні шляхи з прямими
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Відносні шляхи з прямими слешами; hadolint резолвиться через `ensureTool`
|
|
5
|
+
* (PATH → кеш → авто-install brew/scoop/GitHub Release per-platform). Docker-fallback
|
|
6
|
+
* прибрано — hadolint ставиться як **нативний бінарник**, без `docker run`.
|
|
7
|
+
* Використовується `./check.mjs` (check-docker) та `../../lint/lint.mjs` (run-docker).
|
|
7
8
|
*/
|
|
8
9
|
import { spawnSync } from 'node:child_process'
|
|
9
10
|
import { relative, sep } from 'node:path'
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
/** Тег образу для резервного запуску (узгоджуй з docker.mdc). */
|
|
14
|
-
export const HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0'
|
|
12
|
+
import { ensureTool } from '../../../scripts/lib/ensure-tool.mjs'
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
* Відносний шлях від root з прямими слешами (
|
|
15
|
+
* Відносний шлях від root з прямими слешами (стабільний вивід незалежно від OS).
|
|
18
16
|
* @param {string} root корінь
|
|
19
17
|
* @param {string} absPath абсолютний шлях
|
|
20
18
|
* @returns {string} відносний шлях з прямими слешами
|
|
@@ -24,65 +22,39 @@ export function posixRel(root, absPath) {
|
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
|
-
* Запуск hadolint
|
|
25
|
+
* Запуск hadolint як нативного бінарника. hadolint резолвиться через `ensureTool`
|
|
26
|
+
* (PATH → кеш → авто-install); якщо авто-install відключено (`N_CURSOR_NO_AUTO_INSTALL`)
|
|
27
|
+
* чи не вдався — повертаємо `ok: false` з підказкою (без `docker run`).
|
|
28
28
|
* @param {string} root корінь репозиторію
|
|
29
29
|
* @param {string} absPath абсолютний шлях до Dockerfile
|
|
30
|
-
* @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint
|
|
30
|
+
* @returns {{ ok: boolean, stdout: string, stderr: string, via: string }} результат перевірки hadolint
|
|
31
31
|
*/
|
|
32
32
|
export function lintDockerfileWithHadolint(root, absPath) {
|
|
33
33
|
const rel = posixRel(root, absPath)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
encoding: 'utf8',
|
|
39
|
-
maxBuffer: 10 * 1024 * 1024
|
|
40
|
-
})
|
|
41
|
-
const ok = local.status === 0
|
|
42
|
-
return {
|
|
43
|
-
ok,
|
|
44
|
-
stdout: local.stdout ?? '',
|
|
45
|
-
stderr: local.stderr ?? '',
|
|
46
|
-
via: 'hadolint'
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const dockerPath = resolveCmd('docker')
|
|
51
|
-
if (!dockerPath) {
|
|
34
|
+
let hadolintPath
|
|
35
|
+
try {
|
|
36
|
+
hadolintPath = ensureTool('hadolint')
|
|
37
|
+
} catch (error) {
|
|
52
38
|
return {
|
|
53
39
|
ok: false,
|
|
54
40
|
stdout: '',
|
|
55
41
|
stderr:
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
|
|
42
|
+
`Не вдалося отримати hadolint (${error.message}). ` +
|
|
43
|
+
'Встанови: brew install hadolint (macOS) / scoop install hadolint (Windows) / ' +
|
|
44
|
+
'https://github.com/hadolint/hadolint/releases (Linux).',
|
|
45
|
+
via: 'hadolint'
|
|
59
46
|
}
|
|
60
47
|
}
|
|
61
48
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
encoding: 'utf8',
|
|
68
|
-
maxBuffer: 10 * 1024 * 1024
|
|
69
|
-
}
|
|
70
|
-
)
|
|
71
|
-
if (docker.error) {
|
|
72
|
-
return {
|
|
73
|
-
ok: false,
|
|
74
|
-
stdout: '',
|
|
75
|
-
stderr:
|
|
76
|
-
`Не знайдено hadolint у PATH і не вдалося запустити Docker (${docker.error.message}). ` +
|
|
77
|
-
`Встанови hadolint (наприклад brew install hadolint) або Docker (див. docker.mdc).`,
|
|
78
|
-
via: 'docker'
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const ok = docker.status === 0
|
|
49
|
+
const local = spawnSync(hadolintPath, [rel], {
|
|
50
|
+
cwd: root,
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
maxBuffer: 10 * 1024 * 1024
|
|
53
|
+
})
|
|
82
54
|
return {
|
|
83
|
-
ok,
|
|
84
|
-
stdout:
|
|
85
|
-
stderr:
|
|
86
|
-
via: '
|
|
55
|
+
ok: local.status === 0,
|
|
56
|
+
stdout: local.stdout ?? '',
|
|
57
|
+
stderr: local.stderr ?? '',
|
|
58
|
+
via: 'hadolint'
|
|
87
59
|
}
|
|
88
60
|
}
|