@nitra/cursor 3.22.0 → 3.23.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/.pi-template/extensions/n-cursor-adr/docs/index.md +181 -0
- package/AGENTS.template.md +4 -0
- package/CHANGELOG.md +37 -3
- package/bin/docs/n-cursor.md +636 -0
- package/bin/docs/rename-yaml-extensions.md +207 -0
- package/bin/n-cursor.js +30 -3
- package/package.json +1 -1
- package/rules/abie/docs/fix.md +18 -0
- package/rules/abie/js/docs/applies.md +26 -0
- package/rules/abie/js/docs/env_dns.md +32 -0
- package/rules/abie/js/docs/firebase_hosting.md +23 -0
- package/rules/abie/js/docs/hc_pairing.md +35 -0
- package/rules/abie/js/docs/ua_http_route.md +28 -0
- package/rules/abie/js/docs/ua_node_selector.md +28 -0
- package/rules/abie/lib/docs/enabled.md +29 -0
- package/rules/abie/lib/docs/env-dns.md +35 -0
- package/rules/abie/lib/docs/hc-yaml.md +33 -0
- package/rules/abie/lib/docs/http-route.md +44 -0
- package/rules/abie/lib/docs/k8s-tree.md +40 -0
- package/rules/abie/lib/docs/kustomization-patches.md +47 -0
- package/rules/abie/lib/docs/overlay-paths.md +38 -0
- package/rules/abie/lib/docs/yaml.md +29 -0
- package/rules/adr/docs/fix.md +148 -0
- package/rules/adr/js/docs/hooks.md +259 -0
- package/rules/bun/docs/fix.md +156 -0
- package/rules/bun/js/docs/layout.md +393 -0
- package/rules/capacitor/docs/fix.md +121 -0
- package/rules/capacitor/js/docs/platforms.md +295 -0
- package/rules/changelog/changelog.mdc +4 -2
- package/rules/changelog/docs/fix.md +174 -0
- package/rules/changelog/js/consistency.mjs +114 -13
- package/rules/changelog/js/docs/consistency.md +387 -0
- package/rules/changelog/lib/docs/package-manifest.md +210 -0
- package/rules/ci4/docs/fix.md +179 -0
- package/rules/ci4/js/docs/marksman_config.md +128 -0
- package/rules/docker/docker.mdc +8 -3
- package/rules/docker/docs/fix.md +171 -0
- package/rules/docker/js/docs/lint.md +258 -0
- package/rules/docker/lib/docs/docker-hadolint.md +184 -0
- package/rules/docker/lib/docs/docker-mirror.md +247 -0
- package/rules/docker/lib/docs/docker-native-addon.md +170 -0
- package/rules/docker/lib/docs/docker-nginx-user.md +219 -0
- package/rules/docker/lint/docs/lint.md +193 -0
- package/rules/efes/docs/fix.md +203 -0
- package/rules/feedback/docs/fix.md +140 -0
- package/rules/flow/docs/fix.md +152 -0
- package/rules/ga/docs/fix.md +158 -0
- package/rules/ga/js/docs/lint.md +100 -0
- package/rules/ga/js/docs/workflows.md +217 -0
- package/rules/ga/lint/docs/lint.md +209 -0
- package/rules/ga/policy/clean_merged_branch/clean_merged_branch.rego +11 -2
- package/rules/ga/policy/clean_merged_branch/template/clean-merged-branch.yml.snippet.yml +3 -4
- package/rules/graphql/docs/fix.md +126 -0
- package/rules/graphql/js/docs/tooling.md +264 -0
- package/rules/graphql/lib/docs/graphql-gql-scan.md +219 -0
- package/rules/hasura/docs/fix.md +120 -0
- package/rules/hasura/hasura.mdc +14 -0
- package/rules/hasura/js/docs/internal_urls.md +326 -0
- package/rules/image-avif/docs/fix.md +132 -0
- package/rules/image-avif/js/docs/avif_generation.md +241 -0
- package/rules/image-compress/docs/fix.md +150 -0
- package/rules/image-compress/js/docs/package_setup.md +191 -0
- package/rules/js-bun-db/docs/fix.md +148 -0
- package/rules/js-bun-db/js/docs/safety.md +231 -0
- package/rules/js-bun-db/js-bun-db.mdc +42 -13
- package/rules/js-bun-db/lib/docs/bun-sql-scan.md +347 -0
- package/rules/js-bun-redis/docs/fix.md +123 -0
- package/rules/js-bun-redis/js/docs/imports.md +176 -0
- package/rules/js-bun-redis/lib/docs/redis-imports.md +223 -0
- package/rules/js-lint/docs/fix.md +117 -0
- package/rules/js-lint/js/docs/lint.md +250 -0
- package/rules/js-lint/js/docs/tooling.md +348 -0
- package/rules/js-lint/js/docs/utils_imports.md +207 -0
- package/rules/js-lint-ci/docs/fix.md +154 -0
- package/rules/js-lint-ci/js/docs/lint.md +144 -0
- package/rules/js-mssql/docs/fix.md +128 -0
- package/rules/js-mssql/js/docs/deps.md +263 -0
- package/rules/js-mssql/lib/docs/mssql-pool-scan.md +367 -0
- package/rules/js-run/docs/fix.md +144 -0
- package/rules/js-run/js/docs/runtime.md +388 -0
- package/rules/js-run/lib/docs/bunyan-imports.md +117 -0
- package/rules/js-run/lib/docs/check-env-scan.md +433 -0
- package/rules/js-run/lib/docs/conn-file-rules.md +300 -0
- package/rules/js-run/lib/docs/conn-imports-scan.md +204 -0
- package/rules/js-run/lib/docs/promise-settimeout-scan.md +326 -0
- package/rules/k8s/docs/fix.md +129 -0
- package/rules/k8s/js/docs/manifests.md +344 -0
- package/rules/k8s/js/manifests.mjs +6 -2
- package/rules/k8s/k8s.mdc +4 -2
- package/rules/k8s/lint/docs/lint.md +411 -0
- package/rules/k8s/policy/network_policy/template/deployment.snippet.yaml +2 -0
- package/rules/k8s/policy/network_policy/template/stateful-set.snippet.yaml +2 -0
- package/rules/nginx-default-tpl/docs/fix.md +124 -0
- package/rules/nginx-default-tpl/js/docs/template.md +378 -0
- package/rules/npm-module/docs/fix.md +98 -0
- package/rules/npm-module/js/docs/package_structure.md +274 -0
- package/rules/npm-module/js/docs/rule_meta.md +137 -0
- package/rules/npm-module/js/docs/skill_meta.md +190 -0
- package/rules/php/docs/fix.md +107 -0
- package/rules/php/js/docs/tooling.md +152 -0
- package/rules/php/lint/docs/lint.md +215 -0
- package/rules/python/docs/fix.md +163 -0
- package/rules/python/js/docs/applies.md +108 -0
- package/rules/python/js/docs/tooling.md +153 -0
- package/rules/python/lint/docs/lint.md +322 -0
- package/rules/rego/docs/fix.md +121 -0
- package/rules/rego/js/docs/applies.md +174 -0
- package/rules/rego/js/docs/lint.md +118 -0
- package/rules/rego/lint/docs/lint.md +204 -0
- package/rules/release/docs/change.md +185 -0
- package/rules/release/docs/fix.md +119 -0
- package/rules/release/docs/release.md +222 -0
- package/rules/release/lib/docs/aggregate.md +246 -0
- package/rules/release/lib/docs/change-file.md +200 -0
- package/rules/release/lib/docs/fallback.md +203 -0
- package/rules/rust/docs/fix.md +129 -0
- package/rules/rust/js/docs/applies.md +140 -0
- package/rules/rust/lib/docs/has-cargo-toml.md +130 -0
- package/rules/security/docs/fix.md +86 -0
- package/rules/security/js/docs/lint.md +171 -0
- package/rules/security/js/docs/sample_secret.md +190 -0
- package/rules/security/js/docs/trufflehog.md +137 -0
- package/rules/security/js/lint.mjs +9 -1
- package/rules/style-lint/docs/fix.md +155 -0
- package/rules/style-lint/js/docs/lint.md +184 -0
- package/rules/style-lint/js/docs/tooling.md +194 -0
- package/rules/tauri/docs/fix.md +158 -0
- package/rules/tauri/js/docs/cargo_mutants_config.md +168 -0
- package/rules/tauri/js/docs/tooling.md +228 -0
- package/rules/test/coverage/coverage.mjs +15 -3
- package/rules/test/docs/fix.md +132 -0
- package/rules/test/js/data/stryker_config/docs/stryker-vue-macros-ignorer.md +138 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.baseline.md +134 -0
- package/rules/test/js/data/stryker_config/docs/stryker.config.vue.baseline.md +160 -0
- package/rules/test/js/data/vitest_config/docs/vitest.config.baseline.md +195 -0
- package/rules/test/js/docs/cargo_mutants_config.md +173 -0
- package/rules/test/js/docs/location.md +136 -0
- package/rules/test/js/docs/no-process-chdir.md +160 -0
- package/rules/test/js/docs/no-relative-fs-path.md +271 -0
- package/rules/test/js/docs/stryker_config.md +152 -0
- package/rules/test/js/docs/vitest-config-pool-forks.md +174 -0
- package/rules/text/docs/fix.md +118 -0
- package/rules/text/js/docs/forbidden-prettier.md +143 -0
- package/rules/text/js/docs/formatting.md +256 -0
- package/rules/text/js/docs/lint.md +122 -0
- package/rules/text/lint/docs/lint.md +220 -0
- package/rules/text/lint/docs/run-dotenv-linter.md +157 -0
- package/rules/text/lint/docs/run-shellcheck.md +212 -0
- package/rules/text/lint/docs/run-v8r.md +197 -0
- package/rules/vue/docs/fix.md +127 -0
- package/rules/vue/js/docs/packages.md +335 -0
- package/rules/vue/lib/docs/vue-forbidden-imports.md +261 -0
- package/rules/worktree/docs/fix.md +161 -0
- package/schemas/rule-meta.json +5 -1
- package/scripts/auto-rules.mjs +7 -4
- package/scripts/coverage-classify/docs/apply.md +202 -0
- package/scripts/coverage-classify/docs/cache.md +203 -0
- package/scripts/coverage-classify/docs/index.md +218 -0
- package/scripts/coverage-classify/docs/prompt.md +132 -0
- package/scripts/coverage-classify/docs/verdict-schema.md +169 -0
- package/scripts/coverage-fix-extract.mjs +122 -0
- package/scripts/coverage-fix.mjs +1 -1
- package/scripts/dispatcher/docs/graph.md +346 -0
- package/scripts/dispatcher/docs/index.md +236 -0
- package/scripts/dispatcher/docs/trace.md +296 -0
- package/scripts/dispatcher/index.mjs +1 -1
- package/scripts/dispatcher/lib/active.mjs +4 -8
- package/scripts/dispatcher/lib/commands.mjs +7 -11
- package/scripts/dispatcher/lib/docs/active.md +348 -0
- package/scripts/dispatcher/lib/docs/artifact.md +232 -0
- package/scripts/dispatcher/lib/docs/budget.md +167 -0
- package/scripts/dispatcher/lib/docs/capability.md +196 -0
- package/scripts/dispatcher/lib/docs/commands.md +210 -0
- package/scripts/dispatcher/lib/docs/events.md +182 -0
- package/scripts/dispatcher/lib/docs/executor.md +190 -0
- package/scripts/dispatcher/lib/docs/flow-lock.md +161 -0
- package/scripts/dispatcher/lib/docs/flow-resolve.md +267 -0
- package/scripts/dispatcher/lib/docs/gate.md +231 -0
- package/scripts/dispatcher/lib/docs/level.md +335 -0
- package/scripts/dispatcher/lib/docs/plan-panel.md +181 -0
- package/scripts/dispatcher/lib/docs/plan.md +200 -0
- package/scripts/dispatcher/lib/docs/planner.md +269 -0
- package/scripts/dispatcher/lib/docs/review.md +255 -0
- package/scripts/dispatcher/lib/docs/reviewer.md +240 -0
- package/scripts/dispatcher/lib/docs/snapshot.md +247 -0
- package/scripts/dispatcher/lib/docs/spec.md +203 -0
- package/scripts/dispatcher/lib/docs/state-store.md +303 -0
- package/scripts/dispatcher/lib/docs/subagent-runner.md +173 -0
- package/scripts/dispatcher/lib/executor.mjs +6 -1
- package/scripts/dispatcher/lib/flow-resolve.mjs +3 -1
- package/scripts/dispatcher/lib/level.mjs +29 -3
- package/scripts/dispatcher/lib/review.mjs +1 -1
- package/scripts/dispatcher/lib/subagent-runner.mjs +5 -3
- package/scripts/docs/auto-rules.md +376 -0
- package/scripts/docs/auto-skills.md +173 -0
- package/scripts/docs/build-agents-commands.md +183 -0
- package/scripts/docs/cli-entry.md +153 -0
- package/scripts/docs/coverage-fix.md +177 -0
- package/scripts/docs/ensure-nitra-cursor-dev-dependencies.md +189 -0
- package/scripts/lib/changed-files.mjs +4 -1
- package/scripts/lib/docs/changed-files.md +149 -0
- package/scripts/lib/docs/check-mdc-template-refs.md +222 -0
- package/scripts/lib/docs/check-reporter.md +175 -0
- package/scripts/lib/docs/discover-check-rules-from-cursor.md +157 -0
- package/scripts/lib/docs/discover-checkable-rules.md +165 -0
- package/scripts/lib/docs/ensure-tool.md +254 -0
- package/scripts/lib/docs/generated-markdown.md +275 -0
- package/scripts/lib/docs/gha-workflow.md +326 -0
- package/scripts/lib/docs/inline-template-links.md +303 -0
- package/scripts/lib/docs/list-rule-ids.md +156 -0
- package/scripts/lib/docs/load-cursor-config.md +147 -0
- package/scripts/lib/docs/mirror-parity.md +167 -0
- package/scripts/lib/worktree.mjs +26 -0
- package/scripts/worktree-cli.mjs +12 -2
- package/skills/coverage-fix/SKILL.md +34 -45
- package/skills/docgen/SKILL.md +44 -23
- package/skills/docgen/bench/etalon/firebase_hosting.md +19 -0
- package/skills/docgen/bench/etalon/k8s-tree.md +24 -0
- package/skills/docgen/bench/etalon/overlay-paths.md +24 -0
- package/skills/docgen/js/docgen-ignore.mjs +54 -0
- package/skills/docgen/js/docgen-scan.mjs +37 -21
- package/skills/llm-patch/SKILL.md +23 -2
- package/skills/start-check/SKILL.md +26 -53
- package/skills/start-check/js/check.mjs +211 -0
- package/skills/taze/SKILL.md +9 -3
- package/skills/taze/js/diff.mjs +154 -0
- package/types/bin/n-cursor.d.ts +1 -1
- package/skills/fix-tests/SKILL.md +0 -119
- package/skills/fix-tests/meta.json +0 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# consistency.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `consistency.mjs` реалізує перевірку правила `n-changelog` для монорепозиторіїв із кількома воркспейсами (npm та Python). Його завдання — гарантувати, що **будь-яка реліз-релевантна зміна у воркспейсі супроводжується change-файлом** (`<ws>/.changes/*.md`), а поле `version` у маніфесті воркспейсу не зміщене ручним bump-ом поза CI.
|
|
6
|
+
|
|
7
|
+
Ключові інваріанти, що їх стверджує перевірка:
|
|
8
|
+
|
|
9
|
+
- `version` не повинен дрейфувати відносно бази (опублікованої у реєстрі версії або версії в git-базі гілки). Будь-який ручний bump — **fail**, навіть якщо присутній change-файл.
|
|
10
|
+
- Bump `version` і генерацію `CHANGELOG.md` виконує **виключно** `n-cursor release` у CI на гілці `main`.
|
|
11
|
+
- Релевантні зміни без change-файлу — **fail**; зміни лише в інверсних шляхах (`docs/`, `doc/`, `.cursor/`, `.claude/`) — змінами не вважаються.
|
|
12
|
+
- npm-пакети, що публікують `CHANGELOG.md` разом із пакетом, повинні мати рядок `"CHANGELOG.md"` у масиві `files` маніфесту, але це перевіряється лише за наявності pending change-файлів.
|
|
13
|
+
|
|
14
|
+
Передбачено дві моделі визначення бази на рівні воркспейсу:
|
|
15
|
+
|
|
16
|
+
1. **registry-published** — npm-пакети з `name` і `files`, не `private`; Python-проєкти зі статичною `project.version` і `project.name`. База — версія, опублікована в npm-реєстрі або PyPI.
|
|
17
|
+
2. **local-only** — приватні npm без `files`, Python без імені/версії для реєстру. База визначається через git:
|
|
18
|
+
- feature-гілка → `merge-base` з `dev`, інакше з `main`;
|
|
19
|
+
- гілка `main` → diff від `origin/main` (або `HEAD~1` без remote);
|
|
20
|
+
- гілка `dev` → перевірка пропускається (крім незакомічених registry-published).
|
|
21
|
+
|
|
22
|
+
Усі виклики `git` і зовнішні HTTP/CLI — через `execFile` / `fetch`, без shell-інтерполяції (безпека, виключає command injection).
|
|
23
|
+
|
|
24
|
+
## Експорти / API
|
|
25
|
+
|
|
26
|
+
| Експорт | Тип | Призначення |
|
|
27
|
+
| -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `check(opts?)` | `async function` | Єдина публічна точка входу. Запускає весь цикл перевірок для всіх воркспейсів монорепо і повертає exit-код. |
|
|
29
|
+
|
|
30
|
+
Сигнатура `check`:
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
export async function check(opts = {}): Promise<number>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`opts`:
|
|
37
|
+
|
|
38
|
+
- `opts.getPublishedVersion?: (name: string, kind?: 'npm' | 'python') => Promise<string | null>` — перевизначення стандартного резолвера опублікованої версії (для юніт-тестів, оффлайн-режимів).
|
|
39
|
+
- `opts.cwd?: string` — корінь репозиторію; за замовчуванням `process.cwd()`.
|
|
40
|
+
- `opts.autofix?: boolean` — autofix-режим. За замовчуванням береться з env `N_CURSOR_CHANGELOG_AUTOFIX === '1'` (виставляє лише крок `npm-changelog` у `hk.pkl` для pre-commit). Коли увімкнено, замість `fail` на відсутній change-файл правило створює його через `writeChange()` з дефолтами (`bump=patch`, `section=Changed`, `message` = subject останнього коміту, fallback — назва гілки чи `оновлення`) і ставить у git-індекс (`git add`), тож коміт не падає. **Жодної мережі:** у autofix-режимі published-перевірка пропускає реєстровий резолв (`npm view` / PyPI fetch) і drift-перевірку version vs опублікована — лишається лише наявність change-файлу та git-diff. Ручний bump `version` у хуці не ловиться; його далі ловить CI та ручний `fix changelog` без env. Поза хуком режим вимкнено — поведінка лишається fail-on-missing з повною drift-перевіркою, щоб CI не плодив артефактів.
|
|
41
|
+
|
|
42
|
+
Повертає **exit-код** (0 — pass, ≠ 0 — fail), отриманий від `createCheckReporter()`.
|
|
43
|
+
|
|
44
|
+
## Функції
|
|
45
|
+
|
|
46
|
+
### `gitOrNull(args, cwd)`
|
|
47
|
+
|
|
48
|
+
- **Сигнатура:** `async (args: string[], cwd: string) => Promise<string | null>`
|
|
49
|
+
- **Параметри:** `args` — аргументи `git`; `cwd` — робочий каталог процесу.
|
|
50
|
+
- **Повертає:** `stdout` команди або `null` при будь-якій помилці.
|
|
51
|
+
- **Side effects:** виконує дочірній процес `git` через `execFile`.
|
|
52
|
+
|
|
53
|
+
Тиха обгортка над `git`, що ковтає виключення — використовується скрізь, де відсутність гілки/ref/маніфесту є штатним кейсом.
|
|
54
|
+
|
|
55
|
+
### `isInsideGitRepo(cwd)`
|
|
56
|
+
|
|
57
|
+
- **Сигнатура:** `async (cwd: string) => Promise<boolean>`
|
|
58
|
+
- **Повертає:** `true`, якщо `cwd` всередині git working tree.
|
|
59
|
+
- **Side effects:** запит `git rev-parse --is-inside-work-tree`.
|
|
60
|
+
|
|
61
|
+
### `currentBranchName(cwd)`
|
|
62
|
+
|
|
63
|
+
- **Сигнатура:** `async (cwd: string) => Promise<string | null>`
|
|
64
|
+
- **Повертає:** ім'я поточної гілки (`git rev-parse --abbrev-ref HEAD`) або `null`.
|
|
65
|
+
|
|
66
|
+
### `baseRefLabel(ref)`
|
|
67
|
+
|
|
68
|
+
- **Сигнатура:** `(ref: string) => string`
|
|
69
|
+
- **Параметри:** `ref` — git-ref.
|
|
70
|
+
- **Повертає:** човничок без префіксу `origin/` (наприклад, `origin/main` → `main`); інакше повертає `ref` без змін.
|
|
71
|
+
- **Side effects:** немає.
|
|
72
|
+
|
|
73
|
+
### `isGitAncestor(ancestor, descendant, cwd)`
|
|
74
|
+
|
|
75
|
+
- **Сигнатура:** `async (ancestor: string, descendant: string, cwd: string) => Promise<boolean>`
|
|
76
|
+
- **Повертає:** `true`, якщо `ancestor` є предком `descendant` (через `git merge-base --is-ancestor`).
|
|
77
|
+
- **Зауваження:** `git merge-base --is-ancestor` повертає exit-код, тому всередині використовується `gitOrNull`, який ловить ненульовий exit і повертає `null` — у такому випадку результат функції `false`.
|
|
78
|
+
|
|
79
|
+
### `resolveBranchRef(branchName, cwd)`
|
|
80
|
+
|
|
81
|
+
- **Сигнатура:** `async (branchName: string, cwd: string) => Promise<string | null>`
|
|
82
|
+
- **Поведінка:** для `branchName` пробує спочатку локальний ref, потім `origin/<branchName>`; повертає перший, що верифікується через `git rev-parse --verify --quiet`.
|
|
83
|
+
|
|
84
|
+
### `isChangelogIgnoredPath(relPath)`
|
|
85
|
+
|
|
86
|
+
- **Сигнатура:** `(relPath: string) => boolean`
|
|
87
|
+
- **Поведінка:** нормалізує шлях до posix (заміна `\` на `/`, обрізання провідного `./`), повертає `true`, якщо починається з одного з префіксів `CHANGELOG_IGNORE_PATH_PREFIXES`.
|
|
88
|
+
|
|
89
|
+
### `isPathGitIgnored(relPath, cwd)`
|
|
90
|
+
|
|
91
|
+
- **Сигнатура:** `async (relPath: string, cwd: string) => Promise<boolean>`
|
|
92
|
+
- **Поведінка:** виконує `git check-ignore -q -- <relPath>`. Exit-код 0 → ignored (повертає `true`); будь-яка помилка → `false`.
|
|
93
|
+
- **Side effects:** дочірній процес `git`.
|
|
94
|
+
|
|
95
|
+
### `resolveMergeBase(baseRef, cwd)`
|
|
96
|
+
|
|
97
|
+
- **Сигнатура:** `async (baseRef: string, cwd: string) => Promise<string | null>`
|
|
98
|
+
- **Повертає:** SHA `git merge-base baseRef HEAD` або `null`.
|
|
99
|
+
|
|
100
|
+
### `resolveChangelogComparisonPoint(branch, cwd)`
|
|
101
|
+
|
|
102
|
+
- **Сигнатура:** `async (branch: string | null, cwd: string) => Promise<{ ref: string, label: string } | null>`
|
|
103
|
+
- **Логіка:**
|
|
104
|
+
- якщо `branch === 'dev'` → `null` (local-only пропускається);
|
|
105
|
+
- якщо `branch === 'main'`:
|
|
106
|
+
- якщо `origin/main` верифіковано і `origin/main === HEAD` або `origin/main` — предок `HEAD` → `{ ref: 'origin/main', label: 'main' }`;
|
|
107
|
+
- інакше `HEAD~1` → `{ ref: <sha>, label: 'main~1' }`;
|
|
108
|
+
- якщо ні те, ні те — `null`.
|
|
109
|
+
- feature-гілки: ітерує по `FEATURE_BASE_BRANCH_CANDIDATES` (`['dev', 'main']`); перший, для якого вдається резолвити ref **і** обчислити merge-base, дає `{ ref: <merge-base SHA>, label: baseRefLabel(...) }`.
|
|
110
|
+
- **Повертає:** опис точки порівняння (`ref` для `git diff`/`git show`, `label` для повідомлень) або `null`.
|
|
111
|
+
|
|
112
|
+
### `pathspecForWorkspace(ws, subWorkspaces)`
|
|
113
|
+
|
|
114
|
+
- **Сигнатура:** `(ws: string, subWorkspaces: string[]) => string[]`
|
|
115
|
+
- **Поведінка:**
|
|
116
|
+
- для `ws !== '.'` → `[\`${ws}/\`]`;
|
|
117
|
+
- для `ws === '.'` (корінь монорепо) → `['.', ':(exclude)<sub>/' для кожного підворкспейсу]`, щоб залишити лише файли кореня без вкладених воркспейсів.
|
|
118
|
+
- **Повертає:** масив pathspec-ів для передачі в `git diff -- <pathspec>`.
|
|
119
|
+
|
|
120
|
+
### `splitNulPaths(nulSeparated)`
|
|
121
|
+
|
|
122
|
+
- **Сигнатура:** `(nulSeparated: string | null) => string[]`
|
|
123
|
+
- **Поведінка:** ділить вхідний рядок по `\0`, відкидає порожні елементи.
|
|
124
|
+
- **Чому `-z`:** без прапорця git застосовує `core.quotePath` і повертає не-ASCII імена (наприклад, кирилицю) у C-quoted формі (`"docs/\320\262..."`), що ламає префіксне порівняння для `CHANGELOG_IGNORE_PATH_PREFIXES`.
|
|
125
|
+
|
|
126
|
+
### `listChangedPathsAgainstBase(baseRef, pathspec, cwd)`
|
|
127
|
+
|
|
128
|
+
- **Сигнатура:** `async (baseRef: string, pathspec: string[], cwd: string) => Promise<string[]>`
|
|
129
|
+
- **Поведінка:** об'єднує два джерела через `Set`:
|
|
130
|
+
- `git diff --name-only -z <baseRef> -- <pathspec>` — закомічені/staged зміни;
|
|
131
|
+
- `git ls-files --others --exclude-standard -z -- <pathspec>` — нові untracked-файли.
|
|
132
|
+
- **Повертає:** дедуплікований масив відносних шляхів.
|
|
133
|
+
|
|
134
|
+
### `workspaceHasRelevantChangesAgainstBase(baseRef, ws, subWorkspaces, cwd)`
|
|
135
|
+
|
|
136
|
+
- **Сигнатура:** `async (baseRef: string, ws: string, subWorkspaces: string[], cwd: string) => Promise<boolean>`
|
|
137
|
+
- **Поведінка:** обчислює pathspec для `ws`, отримує всі змінені шляхи, ітерує по них:
|
|
138
|
+
- інверсія (`docs/`, `.cursor/`, ...) → пропустити;
|
|
139
|
+
- `git check-ignore` → пропустити;
|
|
140
|
+
- інакше — повернути `true`.
|
|
141
|
+
- **Повертає:** `true`, якщо є хоч один шлях, що вважається релевантною змінною.
|
|
142
|
+
|
|
143
|
+
### `readBaseVersion(baseRef, manifest, cwd)`
|
|
144
|
+
|
|
145
|
+
- **Сигнатура:** `async (baseRef: string, manifest: PackageManifest, cwd: string) => Promise<string | null>`
|
|
146
|
+
- **Поведінка:** виконує `git show <baseRef>:<wsPath>`, де `wsPath` — шлях до маніфесту відносно репозиторію; парсить:
|
|
147
|
+
- `npm` → `JSON.parse(...).version` (`null` при помилці парсу);
|
|
148
|
+
- `python` → `parsePyprojectFields(out).version`.
|
|
149
|
+
- **Повертає:** версію з маніфесту на `baseRef` або `null`.
|
|
150
|
+
|
|
151
|
+
### `defaultGetPublishedNpmVersion(name)`
|
|
152
|
+
|
|
153
|
+
- **Сигнатура:** `async (name: string) => Promise<string | null>`
|
|
154
|
+
- **Поведінка:** `npm view <name> version` із таймаутом `REGISTRY_TIMEOUT_MS` (10 с). Trim і повернення; пуста відповідь / помилка → `null`.
|
|
155
|
+
- **Side effects:** дочірній процес `npm`, мережа.
|
|
156
|
+
|
|
157
|
+
### `defaultGetPublishedPyPiVersion(name)`
|
|
158
|
+
|
|
159
|
+
- **Сигнатура:** `async (name: string) => Promise<string | null>`
|
|
160
|
+
- **Поведінка:** `fetch('https://pypi.org/pypi/<encodedName>/json', { signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS) })`; читає `data.info.version`. Будь-яка помилка / `!res.ok` → `null`.
|
|
161
|
+
- **Side effects:** мережа.
|
|
162
|
+
|
|
163
|
+
### `resolvePublishedVersion(manifest, getPublishedVersion)`
|
|
164
|
+
|
|
165
|
+
- **Сигнатура:** `(manifest: PackageManifest, getPublishedVersion) => Promise<string | null>`
|
|
166
|
+
- **Поведінка:** якщо в маніфесті немає `name` → `Promise.resolve(null)`; інакше делегує до `getPublishedVersion(name, kind)`.
|
|
167
|
+
|
|
168
|
+
### `defaultGetPublishedVersion(name, kind = 'npm')`
|
|
169
|
+
|
|
170
|
+
- **Сигнатура:** `(name: string, kind?: 'npm' | 'python') => Promise<string | null>`
|
|
171
|
+
- **Поведінка:** диспетчер за `kind` (Python → PyPI, інакше — npm).
|
|
172
|
+
|
|
173
|
+
### `createDefaultGetPublishedVersion()`
|
|
174
|
+
|
|
175
|
+
- **Сигнатура:** `() => (name, kind?) => Promise<string | null>`
|
|
176
|
+
- **Поведінка:** фабрика, що повертає `defaultGetPublishedVersion`. Використовується як дефолт у `check` для зручної підміни в тестах.
|
|
177
|
+
|
|
178
|
+
### `checkNpmFilesArrayContainsChangelog(manifest, pass, fail)`
|
|
179
|
+
|
|
180
|
+
- **Сигнатура:** `(manifest: PackageManifest, pass: (msg)=>void, fail: (msg)=>void) => void`
|
|
181
|
+
- **Поведінка:**
|
|
182
|
+
- якщо `kind !== 'npm'` або `npmFiles` відсутній — рання терміновка;
|
|
183
|
+
- `pass`, якщо `npmFiles` містить `'CHANGELOG.md'`;
|
|
184
|
+
- інакше `fail` з рекомендацією додати рядок.
|
|
185
|
+
|
|
186
|
+
### `workspaceLabel(manifest)`
|
|
187
|
+
|
|
188
|
+
- **Сигнатура:** `(manifest: PackageManifest) => string`
|
|
189
|
+
- **Повертає:** `'<root>'` для `ws === '.'`, інакше `manifest.ws`.
|
|
190
|
+
|
|
191
|
+
### `missingChangeFileMessage(label, mf)`
|
|
192
|
+
|
|
193
|
+
- **Сигнатура:** `(label: string, mf: string) => string`
|
|
194
|
+
- **Повертає:** уніфікований текст для `fail` про відсутній change-файл, включно з інструкцією для `npx @nitra/cursor change`.
|
|
195
|
+
|
|
196
|
+
### `hasPendingChangeFiles(ws, cwd)`
|
|
197
|
+
|
|
198
|
+
- **Сигнатура:** `async (ws: string, cwd: string) => Promise<boolean>`
|
|
199
|
+
- **Поведінка:** `(await readChangeFiles(ws, cwd)).length > 0`.
|
|
200
|
+
|
|
201
|
+
### `checkPublishedWorkspacePendingGitChanges(manifest, _Vcurrent, subWorkspaces, pass, fail, cwd)`
|
|
202
|
+
|
|
203
|
+
- **Сигнатура:** `async (...) => Promise<void>`
|
|
204
|
+
- **Параметр `_Vcurrent`:** ігнорується (залишений для сумісності сигнатури; bump робить CI).
|
|
205
|
+
- **Поведінка:**
|
|
206
|
+
1. Якщо `hasPendingChangeFiles` → `pass` про change-файл(и) + перевірка `CHANGELOG.md` у `files` npm-маніфесту. Вихід.
|
|
207
|
+
2. Якщо не в git-репі — вихід без перевірок.
|
|
208
|
+
3. Беремо `currentBranchName`:
|
|
209
|
+
- `branch === 'dev'`: лише перевірка наявності релевантних змін відносно `HEAD` (staged/working tree). Є — `fail` `missingChangeFileMessage`; нема — мовчазний вихід.
|
|
210
|
+
- інакше: резолвимо `comparison`; якщо `comparison` + є зміни відносно `comparison.ref` → `fail`.
|
|
211
|
+
- на `main` додатково перевіряємо ще й `HEAD` (working/staged) — `fail`, якщо є зміни.
|
|
212
|
+
|
|
213
|
+
### `checkPublishedWorkspace(manifest, subWorkspaces, getPublishedVersion, pass, fail, cwd)`
|
|
214
|
+
|
|
215
|
+
- **Сигнатура:** `async (...) => Promise<void>`
|
|
216
|
+
- **Поведінка:**
|
|
217
|
+
1. `manifest.version` відсутній → `fail` («у маніфесті відсутнє поле version»). Вихід.
|
|
218
|
+
2. `manifest.name` відсутній → `fail` («відсутнє ім'я пакета»). Вихід.
|
|
219
|
+
3. `Vpublished = resolvePublishedVersion(...)`; якщо `null` → `pass` («опублікована версія недоступна, перевірку пропущено»). Вихід.
|
|
220
|
+
4. Якщо `Vpublished !== Vcurrent` → `fail` про drift (ручний bump заборонено — навіть із change-файлом). Вихід.
|
|
221
|
+
5. Інакше `pass` про збіг із реєстром і виклик `checkPublishedWorkspacePendingGitChanges`.
|
|
222
|
+
|
|
223
|
+
### `checkLocalOnlyChangedWorkspace(comparisonRef, manifest, baseLabel, pass, fail, cwd)`
|
|
224
|
+
|
|
225
|
+
- **Сигнатура:** `async (...) => Promise<void>`
|
|
226
|
+
- **Поведінка** (виконується для воркспейсів, де `workspaceHasRelevantChangesAgainstBase` дала `true`):
|
|
227
|
+
1. `Vbase = readBaseVersion(comparisonRef, manifest, cwd)`.
|
|
228
|
+
2. Якщо `Vbase && Vcurrent && Vbase !== Vcurrent` → `fail` про drift (`Vbase → Vcurrent`). Вихід.
|
|
229
|
+
3. Якщо `hasPendingChangeFiles` → `pass`. Вихід.
|
|
230
|
+
4. Інакше `fail` `missingChangeFileMessage`.
|
|
231
|
+
- Drift-перевірка йде **перед** перевіркою наявності change-файлу: симетрія з registry-published-шляхом (ручний bump заборонено навіть із change-файлом).
|
|
232
|
+
|
|
233
|
+
### `runLocalOnlyChecks(localOnly, subWorkspaces, pass, fail, cwd)`
|
|
234
|
+
|
|
235
|
+
- **Сигнатура:** `async (localOnly: PackageManifest[], subWorkspaces: string[], pass, fail, cwd) => Promise<void>`
|
|
236
|
+
- **Поведінка:**
|
|
237
|
+
1. Якщо `localOnly` пустий → ранній вихід.
|
|
238
|
+
2. Не git-репозиторій → `pass` про пропуск.
|
|
239
|
+
3. `branch === 'dev'` → `pass` про пропуск.
|
|
240
|
+
4. `comparison` не знайдено (немає `dev`/`main`/`origin/*`) → `pass` про пропуск.
|
|
241
|
+
5. Для кожного `manifest` із `localOnly`: пропустити, якщо немає релевантних змін відносно `comparison.ref`; інакше виставити `checkedAny = true` і викликати `checkLocalOnlyChangedWorkspace`.
|
|
242
|
+
6. Якщо жоден воркспейс не змінено — `pass` («local-only воркспейси без змін відносно `<label>`»).
|
|
243
|
+
|
|
244
|
+
### `check(opts)`
|
|
245
|
+
|
|
246
|
+
- **Сигнатура:** `async (opts?: { getPublishedVersion?, cwd? }) => Promise<number>`
|
|
247
|
+
- **Покрокове виконання:**
|
|
248
|
+
1. Створюється `reporter = createCheckReporter()`; беруться його `pass` і `fail`.
|
|
249
|
+
2. `getPublishedVersion` — з `opts` або `createDefaultGetPublishedVersion()`.
|
|
250
|
+
3. `cwd` — з `opts` або `process.cwd()`.
|
|
251
|
+
4. `workspaces = await getMonorepoProjectRootDirs(cwd)`; `subWorkspaces = workspaces.filter(w => w !== '.')`.
|
|
252
|
+
5. `isMonorepoRoot = subWorkspaces.length > 0` — корінь монорепо вважається glue/конфіг/tooling.
|
|
253
|
+
6. Розділяємо воркспейси на `published` та `localOnly`:
|
|
254
|
+
- корінь `.` за наявності підпакетів → одразу `pass` про пропуск, не читаємо маніфест;
|
|
255
|
+
- `readPackageManifest(ws, cwd)` → якщо `null`, ws пропускається;
|
|
256
|
+
- `manifest.registryPublishable === true` → у `published`, інакше — у `localOnly`.
|
|
257
|
+
7. Послідовно перевіряємо всі `published` через `checkPublishedWorkspace`.
|
|
258
|
+
8. `runLocalOnlyChecks(localOnly, ...)`.
|
|
259
|
+
9. Повертаємо `reporter.getExitCode()`.
|
|
260
|
+
|
|
261
|
+
## Залежності
|
|
262
|
+
|
|
263
|
+
### Стандартна бібліотека Node.js
|
|
264
|
+
|
|
265
|
+
- `node:child_process` → `execFile` — запуск `git`, `npm` без shell-інтерполяції.
|
|
266
|
+
- `node:util` → `promisify` — обгортка `execFileAsync = promisify(execFile)`.
|
|
267
|
+
- Глобальні: `fetch`, `AbortSignal.timeout` — для PyPI (Node ≥ 18).
|
|
268
|
+
|
|
269
|
+
### Внутрішні модулі
|
|
270
|
+
|
|
271
|
+
- `../../../scripts/lib/check-reporter.mjs` → `createCheckReporter` — створює пару `{ pass, fail }` і обчислює `getExitCode()`.
|
|
272
|
+
- `../lib/package-manifest.mjs`:
|
|
273
|
+
- `getMonorepoProjectRootDirs(cwd)` — список воркспейсів (включно з `.`);
|
|
274
|
+
- `manifestFilePath(ws, manifest)` — шлях до маніфесту в повідомленнях;
|
|
275
|
+
- `parsePyprojectFields(text)` — отримання `{ name, version }` із `pyproject.toml`;
|
|
276
|
+
- `readPackageManifest(ws, cwd)` — нормалізований опис воркспейсу (тип `PackageManifest`).
|
|
277
|
+
- `../../release/lib/change-file.mjs` → `readChangeFiles(ws, cwd)` — список pending change-файлів у `<ws>/.changes/`.
|
|
278
|
+
|
|
279
|
+
### Зовнішні системи / процеси
|
|
280
|
+
|
|
281
|
+
- `git` (CLI) — `rev-parse`, `merge-base`, `diff`, `ls-files`, `show`, `check-ignore`.
|
|
282
|
+
- `npm` (CLI) — `npm view <name> version` для registry-published npm-пакетів.
|
|
283
|
+
- PyPI HTTP API — `https://pypi.org/pypi/<name>/json` для Python-пакетів.
|
|
284
|
+
|
|
285
|
+
### Константи модуля
|
|
286
|
+
|
|
287
|
+
- `FEATURE_BASE_BRANCH_CANDIDATES = Object.freeze(['dev', 'main'])` — порядок пошуку бази для feature-гілок.
|
|
288
|
+
- `LOCAL_ONLY_SKIP_BRANCH = 'dev'` — гілка, де local-only перевірка не активна.
|
|
289
|
+
- `CHANGELOG_IGNORE_PATH_PREFIXES = Object.freeze(['docs/', 'doc/', '.cursor/', '.claude/'])` — інверсні префікси (зміни в них не релевантні).
|
|
290
|
+
- `REGISTRY_TIMEOUT_MS = 10_000` — таймаут для `npm view` / PyPI fetch.
|
|
291
|
+
- `LEADING_DOTSLASH_RE = /^\.\//` — для нормалізації шляхів у `isChangelogIgnoredPath`.
|
|
292
|
+
|
|
293
|
+
## Потік виконання / Використання
|
|
294
|
+
|
|
295
|
+
Типовий виклик (із CLI/скрипту):
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
import { check } from './consistency.mjs'
|
|
299
|
+
|
|
300
|
+
const exitCode = await check()
|
|
301
|
+
process.exit(exitCode)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Із кастомним резолвером опублікованої версії (наприклад, у тестах):
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
import { check } from './consistency.mjs'
|
|
308
|
+
|
|
309
|
+
const exitCode = await check({
|
|
310
|
+
cwd: '/tmp/sandbox-repo',
|
|
311
|
+
async getPublishedVersion(name, kind) {
|
|
312
|
+
if (name === '@scope/pkg-a') return '1.0.0'
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Високорівневий потік `check`
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
check(opts)
|
|
322
|
+
├─ createCheckReporter() → { pass, fail, getExitCode }
|
|
323
|
+
├─ getMonorepoProjectRootDirs(cwd) → workspaces
|
|
324
|
+
├─ subWorkspaces = workspaces \ ['.']
|
|
325
|
+
├─ isMonorepoRoot = subWorkspaces.length > 0
|
|
326
|
+
├─ For each ws:
|
|
327
|
+
│ ├─ ws === '.' && isMonorepoRoot → pass (root skipped) ; continue
|
|
328
|
+
│ ├─ manifest = readPackageManifest(ws, cwd)
|
|
329
|
+
│ ├─ !manifest → continue
|
|
330
|
+
│ └─ manifest.registryPublishable ? published.push : localOnly.push
|
|
331
|
+
├─ For each published manifest:
|
|
332
|
+
│ └─ checkPublishedWorkspace(...)
|
|
333
|
+
│ ├─ no version → fail
|
|
334
|
+
│ ├─ no name → fail
|
|
335
|
+
│ ├─ Vpublished == null → pass (skipped)
|
|
336
|
+
│ ├─ drift → fail
|
|
337
|
+
│ └─ checkPublishedWorkspacePendingGitChanges(...)
|
|
338
|
+
│ ├─ hasPendingChangeFiles → pass + checkNpmFilesArrayContainsChangelog
|
|
339
|
+
│ ├─ branch dev → fail iff relevant changes vs HEAD
|
|
340
|
+
│ ├─ comparison ref + relevant changes → fail
|
|
341
|
+
│ └─ main + relevant changes vs HEAD → fail
|
|
342
|
+
├─ runLocalOnlyChecks(localOnly, ...)
|
|
343
|
+
│ ├─ not git → pass (skipped)
|
|
344
|
+
│ ├─ branch dev → pass (skipped)
|
|
345
|
+
│ ├─ no comparison → pass (skipped)
|
|
346
|
+
│ └─ for each localOnly with relevant changes:
|
|
347
|
+
│ └─ checkLocalOnlyChangedWorkspace(...)
|
|
348
|
+
│ ├─ Vbase != Vcurrent → fail (drift)
|
|
349
|
+
│ ├─ hasPendingChangeFiles → pass
|
|
350
|
+
│ └─ else fail (missing change file)
|
|
351
|
+
└─ return reporter.getExitCode()
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Контракти / гарантії
|
|
355
|
+
|
|
356
|
+
- **Безпека:** жодних викликів `exec` / `spawn` із інтерполяцією рядків — лише `execFile` із масивом аргументів.
|
|
357
|
+
- **Idempotency:** функція виконує лише читання (git/fs/network); не змінює нічого на диску.
|
|
358
|
+
- **Деградація:** мережеві / репо-помилки — м'які (повертають `null`); їх результат — `pass` про пропуск, а не `fail`. Виняток: реальні відмінності, які можна спостерігати локально (drift, відсутність change-файлу), завжди дають `fail`.
|
|
359
|
+
- **Симетрія шляхів:** registry-published і local-only обидва ставлять drift-перевірку **перед** перевіркою change-файлу, тому ручний bump поза CI стабільно falsies перевірку незалежно від моделі.
|
|
360
|
+
|
|
361
|
+
### Точки розширення
|
|
362
|
+
|
|
363
|
+
- `opts.getPublishedVersion` — підміна джерела опублікованих версій (стаб для офлайн-тестів або проксі-реєстру).
|
|
364
|
+
- `opts.cwd` — переключення активного репозиторію без `process.chdir`.
|
|
365
|
+
|
|
366
|
+
## Rebuild Test
|
|
367
|
+
|
|
368
|
+
Контрольний перелік для відтворення/верифікації поведінки:
|
|
369
|
+
|
|
370
|
+
1. **Експорт API** — модуль експортує єдину `async function check(opts?)`, що повертає `Promise<number>`.
|
|
371
|
+
2. **Дефолти** — `opts.cwd` за замовчуванням `process.cwd()`; `opts.getPublishedVersion` за замовчуванням `defaultGetPublishedVersion` (npm-view для `kind === 'npm'`, PyPI fetch для `kind === 'python'`).
|
|
372
|
+
3. **Корінь монорепо** — для `ws === '.'` за наявності підворкспейсів виставляється `pass` про пропуск без читання маніфесту.
|
|
373
|
+
4. **Класифікація** — `manifest.registryPublishable === true` → `published`; інакше → `localOnly`. Воркспейси без читабельного маніфесту мовчки пропускаються.
|
|
374
|
+
5. **Drift > change-файл** — для обох моделей перевірка drift `version` спрацьовує **раніше** за перевірку наявності change-файлу і `fail` має пріоритет.
|
|
375
|
+
6. **Гілка `dev`** — `runLocalOnlyChecks` повністю пропускає local-only (`pass`); registry-published у `checkPublishedWorkspacePendingGitChanges` на `dev` перевіряє лише робоче дерево/staged відносно `HEAD`.
|
|
376
|
+
7. **Гілка `main`** — точка порівняння: `origin/main`, якщо це предок `HEAD` або збігається; інакше `HEAD~1`; також додаткова перевірка `HEAD` (working/staged), щоб виявити незакомічені зміни.
|
|
377
|
+
8. **Feature-гілка** — точка порівняння визначається ітерацією по `['dev', 'main']`, береться merge-base першої доступної бази; `label` приводиться до короткої форми (`origin/main` → `main`).
|
|
378
|
+
9. **Інверсні шляхи** — `docs/`, `doc/`, `.cursor/`, `.claude/` (із normalize `\` → `/` і обрізанням `./`) не вважаються релевантними змінами.
|
|
379
|
+
10. **`git -z`** — у `git diff --name-only` та `git ls-files --others` обов'язково використовується `-z`, інакше не-ASCII імена потраплять у C-quoted формі й ламатимуть префіксне порівняння.
|
|
380
|
+
11. **Untracked + tracked** — `listChangedPathsAgainstBase` об'єднує `git diff` (відносно `baseRef`) і `git ls-files --others --exclude-standard`, дедуплікація через `Set`.
|
|
381
|
+
12. **gitignored** — кожен кандидат додатково перевіряється через `git check-ignore -q --`; ігноровані пропускаються.
|
|
382
|
+
13. **`checkNpmFilesArrayContainsChangelog`** — викликається лише в гілці «pending change-файли є» для registry-published; для не-npm або відсутнього `npmFiles` — раннє return без `pass`/`fail`.
|
|
383
|
+
14. **Мовчазний skip** — недоступність опублікованої версії (мережа/реєстр) даює `pass` про пропуск, а не `fail`.
|
|
384
|
+
15. **`workspaceLabel`** — `'<root>'` для `.`, інакше шлях ws.
|
|
385
|
+
16. **`missingChangeFileMessage`** — текст fail містить шлях до маніфесту, інструкцію `npx @nitra/cursor change --bump … --section … --message …` і нагадування «bump зробить CI на main (n-changelog.mdc)».
|
|
386
|
+
17. **Послідовність публічних перевірок** — спершу всі `published` (у порядку, повернутому з `getMonorepoProjectRootDirs`), потім `runLocalOnlyChecks`.
|
|
387
|
+
18. **Exit-код** — повертається з `reporter.getExitCode()` (агрегує всі `pass`/`fail`).
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# package-manifest.mjs
|
|
2
|
+
|
|
3
|
+
## Огляд
|
|
4
|
+
|
|
5
|
+
Модуль `package-manifest.mjs` реалізує **уніфіковану абстракцію маніфесту пакета** для перевірок changelog у багатомовному монорепо. Він приховує відмінності між двома типами маніфестів:
|
|
6
|
+
|
|
7
|
+
- **npm / JS** — файл `package.json` (з полями `name`, `version`, `private`, `files`).
|
|
8
|
+
- **Python** — файл `pyproject.toml` (PEP 621 → таблиця `[project]`, або застаріший формат Poetry → таблиця `[tool.poetry]`).
|
|
9
|
+
|
|
10
|
+
Модуль надає три високорівневі операції:
|
|
11
|
+
|
|
12
|
+
1. **Розбір** `pyproject.toml` для витягання `name` та `version` (через `smol-toml`).
|
|
13
|
+
2. **Читання маніфесту воркспейсу** в єдиній структурі `PackageManifest` (з пріоритетом `package.json` над `pyproject.toml`).
|
|
14
|
+
3. **Виявлення коренів усіх пакетів монорепо**: npm-воркспейси через скрипт `workspaces.mjs` + автоматичний пошук Python-проєктів за `pyproject.toml`.
|
|
15
|
+
|
|
16
|
+
Базова мета — дати правилам категорії `changelog` єдину модель «пакет = (kind, ws, name, version, registryPublishable)», незалежно від мови.
|
|
17
|
+
|
|
18
|
+
## Експорти / API
|
|
19
|
+
|
|
20
|
+
Усі експорти — **іменовані** (`export function …`). Експорту за замовчуванням немає.
|
|
21
|
+
|
|
22
|
+
| Експорт | Тип | Призначення |
|
|
23
|
+
| --------------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
24
|
+
| `parsePyprojectFields(text)` | `(string) => { name: string \| null, version: string \| null }` | Розпарсити сирий TOML-текст `pyproject.toml` і витягти `name` / `version`. |
|
|
25
|
+
| `readPackageManifest(ws, cwd?)` | `(string, string?) => Promise<PackageManifest \| null>` | Прочитати маніфест конкретного воркспейсу (`package.json` або `pyproject.toml`). |
|
|
26
|
+
| `getMonorepoProjectRootDirs(repoRoot?)` | `(string?) => Promise<string[]>` | Перелічити всі каталоги-корені пакетів монорепо (npm + Python). |
|
|
27
|
+
| `manifestFilePath(ws, manifest)` | `(string, PackageManifest) => string` | Зібрати відносний шлях до файлу маніфесту воркспейсу. |
|
|
28
|
+
|
|
29
|
+
### Типи (JSDoc)
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
/** @typedef {'npm' | 'python'} PackageKind */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {object} PackageManifest
|
|
36
|
+
* @property {PackageKind} kind тип маніфесту
|
|
37
|
+
* @property {string} ws відносний шлях воркспейсу ('.' для кореня)
|
|
38
|
+
* @property {string} manifestRel 'package.json' | 'pyproject.toml'
|
|
39
|
+
* @property {string | null} name ім'я пакета (npm / PyPI)
|
|
40
|
+
* @property {string | null} version semver-рядок
|
|
41
|
+
* @property {boolean} registryPublishable чи застосовується режим порівняння з реєстром
|
|
42
|
+
* @property {string[] | null} [npmFiles] лише npm: 'files' з package.json
|
|
43
|
+
*/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Константи модуля
|
|
47
|
+
|
|
48
|
+
- `PYPROJECT_GLOB_IGNORE = ['**/node_modules/**', '**/.git/**', '**/.venv/**', '**/venv/**']` — патерни, що **виключаються** під час сканування `pyproject.toml` глобом, щоб не зачіпати чужі залежності та віртуальні середовища.
|
|
49
|
+
|
|
50
|
+
## Функції
|
|
51
|
+
|
|
52
|
+
### `projectFieldsFromPyprojectDoc(doc)` (внутрішня)
|
|
53
|
+
|
|
54
|
+
- **Сигнатура:** `(doc: unknown) => { name: string | null, version: string | null }`
|
|
55
|
+
- **Параметри:**
|
|
56
|
+
- `doc` — результат `parse` зі `smol-toml` (очікується об'єкт верхнього рівня TOML).
|
|
57
|
+
- **Повертає:** об'єкт `{ name, version }`, де обидва поля — `string` або `null`.
|
|
58
|
+
- **Логіка:**
|
|
59
|
+
1. Якщо `doc` не є чистим об'єктом (`null`, примітив, масив) — повертає `{ null, null }`.
|
|
60
|
+
2. Якщо є таблиця `[project]` (PEP 621): бере `project.name` і `project.version`, але лише якщо вони — `string`; інакше — `null`.
|
|
61
|
+
3. Якщо `[project]` відсутня — пробує застарілу таблицю `[tool.poetry]`: бере `tool.poetry.name` і `tool.poetry.version` за тим самим правилом.
|
|
62
|
+
4. Якщо обидві таблиці відсутні — повертає `{ null, null }`.
|
|
63
|
+
- **Side effects:** немає (чиста функція).
|
|
64
|
+
- **Примітка:** функція не експортується; зовнішній API — це `parsePyprojectFields`, який додає захист від помилок парсингу TOML.
|
|
65
|
+
|
|
66
|
+
### `parsePyprojectFields(text)`
|
|
67
|
+
|
|
68
|
+
- **Сигнатура:** `(text: string) => { name: string | null, version: string | null }`
|
|
69
|
+
- **Параметри:**
|
|
70
|
+
- `text` — повний текст файлу `pyproject.toml`.
|
|
71
|
+
- **Повертає:** ті самі поля, що й `projectFieldsFromPyprojectDoc`.
|
|
72
|
+
- **Логіка:**
|
|
73
|
+
1. Викликає `parseToml(text)` (зі `smol-toml`).
|
|
74
|
+
2. Передає результат у `projectFieldsFromPyprojectDoc`.
|
|
75
|
+
3. На будь-якій помилці парсингу (синтаксис, тощо) — повертає `{ null, null }`. Помилка **проковтується** (catch без логування), щоб некоректний TOML не зривав весь процес перевірки changelog.
|
|
76
|
+
- **Side effects:** немає (CPU-only; жодного IO).
|
|
77
|
+
|
|
78
|
+
### `readPackageManifest(ws, cwd = process.cwd())`
|
|
79
|
+
|
|
80
|
+
- **Сигнатура:** `(ws: string, cwd?: string) => Promise<PackageManifest | null>`
|
|
81
|
+
- **Параметри:**
|
|
82
|
+
- `ws` — відносний шлях воркспейсу від `cwd` (наприклад, `npm/rules/changelog` або `.` для кореня).
|
|
83
|
+
- `cwd` — корінь репозиторію; за замовчуванням `process.cwd()`.
|
|
84
|
+
- **Повертає:** `PackageManifest` або `null`, якщо ні `package.json`, ні `pyproject.toml` у воркспейсі не існують або не парсяться.
|
|
85
|
+
- **Логіка (з пріоритетом npm > python):**
|
|
86
|
+
1. **Гілка npm:**
|
|
87
|
+
- Складає шлях `cwd/ws/package.json` (через `node:path.join`).
|
|
88
|
+
- `existsSync` → якщо файлу немає, переходить до гілки Python.
|
|
89
|
+
- Читає текст через `readFile(pkgPath, 'utf8')` і парсить як JSON.
|
|
90
|
+
- Якщо JSON — не об'єкт (null, масив, примітив), повертає `null`.
|
|
91
|
+
- Обчислює `registryPublishable = name є непорожнім рядком && private !== true && files є масивом`. Тобто пакет вважається «публікованим у реєстр», тільки якщо в `package.json` явно вказано непорожній `name`, не зведено `private: true`, і визначено поле `files` (whitelist того, що публікується).
|
|
92
|
+
- Повертає об'єкт із `kind: 'npm'`, `manifestRel: 'package.json'`, з `name`/`version` (тільки якщо вони — рядки, інакше `null`), `registryPublishable`, `npmFiles = pkg.files` або `null`.
|
|
93
|
+
- Будь-яка помилка читання/парсингу JSON у блоці `try` → повертає `null` (catch без логування).
|
|
94
|
+
2. **Гілка Python:**
|
|
95
|
+
- Складає шлях `cwd/ws/pyproject.toml`.
|
|
96
|
+
- `existsSync` → якщо файлу немає, повертає `null`.
|
|
97
|
+
- Читає файл і викликає `parsePyprojectFields`.
|
|
98
|
+
- `registryPublishable = Boolean(name && version)` — для Python публікація в PyPI потребує лише валідних `name` і `version` (PyPI не має аналога `files` whitelist).
|
|
99
|
+
- Повертає об'єкт із `kind: 'python'`, `manifestRel: 'pyproject.toml'`, `npmFiles: null`.
|
|
100
|
+
- **Side effects:**
|
|
101
|
+
- Синхронний `existsSync` (двічі: на `package.json` і `pyproject.toml`).
|
|
102
|
+
- Асинхронне читання файлу через `fs/promises.readFile`.
|
|
103
|
+
- **Гарантія:** функція **ніколи не кидає** — усі помилки IO/парсингу повертаються як `null`.
|
|
104
|
+
|
|
105
|
+
### `getMonorepoProjectRootDirs(repoRoot = '.')`
|
|
106
|
+
|
|
107
|
+
- **Сигнатура:** `(repoRoot?: string) => Promise<string[]>`
|
|
108
|
+
- **Параметри:**
|
|
109
|
+
- `repoRoot` — корінь репозиторію (за замовчуванням `'.'`).
|
|
110
|
+
- **Повертає:** відсортований масив **унікальних** відносних шляхів воркспейсів — кандидатів на пакет.
|
|
111
|
+
- **Логіка:**
|
|
112
|
+
1. Створює `Set<string>` з результату `getMonorepoPackageRootDirs(repoRoot)` — це npm-воркспейси, оголошені в `package.json` (`workspaces`) кореня.
|
|
113
|
+
2. **Кореневий Python-проєкт:** якщо в корені існує `pyproject.toml`, але **немає** `package.json`, додає `'.'` у множину. Це покриває випадок «репо — чистий Python-проєкт без npm».
|
|
114
|
+
3. **Сканування підкаталогів:** через `fs/promises.glob('**/pyproject.toml', { cwd: repoRoot, ignore: PYPROJECT_GLOB_IGNORE })` ітерує по знайдених файлах:
|
|
115
|
+
- Обчислює абсолютний каталог `dirname(join(repoRoot, relPy))`.
|
|
116
|
+
- Перетворює його на відносний від `repoRoot` шлях; порожній рядок нормалізує до `'.'`.
|
|
117
|
+
- Додає у множину, **тільки якщо** одночасно:
|
|
118
|
+
- воркспейс **не** в чорному списку `isIgnoredWorkspaceRoot(ws)`;
|
|
119
|
+
- у тому самому каталозі **немає** `package.json` (тобто це чисто Python-пакет, а не змішаний).
|
|
120
|
+
4. Фінально фільтрує множину ще раз через `isIgnoredWorkspaceRoot` (захист на випадок, якщо `getMonorepoPackageRootDirs` повернув ігнорований шлях).
|
|
121
|
+
5. **Сортування:** `'.'` завжди першим, далі — лексикографічно за `localeCompare`.
|
|
122
|
+
- **Side effects:**
|
|
123
|
+
- Синхронний `existsSync` (двічі для кореня + по разу для кожного знайденого підкаталогу).
|
|
124
|
+
- Асинхронна ітерація глобом по файловій системі (рекурсивний обхід `repoRoot`).
|
|
125
|
+
- **Чому Python з `package.json` ігнорується:** пріоритет npm — якщо в одному каталозі є обидва файли, він уже потрапить як npm-воркспейс через `getMonorepoPackageRootDirs`; додавати його ще раз як Python — дубль.
|
|
126
|
+
|
|
127
|
+
### `manifestFilePath(ws, manifest)`
|
|
128
|
+
|
|
129
|
+
- **Сигнатура:** `(ws: string, manifest: PackageManifest) => string`
|
|
130
|
+
- **Параметри:**
|
|
131
|
+
- `ws` — відносний шлях воркспейсу.
|
|
132
|
+
- `manifest` — об'єкт `PackageManifest`.
|
|
133
|
+
- **Повертає:** `join(ws, manifest.manifestRel)` — наприклад, `'npm/rules/changelog/package.json'` або `'apps/foo/pyproject.toml'`.
|
|
134
|
+
- **Side effects:** немає (чиста функція над рядками).
|
|
135
|
+
- **Примітка:** не використовує `cwd` — повертає шлях відносний до кореня репо, придатний для логів та повідомлень про помилки.
|
|
136
|
+
|
|
137
|
+
## Залежності
|
|
138
|
+
|
|
139
|
+
### Зовнішні (npm)
|
|
140
|
+
|
|
141
|
+
- **`smol-toml`** — мінімалістичний парсер TOML; імпортується іменований експорт `parse as parseToml`. Використовується тільки в `parsePyprojectFields` для розбору `pyproject.toml`.
|
|
142
|
+
|
|
143
|
+
### Стандартна бібліотека Node.js
|
|
144
|
+
|
|
145
|
+
- **`node:fs`** → `existsSync` — синхронна перевірка існування файлу. Використовується замість асинхронного `access`, бо викликається лінійно (не в гарячому циклі) і простіше у читанні.
|
|
146
|
+
- **`node:fs/promises`** → `glob`, `readFile` — асинхронний обхід FS та читання тексту.
|
|
147
|
+
- **`node:path`** → `dirname`, `join`, `relative` — кросплатформенна робота зі шляхами.
|
|
148
|
+
|
|
149
|
+
### Внутрішні (монорепо)
|
|
150
|
+
|
|
151
|
+
- **`../../../scripts/lib/workspaces.mjs`** (тобто `npm/scripts/lib/workspaces.mjs`):
|
|
152
|
+
- `getMonorepoPackageRootDirs(repoRoot)` — повертає список npm-воркспейсів кореневого `package.json`.
|
|
153
|
+
- `isIgnoredWorkspaceRoot(ws)` — фільтр для воркспейсів, які треба свідомо ігнорувати (`.git`, шаблони, тощо).
|
|
154
|
+
|
|
155
|
+
### Контракти, що **не** імпортуються, але передбачаються
|
|
156
|
+
|
|
157
|
+
- Структура `package.json`: поля `name: string`, `version: string`, `private?: boolean`, `files?: string[]`.
|
|
158
|
+
- Структура `pyproject.toml`: таблиці `[project]` (PEP 621) і `[tool.poetry]` (legacy Poetry) з полями `name`, `version`.
|
|
159
|
+
|
|
160
|
+
## Потік виконання / Використання
|
|
161
|
+
|
|
162
|
+
### Типовий сценарій: перевірка changelog для всього монорепо
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { getMonorepoProjectRootDirs, readPackageManifest, manifestFilePath } from './package-manifest.mjs'
|
|
166
|
+
|
|
167
|
+
const roots = await getMonorepoProjectRootDirs(process.cwd())
|
|
168
|
+
// roots ≈ ['.', 'apps/admin', 'npm/rules/changelog', 'python/tooling/foo', ...]
|
|
169
|
+
|
|
170
|
+
for (const ws of roots) {
|
|
171
|
+
const manifest = await readPackageManifest(ws)
|
|
172
|
+
if (!manifest) continue // ні package.json, ні pyproject.toml — пропускаємо
|
|
173
|
+
if (!manifest.registryPublishable) continue // приватні / без 'files' / без version — не публікуються
|
|
174
|
+
console.log(manifest.kind, manifestFilePath(ws, manifest), manifest.name, manifest.version)
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Послідовність викликів усередині `getMonorepoProjectRootDirs`
|
|
179
|
+
|
|
180
|
+
1. `getMonorepoPackageRootDirs(repoRoot)` → npm-воркспейси.
|
|
181
|
+
2. `existsSync('pyproject.toml')` + `existsSync('package.json')` на корені → опційно `'.'`.
|
|
182
|
+
3. `glob('**/pyproject.toml')` → ітерація по підкаталогах:
|
|
183
|
+
- Для кожного `relPy` обчислити `ws`.
|
|
184
|
+
- `isIgnoredWorkspaceRoot(ws)` + `existsSync(join(repoRoot, ws, 'package.json'))` → фільтр.
|
|
185
|
+
4. Фінальна фільтрація + сортування.
|
|
186
|
+
|
|
187
|
+
### Послідовність викликів усередині `readPackageManifest`
|
|
188
|
+
|
|
189
|
+
1. `existsSync(cwd/ws/package.json)`:
|
|
190
|
+
- **Так** → `readFile` + `JSON.parse` + перевірка типу → npm-маніфест або `null`.
|
|
191
|
+
- **Ні** → крок 2.
|
|
192
|
+
2. `existsSync(cwd/ws/pyproject.toml)`:
|
|
193
|
+
- **Ні** → `null`.
|
|
194
|
+
- **Так** → `readFile` + `parsePyprojectFields` → python-маніфест.
|
|
195
|
+
|
|
196
|
+
### Інваріанти
|
|
197
|
+
|
|
198
|
+
- Якщо `kind === 'npm'`, то `manifestRel === 'package.json'`. Якщо `kind === 'python'`, то `manifestRel === 'pyproject.toml'`.
|
|
199
|
+
- `npmFiles` ніколи не визначений для `kind === 'python'` (там завжди `null`).
|
|
200
|
+
- `registryPublishable` має різний зміст за `kind`:
|
|
201
|
+
- npm: `name && !private && Array.isArray(files)`;
|
|
202
|
+
- python: `name && version`.
|
|
203
|
+
- Усі функції безпечні щодо помилок IO/парсингу: повертають `null` / `{ null, null }` замість викидання винятків.
|
|
204
|
+
|
|
205
|
+
### Обмеження та крайні випадки
|
|
206
|
+
|
|
207
|
+
- **Глобальний обхід `**/pyproject.toml`** може бути повільним на дуже великих репо; список ігнорованих шляхів (`PYPROJECT_GLOB_IGNORE`) покриває типові важкі директорії, але не доменно-специфічні (наприклад, `dist/`).
|
|
208
|
+
- **Перевірка типу JSON-кореня** — захист від `null`, масивів та примітивів, але не від «дивних» значень полів (наприклад, `version: 123`); такі значення безшумно перетворюються на `null`.
|
|
209
|
+
- **`projectFieldsFromPyprojectDoc`** не підтримує **dynamic version** (PEP 621 `dynamic = ['version']`) — у такому разі `version === null`.
|
|
210
|
+
- **`isIgnoredWorkspaceRoot`** застосовується двічі (під час додавання та фінальної фільтрації) — це навмисна перестраховка.
|